From 505eafa7fc8741c3472435609659b799aac65cd6 Mon Sep 17 00:00:00 2001 From: Abdhesh Nayak Date: Sat, 23 Dec 2023 15:03:12 +0530 Subject: [PATCH 1/2] :sparkles: Added app websocket-server Added app websocket-server to push resource update events to the frontend-client. --- .github/workflows/build-container.yml | 2 + .github/workflows/build-tenant-agent.yml | 75 ---- apps/websocket-server/Containerfile.local | 7 + apps/websocket-server/Taskfile.yml | 61 ++++ apps/websocket-server/internal/app/app.go | 77 ++++ .../internal/domain/commons.go | 39 ++ .../internal/domain/domain.go | 337 ++++++++++++++++++ .../internal/domain/suite_test.go | 1 + apps/websocket-server/internal/env/env.go | 38 ++ .../internal/framework/framework.go | 83 +++++ apps/websocket-server/main.go | 63 ++++ 11 files changed, 708 insertions(+), 75 deletions(-) delete mode 100644 .github/workflows/build-tenant-agent.yml create mode 100644 apps/websocket-server/Containerfile.local create mode 100644 apps/websocket-server/Taskfile.yml create mode 100644 apps/websocket-server/internal/app/app.go create mode 100644 apps/websocket-server/internal/domain/commons.go create mode 100644 apps/websocket-server/internal/domain/domain.go create mode 100644 apps/websocket-server/internal/domain/suite_test.go create mode 100644 apps/websocket-server/internal/env/env.go create mode 100644 apps/websocket-server/internal/framework/framework.go create mode 100644 apps/websocket-server/main.go diff --git a/.github/workflows/build-container.yml b/.github/workflows/build-container.yml index 1834ce813..7922a035d 100644 --- a/.github/workflows/build-container.yml +++ b/.github/workflows/build-container.yml @@ -32,7 +32,9 @@ jobs: - infra - worker-audit-logging - webhook + - webhook-server - message-office + - tenant-agent runs-on: ubuntu-latest name: Deploy to Docker Image steps: diff --git a/.github/workflows/build-tenant-agent.yml b/.github/workflows/build-tenant-agent.yml deleted file mode 100644 index c981a66d6..000000000 --- a/.github/workflows/build-tenant-agent.yml +++ /dev/null @@ -1,75 +0,0 @@ -name: Build Tenant Agent - -on: - push: - paths: - - "apps/tenant-agent/**" - -permissions: - contents: read - packages: write - -jobs: - build-agent: - runs-on: ubuntu-latest - name: Build Tenant Agent - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - - name: Setup Golang caches - uses: actions/cache@v3 - with: - path: | - ~/.cache/go-build - ~/go/pkg/mod - key: ${{ runner.os }}-golang-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-golang- - - - name: Install Go - uses: actions/setup-go@v4 - with: - go-version: 1.21 - - - name: Install Task - uses: arduino/setup-task@v1 - with: - version: 3.x - repo-token: ${{ secrets.GITHUB_TOKEN }} - - - name: Install UPX - run: | - curl -L0 https://github.com/upx/upx/releases/download/v4.2.1/upx-4.2.1-amd64_linux.tar.xz > upx.tar.xz - tar -xf upx.tar.xz - sudo mv upx-4.2.1-amd64_linux/upx /usr/local/bin - - - name: Build Binary - run: | - cd apps/tenant-agent - task build Out=$PWD/../../.github/workflows/container-build/app - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - - name: Log in to the Container registry - uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build & Push Image - if: startsWith(github.ref, 'refs/heads/release') - run: | - branch_name=${GITHUB_REF#refs/heads/} - version_string="v${branch_name#release-}-nightly" - - cd .github/workflows/container-build - - upx ./app - task go-container-build Image="ghcr.io/kloudlite/tenant/agent:$version_string" - task go-container-build Image="ghcr.io/kloudlite/tenant/agent:commit-${GITHUB_SHA}" \ No newline at end of file diff --git a/apps/websocket-server/Containerfile.local b/apps/websocket-server/Containerfile.local new file mode 100644 index 000000000..07e5e5250 --- /dev/null +++ b/apps/websocket-server/Containerfile.local @@ -0,0 +1,7 @@ +# syntax=docker/dockerfile:1.4 +FROM gcr.io/distroless/static:nonroot +WORKDIR /tmp +USER 1001:1001 +ARG APP +COPY --from=builder $APP ./websocket-server +CMD ["./websocket-server"] diff --git a/apps/websocket-server/Taskfile.yml b/apps/websocket-server/Taskfile.yml new file mode 100644 index 000000000..f7566450c --- /dev/null +++ b/apps/websocket-server/Taskfile.yml @@ -0,0 +1,61 @@ +version: "3" + +dotenv: + - .secrets/env + +vars: + ImagePrefix: "ghcr.io/kloudlite/platform/apis" + +tasks: + build: + env: + CGO_ENABLED: 0 + GOOS: linux + GOARCH: amd64 + vars: + BuiltAt: + sh: date | sed 's/\s/_/g' + preconditions: + - sh: '[ -n "{{.Out}}" ]' + msg: var Out must have a value + cmds: + - go build -ldflags="-s -w -X kloudlite.io/common.BuiltAt=\"{{.BuiltAt}}\"" -o {{.Out}} + - upx {{.Out}} + + run: + dotenv: + - .secrets/env + cmds: + - go run main.go --dev + + docker-build: + vars: + APP: finance + IMAGE: registry.kloudlite.io/kloudlite/{{.EnvName}}/{{.APP}}-api:{{.Tag}} + preconditions: + - sh: '[[ -n "{{.Tag}}" ]]' + msg: 'var Tag must have a value' + + - sh: '[[ "{{.EnvName}}" == "development" ]] || [[ "{{.EnvName}}" == "staging" ]] || [[ "{{.EnvName}}" == "production" ]]' + msg: 'var EnvName must have one of [development, staging, production] as its value' + cmds: + - docker buildx build -f ./Dockerfile -t {{.IMAGE}} . --build-arg APP={{.APP}} --platform linux/amd64 --build-context project-root=../.. + - docker push {{.IMAGE}} + + local-build: + preconditions: + - sh: '[ -n "{{.Tag}}" ]' + msg: 'var Tag must have a value' + vars: + Dockerfile: "./Containerfile.local" + APP: websocket-server + Image: "{{.ImagePrefix}}/{{.APP}}:{{.Tag}}" + cmds: + - |+ + dir=$(mktemp -d) + task build Out=$dir/{{.APP}} + + podman buildx build -t {{.Image}} -f {{.Dockerfile}} . --build-context builder=$dir --build-arg APP={{.APP}} + podman push {{.Image}} + rm -rf $dir + diff --git a/apps/websocket-server/internal/app/app.go b/apps/websocket-server/internal/app/app.go new file mode 100644 index 000000000..9f1b428c0 --- /dev/null +++ b/apps/websocket-server/internal/app/app.go @@ -0,0 +1,77 @@ +package app + +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/websocket/v2" + "github.com/kloudlite/api/apps/websocket-server/internal/domain" + "github.com/kloudlite/api/apps/websocket-server/internal/env" + "github.com/kloudlite/api/common" + "github.com/kloudlite/api/constants" + "github.com/kloudlite/api/grpc-interfaces/kloudlite.io/rpc/iam" + "github.com/kloudlite/api/pkg/cache" + "github.com/kloudlite/api/pkg/grpc" + httpServer "github.com/kloudlite/api/pkg/http-server" + "go.uber.org/fx" +) + +type AuthCacheClient cache.Client + +type AuthClient grpc.Client + +type ( + ContainerRegistryClient grpc.Client + IAMClient grpc.Client +) + +var Module = fx.Module("app", + + // grpc clients + fx.Provide(func(conn IAMClient) iam.IAMClient { + return iam.NewIAMClient(conn) + }), + + domain.Module, + + fx.Invoke( + func(server httpServer.Server, d domain.Domain, env *env.Env, + sessionRepo cache.Repo[*common.AuthSession], + ) { + + a := server.Raw() + + a.Use( + httpServer.NewSessionMiddleware( + sessionRepo, + constants.CookieName, + env.CookieDomain, + constants.CacheSessionPrefix, + ), + ) + + // Web socket route + a.Use("/ws", func(c *fiber.Ctx) error { + if websocket.IsWebSocketUpgrade(c) { + return c.Next() + } + return fiber.ErrUpgradeRequired + }) + + a.Use("/ws", func(c *fiber.Ctx) error { + ctx := c.Context() + + return websocket.New(func(sockConn *websocket.Conn) { + d.HandleWebSocket(ctx, sockConn) + })(c) + }) + + a.Get("/healthy", func(c *fiber.Ctx) error { + return c.SendString("OK") + }) + + a.All("*", func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusNotFound) + }) + + }, + ), +) diff --git a/apps/websocket-server/internal/domain/commons.go b/apps/websocket-server/internal/domain/commons.go new file mode 100644 index 000000000..6671c5fed --- /dev/null +++ b/apps/websocket-server/internal/domain/commons.go @@ -0,0 +1,39 @@ +package domain + +import ( + "context" + iamT "github.com/kloudlite/api/apps/iam/types" + "github.com/kloudlite/api/grpc-interfaces/kloudlite.io/rpc/iam" + "github.com/kloudlite/api/pkg/errors" + "github.com/kloudlite/api/pkg/repos" +) + +type UserContext struct { + context.Context + UserId repos.ID + UserName string + UserEmail string +} + +func (d *domain) checkAccountAccess(ctx context.Context, accountName string, userId repos.ID, action iamT.Action) error { + co, err := d.iamClient.Can(ctx, &iam.CanIn{ + UserId: string(userId), + ResourceRefs: []string{iamT.NewResourceRef(accountName, iamT.ResourceAccount, accountName)}, + Action: string(action), + }) + + // if err != nil { + // return err + // } + + if err != nil { + d.logger.Errorf(err, "iam.can check for action: ", action) + return errors.Newf("unauthorized to perform action: %s", action) + } + + if !co.Status { + return errors.Newf("unauthorized to perform action: %s", action) + } + + return nil +} diff --git a/apps/websocket-server/internal/domain/domain.go b/apps/websocket-server/internal/domain/domain.go new file mode 100644 index 000000000..c8681463c --- /dev/null +++ b/apps/websocket-server/internal/domain/domain.go @@ -0,0 +1,337 @@ +package domain + +import ( + "context" + "fmt" + "strings" + + "github.com/gofiber/websocket/v2" + iamT "github.com/kloudlite/api/apps/iam/types" + "github.com/kloudlite/api/common" + httpServer "github.com/kloudlite/api/pkg/http-server" + "github.com/kloudlite/api/pkg/repos" + + "github.com/kloudlite/api/grpc-interfaces/kloudlite.io/rpc/iam" + "github.com/kloudlite/api/pkg/logging" + "github.com/kloudlite/api/pkg/nats" + "go.uber.org/fx" + + mnats "github.com/nats-io/nats.go" +) + +type SocketService interface { + HandleWebSocket(ctx context.Context, c *websocket.Conn) error +} + +type ReqData struct { + AccountName string `json:"account"` + ProjectName string `json:"project"` + + // ResourceName string `json:"resource"` + // ResourceType string `json:"resource_type"` + Topic string `json:"topic"` + ReqTopic string `json:"req_topic"` +} + +func (d *domain) parseReq(rt string) (*ReqData, error) { + + entriesStrs := strings.Split(rt, ".") + + rdata := &ReqData{} + + nTopics := "res-updates" + + for _, entryStr := range entriesStrs { + entry := strings.Split(entryStr, ":") + + if len(entry) != 2 { + nTopics += fmt.Sprintf(".%s.*", entry[0]) + } else { + nTopics += fmt.Sprintf(".%s.%s", entry[0], entry[1]) + } + + if (entry[0] == "account" || entry[0] == "project") && len(entry) == 2 { + if entry[0] == "account" { + rdata.AccountName = entry[1] + } + if entry[0] == "project" { + rdata.ProjectName = entry[1] + } + } + + } + + rdata.Topic = nTopics + rdata.ReqTopic = rt + if rdata.AccountName == "" { + return nil, fmt.Errorf("invalid topic %s", rt) + } + + return rdata, nil +} + +func (d *domain) checkAccess(ctx context.Context, rdata *ReqData, userId repos.ID) error { + co, err := d.iamClient.Can(ctx, &iam.CanIn{ + UserId: string(userId), + ResourceRefs: func() []string { + var refs []string + + if rdata.ProjectName != "" { + refs = append(refs, iamT.NewResourceRef(rdata.AccountName, iamT.ResourceProject, rdata.ProjectName)) + } + + refs = append(refs, iamT.NewResourceRef(rdata.AccountName, iamT.ResourceAccount, rdata.AccountName)) + + return refs + + }(), + Action: string(func() iamT.Action { + if rdata.ProjectName != "" { + return iamT.GetAccount + } else { + return iamT.GetProject + } + }()), + }) + + if err != nil { + return err + } + + if !co.Status { + return fmt.Errorf("access denied") + } + + return nil +} + +func (d *domain) HandleWebSocket(ctx context.Context, c *websocket.Conn) error { + + sess := httpServer.GetSession[*common.AuthSession](ctx) + if sess == nil { + fmt.Println("session not found") + return fmt.Errorf("session not found") + } + + defer c.Close() + log := d.logger + + type Subscription struct { + resource ReqData + sub *mnats.Subscription + open bool + } + + resources := make(map[string]*Subscription) + + type Message struct { + Event string `json:"event"` + Data string `json:"data"` + } + + // "account:accid.cluster:clusterid.nodepool:nodepoolid" + + type MessageType string + + const ( + MessageTypeError MessageType = "error" + MessageTypeUpdate MessageType = "update" + MessageTypeInfo MessageType = "info" + ) + + type MessageResponse struct { + Topic string `json:"topic"` + Message string `json:"message"` + Type MessageType `json:"type"` + } + + closed := false + + c.SetCloseHandler(func(code int, text string) error { + closed = true + return nil + }) + + // Keep the connection open + for { + + if closed { + break + } + + var message Message + if err := c.ReadJSON(&message); err != nil { + + if websocket.IsCloseError(err, websocket.CloseGoingAway) { + break + } + if websocket.IsCloseError(err, websocket.CloseAbnormalClosure) { + break + } + + if c != nil { + if err := c.WriteJSON(MessageResponse{ + Type: MessageTypeError, + Message: err.Error(), + }); err != nil { + log.Warnf("websocket write: %w", err) + } + } + + continue + } + + rd, err := d.parseReq(message.Data) + if err != nil { + + if c != nil { + if err := c.WriteJSON(MessageResponse{ + Type: MessageTypeError, + Message: err.Error(), + }); err != nil { + log.Warnf("websocket write: %w", err) + } + } + + continue + } + + if err := d.checkAccess(ctx, rd, sess.UserId); err != nil { + if c != nil { + if err := c.WriteJSON(MessageResponse{ + Type: MessageTypeError, + Message: err.Error(), + }); err != nil { + log.Warnf("websocket write: %w", err) + } + } + + continue + } + + switch message.Event { + case "subscribe": + if _, ok := resources[message.Data]; ok { + if c != nil { + if err := c.WriteJSON(MessageResponse{ + Type: MessageTypeError, + Message: "resource already subscribed", + }); err != nil { + log.Warnf("websocket write: %w", err) + } + } + } + + sub, err := d.natsClient.Conn.Subscribe(rd.Topic, func(m *mnats.Msg) { + + rmessage := MessageResponse{ + Topic: rd.ReqTopic, + Message: resources[rd.Topic].resource.ReqTopic, + Type: MessageTypeUpdate, + } + + if c != nil && resources[rd.Topic] != nil && resources[rd.Topic].open { + if err := c.WriteJSON(rmessage); err != nil { + log.Warnf("websocket write: %w", err) + } + } + + }) + + if err != nil { + + if c != nil { + if err := c.WriteJSON(MessageResponse{ + Type: MessageTypeError, + Message: err.Error(), + }); err != nil { + log.Warnf("websocket write: %w", err) + } + } + + continue + } + + if c != nil { + if err := c.WriteJSON(MessageResponse{ + Type: MessageTypeInfo, + Message: fmt.Sprintf("subscribed to %s", rd.Topic), + }); err != nil { + log.Warnf("websocket write: %w", err) + } + } + + defer sub.Unsubscribe() + + resources[rd.Topic] = &Subscription{ + resource: *rd, + sub: sub, + open: true, + } + + case "unsubscribe": + if _, ok := resources[message.Data]; !ok { + + if c != nil { + if err := c.WriteJSON(MessageResponse{ + Type: MessageTypeError, + Message: "resource not subscribed", + }); err != nil { + log.Warnf("websocket write: %w", err) + } + } + + } + + if resources[rd.Topic].sub != nil { + if err := resources[rd.Topic].sub.Unsubscribe(); err != nil { + if c != nil { + if err := c.WriteJSON(MessageResponse{ + Type: MessageTypeError, + Message: err.Error(), + }); err != nil { + log.Warnf("websocket write: %w", err) + } + } + + break + } + + delete(resources, message.Data) + } + + default: + log.Errorf(fmt.Errorf("websocket read: invalid event %s", message.Event)) + } + + } + + return nil +} + +type Domain interface { + SocketService +} + +type domain struct { + iamClient iam.IAMClient + natsClient *nats.Client + + logger logging.Logger +} + +func NewDomain( + iamCli iam.IAMClient, + + logger logging.Logger, + natsClient *nats.Client, +) Domain { + return &domain{ + iamClient: iamCli, + natsClient: natsClient, + + logger: logger, + } +} + +var Module = fx.Module("domain", fx.Provide(NewDomain)) diff --git a/apps/websocket-server/internal/domain/suite_test.go b/apps/websocket-server/internal/domain/suite_test.go new file mode 100644 index 000000000..8d0d2e733 --- /dev/null +++ b/apps/websocket-server/internal/domain/suite_test.go @@ -0,0 +1 @@ +package domain_test diff --git a/apps/websocket-server/internal/env/env.go b/apps/websocket-server/internal/env/env.go new file mode 100644 index 000000000..8df7a7b83 --- /dev/null +++ b/apps/websocket-server/internal/env/env.go @@ -0,0 +1,38 @@ +package env + +import ( + "github.com/codingconcepts/env" + "github.com/kloudlite/api/pkg/errors" +) + +type Env struct { + SocketPort uint16 `env:"SOCKET_PORT" required:"true"` + + IamGrpcAddr string `env:"IAM_GRPC_ADDR" required:"true"` + CookieDomain string `env:"COOKIE_DOMAIN" required:"true"` + + SessionKVBucket string `env:"SESSION_KV_BUCKET" required:"true"` + NatsURL string `env:"NATS_URL" required:"true"` + IsDev bool + + // HttpPort uint16 `env:"HTTP_PORT" required:"true"` + // HttpCors string `env:"CORS_ORIGINS" required:"false"` + // GrpcPort uint16 `env:"GRPC_PORT" required:"true"` + // + // DBName string `env:"MONGO_DB_NAME" required:"true"` + // DBUrl string `env:"MONGO_URI" required:"true"` + // + // + // CommsGrpcAddr string `env:"COMMS_GRPC_ADDR" required:"true"` + // ContainerRegistryGrpcAddr string `env:"CONTAINER_REGISTRY_GRPC_ADDR" required:"true"` + // ConsoleGrpcAddr string `env:"CONSOLE_GRPC_ADDR" required:"true"` + // KubernetesApiProxy string `env:"KUBERNETES_API_PROXY"` +} + +func LoadEnv() (*Env, error) { + var ev Env + if err := env.Set(&ev); err != nil { + return nil, errors.NewE(err) + } + return &ev, nil +} diff --git a/apps/websocket-server/internal/framework/framework.go b/apps/websocket-server/internal/framework/framework.go new file mode 100644 index 000000000..219cf3243 --- /dev/null +++ b/apps/websocket-server/internal/framework/framework.go @@ -0,0 +1,83 @@ +package framework + +import ( + "context" + "fmt" + + "github.com/kloudlite/api/common" + "github.com/kloudlite/api/pkg/errors" + "github.com/kloudlite/api/pkg/nats" + + "github.com/kloudlite/api/pkg/cache" + + "github.com/kloudlite/api/pkg/logging" + + "github.com/kloudlite/api/apps/websocket-server/internal/app" + "github.com/kloudlite/api/apps/websocket-server/internal/env" + "github.com/kloudlite/api/pkg/grpc" + httpServer "github.com/kloudlite/api/pkg/http-server" + "go.uber.org/fx" +) + +type fm struct { + env *env.Env +} + +var Module = fx.Module("framework", + fx.Provide(func(ev *env.Env) *fm { + return &fm{env: ev} + }), + + fx.Provide(func(ev *env.Env, logger logging.Logger) (*nats.Client, error) { + name := "RUP:nat-client" + return nats.NewClient(ev.NatsURL, nats.ClientOpts{ + Name: name, + Logger: logger, + }) + }), + + fx.Provide(func(ev *env.Env, logger logging.Logger, cli *nats.Client) (*nats.JetstreamClient, error) { + return nats.NewJetstreamClient(cli) + }), + + fx.Provide( + func(ev *env.Env, jc *nats.JetstreamClient) (cache.Repo[*common.AuthSession], error) { + cxt := context.TODO() + return cache.NewNatsKVRepo[*common.AuthSession](cxt, ev.SessionKVBucket, jc) + }, + ), + + fx.Provide(func(ev *env.Env) (app.IAMClient, error) { + return grpc.NewGrpcClient(ev.IamGrpcAddr) + }), + + app.Module, + + fx.Invoke(func(c2 app.IAMClient, lf fx.Lifecycle) { + lf.Append(fx.Hook{ + OnStop: func(context.Context) error { + if err := c2.Close(); err != nil { + return errors.NewE(err) + } + return nil + }, + }) + }), + + fx.Provide(func(logger logging.Logger, e *env.Env) httpServer.Server { + corsOrigins := "https://console.devc.kloudlite.io" + return httpServer.NewServer(httpServer.ServerArgs{Logger: logger, CorsAllowOrigins: &corsOrigins, IsDev: e.IsDev}) + }), + + // have to create socket server here + fx.Invoke(func(lf fx.Lifecycle, server httpServer.Server, ev *env.Env) { + lf.Append(fx.Hook{ + OnStart: func(context.Context) error { + return server.Listen(fmt.Sprintf(":%d", ev.SocketPort)) + }, + OnStop: func(context.Context) error { + return server.Close() + }, + }) + }), +) diff --git a/apps/websocket-server/main.go b/apps/websocket-server/main.go new file mode 100644 index 000000000..b8deadd7f --- /dev/null +++ b/apps/websocket-server/main.go @@ -0,0 +1,63 @@ +package main + +import ( + "context" + "flag" + "github.com/kloudlite/api/pkg/errors" + "os" + "time" + + "github.com/kloudlite/api/common" + "github.com/kloudlite/api/pkg/logging" + "go.uber.org/fx" + + "github.com/kloudlite/api/apps/websocket-server/internal/env" + "github.com/kloudlite/api/apps/websocket-server/internal/framework" +) + +func main() { + var isDev bool + flag.BoolVar(&isDev, "dev", false, "--dev") + flag.Parse() + + logger, err := logging.New(&logging.Options{Name: "RUP", Dev: isDev}) + if err != nil { + panic(err) + } + + app := fx.New( + fx.NopLogger, + + fx.Provide(func() logging.Logger { + return logger + }), + + fx.Provide(func() (*env.Env, error) { + if e, err := env.LoadEnv(); err != nil { + return nil, errors.NewE(err) + } else { + e.IsDev = isDev + return e, nil + } + }), + + framework.Module, + ) + + ctx, cancelFunc := func() (context.Context, context.CancelFunc) { + if isDev { + return context.WithTimeout(context.TODO(), 20*time.Second) + } + return context.WithTimeout(context.TODO(), 5*time.Second) + }() + defer cancelFunc() + + if err := app.Start(ctx); err != nil { + logger.Errorf(err, "error starting websocket-server app") + logger.Infof("EXITING as errors encountered during startup") + os.Exit(1) + } + + common.PrintReadyBanner() + <-app.Done() +} From 366e8c2697d729af23675359b2f90ee227075a7a Mon Sep 17 00:00:00 2001 From: Abdhesh Nayak Date: Sat, 23 Dec 2023 15:04:47 +0530 Subject: [PATCH 2/2] :rocket: Fixed issue with deployment, updated app name --- .github/workflows/build-container.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-container.yml b/.github/workflows/build-container.yml index 7922a035d..9d665eed4 100644 --- a/.github/workflows/build-container.yml +++ b/.github/workflows/build-container.yml @@ -32,7 +32,7 @@ jobs: - infra - worker-audit-logging - webhook - - webhook-server + - websocket-server - message-office - tenant-agent runs-on: ubuntu-latest