Initial commit
This commit is contained in:
22
internal/app/api/router.go
Normal file
22
internal/app/api/router.go
Normal file
@ -0,0 +1,22 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
v1 "git.cesium.pw/niku/virteen/internal/app/api/v1"
|
||||
"git.cesium.pw/niku/virteen/internal/middleware"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/helmet"
|
||||
)
|
||||
|
||||
func ConfigureRoutes(app *fiber.App) error {
|
||||
api := app.Group("/api")
|
||||
|
||||
api.Use(helmet.New())
|
||||
api.Use(middleware.ErrorHandler())
|
||||
|
||||
err := v1.ConfigureRoutes(api)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
9
internal/app/api/v1/ping.go
Normal file
9
internal/app/api/v1/ping.go
Normal file
@ -0,0 +1,9 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func Ping(c *fiber.Ctx) error {
|
||||
return c.SendString("pong")
|
||||
}
|
43
internal/app/api/v1/router.go
Normal file
43
internal/app/api/v1/router.go
Normal file
@ -0,0 +1,43 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"git.cesium.pw/niku/virteen/internal/auth"
|
||||
"git.cesium.pw/niku/virteen/internal/middleware"
|
||||
"git.cesium.pw/niku/virteen/internal/podman"
|
||||
"git.cesium.pw/niku/virteen/internal/podman/models"
|
||||
"git.cesium.pw/niku/virteen/internal/podman/services"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/monitor"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func ConfigureRoutes(router fiber.Router) error {
|
||||
v1 := router.Group("/v1")
|
||||
|
||||
ac := auth.NewPamAuthController()
|
||||
ag := v1.Group("/auth")
|
||||
ag.Post("/token", ac.GetToken)
|
||||
|
||||
v1.Get("/ping", Ping)
|
||||
v1.Get("/metrics", monitor.New())
|
||||
|
||||
v1.Use(middleware.Protected())
|
||||
|
||||
var cs models.PodmanService
|
||||
cs, err := services.NewPodmanContainerService()
|
||||
if err != nil {
|
||||
panic(errors.Wrapf(err, "failed to build podman container service"))
|
||||
}
|
||||
|
||||
cc := podman.NewPodmanController(&cs)
|
||||
|
||||
cg := v1.Group("/containers")
|
||||
cg.Get("/", cc.ListContainers)
|
||||
cg.Post("/", cc.CreateContainer)
|
||||
|
||||
ccg := cg.Group("/:name")
|
||||
ccg.Put("/status", cc.UpdateContainerStatus)
|
||||
ccg.Delete("/", cc.DeleteContainer)
|
||||
|
||||
return nil
|
||||
}
|
19
internal/app/main.go
Normal file
19
internal/app/main.go
Normal file
@ -0,0 +1,19 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"git.cesium.pw/niku/virteen/internal/app/api"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/compress"
|
||||
"github.com/gofiber/fiber/v2/middleware/logger"
|
||||
)
|
||||
|
||||
func Run() error {
|
||||
app := fiber.New()
|
||||
|
||||
app.Use(logger.New())
|
||||
app.Use(compress.New())
|
||||
|
||||
api.ConfigureRoutes(app)
|
||||
|
||||
return app.Listen(":3000")
|
||||
}
|
52
internal/auth/auth_controller.go
Normal file
52
internal/auth/auth_controller.go
Normal file
@ -0,0 +1,52 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"git.cesium.pw/niku/virteen/internal/auth/services"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/golang-jwt/jwt"
|
||||
)
|
||||
|
||||
type PamAuthController struct {
|
||||
pam *services.PamAuthService
|
||||
}
|
||||
|
||||
func NewPamAuthController() *PamAuthController {
|
||||
return &PamAuthController{
|
||||
pam: services.NewPamAuthService(),
|
||||
}
|
||||
}
|
||||
|
||||
type GetTokenBody struct {
|
||||
User string `json:"user"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
func (pac *PamAuthController) GetToken(ctx *fiber.Ctx) error {
|
||||
var body GetTokenBody
|
||||
if err := ctx.BodyParser(&body); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
isValid := (*pac.pam).IsValidUser(body.User, body.Password)
|
||||
if isValid == false {
|
||||
return fmt.Errorf("invalid user or password")
|
||||
}
|
||||
|
||||
claims := jwt.MapClaims{
|
||||
"name": body.User,
|
||||
"exp": time.Now().Add(time.Hour * 72).Unix(),
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
|
||||
t, err := token.SignedString([]byte(os.Getenv("JWT_SECRET")))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ctx.JSON(fiber.Map{"Token": t})
|
||||
}
|
5
internal/auth/models/models.go
Normal file
5
internal/auth/models/models.go
Normal file
@ -0,0 +1,5 @@
|
||||
package models
|
||||
|
||||
type AuthService interface {
|
||||
IsValidUser(user, password string) bool
|
||||
}
|
31
internal/auth/repositories/pam_repository.go
Normal file
31
internal/auth/repositories/pam_repository.go
Normal file
@ -0,0 +1,31 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/msteinert/pam"
|
||||
)
|
||||
|
||||
type PamRepository struct{}
|
||||
|
||||
func NewPamRepository() *PamRepository {
|
||||
return &PamRepository{}
|
||||
}
|
||||
|
||||
func (pr *PamRepository) IsValidUser(user, password string) bool {
|
||||
tx, err := pam.StartFunc("virteen", user, func(s pam.Style, msg string) (string, error) {
|
||||
return password, nil
|
||||
})
|
||||
if err != nil {
|
||||
panic("failed to start PAM transaction")
|
||||
}
|
||||
|
||||
err = tx.Authenticate(pam.Silent)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "authenticate: %s\n", err.Error())
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
17
internal/auth/services/pam_auth_service.go
Normal file
17
internal/auth/services/pam_auth_service.go
Normal file
@ -0,0 +1,17 @@
|
||||
package services
|
||||
|
||||
import "git.cesium.pw/niku/virteen/internal/auth/repositories"
|
||||
|
||||
type PamAuthService struct {
|
||||
pam *repositories.PamRepository
|
||||
}
|
||||
|
||||
func NewPamAuthService() *PamAuthService {
|
||||
return &PamAuthService{
|
||||
pam: repositories.NewPamRepository(),
|
||||
}
|
||||
}
|
||||
|
||||
func (pas PamAuthService) IsValidUser(user, password string) bool {
|
||||
return pas.pam.IsValidUser(user, password)
|
||||
}
|
20
internal/middleware/auth.go
Normal file
20
internal/middleware/auth.go
Normal file
@ -0,0 +1,20 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
jwtware "github.com/gofiber/contrib/jwt"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func errorHandler(c *fiber.Ctx, err error) error {
|
||||
c.Status(401)
|
||||
return err
|
||||
}
|
||||
|
||||
func Protected() fiber.Handler {
|
||||
return jwtware.New(jwtware.Config{
|
||||
SigningKey: jwtware.SigningKey{Key: []byte(os.Getenv("JWT_SECRET"))},
|
||||
ErrorHandler: errorHandler,
|
||||
})
|
||||
}
|
43
internal/middleware/error_hander.go
Normal file
43
internal/middleware/error_hander.go
Normal file
@ -0,0 +1,43 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type ProblemDetails struct {
|
||||
Type string
|
||||
Title string
|
||||
Status int
|
||||
Detail string
|
||||
}
|
||||
|
||||
func ErrorHandler() fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
status := fiber.StatusInternalServerError
|
||||
c.Status(status).JSON(ProblemDetails{
|
||||
Type: fmt.Sprintf("https://httpstatuses.io/%d", status),
|
||||
Title: "Internal Server Error",
|
||||
Status: status,
|
||||
Detail: fmt.Sprint(r),
|
||||
})
|
||||
}
|
||||
}()
|
||||
|
||||
err := c.Next()
|
||||
if err != nil {
|
||||
status := c.Response().StatusCode()
|
||||
c.JSON(ProblemDetails{
|
||||
Type: fmt.Sprintf("https://httpstatuses.io/%d", status),
|
||||
Title: err.Error(),
|
||||
Status: status,
|
||||
Detail: "No further details available",
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
30
internal/podman/models/models.go
Normal file
30
internal/podman/models/models.go
Normal file
@ -0,0 +1,30 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/containers/podman/v4/pkg/domain/entities"
|
||||
)
|
||||
|
||||
type ContainerStatus int
|
||||
|
||||
const (
|
||||
StopContainer ContainerStatus = iota
|
||||
StartContainer
|
||||
RestartContainer
|
||||
)
|
||||
|
||||
type PodmanService interface {
|
||||
List() ([]entities.ListContainer, error)
|
||||
Create(ContainerBlueprint) error
|
||||
Delete(string) error
|
||||
UpdateStatus(string, string) error
|
||||
}
|
||||
|
||||
type PortMapping struct {
|
||||
ContainerPort uint16 `json:"containerPort"`
|
||||
HostPort uint16 `json:"hostPort"`
|
||||
}
|
||||
|
||||
type ContainerBlueprint struct {
|
||||
Image string `json:"image"`
|
||||
PortMapping []PortMapping `json:"portMapping"`
|
||||
}
|
42
internal/podman/podman_controller.go
Normal file
42
internal/podman/podman_controller.go
Normal file
@ -0,0 +1,42 @@
|
||||
package podman
|
||||
|
||||
import (
|
||||
"git.cesium.pw/niku/virteen/internal/podman/models"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type PodmanController struct {
|
||||
containerService *models.PodmanService
|
||||
}
|
||||
|
||||
func NewPodmanController(cs *models.PodmanService) *PodmanController {
|
||||
return &PodmanController{containerService: cs}
|
||||
}
|
||||
|
||||
func (cc *PodmanController) ListContainers(ctx *fiber.Ctx) error {
|
||||
containerList, err := (*cc.containerService).List()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ctx.JSON(containerList)
|
||||
}
|
||||
|
||||
func (cc *PodmanController) CreateContainer(ctx *fiber.Ctx) error {
|
||||
var blueprint models.ContainerBlueprint
|
||||
if err := ctx.BodyParser(&blueprint); err != nil {
|
||||
return err
|
||||
}
|
||||
return (*cc.containerService).Create(blueprint)
|
||||
}
|
||||
|
||||
func (cc *PodmanController) UpdateContainerStatus(ctx *fiber.Ctx) error {
|
||||
name := ctx.Params("name")
|
||||
state := ctx.Query("state")
|
||||
return (*cc.containerService).UpdateStatus(name, state)
|
||||
}
|
||||
|
||||
func (cc *PodmanController) DeleteContainer(ctx *fiber.Ctx) error {
|
||||
name := ctx.Params("name")
|
||||
return (*cc.containerService).Delete(name)
|
||||
}
|
93
internal/podman/repositories/podman_repository.go
Normal file
93
internal/podman/repositories/podman_repository.go
Normal file
@ -0,0 +1,93 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.cesium.pw/niku/virteen/internal/podman/models"
|
||||
"github.com/containers/common/libnetwork/types"
|
||||
"github.com/containers/podman/v4/pkg/bindings"
|
||||
"github.com/containers/podman/v4/pkg/bindings/containers"
|
||||
"github.com/containers/podman/v4/pkg/bindings/images"
|
||||
"github.com/containers/podman/v4/pkg/domain/entities"
|
||||
"github.com/containers/podman/v4/pkg/specgen"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func newPodmanContext() (context.Context, error) {
|
||||
conn, err := bindings.NewConnection(context.Background(), "unix:///run/user/1000/podman/podman.sock")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
type PodmanRepository struct {
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func (pcs *PodmanRepository) ListContainers() ([]entities.ListContainer, error) {
|
||||
return containers.List(pcs.ctx, &containers.ListOptions{})
|
||||
}
|
||||
|
||||
func (pcs *PodmanRepository) CreateContainer(bp models.ContainerBlueprint) error {
|
||||
pullQuiet := true
|
||||
_, err := images.Pull(pcs.ctx, bp.Image, &images.PullOptions{Quiet: &pullQuiet})
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to pull image")
|
||||
}
|
||||
|
||||
s := specgen.NewSpecGenerator("nginx", false)
|
||||
|
||||
portMapping := make([]types.PortMapping, len(bp.PortMapping))
|
||||
for i, pm := range bp.PortMapping {
|
||||
portMapping[i] = types.PortMapping{ContainerPort: pm.ContainerPort, HostPort: pm.HostPort}
|
||||
}
|
||||
|
||||
s.ContainerNetworkConfig.PortMappings = append(s.ContainerNetworkConfig.PortMappings, portMapping...)
|
||||
|
||||
createResponse, err := containers.CreateWithSpec(pcs.ctx, s, nil)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to create spec")
|
||||
}
|
||||
|
||||
if err := containers.Start(pcs.ctx, createResponse.ID, nil); err != nil {
|
||||
return errors.Wrapf(err, "failed to start container")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pcs *PodmanRepository) UpdateContainerStatus(name string, state models.ContainerStatus) error {
|
||||
switch state {
|
||||
case models.StopContainer:
|
||||
return containers.Stop(pcs.ctx, name, nil)
|
||||
case models.StartContainer:
|
||||
return containers.Start(pcs.ctx, name, nil)
|
||||
case models.RestartContainer:
|
||||
return containers.Restart(pcs.ctx, name, nil)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (pcs *PodmanRepository) DeleteContainer(name string) error {
|
||||
err := pcs.UpdateContainerStatus(name, models.StopContainer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = containers.Remove(pcs.ctx, name, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func NewPodmanRepository() (*PodmanRepository, error) {
|
||||
ctx, err := newPodmanContext()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &PodmanRepository{ctx: ctx}, nil
|
||||
}
|
51
internal/podman/services/podman_service.go
Normal file
51
internal/podman/services/podman_service.go
Normal file
@ -0,0 +1,51 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.cesium.pw/niku/virteen/internal/podman/models"
|
||||
"git.cesium.pw/niku/virteen/internal/podman/repositories"
|
||||
"github.com/containers/podman/v4/pkg/domain/entities"
|
||||
)
|
||||
|
||||
type PodmanContainerService struct {
|
||||
podman *repositories.PodmanRepository
|
||||
}
|
||||
|
||||
func (pcs *PodmanContainerService) List() ([]entities.ListContainer, error) {
|
||||
return pcs.podman.ListContainers()
|
||||
}
|
||||
|
||||
func (pcs *PodmanContainerService) Create(blueprint models.ContainerBlueprint) error {
|
||||
return pcs.podman.CreateContainer(blueprint)
|
||||
}
|
||||
|
||||
func (pcs *PodmanContainerService) Delete(name string) error {
|
||||
return pcs.podman.DeleteContainer(name)
|
||||
}
|
||||
|
||||
func (pcs *PodmanContainerService) UpdateStatus(name, status string) error {
|
||||
var state models.ContainerStatus
|
||||
if status == "started" {
|
||||
state = models.StartContainer
|
||||
} else if status == "stopped" {
|
||||
state = models.StopContainer
|
||||
} else if status == "restarting" {
|
||||
state = models.RestartContainer
|
||||
} else {
|
||||
return fmt.Errorf("invalid state, should be started, stopped or restarting")
|
||||
}
|
||||
|
||||
return pcs.podman.UpdateContainerStatus(name, state)
|
||||
}
|
||||
|
||||
func NewPodmanContainerService() (*PodmanContainerService, error) {
|
||||
podmanRepo, err := repositories.NewPodmanRepository()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &PodmanContainerService{
|
||||
podman: podmanRepo,
|
||||
}, nil
|
||||
}
|
Reference in New Issue
Block a user