Skip to content

Commit e08c204

Browse files
authored
feat: add a service commnd (#39)
* feat: add a service commnd The following command will install atest as a Linux service. `atest service --action install`. Set the default service port to be 7070 to avoid confliction * add unit tests * add more unit tests
1 parent 9ee38ff commit e08c204

16 files changed

+248
-36
lines changed

cmd/init.go

+6-5
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
11
package cmd
22

33
import (
4-
"github.com/linuxsuren/api-testing/pkg/exec"
4+
fakeruntime "github.com/linuxsuren/go-fake-runtime"
55
"github.com/spf13/cobra"
66
)
77

88
type initOption struct {
9+
execer fakeruntime.Execer
910
kustomization string
1011
waitNamespace string
1112
waitResource string
1213
}
1314

1415
// createInitCommand returns the init command
15-
func createInitCommand() (cmd *cobra.Command) {
16-
opt := &initOption{}
16+
func createInitCommand(execer fakeruntime.Execer) (cmd *cobra.Command) {
17+
opt := &initOption{execer: execer}
1718
cmd = &cobra.Command{
1819
Use: "init",
1920
Long: "Support to init Kubernetes cluster with kustomization, and wait it with command: kubectl wait",
@@ -30,13 +31,13 @@ func createInitCommand() (cmd *cobra.Command) {
3031

3132
func (o *initOption) runE(cmd *cobra.Command, args []string) (err error) {
3233
if o.kustomization != "" {
33-
if err = exec.RunCommand("kubectl", "apply", "-k", o.kustomization, "--wait=true"); err != nil {
34+
if err = o.execer.RunCommand("kubectl", "apply", "-k", o.kustomization, "--wait=true"); err != nil {
3435
return
3536
}
3637
}
3738

3839
if o.waitNamespace != "" && o.waitResource != "" {
39-
if err = exec.RunCommand("kubectl", "wait", "-n", o.waitNamespace, o.waitResource, "--for", "condition=Available=True", "--timeout=900s"); err != nil {
40+
if err = o.execer.RunCommand("kubectl", "wait", "-n", o.waitNamespace, o.waitResource, "--for", "condition=Available=True", "--timeout=900s"); err != nil {
4041
return
4142
}
4243
}

cmd/jsonschema_test.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@ import (
66
"testing"
77

88
"github.com/linuxsuren/api-testing/cmd"
9+
fakeruntime "github.com/linuxsuren/go-fake-runtime"
910
"github.com/stretchr/testify/assert"
1011
)
1112

1213
func TestJSONSchemaCmd(t *testing.T) {
13-
c := cmd.NewRootCmd()
14+
c := cmd.NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"})
1415

1516
buf := new(bytes.Buffer)
1617
c.SetOut(buf)

cmd/root.go

+7-3
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,23 @@ import (
44
"os"
55

66
"github.com/linuxsuren/api-testing/pkg/version"
7+
fakeruntime "github.com/linuxsuren/go-fake-runtime"
78
"github.com/spf13/cobra"
9+
"google.golang.org/grpc"
810
)
911

1012
// NewRootCmd creates the root command
11-
func NewRootCmd() (c *cobra.Command) {
13+
func NewRootCmd(execer fakeruntime.Execer) (c *cobra.Command) {
1214
c = &cobra.Command{
1315
Use: "atest",
1416
Short: "API testing tool",
1517
}
1618
c.SetOut(os.Stdout)
1719
c.Version = version.GetVersion()
18-
c.AddCommand(createInitCommand(),
20+
gRPCServer := grpc.NewServer()
21+
c.AddCommand(createInitCommand(execer),
1922
createRunCommand(), createSampleCmd(),
20-
createServerCmd(), createJSONSchemaCmd())
23+
createServerCmd(gRPCServer), createJSONSchemaCmd(),
24+
createServiceCommand(execer))
2125
return
2226
}

cmd/root_test.go

+8-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"github.com/stretchr/testify/assert"
77

88
atesting "github.com/linuxsuren/api-testing/pkg/testing"
9+
exec "github.com/linuxsuren/go-fake-runtime"
910
)
1011

1112
func Test_setRelativeDir(t *testing.T) {
@@ -43,10 +44,15 @@ func TestCreateRunCommand(t *testing.T) {
4344
cmd := createRunCommand()
4445
assert.Equal(t, "run", cmd.Use)
4546

46-
init := createInitCommand()
47+
init := createInitCommand(exec.FakeExecer{})
4748
assert.Equal(t, "init", init.Use)
4849

49-
server := createServerCmd()
50+
server := createServerCmd(&fakeGRPCServer{})
5051
assert.NotNil(t, server)
5152
assert.Equal(t, "server", server.Use)
53+
54+
root := NewRootCmd(exec.FakeExecer{})
55+
root.SetArgs([]string{"init", "-k=demo.yaml", "--wait-namespace", "demo", "--wait-resource", "demo"})
56+
err := root.Execute()
57+
assert.Nil(t, err)
5258
}

cmd/run_test.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88

99
"github.com/h2non/gock"
1010
"github.com/linuxsuren/api-testing/pkg/limit"
11+
fakeruntime "github.com/linuxsuren/go-fake-runtime"
1112
"github.com/spf13/cobra"
1213
"github.com/stretchr/testify/assert"
1314
)
@@ -103,7 +104,7 @@ func TestRunCommand(t *testing.T) {
103104
}
104105

105106
func TestRootCmd(t *testing.T) {
106-
c := NewRootCmd()
107+
c := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"})
107108
assert.NotNil(t, c)
108109
assert.Equal(t, "atest", c.Use)
109110
}

cmd/sample_test.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@ import (
66

77
"github.com/linuxsuren/api-testing/cmd"
88
"github.com/linuxsuren/api-testing/sample"
9+
fakeruntime "github.com/linuxsuren/go-fake-runtime"
910
"github.com/stretchr/testify/assert"
1011
)
1112

1213
func TestSampleCmd(t *testing.T) {
13-
c := cmd.NewRootCmd()
14+
c := cmd.NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"})
1415

1516
buf := new(bytes.Buffer)
1617
c.SetOut(buf)

cmd/server.go

+22-4
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,21 @@ import (
1111
"google.golang.org/grpc"
1212
)
1313

14-
func createServerCmd() (c *cobra.Command) {
15-
opt := &serverOption{}
14+
func createServerCmd(gRPCServer gRPCServer) (c *cobra.Command) {
15+
opt := &serverOption{gRPCServer: gRPCServer}
1616
c = &cobra.Command{
1717
Use: "server",
1818
Short: "Run as a server mode",
1919
RunE: opt.runE,
2020
}
2121
flags := c.Flags()
22-
flags.IntVarP(&opt.port, "port", "p", 9090, "The RPC server port")
22+
flags.IntVarP(&opt.port, "port", "p", 7070, "The RPC server port")
2323
flags.BoolVarP(&opt.printProto, "print-proto", "", false, "Print the proto content and exit")
2424
return
2525
}
2626

2727
type serverOption struct {
28+
gRPCServer gRPCServer
2829
port int
2930
printProto bool
3031
}
@@ -43,9 +44,26 @@ func (o *serverOption) runE(cmd *cobra.Command, args []string) (err error) {
4344
return
4445
}
4546

46-
s := grpc.NewServer()
47+
s := o.gRPCServer
4748
server.RegisterRunnerServer(s, server.NewRemoteServer())
4849
log.Printf("server listening at %v", lis.Addr())
4950
s.Serve(lis)
5051
return
5152
}
53+
54+
type gRPCServer interface {
55+
Serve(lis net.Listener) error
56+
grpc.ServiceRegistrar
57+
}
58+
59+
type fakeGRPCServer struct {
60+
}
61+
62+
// Serve is a fake method
63+
func (s *fakeGRPCServer) Serve(net.Listener) error {
64+
return nil
65+
}
66+
67+
// RegisterService is a fake method
68+
func (s *fakeGRPCServer) RegisterService(desc *grpc.ServiceDesc, impl interface{}) {
69+
}

cmd/server_test.go

+6-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"strings"
66
"testing"
77

8+
fakeruntime "github.com/linuxsuren/go-fake-runtime"
89
"github.com/stretchr/testify/assert"
910
)
1011

@@ -30,11 +31,15 @@ func TestPrintProto(t *testing.T) {
3031
for _, tt := range tests {
3132
t.Run(tt.name, func(t *testing.T) {
3233
buf := new(bytes.Buffer)
33-
root := NewRootCmd()
34+
root := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"})
3435
root.SetOut(buf)
3536
root.SetArgs(tt.args)
3637
err := root.Execute()
3738
tt.verify(t, buf, err)
3839
})
3940
}
41+
42+
server := createServerCmd(&fakeGRPCServer{})
43+
err := server.Execute()
44+
assert.Nil(t, err)
4045
}

cmd/service.go

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Package cmd provides a service command
2+
package cmd
3+
4+
import (
5+
"fmt"
6+
"os"
7+
8+
fakeruntime "github.com/linuxsuren/go-fake-runtime"
9+
"github.com/spf13/cobra"
10+
)
11+
12+
func createServiceCommand(execer fakeruntime.Execer) (c *cobra.Command) {
13+
opt := &serviceOption{
14+
Execer: execer,
15+
}
16+
c = &cobra.Command{
17+
Use: "service",
18+
Aliases: []string{"s"},
19+
Short: "Install atest as a Linux service",
20+
PreRunE: opt.preRunE,
21+
RunE: opt.runE,
22+
}
23+
flags := c.Flags()
24+
flags.StringVarP(&opt.action, "action", "a", "", "The action of service, support actions: install, start, stop, restart, status")
25+
flags.StringVarP(&opt.scriptPath, "script-path", "", "/lib/systemd/system/atest.service", "The service script file path")
26+
return
27+
}
28+
29+
type serviceOption struct {
30+
action string
31+
scriptPath string
32+
fakeruntime.Execer
33+
}
34+
35+
func (o *serviceOption) preRunE(c *cobra.Command, args []string) (err error) {
36+
if o.Execer.OS() != "linux" {
37+
err = fmt.Errorf("only support on Linux")
38+
}
39+
if o.action == "" && len(args) > 0 {
40+
o.action = args[0]
41+
}
42+
return
43+
}
44+
45+
func (o *serviceOption) runE(c *cobra.Command, args []string) (err error) {
46+
var output string
47+
switch o.action {
48+
case "install", "i":
49+
err = os.WriteFile(o.scriptPath, []byte(script), os.ModeAppend)
50+
case "start":
51+
output, err = o.Execer.RunCommandAndReturn("systemctl", "", "start", "atest")
52+
case "stop":
53+
output, err = o.Execer.RunCommandAndReturn("systemctl", "", "stop", "atest")
54+
case "restart":
55+
output, err = o.Execer.RunCommandAndReturn("systemctl", "", "restart", "atest")
56+
case "status":
57+
output, err = o.Execer.RunCommandAndReturn("systemctl", "", "status", "atest")
58+
default:
59+
err = fmt.Errorf("not support action: '%s'", o.action)
60+
}
61+
62+
if output != "" {
63+
c.Println(output)
64+
}
65+
return
66+
}
67+
68+
var script = `[Unit]
69+
Description=API Testing
70+
71+
[Service]
72+
ExecStart=atest server
73+
74+
[Install]
75+
WantedBy=multi-user.target
76+
`

cmd/service_test.go

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package cmd
2+
3+
import (
4+
"bytes"
5+
"os"
6+
"testing"
7+
8+
fakeruntime "github.com/linuxsuren/go-fake-runtime"
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func TestService(t *testing.T) {
13+
root := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"})
14+
root.SetArgs([]string{"service", "fake"})
15+
err := root.Execute()
16+
assert.NotNil(t, err)
17+
18+
notLinux := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "fake"})
19+
notLinux.SetArgs([]string{"service", "--action", "install"})
20+
err = notLinux.Execute()
21+
assert.NotNil(t, err)
22+
23+
tmpFile, err := os.CreateTemp(os.TempDir(), "service")
24+
assert.Nil(t, err)
25+
defer func() {
26+
os.RemoveAll(tmpFile.Name())
27+
}()
28+
29+
targetScript := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"})
30+
targetScript.SetArgs([]string{"service", "--action", "install", "--script-path", tmpFile.Name()})
31+
err = targetScript.Execute()
32+
assert.Nil(t, err)
33+
data, err := os.ReadFile(tmpFile.Name())
34+
assert.Nil(t, err)
35+
assert.Equal(t, script, string(data))
36+
37+
tests := []struct {
38+
name string
39+
action string
40+
expectOutput string
41+
}{{
42+
name: "action: start",
43+
action: "start",
44+
expectOutput: "output1",
45+
}, {
46+
name: "action: stop",
47+
action: "stop",
48+
expectOutput: "output2",
49+
}, {
50+
name: "action: restart",
51+
action: "restart",
52+
expectOutput: "output3",
53+
}, {
54+
name: "action: status",
55+
action: "status",
56+
expectOutput: "output4",
57+
}}
58+
for _, tt := range tests {
59+
t.Run(tt.name, func(t *testing.T) {
60+
buf := new(bytes.Buffer)
61+
normalRoot := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux", ExpectOutput: tt.expectOutput})
62+
normalRoot.SetOut(buf)
63+
normalRoot.SetArgs([]string{"service", "--action", tt.action})
64+
err = normalRoot.Execute()
65+
assert.Nil(t, err)
66+
assert.Equal(t, tt.expectOutput+"\n", buf.String())
67+
})
68+
}
69+
}

go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ require (
2828
github.com/imdario/mergo v0.3.11 // indirect
2929
github.com/inconshreveable/mousetrap v1.0.1 // indirect
3030
github.com/invopop/jsonschema v0.7.0 // indirect
31+
github.com/linuxsuren/go-fake-runtime v0.0.0-20230413085645-15e77ab55dbd // indirect
3132
github.com/mitchellh/copystructure v1.0.0 // indirect
3233
github.com/mitchellh/reflectwalk v1.0.0 // indirect
3334
github.com/pmezard/go-difflib v1.0.0 // indirect

go.sum

+2
Original file line numberDiff line numberDiff line change
@@ -563,6 +563,8 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
563563
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
564564
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
565565
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
566+
github.com/linuxsuren/go-fake-runtime v0.0.0-20230413085645-15e77ab55dbd h1:2Avir30WOgcDqG3sA4hlW4bC4c/tgseAUntPhf5JQ6E=
567+
github.com/linuxsuren/go-fake-runtime v0.0.0-20230413085645-15e77ab55dbd/go.mod h1:zmh6J78hSnWZo68faMA2eKOdaEp8eFbERHi3ZB9xHCQ=
566568
github.com/linuxsuren/unstructured v0.0.1 h1:ilUA8MUYbR6l9ebo/YPV2bKqlf62bzQursDSE+j00iU=
567569
github.com/linuxsuren/unstructured v0.0.1/go.mod h1:KH6aTj+FegzGBzc1vS6mzZx3/duhTUTEVyW5sO7p4as=
568570
github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA=

main.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ import (
44
"os"
55

66
"github.com/linuxsuren/api-testing/cmd"
7+
exec "github.com/linuxsuren/go-fake-runtime"
78
)
89

910
func main() {
10-
c := cmd.NewRootCmd()
11+
c := cmd.NewRootCmd(exec.DefaultExecer{})
1112
if err := c.Execute(); err != nil {
1213
os.Exit(1)
1314
}

0 commit comments

Comments
 (0)