Skip to content

Commit db2c967

Browse files
committed
Introduce serve command
See #53.
1 parent 65368b9 commit db2c967

File tree

4 files changed

+213
-0
lines changed

4 files changed

+213
-0
lines changed

internal/commands/root.go

+1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ func NewRootCmd() *cobra.Command {
3737
cmd.AddCommand(
3838
newValidateCmd(),
3939
newRunCmd(),
40+
newServeCmd(),
4041
)
4142

4243
return cmd

internal/commands/serve.go

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package commands
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"github.com/cirruslabs/cirrus-cli/internal/evaluator"
7+
"github.com/spf13/cobra"
8+
"net"
9+
)
10+
11+
var ErrServe = errors.New("serve failed")
12+
13+
var address string
14+
15+
func serve(cmd *cobra.Command, args []string) error {
16+
// https://github.com/spf13/cobra/issues/340#issuecomment-374617413
17+
cmd.SilenceUsage = true
18+
19+
lis, err := net.Listen("tcp", address)
20+
if err != nil {
21+
return fmt.Errorf("%w: %v", ErrServe, err)
22+
}
23+
24+
fmt.Printf("listening on %s\n", lis.Addr().String())
25+
26+
if err := evaluator.Serve(cmd.Context(), lis); err != nil {
27+
return fmt.Errorf("%w: %v", ErrServe, err)
28+
}
29+
30+
return nil
31+
}
32+
33+
func newServeCmd() *cobra.Command {
34+
cmd := &cobra.Command{
35+
Use: "serve [flags]",
36+
Short: "Run RPC server that evaluates YAML and Starlark configurations",
37+
RunE: serve,
38+
Hidden: true,
39+
}
40+
41+
cmd.PersistentFlags().StringVarP(&address, "listen", "l", ":8080", "address to listen on")
42+
43+
return cmd
44+
}

internal/evaluator/evaluator.go

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package evaluator
2+
3+
import (
4+
"context"
5+
"github.com/cirruslabs/cirrus-ci-agent/api"
6+
"github.com/cirruslabs/cirrus-cli/pkg/larker"
7+
"github.com/cirruslabs/cirrus-cli/pkg/parser"
8+
"google.golang.org/grpc"
9+
"google.golang.org/grpc/codes"
10+
"google.golang.org/grpc/status"
11+
"net"
12+
"strings"
13+
)
14+
15+
func Serve(ctx context.Context, lis net.Listener) error {
16+
server := grpc.NewServer()
17+
18+
api.RegisterCirrusConfigurationEvaluatorServiceService(server, &api.CirrusConfigurationEvaluatorServiceService{
19+
EvaluateConfig: evaluateConfig,
20+
})
21+
22+
errChan := make(chan error)
23+
24+
go func() {
25+
errChan <- server.Serve(lis)
26+
}()
27+
28+
select {
29+
case <-ctx.Done():
30+
server.GracefulStop()
31+
32+
if err := <-errChan; err != nil {
33+
return err
34+
}
35+
36+
return ctx.Err()
37+
case err := <-errChan:
38+
return err
39+
}
40+
}
41+
42+
func evaluateConfig(ctx context.Context, request *api.EvaluateConfigRequest) (*api.EvaluateConfigResponse, error) {
43+
var yamlConfigs []string
44+
45+
// Register YAML configuration (if any)
46+
if request.YamlConfig != "" {
47+
yamlConfigs = append(yamlConfigs, request.YamlConfig)
48+
}
49+
50+
// Run Starlark script and register generated YAML configuration (if any)
51+
if request.StarlarkConfig != "" {
52+
lrk := larker.New(larker.WithEnvironment(request.Environment))
53+
54+
generatedYamlConfig, err := lrk.Main(ctx, request.StarlarkConfig)
55+
if err != nil {
56+
return nil, status.Error(codes.InvalidArgument, err.Error())
57+
}
58+
59+
yamlConfigs = append(yamlConfigs, generatedYamlConfig)
60+
}
61+
62+
// Parse combined YAML
63+
p := parser.New(
64+
parser.WithEnvironment(request.Environment),
65+
parser.WithFilesContents(request.FilesContents),
66+
)
67+
68+
result, err := p.Parse(strings.Join(yamlConfigs, "\n"))
69+
if err != nil {
70+
return nil, status.Error(codes.InvalidArgument, err.Error())
71+
}
72+
73+
if len(result.Errors) != 0 {
74+
return nil, status.Error(codes.InvalidArgument, result.Errors[0])
75+
}
76+
77+
return &api.EvaluateConfigResponse{Tasks: result.Tasks}, nil
78+
}

internal/evaluator/evaluator_test.go

+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package evaluator_test
2+
3+
import (
4+
"context"
5+
"errors"
6+
"github.com/cirruslabs/cirrus-ci-agent/api"
7+
"github.com/cirruslabs/cirrus-cli/internal/evaluator"
8+
"github.com/stretchr/testify/require"
9+
"google.golang.org/grpc"
10+
"net"
11+
"testing"
12+
)
13+
14+
func evaluateHelper(t *testing.T, request *api.EvaluateConfigRequest) (*api.EvaluateConfigResponse, error) {
15+
ctx, cancel := context.WithCancel(context.Background())
16+
17+
lis, err := net.Listen("tcp", "localhost:0")
18+
if err != nil {
19+
t.Fatal(err)
20+
}
21+
22+
errChan := make(chan error)
23+
24+
go func() {
25+
errChan <- evaluator.Serve(ctx, lis)
26+
}()
27+
28+
defer func() {
29+
cancel()
30+
31+
if err := <-errChan; err != nil && !errors.Is(err, context.Canceled) {
32+
t.Fatal(err)
33+
}
34+
}()
35+
36+
conn, err := grpc.Dial(lis.Addr().String(), grpc.WithInsecure())
37+
if err != nil {
38+
t.Fatal(err)
39+
}
40+
41+
client := api.NewCirrusConfigurationEvaluatorServiceClient(conn)
42+
43+
response, err := client.EvaluateConfig(context.Background(), request)
44+
if err != nil {
45+
return nil, err
46+
}
47+
48+
return response, nil
49+
}
50+
51+
// TestCrossDependencies ensures that tasks declared in YAML and generated from Starlark can reference each other.
52+
func TestCrossDependencies(t *testing.T) {
53+
yamlConfig := `
54+
container:
55+
image: debian:latest
56+
57+
task:
58+
name: Black
59+
depends_on:
60+
- White
61+
script: true
62+
63+
task:
64+
name: Green
65+
script: true
66+
`
67+
68+
starlarkConfig := `
69+
def main(ctx):
70+
return [
71+
{
72+
"name": "White",
73+
"script": "true",
74+
},
75+
{
76+
"name": "Yellow",
77+
"depends_on": [
78+
"Green",
79+
],
80+
"script": "true",
81+
}
82+
]
83+
`
84+
85+
_, err := evaluateHelper(t, &api.EvaluateConfigRequest{
86+
YamlConfig: yamlConfig,
87+
StarlarkConfig: starlarkConfig,
88+
})
89+
require.NoError(t, err)
90+
}

0 commit comments

Comments
 (0)