Skip to content

Commit a623694

Browse files
committed
Move compute package utilities
1 parent 6d78d03 commit a623694

13 files changed

+1019
-2
lines changed

compute/go.mod

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,29 +6,32 @@ replace github.com/databricks/databricks-sdk-go/databricks => ../databricks
66

77
require (
88
github.com/databricks/databricks-sdk-go/databricks v0.0.0-00010101000000-000000000000
9-
golang.org/x/oauth2 v0.25.0
9+
github.com/stretchr/testify v1.10.0
10+
golang.org/x/mod v0.22.0
1011
)
1112

1213
require (
1314
cloud.google.com/go/auth v0.13.0 // indirect
1415
cloud.google.com/go/auth/oauth2adapt v0.2.6 // indirect
1516
cloud.google.com/go/compute/metadata v0.6.0 // indirect
1617
github.com/databricks/databricks-sdk-go v0.55.0 // indirect
18+
github.com/davecgh/go-spew v1.1.1 // indirect
1719
github.com/felixge/httpsnoop v1.0.4 // indirect
1820
github.com/go-logr/logr v1.4.2 // indirect
1921
github.com/go-logr/stdr v1.2.2 // indirect
2022
github.com/google/go-querystring v1.1.0 // indirect
2123
github.com/google/s2a-go v0.1.8 // indirect
2224
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
2325
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
26+
github.com/pmezard/go-difflib v1.0.0 // indirect
2427
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect
2528
go.opentelemetry.io/otel v1.31.0 // indirect
2629
go.opentelemetry.io/otel/metric v1.31.0 // indirect
2730
go.opentelemetry.io/otel/trace v1.31.0 // indirect
2831
golang.org/x/crypto v0.31.0 // indirect
2932
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect
30-
golang.org/x/mod v0.22.0 // indirect
3133
golang.org/x/net v0.33.0 // indirect
34+
golang.org/x/oauth2 v0.25.0 // indirect
3235
golang.org/x/sys v0.28.0 // indirect
3336
golang.org/x/text v0.21.0 // indirect
3437
golang.org/x/time v0.9.0 // indirect
@@ -37,4 +40,5 @@ require (
3740
google.golang.org/grpc v1.69.2 // indirect
3841
google.golang.org/protobuf v1.36.1 // indirect
3942
gopkg.in/ini.v1 v1.67.0 // indirect
43+
gopkg.in/yaml.v3 v3.0.1 // indirect
4044
)

compute/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ google.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU=
7575
google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
7676
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
7777
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
78+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
79+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
7880
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
7981
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
8082
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

compute/v2/ext_commands.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// TODO : Add the missing methods and implement the methods
2+
// This file has not been completely shifted from the SDK-Beta
3+
// as we still don't have the wait for state methods in the SDK-mod
4+
package compute
5+
6+
import (
7+
"context"
8+
)
9+
10+
type CommandExecutorV2 struct {
11+
clustersAPI *ClustersAPI
12+
executionAPI *CommandExecutionAPI
13+
language Language
14+
clusterID string
15+
contextID string
16+
}
17+
18+
type commandExecutionAPIUtilities interface {
19+
Start(ctx context.Context, clusterID string, language Language) (*CommandExecutorV2, error)
20+
}
21+
22+
// Start the command execution context on a cluster and ensure it transitions to a running state
23+
func (c *CommandExecutorV2) Destroy(ctx context.Context) error {
24+
return c.executionAPI.Destroy(ctx, DestroyContext{
25+
ClusterId: c.clusterID,
26+
ContextId: c.contextID,
27+
})
28+
}
29+
30+
// CommandExecutor creates a spark context and executes a command and then closes context
31+
type CommandExecutor interface {
32+
Execute(ctx context.Context, clusterID, language, commandStr string) Results
33+
}
34+
35+
// CommandMock mocks the execution of command
36+
type CommandMock func(commandStr string) Results
37+
38+
func (m CommandMock) Execute(_ context.Context, _, _, commandStr string) Results {
39+
return m(commandStr)
40+
}
41+
42+
// CommandsHighLevelAPI exposes more friendly wrapper over command execution
43+
type CommandsHighLevelAPI struct {
44+
clusters *ClustersAPI
45+
execution *CommandExecutionAPI
46+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package compute
2+
3+
import (
4+
"strings"
5+
)
6+
7+
// TrimLeadingWhitespace removes leading whitespace, so that Python code blocks
8+
// that are embedded into Go code still could be interpreted properly.
9+
func TrimLeadingWhitespace(commandStr string) (newCommand string) {
10+
lines := strings.Split(strings.ReplaceAll(commandStr, "\t", " "), "\n")
11+
leadingWhitespace := 1<<31 - 1
12+
for _, line := range lines {
13+
for pos, char := range line {
14+
if char == ' ' || char == '\t' {
15+
continue
16+
}
17+
// first non-whitespace character
18+
if pos < leadingWhitespace {
19+
leadingWhitespace = pos
20+
}
21+
// is not needed further
22+
break
23+
}
24+
}
25+
for i := 0; i < len(lines); i++ {
26+
if lines[i] == "" || strings.Trim(lines[i], " \t") == "" {
27+
continue
28+
}
29+
if len(lines[i]) < leadingWhitespace {
30+
newCommand += lines[i] + "\n" // or not..
31+
} else {
32+
newCommand += lines[i][leadingWhitespace:] + "\n"
33+
}
34+
}
35+
return
36+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package compute
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
func TestTrimLeadingWhitespace(t *testing.T) {
10+
assert.Equal(t, "foo\nbar\n", TrimLeadingWhitespace(`
11+
12+
foo
13+
bar
14+
15+
`))
16+
}
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
package compute
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"sort"
7+
"strings"
8+
"time"
9+
10+
"github.com/databricks/databricks-sdk-go/databricks/apierr"
11+
"github.com/databricks/databricks-sdk-go/databricks/log"
12+
"github.com/databricks/databricks-sdk-go/databricks/retries"
13+
"github.com/databricks/databricks-sdk-go/databricks/useragent"
14+
)
15+
16+
type Wait struct {
17+
ClusterID string
18+
Libraries []Library
19+
IsRunning bool
20+
IsRefresh bool
21+
}
22+
23+
func (library Library) String() string {
24+
if library.Whl != "" {
25+
return fmt.Sprintf("whl:%s", library.Whl)
26+
}
27+
if library.Jar != "" {
28+
return fmt.Sprintf("jar:%s", library.Jar)
29+
}
30+
if library.Pypi != nil && library.Pypi.Package != "" {
31+
return fmt.Sprintf("pypi:%s%s", library.Pypi.Repo, library.Pypi.Package)
32+
}
33+
if library.Maven != nil && library.Maven.Coordinates != "" {
34+
mvn := library.Maven
35+
return fmt.Sprintf("mvn:%s%s%s", mvn.Repo, mvn.Coordinates,
36+
strings.Join(mvn.Exclusions, ""))
37+
}
38+
if library.Egg != "" {
39+
return fmt.Sprintf("egg:%s", library.Egg)
40+
}
41+
if library.Cran != nil && library.Cran.Package != "" {
42+
return fmt.Sprintf("cran:%s%s", library.Cran.Repo, library.Cran.Package)
43+
}
44+
return "unknown"
45+
}
46+
47+
func (cll *InstallLibraries) Sort() {
48+
sort.Slice(cll.Libraries, func(i, j int) bool {
49+
return cll.Libraries[i].String() < cll.Libraries[j].String()
50+
})
51+
}
52+
53+
// ToLibraryList convert to envity for convenient comparison
54+
func (cls ClusterLibraryStatuses) ToLibraryList() InstallLibraries {
55+
cll := InstallLibraries{ClusterId: cls.ClusterId}
56+
for _, lib := range cls.LibraryStatuses {
57+
cll.Libraries = append(cll.Libraries, *lib.Library)
58+
}
59+
cll.Sort()
60+
return cll
61+
}
62+
63+
func (w *Wait) IsNotInScope(lib *Library) bool {
64+
// if we don't know concrete libraries
65+
if len(w.Libraries) == 0 {
66+
return false
67+
}
68+
// if we know concrete libraries
69+
for _, v := range w.Libraries {
70+
if v.String() == lib.String() {
71+
return false
72+
}
73+
}
74+
return true
75+
}
76+
77+
// IsRetryNeeded returns first bool if there needs to be retry.
78+
// If there needs to be retry, error message will explain why.
79+
// If retry does not need to happen and error is not nil - it failed.
80+
func (cls ClusterLibraryStatuses) IsRetryNeeded(w Wait) (bool, error) {
81+
pending := 0
82+
ready := 0
83+
errors := []string{}
84+
for _, lib := range cls.LibraryStatuses {
85+
if lib.IsLibraryForAllClusters {
86+
continue
87+
}
88+
if w.IsNotInScope(lib.Library) {
89+
continue
90+
}
91+
switch lib.Status {
92+
// No action has yet been taken to install the library. This state should be very short lived.
93+
case "PENDING":
94+
pending++
95+
// Metadata necessary to install the library is being retrieved from the provided repository.
96+
case "RESOLVING":
97+
pending++
98+
// The library is actively being installed, either by adding resources to Spark
99+
// or executing system commands inside the Spark nodes.
100+
case "INSTALLING":
101+
pending++
102+
// The library has been successfully installed.
103+
case "INSTALLED":
104+
ready++
105+
// Installation on a Databricks Runtime 7.0 or above cluster was skipped due to Scala version incompatibility.
106+
case "SKIPPED":
107+
ready++
108+
// The library has been marked for removal. Libraries can be removed only when clusters are restarted.
109+
case "UNINSTALL_ON_RESTART":
110+
ready++
111+
//Some step in installation failed. More information can be found in the messages field.
112+
case "FAILED":
113+
if w.IsRefresh {
114+
// we're reading library list on a running cluster and some of the libs failed to install
115+
continue
116+
}
117+
errors = append(errors, fmt.Sprintf("%s failed: %s", lib.Library, strings.Join(lib.Messages, ", ")))
118+
continue
119+
}
120+
}
121+
if pending > 0 {
122+
return true, fmt.Errorf("%d libraries are ready, but there are still %d pending", ready, pending)
123+
}
124+
if len(errors) > 0 {
125+
return false, fmt.Errorf("%s", strings.Join(errors, "\n"))
126+
}
127+
return false, nil
128+
}
129+
130+
type Update struct {
131+
ClusterId string
132+
// The libraries to install.
133+
Install []Library
134+
// The libraries to install.
135+
Uninstall []Library
136+
}
137+
138+
type librariesAPIUtilities interface {
139+
UpdateAndWait(ctx context.Context, update Update, options ...retries.Option[ClusterLibraryStatuses]) error
140+
}
141+
142+
func (a *LibrariesAPI) UpdateAndWait(ctx context.Context, update Update,
143+
options ...retries.Option[ClusterLibraryStatuses]) error {
144+
ctx = useragent.InContext(ctx, "sdk-feature", "update-libraries")
145+
if len(update.Uninstall) > 0 {
146+
err := a.Uninstall(ctx, UninstallLibraries{
147+
ClusterId: update.ClusterId,
148+
Libraries: update.Uninstall,
149+
})
150+
if err != nil {
151+
return fmt.Errorf("uninstall: %w", err)
152+
}
153+
}
154+
if len(update.Install) > 0 {
155+
err := a.Install(ctx, InstallLibraries{
156+
ClusterId: update.ClusterId,
157+
Libraries: update.Install,
158+
})
159+
if err != nil {
160+
return fmt.Errorf("install: %w", err)
161+
}
162+
}
163+
// this helps to avoid erroring out when out-of-list library gets added to
164+
// the cluster manually and thereforce fails the wait on error
165+
scope := make([]Library, len(update.Install)+len(update.Uninstall))
166+
scope = append(scope, update.Install...)
167+
scope = append(scope, update.Uninstall...)
168+
_, err := a.Wait(ctx, Wait{
169+
ClusterID: update.ClusterId,
170+
Libraries: scope,
171+
IsRunning: true,
172+
IsRefresh: false,
173+
}, options...)
174+
return err
175+
}
176+
177+
// clusterID string, timeout time.Duration, isActive bool, refresh bool
178+
func (a *LibrariesAPI) Wait(ctx context.Context, wait Wait,
179+
options ...retries.Option[ClusterLibraryStatuses]) (*ClusterLibraryStatuses, error) {
180+
ctx = useragent.InContext(ctx, "sdk-feature", "wait-for-libraries")
181+
i := retries.Info[ClusterLibraryStatuses]{Timeout: 30 * time.Minute}
182+
for _, o := range options {
183+
o(&i)
184+
}
185+
result, err := retries.Poll(ctx, i.Timeout, func() (*ClusterLibraryStatuses, *retries.Err) {
186+
status, err := a.ClusterStatusByClusterId(ctx, wait.ClusterID)
187+
if apierr.IsMissing(err) {
188+
// eventual consistency error
189+
return nil, retries.Continue(err)
190+
}
191+
for _, o := range options {
192+
o(&retries.Info[ClusterLibraryStatuses]{
193+
Timeout: i.Timeout,
194+
Info: status,
195+
})
196+
}
197+
if err != nil {
198+
return nil, retries.Halt(err)
199+
}
200+
if !wait.IsRunning {
201+
log.InfoContext(ctx, "Cluster %s is currently not running, so just returning list of %d libraries",
202+
wait.ClusterID, len(status.LibraryStatuses))
203+
return status, nil
204+
}
205+
retry, err := status.IsRetryNeeded(wait)
206+
if retry {
207+
return status, retries.Continue(err)
208+
}
209+
if err != nil {
210+
return status, retries.Halt(err)
211+
}
212+
return status, nil
213+
})
214+
if err != nil {
215+
return nil, err
216+
}
217+
if wait.IsRunning {
218+
installed := []LibraryFullStatus{}
219+
cleanup := UninstallLibraries{
220+
ClusterId: wait.ClusterID,
221+
Libraries: []Library{},
222+
}
223+
// cleanup libraries that failed to install
224+
for _, v := range result.LibraryStatuses {
225+
if v.Status == "FAILED" {
226+
log.WarningContext(ctx, "Removing failed library %s from %s",
227+
v.Library, wait.ClusterID)
228+
cleanup.Libraries = append(cleanup.Libraries, *v.Library)
229+
continue
230+
}
231+
installed = append(installed, v)
232+
}
233+
// and result contains only the libraries that were successfully installed
234+
result.LibraryStatuses = installed
235+
if len(cleanup.Libraries) > 0 {
236+
err = a.Uninstall(ctx, cleanup)
237+
if err != nil {
238+
return nil, fmt.Errorf("cannot cleanup libraries: %w", err)
239+
}
240+
}
241+
}
242+
return result, nil
243+
}

0 commit comments

Comments
 (0)