Skip to content

Commit 242255c

Browse files
authored
Merge pull request #36 from veertuinc/release/v1.5.0
v1.5.0
2 parents b58b7de + bfc0993 commit 242255c

File tree

8 files changed

+294
-24
lines changed

8 files changed

+294
-24
lines changed

AGENT.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Anka Cloud Gitlab Executor Agent Guidelines
2+
3+
1. Keep code D.R.Y (Don't Repeat Yourself) to avoid code duplication and improve readability.
4+
2. Use meaningful variable names that can be read by a human easily.
5+
3. Prioritize adding logs and having the developer run the code locally to debug issues if you don't know the answer.

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ go 1.24
55
require (
66
github.com/spf13/cobra v1.7.0
77
github.com/spf13/pflag v1.0.5
8-
golang.org/x/crypto v0.31.0
8+
golang.org/x/crypto v0.35.0
99
)
1010

1111
require (
1212
github.com/inconshreveable/mousetrap v1.1.0 // indirect
13-
golang.org/x/sys v0.28.0 // indirect
13+
golang.org/x/sys v0.30.0 // indirect
1414
)

go.sum

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
66
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
77
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
88
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
9-
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
10-
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
11-
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
12-
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
13-
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
14-
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
9+
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
10+
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
11+
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
12+
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
13+
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
14+
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
1515
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
1616
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

internal/ankacloud/api.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ const (
55
)
66

77
type response struct {
8-
Status string `json:"status"`
9-
Message string `json:"message"`
10-
Body interface{} `json:"body,omitempty"`
8+
Status string `json:"status"`
9+
Message string `json:"message"`
10+
Body any `json:"body,omitempty"`
1111
}
1212

1313
type StartupScriptCondition int

internal/ankacloud/client.go

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"io"
99
"net/http"
1010
"net/url"
11+
"strings"
1112
"time"
1213

1314
"github.com/veertuinc/anka-cloud-gitlab-executor/internal/gitlab"
@@ -39,6 +40,30 @@ func (c *APIClient) parse(body []byte) (response, error) {
3940
return r, nil
4041
}
4142

43+
// readResponseBodyWithRetry reads the response body and retries once on unexpected EOF
44+
func (c *APIClient) readResponseBodyWithRetry(resp *http.Response, req *http.Request) ([]byte, *http.Response, error) {
45+
bodyBytes, err := io.ReadAll(resp.Body)
46+
if err != nil {
47+
if strings.Contains(err.Error(), "unexpected EOF") {
48+
time.Sleep(5 * time.Second)
49+
// Retry once on unexpected EOF
50+
retryResp, retryErr := c.HttpClient.Do(req)
51+
if retryErr != nil {
52+
return nil, nil, retryErr
53+
}
54+
defer retryResp.Body.Close()
55+
56+
bodyBytes, retryErr = io.ReadAll(retryResp.Body)
57+
if retryErr != nil {
58+
return nil, nil, fmt.Errorf("failed to read response body (retry): %w", retryErr)
59+
}
60+
return bodyBytes, retryResp, nil
61+
}
62+
return nil, nil, fmt.Errorf("failed to read response body: %w", err)
63+
}
64+
return bodyBytes, resp, nil
65+
}
66+
4267
func toQueryParams(params map[string]string) url.Values {
4368
query := url.Values{}
4469
for k, v := range params {
@@ -75,9 +100,12 @@ func (c *APIClient) Post(ctx context.Context, endpoint string, payload interface
75100
}
76101
defer r.Body.Close()
77102

78-
bodyBytes, err := io.ReadAll(r.Body)
103+
bodyBytes, r, err := c.readResponseBodyWithRetry(r, req)
79104
if err != nil {
80-
return nil, fmt.Errorf("failed to read response body: %w", err)
105+
if e, ok := err.(*url.Error); ok && e.Timeout() {
106+
return nil, gitlab.TransientError(fmt.Errorf("failed to send POST request to %s with payload %+v (retry): %w", endpointUrl, payload, e))
107+
}
108+
return nil, err
81109
}
82110

83111
baseResponse, err := c.parse(bodyBytes)
@@ -121,9 +149,12 @@ func (c *APIClient) Delete(ctx context.Context, endpoint string, payload interfa
121149
}
122150
defer r.Body.Close()
123151

124-
bodyBytes, err := io.ReadAll(r.Body)
152+
bodyBytes, r, err := c.readResponseBodyWithRetry(r, req)
125153
if err != nil {
126-
return nil, fmt.Errorf("failed to read response body: %w", err)
154+
if e, ok := err.(*url.Error); ok && e.Timeout() {
155+
return nil, gitlab.TransientError(fmt.Errorf("failed to send DELETE request to %s with payload %+v (retry): %w", endpointUrl, payload, e))
156+
}
157+
return nil, err
127158
}
128159

129160
baseResponse, err := c.parse(bodyBytes)
@@ -164,9 +195,12 @@ func (c *APIClient) Get(ctx context.Context, endpoint string, queryParams map[st
164195
}
165196
defer r.Body.Close()
166197

167-
bodyBytes, err := io.ReadAll(r.Body)
198+
bodyBytes, r, err := c.readResponseBodyWithRetry(r, req)
168199
if err != nil {
169-
return nil, fmt.Errorf("failed to read response body: %w", err)
200+
if e, ok := err.(*url.Error); ok && e.Timeout() {
201+
return nil, gitlab.TransientError(fmt.Errorf("failed to send GET request to %s (retry): %w", endpointUrl, e))
202+
}
203+
return nil, err
170204
}
171205

172206
baseResponse, err := c.parse(bodyBytes)

internal/ankacloud/controller.go

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ const (
3535
StateTerminating InstanceState = "Terminating"
3636
StateTerminated InstanceState = "Terminated"
3737
StateError InstanceState = "Error"
38-
StatePushing InstanceState = "Pushing"
3938
)
4039

4140
type VM struct {
@@ -176,7 +175,7 @@ func (c *controller) GetAllInstances(ctx context.Context) ([]Instance, error) {
176175

177176
body, err := c.APIClient.Get(ctx, "/api/v1/vm", nil)
178177
if err != nil {
179-
return nil, fmt.Errorf("failed to get instances: %w", err)
178+
return nil, fmt.Errorf("failed to get all instances: %w", err)
180179
}
181180

182181
var response getAllInstancesResponse
@@ -185,6 +184,13 @@ func (c *controller) GetAllInstances(ctx context.Context) ([]Instance, error) {
185184
return nil, fmt.Errorf("failed to parse response body %q: %w", string(body), err)
186185
}
187186

187+
log.ConditionalColorf("got %d instances back from controller\n", len(response.Instances))
188+
body, err = json.Marshal(response)
189+
if err != nil {
190+
return nil, fmt.Errorf("failed to marshal response body %q: %w", string(body), err)
191+
}
192+
log.ConditionalColorf("instances response: %s\n", string(body))
193+
188194
var instances []Instance
189195
for _, instanceWrapper := range response.Instances {
190196
instances = append(instances, *instanceWrapper.Instance)
@@ -195,17 +201,38 @@ func (c *controller) GetAllInstances(ctx context.Context) ([]Instance, error) {
195201

196202
func (c *controller) GetInstanceByExternalId(ctx context.Context, externalId string) (*Instance, error) {
197203
instances, err := c.GetAllInstances(ctx)
204+
205+
if len(instances) == 0 {
206+
return nil, fmt.Errorf("no instances returned from controller: %w", err)
207+
}
208+
198209
if err != nil {
199-
return nil, fmt.Errorf("failed to get instances: %w", err)
210+
return nil, fmt.Errorf("failed to get instance by external id %s: %w", externalId, err)
200211
}
201212

213+
var matchingInstances []*Instance
202214
for _, instance := range instances {
203215
if instance.ExternalId == externalId {
204-
return &instance, nil
216+
matchingInstances = append(matchingInstances, &instance)
217+
}
218+
}
219+
220+
if len(matchingInstances) == 0 {
221+
return nil, fmt.Errorf("instance with external id %s not found", externalId)
222+
}
223+
224+
// If multiple instances with the same external ID exist, prioritize by state
225+
// Return the first instance that is in a good state (Started, Scheduling, Pulling)
226+
for _, instance := range matchingInstances {
227+
switch instance.State {
228+
case StateStarted, StateScheduling, StatePulling:
229+
return instance, nil
205230
}
206231
}
207232

208-
return nil, fmt.Errorf("instance with external id %s not found", externalId)
233+
// No instances in a usable state - fail explicitly instead of returning Error/Terminated instances
234+
return nil, fmt.Errorf("instance with external id %s exists but is not in a usable state (found state: %s)",
235+
externalId, matchingInstances[0].State)
209236
}
210237

211238
func (c *controller) GetTemplateIdByName(ctx context.Context, templateName string) (string, error) {

0 commit comments

Comments
 (0)