Initial commit

This commit is contained in:
2023-06-09 11:33:10 +02:00
commit 7c111f97ab
22 changed files with 2221 additions and 0 deletions

View 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
}

View File

@ -0,0 +1,9 @@
package v1
import (
"github.com/gofiber/fiber/v2"
)
func Ping(c *fiber.Ctx) error {
return c.SendString("pong")
}

View 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
View 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")
}

View 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})
}

View File

@ -0,0 +1,5 @@
package models
type AuthService interface {
IsValidUser(user, password string) bool
}

View 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
}

View 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)
}

View 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,
})
}

View 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
}
}

View 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"`
}

View 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)
}

View 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
}

View 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
}