Skip to content

Commit 42b0b84

Browse files
authored
feat(BREV-1712): launchpad as cloud broker (#45)
1 parent 369ee7d commit 42b0b84

269 files changed

Lines changed: 154941 additions & 4 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,15 @@ require (
1010
github.com/cenkalti/backoff v2.2.1+incompatible
1111
github.com/cenkalti/backoff/v4 v4.3.0
1212
github.com/gliderlabs/ssh v0.3.8
13+
github.com/go-ozzo/ozzo-validation/v4 v4.3.0
1314
github.com/google/go-cmp v0.7.0
1415
github.com/google/uuid v1.6.0
1516
github.com/jarcoal/httpmock v1.4.0
1617
github.com/nebius/gosdk v0.0.0-20250826102719-940ad1dfb5de
18+
github.com/pkg/errors v0.9.1
1719
github.com/stretchr/testify v1.11.0
1820
golang.org/x/crypto v0.41.0
21+
gopkg.in/validator.v2 v2.0.1
1922
)
2023

2124
require (

go.sum

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vS
44
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs=
55
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
66
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
7+
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496 h1:zV3ejI06GQ59hwDQAvmK1qxOQGB3WuVTRoY0okPTAv0=
8+
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
79
github.com/bojanz/currency v1.3.1 h1:3BUAvy/5hU/Pzqg5nrQslVihV50QG+A2xKPoQw1RKH4=
810
github.com/bojanz/currency v1.3.1/go.mod h1:jNoZiJyRTqoU5DFoa+n+9lputxPUDa8Fz8BdDrW06Go=
911
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
@@ -22,6 +24,8 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
2224
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
2325
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
2426
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
27+
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 h1:byhDUpfEwjsVQb1vBunvIjh2BHQ9ead57VkAEY4V+Es=
28+
github.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew=
2529
github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
2630
github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0=
2731
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
@@ -46,6 +50,8 @@ github.com/maxatome/go-testdeep v1.14.0 h1:rRlLv1+kI8eOI3OaBXZwb3O7xY3exRzdW5QyX
4650
github.com/maxatome/go-testdeep v1.14.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM=
4751
github.com/nebius/gosdk v0.0.0-20250826102719-940ad1dfb5de h1:7GbDUDyH22dvN7ata8HuNVuDlcyaDzUs/s+03Y3pDqU=
4852
github.com/nebius/gosdk v0.0.0-20250826102719-940ad1dfb5de/go.mod h1:eVbm4Qc4GPzBn3EL4rLvy1WS9zqJDw+giksOA2NZERY=
53+
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
54+
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
4955
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
5056
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
5157
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -55,6 +61,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
5561
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
5662
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
5763
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
64+
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
5865
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
5966
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
6067
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
@@ -98,6 +105,9 @@ google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXn
98105
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
99106
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
100107
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
108+
gopkg.in/validator.v2 v2.0.1 h1:xF0KWyGWXm/LM2G1TrEjqOu4pa6coO9AlWSf3msVfDY=
109+
gopkg.in/validator.v2 v2.0.1/go.mod h1:lIUZBlB3Im4s/eYp39Ry/wkR02yOPhZ9IwIRBjuPuG8=
110+
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
101111
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
102112
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
103113
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

internal/errors/errors.go

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
package errors
2+
3+
import (
4+
stderrors "errors"
5+
"fmt"
6+
"runtime"
7+
"strconv"
8+
"strings"
9+
10+
pkgerrors "github.com/pkg/errors"
11+
)
12+
13+
type ValidationError struct {
14+
Message string
15+
}
16+
17+
var _ error = ValidationError{}
18+
19+
func NewValidationError(message string) ValidationError {
20+
return ValidationError{Message: message}
21+
}
22+
23+
func (v ValidationError) Error() string {
24+
return v.Message
25+
}
26+
27+
var New = stderrors.New
28+
29+
var Errorf = fmt.Errorf
30+
31+
func Wrap(err error, msg string) error {
32+
if err == nil {
33+
return nil
34+
}
35+
return Errorf("%s: %w", msg, err)
36+
}
37+
38+
var As = stderrors.As
39+
40+
var Unwrap = stderrors.Unwrap
41+
42+
func Unwraps(err error) []error {
43+
u, ok := err.(interface {
44+
Unwrap() []error
45+
})
46+
if !ok {
47+
return nil
48+
}
49+
return u.Unwrap()
50+
}
51+
52+
func Root(err error) error {
53+
for Unwrap(err) != nil {
54+
err = Unwrap(err)
55+
}
56+
joinedErrs := Unwraps(err)
57+
if len(joinedErrs) == 0 {
58+
return err
59+
}
60+
return Roots(joinedErrs)
61+
}
62+
63+
func Roots(errs []error) error {
64+
if len(errs) == 0 {
65+
return nil
66+
}
67+
rootedErrs := make([]error, len(errs))
68+
for i, e := range errs {
69+
rootedErrs[i] = Root(e)
70+
}
71+
return Join(rootedErrs...)
72+
}
73+
74+
// flattens error tree
75+
func Flatten(err error) []error {
76+
if err == nil {
77+
return nil
78+
}
79+
joinedErrs := Unwraps(err)
80+
if joinedErrs == nil {
81+
return []error{err}
82+
}
83+
flatErrs := []error{}
84+
for _, e := range joinedErrs {
85+
flatErrs = append(flatErrs, Flatten(e)...)
86+
}
87+
return flatErrs
88+
}
89+
90+
// var ReturnTrace = errtrace.Wrap
91+
92+
func Join(errs ...error) error {
93+
noNilErrs := make([]error, 0, len(errs))
94+
for _, err := range errs {
95+
if err != nil {
96+
noNilErrs = append(noNilErrs, err)
97+
}
98+
}
99+
if len(noNilErrs) == 0 {
100+
return nil
101+
}
102+
if len(noNilErrs) == 1 {
103+
return noNilErrs[0]
104+
}
105+
return stderrors.Join(errs...) //nolint:wrapcheck // this is a wrapper
106+
}
107+
108+
// if multi err, combine similar errors
109+
func CombineByString(err error) error {
110+
if err == nil {
111+
return nil
112+
}
113+
errs := Flatten(err)
114+
mapE := make(map[string]error)
115+
mapEList := []error{}
116+
for _, e := range errs {
117+
_, ok := mapE[e.Error()]
118+
if !ok {
119+
mapE[e.Error()] = e
120+
mapEList = append(mapEList, e)
121+
}
122+
}
123+
errsOut := make([]error, 0, len(mapE))
124+
for _, e := range mapEList {
125+
errsOut = append(errsOut, e)
126+
}
127+
return Join(errsOut...)
128+
}
129+
130+
var Is = stderrors.Is
131+
132+
var WrapAndTrace = WrapAndTraceInMsg
133+
134+
func WrapAndTraceInMsg(err error) error {
135+
if err == nil {
136+
return nil
137+
}
138+
return pkgerrors.Wrap(err, makeErrorMessage("", 0)) // this wrap also adds a stacktrace which can be nice
139+
}
140+
141+
func WrapAndTrace2[T any](t T, err error) (T, error) {
142+
if err == nil {
143+
return t, nil
144+
}
145+
return t, pkgerrors.Wrap(err, makeErrorMessage("", 0))
146+
}
147+
148+
func makeErrorMessage(message string, skip int) string {
149+
skip += 2
150+
pc, file, line, _ := runtime.Caller(skip)
151+
152+
funcName := "unknown"
153+
fn := runtime.FuncForPC(pc)
154+
if fn != nil {
155+
funcName = fn.Name()
156+
}
157+
158+
lineNum := strconv.Itoa(line)
159+
return fmt.Sprintf("[error] %s\n%s\n%s:%s\n", message, funcName, file, lineNum)
160+
}
161+
162+
func HandleErrDefer(f func() error) {
163+
_ = f()
164+
// logger.L().Error("", zap.Error(err))
165+
}
166+
167+
func ErrorContainsAny(err error, substrs ...string) bool {
168+
for _, substr := range substrs {
169+
if ErrorContains(err, substr) {
170+
return true
171+
}
172+
}
173+
return false
174+
}
175+
176+
func ErrorContains(err error, substr string) bool {
177+
return err != nil && strings.Contains(err.Error(), substr)
178+
}
179+
180+
func IsErrorExcept(err error, errs ...error) bool {
181+
return err != nil && !IsAny(err, errs...)
182+
}
183+
184+
func IsErrorExceptSubstr(err error, substr ...string) bool {
185+
return err != nil && !ErrorContainsAny(err, substr...)
186+
}
187+
188+
func IsAny(err error, errs ...error) bool {
189+
for _, e := range errs {
190+
if Is(err, e) {
191+
return true
192+
}
193+
}
194+
return false
195+
}
196+
197+
// TruncateErrorForLogging truncates a long error message to a more manageable size
198+
// while preserving the most important parts of the error message
199+
func TruncateErrorForLogging(err error, maxLength int) error {
200+
if err == nil {
201+
return nil
202+
}
203+
204+
errStr := err.Error()
205+
if len(errStr) <= maxLength {
206+
return err
207+
}
208+
209+
// Otherwise truncate with indication
210+
return New(fmt.Sprintf("ERROR (truncated): %s... (truncated)", errStr[:maxLength-20]))
211+
}

v1/hashutil.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package v1
2+
3+
import (
4+
"crypto/sha256"
5+
"encoding/hex"
6+
)
7+
8+
func HashSensitiveString(s string) (string, error) {
9+
// Preprocess the password with SHA-256
10+
sha256Hash := sha256.Sum256([]byte(s))
11+
sha256HashString := hex.EncodeToString(sha256Hash[:])
12+
return sha256HashString, nil
13+
}

v1/instancetype.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ type InstanceType struct {
8787
Provider string
8888
Cloud string
8989
CanModifyFirewallRules bool
90+
ReservedInstancePoolID string
9091
}
9192

9293
func MakeGenericInstanceTypeID(instanceType InstanceType) InstanceTypeID {

v1/location.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package v1
33
import (
44
"context"
55
"fmt"
6+
"slices"
67
)
78

89
type CloudLocation interface {
@@ -26,15 +27,22 @@ type LocationsFilter []string
2627

2728
var All = []string{"all"}
2829

29-
func (l LocationsFilter) IsAll() bool {
30-
for _, v := range l {
30+
func (f LocationsFilter) IsAll() bool {
31+
for _, v := range f {
3132
if v == "*" || v == "all" {
3233
return true
3334
}
3435
}
3536
return false
3637
}
3738

39+
func (f LocationsFilter) IsAllowed(location string) bool {
40+
if f.IsAll() {
41+
return true
42+
}
43+
return slices.Contains(f, location)
44+
}
45+
3846
// ValidateGetLocations validates that the CloudLocation implementation returns at least one available location without error.
3947
func ValidateGetLocations(ctx context.Context, client CloudLocation) error {
4048
locs, err := client.GetLocations(ctx, GetLocationsArgs{})

v1/log.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package v1
2+
3+
import "context"
4+
5+
type Field struct {
6+
Key string
7+
Value any
8+
}
9+
10+
func LogField(key string, value any) Field {
11+
return Field{Key: key, Value: value}
12+
}
13+
14+
type Logger interface {
15+
Debug(ctx context.Context, msg string, fields ...Field)
16+
Info(ctx context.Context, msg string, fields ...Field)
17+
Warn(ctx context.Context, msg string, args ...Field)
18+
Error(ctx context.Context, err error, fields ...Field)
19+
}
20+
21+
type NoopLogger struct{}
22+
23+
func (l *NoopLogger) Debug(_ context.Context, _ string, _ ...Field) {}
24+
func (l *NoopLogger) Info(_ context.Context, _ string, _ ...Field) {}
25+
func (l *NoopLogger) Warn(_ context.Context, _ string, _ ...Field) {}
26+
func (l *NoopLogger) Error(_ context.Context, _ error, _ ...Field) {}
27+
28+
var _ Logger = &NoopLogger{}

v1/providers/launchpad/Makefile

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
SPEC_VERSION ?= v2.36.1
2+
SPEC_FILE ?= /local/swagger-${SPEC_VERSION}.yaml
3+
OUTPUT_DIR ?= launchpad
4+
# To understand disallowAdditionalPropertiesIfNotPresent=false
5+
# Check out https://openapi-generator.tech/docs/generators/go/#config-options
6+
generate-launchpad-client:
7+
rm -rf gen/${OUTPUT_DIR}
8+
mkdir -p gen/${OUTPUT_DIR}
9+
docker run --rm -v ${PWD}:/local openapitools/openapi-generator-cli:v7.8.0 generate \
10+
--additional-properties disallowAdditionalPropertiesIfNotPresent=false \
11+
-i ${SPEC_FILE} \
12+
-g go \
13+
--git-user-id brevdev \
14+
--git-repo-id cloud \
15+
--skip-validate-spec \
16+
-o /local/gen/${OUTPUT_DIR}
17+
sudo chown -R $(shell id -u):$(shell id -g) gen/${OUTPUT_DIR}//
18+
find gen/${OUTPUT_DIR} -name "*.go" -type f -exec sed -i.bak 's|openapiclient "github.com/brevdev/cloud"|openapiclient "github.com/brevdev/cloud/v1/providers/launchpad/gen/launchpad"|g' {} \; && find gen/${OUTPUT_DIR} -name "*.go.bak" -delete
19+
gofmt -s -w gen/${OUTPUT_DIR}
20+
rm -rf gen/${OUTPUT_DIR}/go.mod gen/${OUTPUT_DIR}/go.sum

0 commit comments

Comments
 (0)