diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e053135..51da689 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -4,6 +4,7 @@ on: branches: [ main ] pull_request: branches: [] + workflow_dispatch: jobs: build: runs-on: ubuntu-latest @@ -74,3 +75,5 @@ jobs: output-file-path: ./gen/python/grpc/output.json external-data-json-path: ./cache/benchmark-data.json fail-on-alert: true + upi-echo-server: + uses: ./.github/workflows/publish-upi-echo-server.yaml diff --git a/.github/workflows/publish-upi-echo-server.yaml b/.github/workflows/publish-upi-echo-server.yaml new file mode 100644 index 0000000..b823b03 --- /dev/null +++ b/.github/workflows/publish-upi-echo-server.yaml @@ -0,0 +1,96 @@ +name: upi-echo-server +on: + workflow_call: + workflow_dispatch: + +env: + DOCKER_REGISTRY: ghcr.io + ARTIFACT_RETENTION_DAYS: 1 + GO_VERSION: 1.18 + +jobs: + create-version: + runs-on: ubuntu-latest + outputs: + version: ${{ steps.create_version.outputs.version }} + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - id: create_version + name: Create version string + run: | + # Strip git ref prefix from version + VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,') + [ "$VERSION" == "main" ] && VERSION=$(git describe --tags --always --first-parent) + # Strip "v" prefix + [[ "${VERSION}" == "v"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//') + # If it's pull request the version string is prefixed by 0.0.0- + [ ${{ github.event_name}} == "pull_request" ] && VERSION="0.0.0-${{ github.event.pull_request.head.sha }}" + echo ${VERSION} + echo "::set-output name=version::${VERSION}" + + build: + runs-on: ubuntu-latest + needs: + - create-version + steps: + - uses: actions/checkout@v2 + - name: Build Docker image + run: docker build -t upi-echo-server:${{ needs.create-version.outputs.version }} -f example/go/simple/Dockerfile . + - name: Save Docker image + run: docker image save --output upi-echo-server.${{ needs.create-version.outputs.version }}.tar upi-echo-server:${{ needs.create-version.outputs.version }} + - name: Publish Docker Artifact + uses: actions/upload-artifact@v2 + with: + name: upi-echo-server.${{ needs.create-version.outputs.version }}.tar + path: upi-echo-server.${{ needs.create-version.outputs.version }}.tar + retention-days: ${{ env.ARTIFACT_RETENTION_DAYS }} + + test: + runs-on: ubuntu-latest + defaults: + run: + working-directory: example/go/simple + env: + GOPATH: ${{ github.workspace }}/example/go/simple/.go + steps: + - name: Check out code + uses: actions/checkout@v2 + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: ${{ env.GO_VERSION }} + - name: Test + run: | + go mod tidy + go test ./... -v + + release: + if: github.event_name != 'pull_request' + runs-on: ubuntu-latest + needs: + - create-version + - build + - test + steps: + - name: Log in to the Container registry + uses: docker/login-action@v1 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Download API Docker Artifact + uses: actions/download-artifact@v2 + with: + name: upi-echo-server.${{ needs.create-version.outputs.version }}.tar + + - name: Publish Docker Image + env: + CONTAINER_REGISTRY: ${{ env.DOCKER_REGISTRY }}/${{ github.repository_owner }} + run: | + IMAGE_TAG="${{ env.CONTAINER_REGISTRY }}/upi-echo-server:${{ needs.create-version.outputs.version }}" + docker image load --input upi-echo-server.${{ needs.create-version.outputs.version }}.tar + docker tag upi-echo-server:${{ needs.create-version.outputs.version }} ${IMAGE_TAG} + docker push ${IMAGE_TAG} diff --git a/example/README.md b/example/README.md new file mode 100644 index 0000000..d651393 --- /dev/null +++ b/example/README.md @@ -0,0 +1,14 @@ +# Universal Prediction Interface Example + +## Go Example +### UPI Echo Server +Server will return a dummy metadata and response table with request table if any. + + . + ├── go/simple + │ ├── client + │ │ └── main.go + │ └── server + │ ├── server.go + │ └── main.go + └── README.md \ No newline at end of file diff --git a/example/go/simple/Dockerfile b/example/go/simple/Dockerfile new file mode 100644 index 0000000..043cc84 --- /dev/null +++ b/example/go/simple/Dockerfile @@ -0,0 +1,18 @@ +FROM golang:1.18-alpine as build + +WORKDIR /app + +COPY example/go/simple ./ +RUN go mod download +RUN go build -o /upi-echo-server ./server + +# Build the application image +FROM alpine:latest + +RUN addgroup -S app && adduser -S app -G app +WORKDIR /app +RUN chown -R app:app /app + +COPY --chown=app:app --from=build /upi-echo-server /upi-echo-server +USER app +ENTRYPOINT ["/upi-echo-server"] \ No newline at end of file diff --git a/example/go/simple/client/main.go b/example/go/simple/client/main.go new file mode 100644 index 0000000..433b8bb --- /dev/null +++ b/example/go/simple/client/main.go @@ -0,0 +1,54 @@ +package main + +import ( + "context" + "flag" + "log" + "time" + + upiv1 "github.com/caraml-dev/universal-prediction-interface/gen/go/grpc/caraml/upi/v1" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +var ( + addr = flag.String("addr", "localhost:50051", "the address to connect to") +) + +func main() { + flag.Parse() + // Set up a connection to the server. + conn, err := grpc.Dial(*addr, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + log.Fatalf("did not connect: %v", err) + } + defer conn.Close() + c := upiv1.NewUniversalPredictionServiceClient(conn) + + // Contact the server and print out its response. + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + r, err := c.PredictValues(ctx, &upiv1.PredictValuesRequest{ + PredictionTable: &upiv1.Table{ + Name: "Test", + Columns: []*upiv1.Column{ + { + Name: "col1", + Type: upiv1.Type_TYPE_DOUBLE, + }, + }, + Rows: []*upiv1.Row{ + { + RowId: "1", + Values: []*upiv1.Value{ + {}, + }, + }, + }, + }, + }) + if err != nil { + log.Fatalf("could not call upi server: %v", err) + } + log.Printf("Response: %s", r.String()) +} diff --git a/example/go/simple/go.mod b/example/go/simple/go.mod new file mode 100644 index 0000000..0cbd292 --- /dev/null +++ b/example/go/simple/go.mod @@ -0,0 +1,18 @@ +module github.com/caraml-dev/universal-prediction-interface/example/simple + +go 1.18 + +require ( + github.com/caraml-dev/universal-prediction-interface v0.0.0-20220913065109-8170722e7551 + google.golang.org/grpc v1.49.0 +) + +require ( + github.com/golang/protobuf v1.5.2 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.2 // indirect + golang.org/x/net v0.0.0-20220809184613-07c6da5e1ced // indirect + golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // indirect + golang.org/x/text v0.3.7 // indirect + google.golang.org/genproto v0.0.0-20220810155839-1856144b1d9c // indirect + google.golang.org/protobuf v1.28.1 // indirect +) diff --git a/example/go/simple/go.sum b/example/go/simple/go.sum new file mode 100644 index 0000000..15fc643 --- /dev/null +++ b/example/go/simple/go.sum @@ -0,0 +1,25 @@ +github.com/caraml-dev/universal-prediction-interface v0.0.0-20220913065109-8170722e7551 h1:nK41mSx5EZWigXbqDropyQLUegcYlV7CUGXjwBYRj0w= +github.com/caraml-dev/universal-prediction-interface v0.0.0-20220913065109-8170722e7551/go.mod h1:dVNvxu+vducr8/hF2g4HUJ51dwV+fJhUd/TizJYWiO4= +github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.2 h1:BqHID5W5qnMkug0Z8UmL8tN0gAy4jQ+B4WFt8cCgluU= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.2/go.mod h1:ZbS3MZTZq/apAfAEHGoB5HbsQQstoqP92SjAqtQ9zeg= +golang.org/x/net v0.0.0-20220809184613-07c6da5e1ced h1:3dYNDff0VT5xj+mbj2XucFst9WKk6PdGOrb9n+SbIvw= +golang.org/x/net v0.0.0-20220809184613-07c6da5e1ced/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto v0.0.0-20220810155839-1856144b1d9c h1:IooGDWedfLC6KLczH/uduUsKQP42ZZYhKx+zd50L1Sk= +google.golang.org/genproto v0.0.0-20220810155839-1856144b1d9c/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/grpc v1.49.0 h1:WTLtQzmQori5FUH25Pq4WT22oCsv8USpQ+F6rqtsmxw= +google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= diff --git a/example/go/simple/server/main.go b/example/go/simple/server/main.go new file mode 100644 index 0000000..294ff4a --- /dev/null +++ b/example/go/simple/server/main.go @@ -0,0 +1,16 @@ +package main + +import ( + "flag" + "fmt" +) + +var ( + port = flag.Int("port", 50051, "The server port") +) + +func main() { + flag.Parse() + upiServer := UpiServer{} + upiServer.Run(fmt.Sprintf(":%d", *port)) +} diff --git a/example/go/simple/server/server.go b/example/go/simple/server/server.go new file mode 100644 index 0000000..4bc6466 --- /dev/null +++ b/example/go/simple/server/server.go @@ -0,0 +1,46 @@ +package main + +import ( + "context" + "log" + "net" + + upiv1 "github.com/caraml-dev/universal-prediction-interface/gen/go/grpc/caraml/upi/v1" + "google.golang.org/grpc" + "google.golang.org/grpc/reflection" +) + +type UpiServer struct { + upiv1.UnimplementedUniversalPredictionServiceServer +} + +func (_ *UpiServer) PredictValues( + _ context.Context, + req *upiv1.PredictValuesRequest, +) (*upiv1.PredictValuesResponse, error) { + return &upiv1.PredictValuesResponse{ + Metadata: &upiv1.ResponseMetadata{ + Models: []*upiv1.ModelMetadata{ + { + Name: "Echo Request Table", + Version: "1", + }, + }, + }, + PredictionResultTable: req.GetPredictionTable(), + }, nil +} + +func (us *UpiServer) Run(address string) { + lis, err := net.Listen("tcp", address) + if err != nil { + log.Fatalf("failed to listen: %v", err) + } + s := grpc.NewServer() + upiv1.RegisterUniversalPredictionServiceServer(s, us) + reflection.Register(s) + log.Printf("listening on port %s", address) + if err := s.Serve(lis); err != nil { + log.Fatalf("failed to serve: %v", err) + } +} diff --git a/example/go/simple/server/server_test.go b/example/go/simple/server/server_test.go new file mode 100644 index 0000000..14ce800 --- /dev/null +++ b/example/go/simple/server/server_test.go @@ -0,0 +1,61 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + "testing" + "time" + + upiv1 "github.com/caraml-dev/universal-prediction-interface/gen/go/grpc/caraml/upi/v1" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +const testPort = 50555 + +var address = fmt.Sprintf(":%d", testPort) + +func TestMain(m *testing.M) { + + upiServer := UpiServer{} + go upiServer.Run(address) + os.Exit(m.Run()) +} + +func TestUpiServer_Run(t *testing.T) { + + conn, err := grpc.Dial(address, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithBlock()) + if err != nil { + log.Fatalf("did not connect: %v", err) + } + + defer conn.Close() + c := upiv1.NewUniversalPredictionServiceClient(conn) + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + r, err := c.PredictValues(ctx, &upiv1.PredictValuesRequest{ + PredictionTable: &upiv1.Table{ + Name: "Test", + Columns: []*upiv1.Column{ + { + Name: "col1", + Type: upiv1.Type_TYPE_DOUBLE, + }, + }, + Rows: []*upiv1.Row{ + { + RowId: "1", + Values: []*upiv1.Value{ + {}, + }, + }, + }, + }, + }) + if err != nil { + log.Fatalf("could not call upi server: %v", err) + } + log.Printf("Response: %s", r.String()) +}