diff --git a/cmd/config/config.go b/cmd/config/config.go index ea37422..41eedb4 100644 --- a/cmd/config/config.go +++ b/cmd/config/config.go @@ -8,17 +8,17 @@ var ( Version = os.Getenv("VERSION") HttpAddress = os.Getenv("HTTP_ADDRESS") RpcAddress = os.Getenv("RPC_ADDRESS") - StoreAddress = os.Getenv("STORE_ADDRESS") - BrokerAddress = os.Getenv("BROKER_ADDRESS") + ServiceName = os.Getenv("SERVICE_NAME") + ServicePort = os.Getenv("SERVICE_PORT") + ServiceProtocol = os.Getenv("SERVICE_PROTOCOL") Store = os.Getenv("STORE") + StoreAddress = os.Getenv("STORE_ADDRESS") + DB = os.Getenv("DB") Stores = Split(os.Getenv("STORES")) Broker = os.Getenv("BROKER") + BrokerAddress = os.Getenv("BROKER_ADDRESS") Producers = Split(os.Getenv("PRODUCERS")) Consumers = Split(os.Getenv("CONSUMERS")) - ServiceName = os.Getenv("SERVICE_NAME") - ServicePort = os.Getenv("SERVICE_PORT") - ServiceProtocol = os.Getenv("SERVICE_PROTOCOL") - Memory = os.Getenv("MEMORY") AwsAccessKeyId = os.Getenv("AWS_ACCESS_KEY_ID") AwsSecretAccessKey = os.Getenv("AWS_SECRET_ACCESS_KEY") ) diff --git a/cmd/run.go b/cmd/run.go index e1f3e20..157601d 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -43,7 +43,7 @@ func run(ctx *cli.Context) { continue } - stores[s] = MakeStore(st, []string{config.StoreAddress}, config.ServiceName, s) + stores[s] = MakeStore(st, []string{config.StoreAddress}, config.DB, s) } bk, err := GetBrokerBuilder(config.Broker) @@ -64,7 +64,7 @@ func run(ctx *cli.Context) { continue } - brokers[s] = MakeConsumer(bk, []string{config.BrokerAddress}, s, len(config.Memory) > 0) + brokers[s] = MakeConsumer(bk, []string{config.BrokerAddress}, s, config.Broker == "memory") } // get services diff --git a/go.mod b/go.mod index d77bd76..08c8a8c 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,11 @@ module github.com/w-h-a/sidecar go 1.23.0 require ( + github.com/google/uuid v1.6.0 github.com/gorilla/mux v1.8.1 github.com/stretchr/testify v1.9.0 github.com/urfave/cli v1.22.15 - github.com/w-h-a/pkg v0.27.0 + github.com/w-h-a/pkg v0.28.0 google.golang.org/protobuf v1.34.2 ) @@ -39,7 +40,6 @@ require ( github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/gorilla/handlers v1.5.2 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect github.com/kr/pretty v0.3.1 // indirect diff --git a/go.sum b/go.sum index 338314d..0ab9102 100644 --- a/go.sum +++ b/go.sum @@ -124,8 +124,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/urfave/cli v1.22.15 h1:nuqt+pdC/KqswQKhETJjo7pvn/k4xMUxgW6liI7XpnM= github.com/urfave/cli v1.22.15/go.mod h1:wSan1hmo5zeyLGBjRJbzRTNk8gwoYa2B9n4q9dmRIc0= -github.com/w-h-a/pkg v0.27.0 h1:cDOlYt+b9pK4NAUWQvAVQosxLFjamHVjarg2U7O6aH4= -github.com/w-h-a/pkg v0.27.0/go.mod h1:S/BTwm6C36WyXsi+cSYMhPDvwk3y4K3LgUGz2GTIF/4= +github.com/w-h-a/pkg v0.28.0 h1:oM3FEdyPBq+X5f9vzeWPZxou+HQSZjxYY7E5p3+DcLw= +github.com/w-h-a/pkg v0.28.0/go.mod h1:S/BTwm6C36WyXsi+cSYMhPDvwk3y4K3LgUGz2GTIF/4= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= diff --git a/tests/e2e/helloworld/helloworld_test.go b/tests/e2e/helloworld/helloworld_test.go index b969a0c..625aa89 100644 --- a/tests/e2e/helloworld/helloworld_test.go +++ b/tests/e2e/helloworld/helloworld_test.go @@ -43,7 +43,7 @@ type Case struct { ExpectedRsp string } -type AppResponse struct { +type ServiceResponse struct { Message string `json:"message,omitempty"` StartTime int `json:"start_time,omitempty"` EndTime int `json:"end_time,omitempty"` @@ -69,21 +69,21 @@ func TestHelloWorld(t *testing.T) { for _, testCase := range helloTests { t.Run(testCase.Description, func(t *testing.T) { - _, err := httputils.HttpGet("http://localhost:3000") + _, err := httputils.HttpGet("http://localhost:3001") require.NoError(t, err) body, err := json.Marshal(TestCommandRequest{Message: "Hello!"}) require.NoError(t, err) - rsp, err := httputils.HttpPost(fmt.Sprintf("http://localhost:3000/tests/%s", testCase.TestCommand), body) + rsp, err := httputils.HttpPost(fmt.Sprintf("http://localhost:3001/tests/%s", testCase.TestCommand), body) require.NoError(t, err) - var appRsp AppResponse + var svcRsp ServiceResponse - err = json.Unmarshal(rsp, &appRsp) + err = json.Unmarshal(rsp, &svcRsp) require.NoError(t, err) - require.Equal(t, testCase.ExpectedRsp, appRsp.Message) + require.Equal(t, testCase.ExpectedRsp, svcRsp.Message) }) } } diff --git a/tests/e2e/helloworld/resources/docker-compose.yml b/tests/e2e/helloworld/resources/docker-compose.yml index f19f83c..857a0fe 100644 --- a/tests/e2e/helloworld/resources/docker-compose.yml +++ b/tests/e2e/helloworld/resources/docker-compose.yml @@ -7,9 +7,9 @@ services: build: dockerfile: Dockerfile ports: - - '3000:3000' + - '3001:3000' networks: - - sidecar + - helloworld helloworld-sidecar: container_name: helloworld-sidecar command: sidecar @@ -23,10 +23,10 @@ services: HTTP_ADDRESS: ':3501' RPC_ADDRESS: ':50001' SERVICE_NAME: 'localhost' - SERVICE_PORT: '3000' + SERVICE_PORT: '3001' SERVICE_PROTOCOL: 'http' STORE: 'memory' BROKER: 'memory' network_mode: 'service:helloworld' networks: - sidecar: + helloworld: diff --git a/tests/e2e/helloworld/resources/service.go b/tests/e2e/helloworld/resources/service.go index 1b3c005..9146e99 100644 --- a/tests/e2e/helloworld/resources/service.go +++ b/tests/e2e/helloworld/resources/service.go @@ -39,6 +39,8 @@ func indexHandler(w http.ResponseWriter, r *http.Request) { } func testHandler(w http.ResponseWriter, r *http.Request) { + log.Println("testHander has been called") + testCommand := mux.Vars(r)["test"] var commandBody TestCommandRequest diff --git a/tests/e2e/state/resources/Dockerfile b/tests/e2e/state/resources/Dockerfile new file mode 100644 index 0000000..a45baf8 --- /dev/null +++ b/tests/e2e/state/resources/Dockerfile @@ -0,0 +1,7 @@ +FROM golang +WORKDIR /service +COPY service.go . +RUN go mod init service +RUN go mod tidy +RUN CGO_ENABLED=0 go build -o /service ./ +CMD ["./service"] \ No newline at end of file diff --git a/tests/e2e/state/resources/docker-compose-db.yml b/tests/e2e/state/resources/docker-compose-db.yml new file mode 100644 index 0000000..8699a8d --- /dev/null +++ b/tests/e2e/state/resources/docker-compose-db.yml @@ -0,0 +1,13 @@ +services: + roach: + container_name: roach + command: start-single-node --advertise-addr 'localhost' --insecure + image: cockroachdb/cockroach:latest + ports: + - '26257:26257' + - '9000:8080' + networks: + - state-net +networks: + state-net: + name: state-net diff --git a/tests/e2e/state/resources/docker-compose.yml b/tests/e2e/state/resources/docker-compose.yml new file mode 100644 index 0000000..3157bf6 --- /dev/null +++ b/tests/e2e/state/resources/docker-compose.yml @@ -0,0 +1,38 @@ +services: + ############################ + # state service + sidecar + ############################ + state: + container_name: state + build: + dockerfile: Dockerfile + restart: on-failure:10 + ports: + - '3002:3000' + networks: + - state-net + state-sidecar: + container_name: state-sidecar + command: sidecar + build: + context: ../../../.. + dockerfile: Dockerfile + restart: on-failure:10 + environment: + NAMESPACE: 'state' + NAME: 'state-sidecar' + VERSION: '0.1.0-alpha.0' + HTTP_ADDRESS: ':3501' + RPC_ADDRESS: ':50001' + SERVICE_NAME: 'localhost' + SERVICE_PORT: '3002' + SERVICE_PROTOCOL: 'http' + STORE: 'cockroach' + STORE_ADDRESS: 'postgresql://root@roach:26257?sslmode=disable' + DB: 'test' + STORES: 'test' + BROKER: 'memory' + network_mode: 'service:state' +networks: + state-net: + external: true diff --git a/tests/e2e/state/resources/service.go b/tests/e2e/state/resources/service.go new file mode 100644 index 0000000..b08c892 --- /dev/null +++ b/tests/e2e/state/resources/service.go @@ -0,0 +1,172 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "time" + + "github.com/gorilla/mux" +) + +const servicePort = 3000 + +const stateURL = "http://localhost:3501/state" + +type RequestResponse struct { + StartTime int `json:"start_time,omitempty"` + EndTime int `json:"end_time,omitempty"` + States []SidecarState `json:"states,omitempty"` + Message string `json:"message,omitempty"` +} + +type SidecarState struct { + Key string `json:"key,omitempty"` + Value *ServiceState `json:"value,omitempty"` +} + +type ServiceState struct { + Data string `json:"data,omitempty"` +} + +func indexHandler(w http.ResponseWriter, r *http.Request) { + log.Println("indexHandler has been called") + + w.WriteHeader(http.StatusOK) +} + +func testHandler(w http.ResponseWriter, r *http.Request) { + log.Println("testHander has been called") + + var req RequestResponse + + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(RequestResponse{ + Message: err.Error(), + }) + return + } + + statusCode := http.StatusOK + + uri := r.URL.RequestURI() + + cmd := mux.Vars(r)["command"] + + rsp := RequestResponse{} + + var err error + + rsp.StartTime = int(time.Now().UnixMilli()) + + switch cmd { + case "create": + err = create(req.States) + case "list": + rsp.States, err = list() + case "delete": + err = delete(req.States) + default: + err = fmt.Errorf("invalid URI: %s", uri) + statusCode = http.StatusBadRequest + rsp.Message = err.Error() + } + + if err != nil && statusCode == http.StatusOK { + statusCode = http.StatusInternalServerError + rsp.Message = err.Error() + } + + rsp.EndTime = int(time.Now().UnixMilli()) + + w.Header().Set("content-type", "application/json") + w.WriteHeader(statusCode) + json.NewEncoder(w).Encode(rsp) +} + +func create(states []SidecarState) error { + log.Printf("processing create request for %d entries", len(states)) + + bs, err := json.Marshal(states) + if err != nil { + return err + } + + rsp, err := http.Post(fmt.Sprintf("%s/%s", stateURL, "test"), "application/json", bytes.NewBuffer(bs)) + if err != nil { + return err + } + + defer rsp.Body.Close() + + return nil +} + +func list() ([]SidecarState, error) { + log.Println("processing list request") + + rsp, err := http.Get(fmt.Sprintf("%s/%s", stateURL, "test")) + if err != nil { + return nil, err + } + + defer rsp.Body.Close() + + body, err := io.ReadAll(rsp.Body) + if err != nil { + return nil, err + } + + var states []SidecarState + + if err := json.Unmarshal(body, &states); err != nil { + return nil, err + } + + return states, nil +} + +func delete(states []SidecarState) error { + log.Printf("processing delete request for %d entries", len(states)) + + for _, state := range states { + url := fmt.Sprintf("%s/%s/%s", stateURL, "test", state.Key) + + req, err := http.NewRequest("DELETE", url, nil) + if err != nil { + return err + } + + client := &http.Client{} + + rsp, err := client.Do(req) + if err != nil { + return err + } + + rsp.Body.Close() + } + + return nil +} + +func appRouter() *mux.Router { + router := mux.NewRouter() + + router.HandleFunc("/", indexHandler).Methods("GET") + router.HandleFunc("/test/{command}", testHandler).Methods("POST") + + return router +} + +func main() { + log.Printf("state test service is listening on port %d", servicePort) + + if err := http.ListenAndServe(fmt.Sprintf(":%d", servicePort), appRouter()); err != nil { + log.Fatal(err) + } +} diff --git a/tests/e2e/state/state_test.go b/tests/e2e/state/state_test.go new file mode 100644 index 0000000..cb12f6c --- /dev/null +++ b/tests/e2e/state/state_test.go @@ -0,0 +1,269 @@ +package state + +import ( + "encoding/json" + "fmt" + "os" + "reflect" + "testing" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + "github.com/w-h-a/pkg/runner" + "github.com/w-h-a/pkg/runner/docker" + "github.com/w-h-a/pkg/telemetry/log" + "github.com/w-h-a/pkg/utils/httputils" +) + +const ( + numHealthChecks = 3 + externalURL = "http://localhost:3002" + manyEntriesCount = 5 +) + +func TestMain(m *testing.M) { + if len(os.Getenv("E2E")) == 0 { + os.Exit(0) + } + + dir, err := os.Getwd() + if err != nil { + log.Fatal(err) + } + + testFiles := []runner.File{ + { + Path: fmt.Sprintf("%s/resources/docker-compose-db.yml", dir), + }, + { + Path: fmt.Sprintf("%s/resources/docker-compose.yml", dir), + }, + } + + r := docker.NewTestRunner( + runner.RunnerWithId("state"), + runner.RunnerWithFiles(testFiles...), + ) + + os.Exit(r.Start(m)) +} + +type Case struct { + Name string + Steps []Step +} + +type Step struct { + Command string + Request RequestResponse + Expected RequestResponse +} + +type RequestResponse struct { + States []State `json:"states,omitempty"` +} + +type State struct { + Key string `json:"key,omitempty"` + Value *Value `json:"value,omitempty"` +} + +type Value struct { + Data string `json:"data,omitempty"` +} + +type SimpleKeyValue struct { + Key interface{} + Value interface{} +} + +func TestState(t *testing.T) { + _, err := httputils.HttpGetNTimes(externalURL, numHealthChecks) + require.NoError(t, err) + + testCases := generateTestCases() + + for _, testCase := range testCases { + t.Run(testCase.Name, func(t *testing.T) { + for _, step := range testCase.Steps { + body, err := json.Marshal(step.Request) + require.NoError(t, err) + + url := fmt.Sprintf("%s/test/%s", externalURL, step.Command) + + rsp, err := httputils.HttpPost(url, body) + require.NoError(t, err) + + var svcRsp RequestResponse + + err = json.Unmarshal(rsp, &svcRsp) + require.NoError(t, err) + + require.True(t, slicesEqual(step.Expected.States, svcRsp.States)) + + time.Sleep(1 * time.Second) + } + }) + } +} + +func generateTestCases() []Case { + testCaseSingleKey := uuid.New().String() + testCaseSingleValue := "The best song ever is 'Jockey Full of Bourbon' by Tom Waits" + + testCaseManyKeys := generateRandomStringKeys(manyEntriesCount) + testCaseManyKeyValues := generateRandomStringValues(testCaseManyKeys) + + return []Case{ + { + Name: "test empty create, list, and delete", + Steps: []Step{ + { + Command: "create", + Request: RequestResponse{}, + Expected: RequestResponse{}, + }, + { + Command: "list", + Request: RequestResponse{}, + Expected: RequestResponse{}, + }, + { + Command: "delete", + Request: RequestResponse{}, + Expected: RequestResponse{}, + }, + }, + }, + { + Name: "test single-item create, list, and delete", + Steps: []Step{ + { + Command: "create", + Request: newRequest(SimpleKeyValue{testCaseSingleKey, testCaseSingleValue}), + Expected: RequestResponse{}, + }, + { + Command: "list", + Request: RequestResponse{}, + Expected: newResponse(SimpleKeyValue{testCaseSingleKey, testCaseSingleValue}), + }, + { + Command: "delete", + Request: newRequest(SimpleKeyValue{testCaseSingleKey, nil}), + Expected: RequestResponse{}, + }, + { + Command: "list", + Request: RequestResponse{}, + Expected: RequestResponse{}, + }, + }, + }, + { + Name: "test many-item create, list, and delete", + Steps: []Step{ + { + Command: "create", + Request: newRequest(testCaseManyKeyValues...), + Expected: RequestResponse{}, + }, + { + Command: "list", + Request: RequestResponse{}, + Expected: newResponse(testCaseManyKeyValues...), + }, + { + Command: "delete", + Request: newRequest(testCaseManyKeys...), + Expected: RequestResponse{}, + }, + { + Command: "list", + Request: RequestResponse{}, + Expected: RequestResponse{}, + }, + }, + }, + } +} + +func newRequest(kvs ...SimpleKeyValue) RequestResponse { + return newRequestResponse(kvs...) +} + +func newResponse(kvs ...SimpleKeyValue) RequestResponse { + return newRequestResponse(kvs...) +} + +func newRequestResponse(kvs ...SimpleKeyValue) RequestResponse { + states := make([]State, 0, len(kvs)) + + for _, kv := range kvs { + states = append(states, generateState(kv)) + } + + return RequestResponse{ + States: states, + } +} + +func generateState(kv SimpleKeyValue) State { + if kv.Key == nil { + return State{} + } + + key := fmt.Sprintf("%v", kv.Key) + + if kv.Value == nil { + return State{key, nil} + } + + value := fmt.Sprintf("%v", kv.Value) + + return State{key, &Value{value}} +} + +func generateRandomStringKeys(num int) []SimpleKeyValue { + if num < 0 { + return []SimpleKeyValue{} + } + + output := make([]SimpleKeyValue, 0, num) + + for i := 1; i <= num; i++ { + key := uuid.New().String() + output = append(output, SimpleKeyValue{key, nil}) + } + + return output +} + +func generateRandomStringValues(kvs []SimpleKeyValue) []SimpleKeyValue { + output := make([]SimpleKeyValue, 0, len(kvs)) + + for i, kv := range kvs { + key := kv.Key + value := fmt.Sprintf("value for entry #%d with key %v", i+1, key) + output = append(output, SimpleKeyValue{key, value}) + } + + return output +} + +func slicesEqual(want, got []State) bool { + w := map[string]*Value{} + + g := map[string]*Value{} + + for _, pair := range want { + w[pair.Key] = pair.Value + } + + for _, pair := range got { + g[pair.Key] = pair.Value + } + + return reflect.DeepEqual(w, g) +}