Skip to content

Commit

Permalink
Merge pull request #3 from mehranzand/action-feature
Browse files Browse the repository at this point in the history
Basic API development for action and observatory feature (not applied to UI yet)
  • Loading branch information
mehranzand authored Nov 13, 2024
2 parents a308363 + 76267ca commit d792567
Show file tree
Hide file tree
Showing 36 changed files with 746 additions and 78 deletions.
2 changes: 1 addition & 1 deletion .air.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ tmp_dir = "build"
[build]
args_bin = []
bin = "./build/pulseup"
cmd = 'go build -ldflags "-X main.version=`git tag`" -o ./build/pulseup .'
cmd = 'CGO_ENABLED=1 go build -ldflags "-X main.version=`git tag`" -o ./build/pulseup .'
delay = 1000
exclude_dir = ["build", "vendor", "testdata", "web/node_modules"]
exclude_file = []
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ logs
*.local
.vscode/*
!.vscode/extensions.json
.idea
.idea
pulseup.db
15 changes: 12 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,15 @@ RUN yarn build
FROM golang:1.22.2-alpine AS builder

RUN mkdir /pulseup

WORKDIR /pulseup

# Important: required for go-sqlite3
ENV CGO_ENABLED=1
RUN apk add --no-cache \
gcc \
# Required for Alpine
musl-dev

# Copy go mod files
COPY go.* ./
RUN go mod download
Expand All @@ -34,13 +40,16 @@ COPY .env ./
# Args
ARG TAG=head
ARG TARGETOS TARGETARCH
ENV TARGETOS=linux
ENV TARGETARCH=amd64

# Build binary
RUN GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags "-s -w -X main.version=$TAG" -o pulseup
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-s -w -X main.version=$TAG -extldflags '-static'" -o pulseup

FROM scratch

COPY --from=builder /pulseup ./
COPY --from=builder /pulseup/.env ./
COPY --from=builder /pulseup/pulseup ./

EXPOSE 7070

Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ TAG := "$$(git describe --abbrev=0 --tags)"
docker-build:
docker build --build-arg TAG=$(TAG) -t mehranzand/pulseup:$(TAG) -t mehranzand/pulseup:latest .

.PHONY: run-dev
.PHONY: dev
dev:
cd web && yarn dev & air && fg

Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
version: "3.4"
services:
pulseup:
image: mehranzand/pulseup:v1.0.2
image: mehranzand/pulseup:latest
container_name: pulseup
ports:
- 7070:7070
Expand Down
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ require (
github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.9.0
gopkg.in/go-playground/validator.v9 v9.31.0
gorm.io/driver/sqlite v1.5.5
gorm.io/gorm v1.25.10
)

require (
Expand All @@ -28,9 +30,12 @@ require (
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.22 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/morikuni/aec v1.0.0 // indirect
Expand Down
10 changes: 10 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
Expand All @@ -58,6 +62,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
Expand Down Expand Up @@ -156,5 +162,9 @@ gopkg.in/go-playground/validator.v9 v9.31.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWd
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/sqlite v1.5.5 h1:7MDMtUZhV065SilG62E0MquljeArQZNfJnjd9i9gx3E=
gorm.io/driver/sqlite v1.5.5/go.mod h1:6NgQ7sQWAIFsPrJJl1lSNSu2TABh0ZZ/zm5fosATavE=
gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s=
gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
8 changes: 7 additions & 1 deletion internal/api/handler/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"log"

"github.com/mehranzand/pulseup/internal/docker"
"github.com/mehranzand/pulseup/internal/monitoring"
"gorm.io/gorm"
)

type AuthProvider string
Expand All @@ -29,9 +31,11 @@ type Handler struct {
config *Config
indexTmpl *template.Template
clients map[string]docker.Client
db *gorm.DB
watcher *monitoring.LogWatcher
}

func NewHandler(config *Config, clients map[string]docker.Client, assets fs.FS) *Handler {
func NewHandler(config *Config, clients map[string]docker.Client, assets fs.FS, db *gorm.DB, w *monitoring.LogWatcher) *Handler {
file, err := assets.Open("index.html")
if err != nil {
log.Fatal(err)
Expand All @@ -56,5 +60,7 @@ func NewHandler(config *Config, clients map[string]docker.Client, assets fs.FS)
config: config,
indexTmpl: tmpl,
clients: clients,
db: db,
watcher: w,
}
}
1 change: 0 additions & 1 deletion internal/api/handler/logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,6 @@ outerloop:
fmt.Fprintln(c.Response().Writer, "ping:")
f.Flush()
}

}

select {
Expand Down
117 changes: 117 additions & 0 deletions internal/api/handler/monitoring.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package handler

import (
"net/http"
"strconv"
"time"

"github.com/labstack/echo/v4"
"github.com/mehranzand/pulseup/internal/api/middleware"
"github.com/mehranzand/pulseup/internal/models"
)

// SaveTrigger
// @Summary add or edit triggers
func (h *Handler) SaveTrigger(c echo.Context) error {
cc := c.(*middleware.DockerContext)
r := new(models.AddTriggerRequest)
if err := c.Bind(r); err != nil {
return err
}

if len(r.ContainerId) == 0 {
return c.JSON(http.StatusUnprocessableEntity, map[string]interface{}{"message": "container_id is required"})
}

if len(r.Triggers) == 0 {
return c.JSON(http.StatusUnprocessableEntity, map[string]interface{}{"message": "no triggers were found to create"})
}

var monitoredContainer models.MonitoredContainer
result := h.db.Preload("Triggers").Find(&monitoredContainer, "container_id = ?", r.ContainerId)

if result.Error != nil {
return c.JSON(http.StatusOK, map[string]interface{}{"err": result.Error.Error()})
}

if result.RowsAffected == 0 {
monitoredContainer = models.MonitoredContainer{ContainerId: r.ContainerId, Host: cc.Client.Host().ID, Active: true}
h.db.Create(&monitoredContainer)
} else if !monitoredContainer.Active {
h.db.Model(&monitoredContainer).Updates(map[string]interface{}{"active": 1, "updated_at": time.Now()})
}

for _, trigger := range r.Triggers {
var duplicate *models.Trigger
for _, t := range monitoredContainer.Triggers {
if t.Type == trigger.Type && t.Criteria == trigger.Criteria {
duplicate = &t
break
}
}

if duplicate != nil {
continue
}

if trigger.Id != 0 {
var triggerToUpdate *models.Trigger
for _, t := range monitoredContainer.Triggers {
if t.ID == trigger.Id && t.Type == trigger.Type && t.Criteria != trigger.Criteria {
triggerToUpdate = &t
break
}
}

if triggerToUpdate != nil {
triggerToUpdate.Type = trigger.Type
triggerToUpdate.Criteria = trigger.Criteria
triggerToUpdate.Active = trigger.Active
triggerToUpdate.UpdatedAt = time.Now()

h.db.Save(&triggerToUpdate)
} else {
return c.JSON(http.StatusForbidden, map[string]interface{}{"message": "trigger not found to update"})
}

} else {
trigger := models.Trigger{Type: trigger.Type, Criteria: trigger.Criteria, Active: trigger.Active}
h.db.Model(&monitoredContainer).Association("Triggers").Append(&trigger)
}
}

h.watcher.TrackContainer(monitoredContainer)

return c.JSON(http.StatusOK, map[string]interface{}{"message": "ok"})
}

// DeleteTrigger
// @Summary delete a trigger
func (h *Handler) DeleteTrigger(c echo.Context) error {
id := c.Param("id")

if len(id) == 0 {
return c.JSON(http.StatusUnprocessableEntity, map[string]interface{}{"message": "trigger id is required"})
}

trigger := models.Trigger{}
result := h.db.First(&trigger, id)
if result.Error != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"err": result.Error.Error()})
}

monitoredContainer := models.MonitoredContainer{}
result = h.db.First(&monitoredContainer, trigger.MonitoredContainerID)
if result.Error != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"err": result.Error.Error()})
}

deleteResult := h.db.Delete(&trigger, id)
if deleteResult.Error != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"err": deleteResult.Error})
} else {
i, _ := strconv.Atoi(id)
h.watcher.RemoveTrigger(monitoredContainer.ContainerId, uint(i))
return c.JSON(http.StatusOK, map[string]interface{}{"message": "ok"})
}
}
11 changes: 7 additions & 4 deletions internal/api/handler/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ import (
log "github.com/sirupsen/logrus"
)

func (h *Handler) Register(v1 *echo.Group) {
v1.GET("/:host/containers", h.GetContainers)
v1.GET("/logs/stream/:host/:id", h.StreamLogs)
v1.GET("/events/stream/:host", h.StreamContainerEvents)
func (h *Handler) Register(api *echo.Group) {
api.GET("/:host/containers", h.GetContainers)
api.GET("/logs/stream/:host/:id", h.StreamLogs)
api.GET("/events/stream/:host", h.StreamContainerEvents)

api.POST("/:host/monitoring", h.SaveTrigger)
api.DELETE("/:host/monitoring/:id", h.DeleteTrigger)
}

func (h *Handler) IndexHandler(c echo.Context) error {
Expand Down
6 changes: 4 additions & 2 deletions internal/api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import (
"github.com/mehranzand/pulseup/internal/api/middleware"
"github.com/mehranzand/pulseup/internal/api/router"
"github.com/mehranzand/pulseup/internal/docker"
"github.com/mehranzand/pulseup/internal/monitoring"
"gorm.io/gorm"
)

func CreateServer(clients map[string]docker.Client, config *handler.Config, assets fs.FS) {
h := handler.NewHandler(config, clients, assets)
func CreateServer(clients map[string]docker.Client, config *handler.Config, assets fs.FS, db *gorm.DB, w *monitoring.LogWatcher) {
h := handler.NewHandler(config, clients, assets, db, w)
r := router.New(assets, h)
r.Use(middleware.DockerMiddleware(r, clients))
base := r.Group(config.Base + "/api")
Expand Down
Loading

0 comments on commit d792567

Please sign in to comment.