Skip to content

Commit 3e5654e

Browse files
committed
Add runner readiness waiting support
Now `terraform apply` will wait until the runner is registered and ready before considering the plan application as successful.
1 parent 55fdc91 commit 3e5654e

File tree

4 files changed

+145
-0
lines changed

4 files changed

+145
-0
lines changed

iterative/resource_runner.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,19 @@ import (
66
"encoding/base64"
77
"encoding/json"
88
"fmt"
9+
"log"
10+
"net"
911
"os"
1012
"strconv"
1113
"text/template"
14+
"time"
1215

1316
"terraform-provider-iterative/iterative/aws"
1417
"terraform-provider-iterative/iterative/azure"
1518
"terraform-provider-iterative/iterative/utils"
1619

1720
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
21+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
1822
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
1923
)
2024

@@ -176,6 +180,43 @@ func resourceRunnerCreate(ctx context.Context, d *schema.ResourceData, m interfa
176180
diags = resourceMachineCreate(ctx, d, m)
177181
}
178182

183+
if diags.HasError() {
184+
return diags
185+
}
186+
187+
machineUser := "ubuntu"
188+
machineKey := d.Get("ssh_private").(string)
189+
machineAddress := net.JoinHostPort(d.Get("instance_ip").(string), "22")
190+
machineLogCommand := "journalctl --unit cml"
191+
192+
var logError error
193+
var logEvents string
194+
err = resource.Retry(d.Timeout(schema.TimeoutCreate), func() *resource.RetryError {
195+
logEvents, logError = utils.RunCommand(machineLogCommand, 10*time.Second, machineAddress, machineUser, machineKey)
196+
log.Printf("[DEBUG] Collected log events: %#v", logEvents)
197+
log.Printf("[DEBUG] Connection errors: %#v", logError)
198+
199+
if logError != nil {
200+
return resource.RetryableError(fmt.Errorf("Waiting for the machine to accept connections... %s", logError))
201+
} else if !utils.IsReady(logEvents) {
202+
return resource.RetryableError(fmt.Errorf("Waiting for the runner to be ready... %s", logError))
203+
} else {
204+
return nil
205+
}
206+
})
207+
208+
if logError != nil {
209+
logEvents = logError.Error()
210+
}
211+
212+
if err != nil {
213+
diags = append(diags, diag.Diagnostic{
214+
Severity: diag.Error,
215+
Summary: fmt.Sprintf("Error checking the runner status"),
216+
Detail: logEvents,
217+
})
218+
}
219+
179220
return diags
180221
}
181222

iterative/utils/runner.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package utils
2+
3+
import (
4+
"bufio"
5+
"encoding/json"
6+
"regexp"
7+
"strings"
8+
)
9+
10+
type LogEvent struct {
11+
Level string `json:"level"`
12+
Time string `json:"time"`
13+
Repository string `json:"repo"`
14+
Job string `json:"job"`
15+
Status string `json:"status"`
16+
Success bool `json:"success"`
17+
}
18+
19+
func ParseLogEvent(logEvent string) (LogEvent, error) {
20+
var result LogEvent
21+
err := json.Unmarshal([]byte(logEvent), &result)
22+
return result, err
23+
}
24+
25+
// IsReady checks whether a runner is ready or not by
26+
// parsing the JSONL records from the logs it produces.
27+
func IsReady(logs string) bool {
28+
scanner := bufio.NewScanner(strings.NewReader(logs))
29+
for scanner.Scan() {
30+
line := scanner.Text()
31+
// Extract the JSON between curly braces from the log line.
32+
record := regexp.MustCompile(`\{.+\}`).Find([]byte(line))
33+
// Try to parse the retrieved JSON string into a LogEvent structure.
34+
if event, err := ParseLogEvent(string(record)); err == nil {
35+
if event.Status == "ready" {
36+
return true
37+
}
38+
}
39+
}
40+
return false
41+
}

iterative/utils/runner_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package utils
2+
3+
import "testing"
4+
5+
func TestPositiveReadinessCheck(t *testing.T) {
6+
result := IsReady(`
7+
-- Logs begin at Wed 2021-01-20 00:25:37 UTC, end at Fri 2021-02-26 15:37:56 UTC. --
8+
Feb 26 15:37:20 ip-172-31-6-188 cml.sh[2203]: {"level":"info","time":"···","repo":"···","status":"ready"}
9+
`)
10+
if !result {
11+
t.Errorf("Positive readiness check")
12+
}
13+
}
14+
15+
func TestNegativeReadinessCheck(t *testing.T) {
16+
result := IsReady(`
17+
-- Logs begin at Wed 2021-01-20 00:25:37 UTC, end at Fri 2021-02-26 15:37:56 UTC. --
18+
Feb 26 15:37:20 ip-172-31-6-188 cml.sh[2203]: {"level":"info","time":"···","repo":"···"}
19+
`)
20+
if result {
21+
t.Errorf("Negative readiness check")
22+
}
23+
}

iterative/utils/ssh.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package utils
22

33
import (
4+
"bytes"
45
"crypto/rand"
56
"crypto/rsa"
67
"crypto/x509"
78
"encoding/pem"
89
"strings"
10+
"time"
911

1012
"golang.org/x/crypto/ssh"
1113
)
@@ -42,3 +44,41 @@ func PublicFromPrivatePEM(privateKey string) (string, error) {
4244

4345
return pubKeyBuf.String(), nil
4446
}
47+
48+
func RunCommand(command string, timeout time.Duration, hostAddress string, userName string, privateKey string) (string, error) {
49+
parsedPrivateKey, err := ssh.ParsePrivateKey([]byte(privateKey))
50+
if err != nil {
51+
return "", err
52+
}
53+
54+
configuration := &ssh.ClientConfig{
55+
User: userName,
56+
Auth: []ssh.AuthMethod{
57+
ssh.PublicKeys(parsedPrivateKey),
58+
},
59+
HostKeyCallback: ssh.InsecureIgnoreHostKey(), // Doesn't matter for this use case, but isn't a good practice either.
60+
Timeout: timeout,
61+
}
62+
63+
client, err := ssh.Dial("tcp", hostAddress, configuration)
64+
if err != nil {
65+
return "", err
66+
}
67+
defer client.Close()
68+
69+
session, err := client.NewSession()
70+
if err != nil {
71+
return "", err
72+
}
73+
defer session.Close()
74+
75+
var buffer bytes.Buffer
76+
session.Stdout = &buffer
77+
session.Stderr = &buffer
78+
79+
if err := session.Run(command); err == nil {
80+
return buffer.String(), err
81+
} else {
82+
return "", err
83+
}
84+
}

0 commit comments

Comments
 (0)