Initial Upload

This commit is contained in:
Pacerino
2022-09-26 15:41:00 +02:00
commit 15842e3c45
51 changed files with 40124 additions and 0 deletions

38
backend/cmd/main.go Normal file
View File

@@ -0,0 +1,38 @@
package main
import (
"os"
"os/signal"
"syscall"
"github.com/Pacerino/cpm/internal/api"
"github.com/Pacerino/cpm/internal/config"
"github.com/Pacerino/cpm/internal/database"
"github.com/Pacerino/cpm/internal/jobqueue"
"github.com/Pacerino/cpm/internal/logger"
)
var (
version = "3.0.0"
commit = "abcdefgh"
)
func main() {
config.Init(&version, &commit)
database.NewDB()
jobqueue.Start()
// HTTP Server
api.StartServer()
// Clean Quit
irqchan := make(chan os.Signal, 1)
signal.Notify(irqchan, syscall.SIGINT, syscall.SIGTERM)
for irq := range irqchan {
if irq == syscall.SIGINT || irq == syscall.SIGTERM {
logger.Info("Got ", irq, " shutting server down ...")
break
}
}
}

View File

@@ -0,0 +1,15 @@
{{host.domains}} {
{{#if host.upstreams}}
{{#if host.matcher}}
reverse_proxy {{host.matcher}} {{#each host.upstreams}} {{backend}} {{/each}}
{{else}}
reverse_proxy {{#each host.upstreams}} {{backend}} {{/each}}
{{/if}}
{{/if}}
{{#if LogPath}}
log {
output file {{LogPath}}
}
{{/if}}
}

13
backend/embed/main.go Normal file
View File

@@ -0,0 +1,13 @@
package embed
import "embed"
// Frontend Files served by this app
//
//go:embed all:assets/**
var Assets embed.FS
// CaddyFiles hold all template for caddy
//
//go:embed caddy
var CaddyFiles embed.FS

34
backend/go.mod Normal file
View File

@@ -0,0 +1,34 @@
module github.com/Pacerino/cpm
go 1.19
require (
github.com/aymerick/raymond v2.0.2+incompatible
github.com/go-playground/validator/v10 v10.11.1
gorm.io/driver/sqlite v1.3.6
gorm.io/gorm v1.23.9
)
require (
github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/mattn/go-colorable v0.1.9 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/qri-io/jsonpointer v0.1.1 // indirect
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 // indirect
golang.org/x/text v0.3.7 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
require (
github.com/fatih/color v1.13.0
github.com/go-chi/chi/v5 v5.0.7
github.com/go-chi/cors v1.2.1
github.com/mattn/go-sqlite3 v1.14.15 // indirect
github.com/qri-io/jsonschema v0.2.1
github.com/vrischmann/envconfig v1.3.0
golang.org/x/sys v0.0.0-20220915200043-7b5979e65e41 // indirect
)

93
backend/go.sum Normal file
View File

@@ -0,0 +1,93 @@
github.com/aymerick/raymond v2.0.2+incompatible h1:VEp3GpgdAnv9B2GFyTvqgcKvY+mfKMjPOA3SbKLtnU0=
github.com/aymerick/raymond v2.0.2+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8=
github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ=
github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
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.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/qri-io/jsonpointer v0.1.1 h1:prVZBZLL6TW5vsSB9fFHFAMBLI4b0ri5vribQlTJiBA=
github.com/qri-io/jsonpointer v0.1.1/go.mod h1:DnJPaYgiKu56EuDp8TU5wFLdZIcAnb/uH9v37ZaMV64=
github.com/qri-io/jsonschema v0.2.1 h1:NNFoKms+kut6ABPf6xiKNM5214jzxAhDBrPHCJ97Wg0=
github.com/qri-io/jsonschema v0.2.1/go.mod h1:g7DPkiOsK1xv6T/Ao5scXRkd+yTFygcANPBaaqW+VrI=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/vrischmann/envconfig v1.3.0 h1:4XIvQTXznxmWMnjouj0ST5lFo/WAYf5Exgl3x82crEk=
github.com/vrischmann/envconfig v1.3.0/go.mod h1:bbvxFYJdRSpXrhS63mBFtKJzkDiNkyArOLXtY6q0kuI=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 h1:0es+/5331RGQPcXlMfP+WrnIIS6dNnNRe0WB02W0F4M=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220915200043-7b5979e65e41 h1:ohgcoMbSofXygzo6AD2I1kz3BFmW1QArPYTtwEM3UXc=
golang.org/x/sys v0.0.0-20220915200043-7b5979e65e41/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
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/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/sqlite v1.3.6 h1:Fi8xNYCUplOqWiPa3/GuCeowRNBRGTf62DEmhMDHeQQ=
gorm.io/driver/sqlite v1.3.6/go.mod h1:Sg1/pvnKtbQ7jLXxfZa+jSHvoX8hoZA8cn4xllOMTgE=
gorm.io/gorm v1.23.4/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
gorm.io/gorm v1.23.9 h1:NSHG021i+MCznokeXR3udGaNyFyBQJW8MbjrJMVCfGw=
gorm.io/gorm v1.23.9/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=

View File

@@ -0,0 +1,19 @@
package context
var (
// BodyCtxKey is the name of the Body value on the context
BodyCtxKey = &contextKey{"Body"}
// PrettyPrintCtxKey is the name of the pretty print context
PrettyPrintCtxKey = &contextKey{"Pretty"}
)
// contextKey is a value for use with context.WithValue. It's used as
// a pointer so it fits in an interface{} without allocation. This technique
// for defining context keys was copied from Go 1.7's new use of context in net/http.
type contextKey struct {
name string
}
func (k *contextKey) String() string {
return "context value: " + k.name
}

View File

@@ -0,0 +1,19 @@
package handler
import (
"github.com/Pacerino/cpm/internal/database"
"gorm.io/gorm"
)
type Handler struct {
DB *gorm.DB
}
func NewHandler() *Handler {
db := database.GetInstance()
handler := &Handler{
DB: db,
}
return handler
}

View File

@@ -0,0 +1,29 @@
package handler
import (
"fmt"
"net/http"
"strconv"
"github.com/go-chi/chi/v5"
)
func getURLParamInt(r *http.Request, varName string) (int, error) {
required := true
defaultValue := 0
paramStr := chi.URLParam(r, varName)
var err error
var paramInt int
if paramStr == "" && required {
return 0, fmt.Errorf("%v was not supplied in the request", varName)
} else if paramStr == "" {
return defaultValue, nil
}
if paramInt, err = strconv.Atoi(paramStr); err != nil {
return 0, fmt.Errorf("%v is not a valid number", varName)
}
return paramInt, nil
}

View File

@@ -0,0 +1,152 @@
package handler
import (
"encoding/json"
"net/http"
h "github.com/Pacerino/cpm/internal/api/http"
"github.com/Pacerino/cpm/internal/caddy"
"github.com/Pacerino/cpm/internal/database"
"github.com/Pacerino/cpm/internal/jobqueue"
"github.com/go-playground/validator/v10"
"gorm.io/gorm"
)
// GetHosts will return a list of Hosts
// Route: GET /hosts
func (s Handler) GetHosts() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var hosts []database.Host
if err := s.DB.Preload("Upstreams").Find(&hosts).Error; err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
}
h.ResultResponseJSON(w, r, http.StatusOK, hosts)
}
}
// GetHost will return a single Host
// Route: GET /hosts/{hostID}
func (s Handler) GetHost() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var err error
var hostID int
var host database.Host
if hostID, err = getURLParamInt(r, "hostID"); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
if err = s.DB.Where("id = ?", hostID).Preload("Upstreams").First(&host).Error; err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
h.ResultResponseJSON(w, r, http.StatusOK, host)
}
}
}
// CreateHost will create a Host
// Route: POST /hosts
func (s Handler) CreateHost() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var newHost database.Host
err := json.NewDecoder(r.Body).Decode(&newHost)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
validate := validator.New()
if err := validate.Struct(newHost); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
result := s.DB.Create(&newHost)
if result.Error != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, result.Error.Error(), nil)
return
}
if err := jobqueue.AddJob(jobqueue.Job{
Name: "CaddyConfigureHost",
Action: func() error {
return caddy.WriteHost(newHost)
},
}); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
h.ResultResponseJSON(w, r, http.StatusOK, newHost)
}
}
// UpdateHost updates a host
// Route: PUT /hosts
func (s Handler) UpdateHost() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var newHost database.Host
err := json.NewDecoder(r.Body).Decode(&newHost)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
validate := validator.New()
if err := validate.Struct(newHost); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
if err := jobqueue.AddJob(jobqueue.Job{
Name: "CaddyConfigureHost",
Action: func() error {
return caddy.WriteHost(newHost)
},
}); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
result := s.DB.Session(&gorm.Session{FullSaveAssociations: true}).Save(&newHost)
if result.Error != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, result.Error.Error(), nil)
return
}
h.ResultResponseJSON(w, r, http.StatusOK, newHost)
}
}
// DeleteHost removes a host
// Route: DELETE /hosts/{hostID}
func (s Handler) DeleteHost() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var err error
var hostID int
if hostID, err = getURLParamInt(r, "hostID"); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
if err := jobqueue.AddJob(jobqueue.Job{
Name: "CaddyConfigureHost",
Action: func() error {
return caddy.RemoveHost(hostID)
},
}); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
result := s.DB.Delete(&database.Host{}, hostID)
if result.Error != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, result.Error.Error(), nil)
return
}
if result.RowsAffected > 0 {
h.ResultResponseJSON(w, r, http.StatusOK, true)
} else {
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrIDNotFound.Error(), nil)
}
}
}

View File

@@ -0,0 +1,48 @@
package http
import (
"context"
"encoding/json"
"errors"
"github.com/qri-io/jsonschema"
)
var (
// ErrInvalidJSON is an error for invalid json
ErrInvalidJSON = errors.New("json is invalid")
// ErrInvalidPayload is an error for invalid incoming data
ErrInvalidPayload = errors.New("payload is invalid")
// ErrIDNotFound is an error for invalid or missing ID in database
ErrIDNotFound = errors.New("id is missing in db")
)
// ValidateRequestSchema takes a Schema and the Content to validate against it
func ValidateRequestSchema(schema string, requestBody []byte) ([]jsonschema.KeyError, error) {
var jsonErrors []jsonschema.KeyError
var schemaBytes = []byte(schema)
// Make sure the body is valid JSON
if !isJSON(requestBody) {
return jsonErrors, ErrInvalidJSON
}
rs := &jsonschema.Schema{}
if err := json.Unmarshal(schemaBytes, rs); err != nil {
return jsonErrors, err
}
var validationErr error
ctx := context.TODO()
if jsonErrors, validationErr = rs.ValidateBytes(ctx, requestBody); len(jsonErrors) > 0 {
return jsonErrors, validationErr
}
// Valid
return nil, nil
}
func isJSON(bytes []byte) bool {
var js map[string]interface{}
return json.Unmarshal(bytes, &js) == nil
}

View File

@@ -0,0 +1,91 @@
package http
import (
"encoding/json"
"fmt"
"net/http"
"reflect"
c "github.com/Pacerino/cpm/internal/api/context"
"github.com/Pacerino/cpm/internal/errors"
"github.com/Pacerino/cpm/internal/logger"
"github.com/qri-io/jsonschema"
)
// Response interface for standard API results
type Response struct {
Result interface{} `json:"result"`
Error interface{} `json:"error,omitempty"`
}
// ErrorResponse interface for errors returned via the API
type ErrorResponse struct {
Code interface{} `json:"code"`
Message interface{} `json:"message"`
Invalid interface{} `json:"invalid,omitempty"`
}
// ResultResponseJSON will write the result as json to the http output
func ResultResponseJSON(w http.ResponseWriter, r *http.Request, status int, result interface{}) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
var response Response
resultClass := fmt.Sprintf("%v", reflect.TypeOf(result))
if resultClass == "http.ErrorResponse" {
response = Response{
Error: result,
}
} else {
response = Response{
Result: result,
}
}
var payload []byte
var err error
if getPrettyPrintFromContext(r) {
payload, err = json.MarshalIndent(response, "", " ")
} else {
payload, err = json.Marshal(response)
}
if err != nil {
logger.Error("ResponseMarshalError", err)
}
fmt.Fprint(w, string(payload))
}
// ResultSchemaErrorJSON will format the result as a standard error object and send it for output
func ResultSchemaErrorJSON(w http.ResponseWriter, r *http.Request, errs []jsonschema.KeyError) {
errorResponse := ErrorResponse{
Code: http.StatusBadRequest,
Message: errors.ErrValidationFailed,
Invalid: errs,
}
ResultResponseJSON(w, r, http.StatusBadRequest, errorResponse)
}
// ResultErrorJSON will format the result as a standard error object and send it for output
func ResultErrorJSON(w http.ResponseWriter, r *http.Request, status int, message string, extended interface{}) {
errorResponse := ErrorResponse{
Code: status,
Message: message,
Invalid: extended,
}
ResultResponseJSON(w, r, status, errorResponse)
}
// getPrettyPrintFromContext returns the PrettyPrint setting
func getPrettyPrintFromContext(r *http.Request) bool {
pretty, ok := r.Context().Value(c.PrettyPrintCtxKey).(bool)
if !ok {
return false
}
return pretty
}

View File

@@ -0,0 +1,61 @@
package api
import (
"io/fs"
"net/http"
"strings"
"github.com/Pacerino/cpm/embed"
"github.com/Pacerino/cpm/internal/api/handler"
"github.com/go-chi/chi/v5"
"github.com/go-chi/cors"
)
func NewRouter() http.Handler {
cors := cors.Handler(cors.Options{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-Requested-With"},
AllowCredentials: true,
MaxAge: 300,
})
r := chi.NewRouter()
h := handler.NewHandler()
r.Use(
cors,
)
return generateRoutes(r, h)
}
func generateRoutes(r chi.Router, h *handler.Handler) chi.Router {
r.Route("/api", func(r chi.Router) {
//Hosts
r.Route("/hosts", func(r chi.Router) {
r.Get("/", h.GetHosts()) // Get List of Hosts
r.Post("/", h.CreateHost()) // Create Host & save to the DB
r.Get("/{hostID:[0-9]+}", h.GetHost()) // Get specific Host by ID
r.Delete("/{hostID:[0-9]+}", h.DeleteHost()) // Delete Host by ID
r.Put("/", h.UpdateHost()) // Update Host by ID
})
})
fileServer(r)
return r
}
func fileServer(r chi.Router) {
r.Get("/*", func(w http.ResponseWriter, r *http.Request) {
rctx := chi.RouteContext(r.Context())
pathPrefix := strings.TrimSuffix(rctx.RoutePattern(), "/*")
fSys, err := fs.Sub(embed.Assets, "assets")
if err != nil {
panic(err)
}
fs := http.StripPrefix(pathPrefix, http.FileServer(http.FS(fSys)))
fs.ServeHTTP(w, r)
})
}

View File

@@ -0,0 +1,19 @@
package api
import (
"fmt"
"net/http"
"github.com/Pacerino/cpm/internal/logger"
)
const httpPort = 3001
// StartServer creates a http server
func StartServer() {
logger.Info("Server starting on port %v", httpPort)
err := http.ListenAndServe(fmt.Sprintf("0.0.0.0:%v", httpPort), NewRouter())
if err != nil {
logger.Error("HttpListenError", err)
}
}

View File

@@ -0,0 +1,53 @@
package caddy
import (
"fmt"
"os"
"github.com/Pacerino/cpm/embed"
"github.com/Pacerino/cpm/internal/config"
"github.com/Pacerino/cpm/internal/database"
"github.com/Pacerino/cpm/internal/logger"
"github.com/aymerick/raymond"
)
type hostEntity struct {
Host database.Host
LogPath string
}
func WriteHost(h database.Host) error {
data := &hostEntity{
Host: h,
LogPath: fmt.Sprintf("%s/host_%d.log", config.Configuration.LogFolder, h.ID),
}
filename := fmt.Sprintf("%s/host_%d.conf", config.Configuration.DataFolder, h.ID)
// Read Template from Embed FS
template, err := embed.CaddyFiles.ReadFile("caddy/host.hbs")
if err != nil {
logger.Error(err.Error(), err)
return err
}
// Parse Data into Template
tmplOutput, err := raymond.Render(string(template), data)
if err != nil {
logger.Error(err.Error(), err)
return err
}
// Write filled out template to the config folder
if err := os.WriteFile(filename, []byte(tmplOutput), 0644); err != nil {
return err
}
return ReloadCaddy()
}
func RemoveHost(hostID int) error {
filename := fmt.Sprintf("%s/host_%d.conf", config.Configuration.DataFolder, hostID)
if err := os.Remove(filename); err != nil {
return err
}
return ReloadCaddy()
}

View File

@@ -0,0 +1,44 @@
package caddy
import (
"fmt"
"os/exec"
"github.com/Pacerino/cpm/internal/config"
"github.com/Pacerino/cpm/internal/logger"
)
func ReloadCaddy() error {
_, err := shExec([]string{"reload", "--config", config.Configuration.CaddyFile})
return err
}
func getCaddyFilePath() (string, error) {
path, err := exec.LookPath("caddy")
if err != nil {
return path, fmt.Errorf("cannot find caddy execuatable script in PATH")
}
return path, nil
}
// shExec executes caddy with arguments
func shExec(args []string) (string, error) {
ng, err := getCaddyFilePath()
if err != nil {
logger.Error("CaddyError", err)
return "", err
}
logger.Debug("CMD: %s %v", ng, args)
// nolint: gosec
c := exec.Command(ng, args...)
b, e := c.Output()
if e != nil {
logger.Error("CaddyError", fmt.Errorf("command error: %s -- %v\n%+v", ng, args, e))
logger.Warn(string(b))
}
return string(b), e
}

View File

@@ -0,0 +1,70 @@
package config
import (
"fmt"
golog "log"
"github.com/Pacerino/cpm/internal/logger"
"github.com/vrischmann/envconfig"
)
// Version is the version set by ldflags
var Version string
// Commit is the git commit set by ldflags
var Commit string
// Init will parse environment variables into the Env struct
func Init(version, commit *string) {
Version = *version
Commit = *commit
if err := envconfig.InitWithPrefix(&Configuration, "CPM"); err != nil {
fmt.Printf("%+v\n", err)
}
initLogger()
logger.Info("Build Version: %s (%s)", Version, Commit)
createDataFolders()
}
// Init initialises the Log object and return it
func initLogger() {
// this removes timestamp prefixes from logs
golog.SetFlags(0)
switch Configuration.Log.Level {
case "debug":
logLevel = logger.DebugLevel
case "warn":
logLevel = logger.WarnLevel
case "error":
logLevel = logger.ErrorLevel
default:
logLevel = logger.InfoLevel
}
err := logger.Configure(&logger.Config{
LogThreshold: logLevel,
Formatter: Configuration.Log.Format,
})
if err != nil {
logger.Error("LoggerConfigurationError", err)
}
}
// GetLogLevel returns the logger const level
func GetLogLevel() logger.Level {
return logLevel
}
/* func isError(errorClass string, err error) bool {
if err != nil {
logger.Error(errorClass, err)
return true
}
return false
}
*/

View File

@@ -0,0 +1,28 @@
package config
import (
"fmt"
"os"
"github.com/Pacerino/cpm/internal/logger"
)
func createDataFolders() {
folders := []string{
"hosts",
}
for _, folder := range folders {
path := folder
if path[0:1] != "/" {
path = fmt.Sprintf("%s/%s", Configuration.DataFolder, folder)
}
if _, err := os.Stat(path); os.IsNotExist(err) {
// folder does not exist
logger.Debug("Creating folder: %s", path)
if err := os.MkdirAll(path, os.ModePerm); err != nil {
logger.Error("CreateDataFolderError", err)
}
}
}
}

View File

@@ -0,0 +1,21 @@
package config
import "github.com/Pacerino/cpm/internal/logger"
// IsSetup defines whether we have an admin user or not
var IsSetup bool
var logLevel logger.Level
type log struct {
Level string `json:"level" envconfig:"optional,default=debug"`
Format string `json:"format" envconfig:"optional,default=nice"`
}
// Configuration is the main configuration object
var Configuration struct {
DataFolder string `json:"data_folder" envconfig:"optional,default=/etc/caddy/"`
LogFolder string `json:"log_folder" envconfig:"optional,default=/var/log/caddy"`
CaddyFile string `json:"caddy_file" envconfig:"optional,default=/etc/caddy/Caddyfile"`
Log log `json:"log"`
}

View File

@@ -0,0 +1,16 @@
package database
import "gorm.io/gorm"
type Host struct {
gorm.Model
Domains string `json:"domains" validate:"required,fqdn|hostname_port"`
Matcher string `json:"matcher"`
Upstreams []Upstream
}
type Upstream struct {
gorm.Model
HostID uint `json:"hostId"`
Backend string `json:"backend" validate:"required,hostname_port"`
}

View File

@@ -0,0 +1,40 @@
package database
import (
"fmt"
"github.com/Pacerino/cpm/internal/config"
"github.com/Pacerino/cpm/internal/logger"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
var dbInstance *gorm.DB
// NewDB creates a new connection
func NewDB() {
logger.Info("Creating new DB instance")
dbInstance = SqliteDB()
dbInstance.AutoMigrate(&Host{}, &Upstream{})
}
// GetInstance returns an existing or new instance
func GetInstance() *gorm.DB {
if dbInstance == nil {
NewDB()
}
return dbInstance
}
// SqliteDB Create sqlite client
func SqliteDB() *gorm.DB {
dbFile := fmt.Sprintf("%s/caddyproxymanager.db", config.Configuration.DataFolder)
db, err := gorm.Open(sqlite.Open(dbFile), &gorm.Config{})
if err != nil {
logger.Error("SqliteError", err)
return nil
}
return db
}

View File

@@ -0,0 +1,16 @@
package errors
import "errors"
// All error messages used by the service package to report
// problems back to calling clients
var (
ErrDatabaseUnavailable = errors.New("database-unavailable")
ErrDuplicateEmailUser = errors.New("email-already-exists")
ErrInvalidLogin = errors.New("invalid-login-credentials")
ErrUserDisabled = errors.New("user-disabled")
ErrSystemUserReadonly = errors.New("cannot-save-system-users")
ErrValidationFailed = errors.New("request-failed-validation")
ErrCurrentPasswordInvalid = errors.New("current-password-invalid")
ErrCABundleDoesNotExist = errors.New("ca-bundle-does-not-exist")
)

View File

@@ -0,0 +1,46 @@
package jobqueue
import (
"context"
"errors"
)
var (
ctx context.Context
cancel context.CancelFunc
worker *Worker
)
// Start will intantiate the queue and start doing work
func Start() {
ctx, cancel = context.WithCancel(context.Background())
q := &Queue{
jobs: make(chan Job),
ctx: ctx,
cancel: cancel,
}
// Defines a queue worker, which will execute our queue.
worker = newWorker(q)
// Execute jobs in queue.
go worker.doWork()
}
// Shutdown will gracefully stop the queue
func Shutdown() error {
if cancel == nil {
return errors.New("unable to shutdown, jobqueue has not been started")
}
cancel()
return nil
}
// AddJob adds a job to the queue for processing
func AddJob(j Job) error {
if worker == nil {
return errors.New("unable to add job, jobqueue has not been started")
}
worker.Queue.AddJob(j)
return nil
}

View File

@@ -0,0 +1,54 @@
package jobqueue
import (
"context"
"sync"
)
// Queue holds name, list of jobs and context with cancel.
type Queue struct {
jobs chan Job
ctx context.Context
cancel context.CancelFunc
}
// Job - holds logic to perform some operations during queue execution.
type Job struct {
Name string
Action func() error // A function that should be executed when the job is running.
}
// AddJobs adds jobs to the queue and cancels channel.
func (q *Queue) AddJobs(jobs []Job) {
var wg sync.WaitGroup
wg.Add(len(jobs))
for _, job := range jobs {
// Goroutine which adds job to the queue.
go func(job Job) {
q.AddJob(job)
wg.Done()
}(job)
}
go func() {
wg.Wait()
// Cancel queue channel, when all goroutines were done.
q.cancel()
}()
}
// AddJob sends job to the channel.
func (q *Queue) AddJob(job Job) {
q.jobs <- job
}
// Run performs job execution.
func (j Job) Run() error {
err := j.Action()
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,37 @@
package jobqueue
import (
"fmt"
"github.com/Pacerino/cpm/internal/logger"
)
// Worker responsible for queue serving.
type Worker struct {
Queue *Queue
}
func newWorker(queue *Queue) *Worker {
return &Worker{
Queue: queue,
}
}
// doWork processes jobs from the queue (jobs channel).
func (w *Worker) doWork() bool {
for {
select {
// if context was canceled.
case <-w.Queue.ctx.Done():
logger.Info("JobQueue worker graceful shutdown")
return true
// if job received.
case job := <-w.Queue.jobs:
err := job.Run()
if err != nil {
logger.Error(fmt.Sprintf("%sError", job.Name), err)
continue
}
}
}
}

View File

@@ -0,0 +1,37 @@
package logger
// Level type
type Level int
// Log level definitions
const (
// DebugLevel usually only enabled when debugging. Very verbose logging.
DebugLevel Level = 10
// InfoLevel general operational entries about what's going on inside the application.
InfoLevel Level = 20
// WarnLevel non-critical entries that deserve eyes.
WarnLevel Level = 30
// ErrorLevel used for errors that should definitely be noted.
ErrorLevel Level = 40
)
// Config options for the logger.
type Config struct {
LogThreshold Level
Formatter string
}
// Interface for a logger
type Interface interface {
GetLogLevel() Level
Debug(format string, args ...interface{})
Info(format string, args ...interface{})
Warn(format string, args ...interface{})
Error(errorClass string, err error, args ...interface{})
Errorf(errorClass, format string, err error, args ...interface{})
}
// ConfigurableLogger is an interface for a logger that can be configured
type ConfigurableLogger interface {
Configure(c *Config) error
}

View File

@@ -0,0 +1,243 @@
package logger
import (
"encoding/json"
"fmt"
stdlog "log"
"os"
"runtime/debug"
"sync"
"time"
"github.com/fatih/color"
/* "github.com/getsentry/sentry-go" */)
var colorReset, colorGray, colorYellow, colorBlue, colorRed, colorMagenta, colorBlack, colorWhite *color.Color
// Log message structure.
type Log struct {
Timestamp string `json:"timestamp"`
Level string `json:"level"`
Message string `json:"message"`
Pid int `json:"pid"`
Summary string `json:"summary,omitempty"`
Caller string `json:"caller,omitempty"`
StackTrace []string `json:"stack_trace,omitempty"`
}
// Logger instance
type Logger struct {
Config
mux sync.Mutex
}
// global logging configuration.
var logger = NewLogger()
// NewLogger creates a new logger instance
func NewLogger() *Logger {
color.NoColor = false
colorReset = color.New(color.Reset)
colorGray = color.New(color.FgWhite)
colorYellow = color.New(color.Bold, color.FgYellow)
colorBlue = color.New(color.Bold, color.FgBlue)
colorRed = color.New(color.Bold, color.FgRed)
colorMagenta = color.New(color.Bold, color.FgMagenta)
colorBlack = color.New(color.Bold, color.FgBlack)
colorWhite = color.New(color.Bold, color.FgWhite)
return &Logger{
Config: NewConfig(),
}
}
// NewConfig returns the default config
func NewConfig() Config {
return Config{
LogThreshold: InfoLevel,
Formatter: "json",
}
}
// Configure logger and will return error if missing required fields.
func Configure(c *Config) error {
return logger.Configure(c)
}
// GetLogLevel currently configured
func GetLogLevel() Level {
return logger.GetLogLevel()
}
// Debug logs if the log level is set to DebugLevel or below. Arguments are handled in the manner of fmt.Printf.
func Debug(format string, args ...interface{}) {
logger.Debug(format, args...)
}
// Info logs if the log level is set to InfoLevel or below. Arguments are handled in the manner of fmt.Printf.
func Info(format string, args ...interface{}) {
logger.Info(format, args...)
}
// Warn logs if the log level is set to WarnLevel or below. Arguments are handled in the manner of fmt.Printf.
func Warn(format string, args ...interface{}) {
logger.Warn(format, args...)
}
// Error logs error given if the log level is set to ErrorLevel or below. Arguments are not logged.
// Attempts to log to bugsang.
func Error(errorClass string, err error) {
logger.Error(errorClass, err)
os.Exit(1)
}
// Configure logger and will return error if missing required fields.
func (l *Logger) Configure(c *Config) error {
// ensure updates to the config are atomic
l.mux.Lock()
defer l.mux.Unlock()
if c == nil {
return fmt.Errorf("a non nil Config is mandatory")
}
if err := c.LogThreshold.validate(); err != nil {
return err
}
l.LogThreshold = c.LogThreshold
l.Formatter = c.Formatter
/* l.SentryConfig = c.SentryConfig */
/* if c.SentryConfig.Dsn != "" {
if sentryErr := sentry.Init(c.SentryConfig); sentryErr != nil {
fmt.Printf("Sentry initialization failed: %v\n", sentryErr)
}
} */
stdlog.SetFlags(0) // this removes timestamp prefixes from logs
return nil
}
// validate the log level is in the accepted list.
func (l Level) validate() error {
switch l {
case DebugLevel, InfoLevel, WarnLevel, ErrorLevel:
return nil
default:
return fmt.Errorf("invalid \"Level\" %d", l)
}
}
var logLevels = map[Level]string{
DebugLevel: "DEBUG",
InfoLevel: "INFO",
WarnLevel: "WARN",
ErrorLevel: "ERROR",
}
func (l *Logger) logLevel(logLevel Level, format string, args ...interface{}) {
if logLevel < l.LogThreshold {
return
}
errorClass := ""
if logLevel == ErrorLevel {
// First arg is the errorClass
errorClass = args[0].(string)
if len(args) > 1 {
args = args[1:]
} else {
args = []interface{}{}
}
}
stringMessage := fmt.Sprintf(format, args...)
if l.Formatter == "json" {
// JSON Log Format
jsonLog, _ := json.Marshal(
Log{
Timestamp: time.Now().Format(time.RFC3339Nano),
Level: logLevels[logLevel],
Message: stringMessage,
Pid: os.Getpid(),
},
)
stdlog.Println(string(jsonLog))
} else {
// Nice Log Format
var colorLevel *color.Color
switch logLevel {
case DebugLevel:
colorLevel = colorMagenta
case InfoLevel:
colorLevel = colorBlue
case WarnLevel:
colorLevel = colorYellow
case ErrorLevel:
colorLevel = colorRed
stringMessage = fmt.Sprintf("%s: %s", errorClass, stringMessage)
}
t := time.Now()
stdlog.Println(
colorBlack.Sprint("["),
colorWhite.Sprint(t.Format("2006-01-02 15:04:05")),
colorBlack.Sprint("] "),
colorLevel.Sprintf("%-8v", logLevels[logLevel]),
colorGray.Sprint(stringMessage),
colorReset.Sprint(""),
)
if logLevel == ErrorLevel && l.LogThreshold == DebugLevel {
// Print a stack trace too
debug.PrintStack()
}
}
}
// GetLogLevel currently configured
func (l *Logger) GetLogLevel() Level {
return l.LogThreshold
}
// Debug logs if the log level is set to DebugLevel or below. Arguments are handled in the manner of fmt.Printf.
func (l *Logger) Debug(format string, args ...interface{}) {
l.logLevel(DebugLevel, format, args...)
}
// Info logs if the log level is set to InfoLevel or below. Arguments are handled in the manner of fmt.Printf.
func (l *Logger) Info(format string, args ...interface{}) {
l.logLevel(InfoLevel, format, args...)
}
// Warn logs if the log level is set to WarnLevel or below. Arguments are handled in the manner of fmt.Printf.
func (l *Logger) Warn(format string, args ...interface{}) {
l.logLevel(WarnLevel, format, args...)
}
// Error logs error given if the log level is set to ErrorLevel or below. Arguments are not logged.
// Attempts to log to bugsang.
func (l *Logger) Error(errorClass string, err error) {
l.logLevel(ErrorLevel, err.Error(), errorClass)
/* l.notifySentry(errorClass, err) */
}
/* func (l *Logger) notifySentry(errorClass string, err error) {
if l.SentryConfig.Dsn != "" && l.SentryConfig.Dsn != "-" {
sentry.ConfigureScope(func(scope *sentry.Scope) {
scope.SetLevel(sentry.LevelError)
scope.SetTag("service", "backend")
scope.SetTag("error_class", errorClass)
})
sentry.CaptureException(err)
// Since sentry emits events in the background we need to make sure
// they are sent before we shut down
sentry.Flush(time.Second * 5)
}
}
*/

View File

@@ -0,0 +1,36 @@
package util
// FindItemInInterface Find key in interface (recursively) and return value as interface
func FindItemInInterface(key string, obj interface{}) (interface{}, bool) {
// if the argument is not a map, ignore it
mobj, ok := obj.(map[string]interface{})
if !ok {
return nil, false
}
for k, v := range mobj {
// key match, return value
if k == key {
return v, true
}
// if the value is a map, search recursively
if m, ok := v.(map[string]interface{}); ok {
if res, ok := FindItemInInterface(key, m); ok {
return res, true
}
}
// if the value is an array, search recursively
// from each element
if va, ok := v.([]interface{}); ok {
for _, a := range va {
if res, ok := FindItemInInterface(key, a); ok {
return res, true
}
}
}
}
// element not found
return nil, false
}

View File

@@ -0,0 +1,44 @@
package util
import (
"strconv"
"strings"
)
// SliceContainsItem returns whether the slice given contains the item given
func SliceContainsItem(slice []string, item string) bool {
for _, a := range slice {
if a == item {
return true
}
}
return false
}
// SliceContainsInt returns whether the slice given contains the item given
func SliceContainsInt(slice []int, item int) bool {
for _, a := range slice {
if a == item {
return true
}
}
return false
}
// ConvertIntSliceToString returns a comma separated string of all items in the slice
func ConvertIntSliceToString(slice []int) string {
strs := []string{}
for _, item := range slice {
strs = append(strs, strconv.Itoa(item))
}
return strings.Join(strs, ",")
}
// ConvertStringSliceToInterface is required in some special cases
func ConvertStringSliceToInterface(slice []string) []interface{} {
res := make([]interface{}, len(slice))
for i := range slice {
res[i] = slice[i]
}
return res
}