init repo
This commit is contained in:
commit
04e14928f3
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
map.json
|
||||||
|
|
||||||
|
hmdeploy
|
||||||
146
.golangci.yml
Normal file
146
.golangci.yml
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
linters-settings:
|
||||||
|
depguard:
|
||||||
|
rules:
|
||||||
|
Main:
|
||||||
|
files:
|
||||||
|
- $all
|
||||||
|
- '!$test'
|
||||||
|
allow:
|
||||||
|
- $gostd
|
||||||
|
- gitea.thegux.fr
|
||||||
|
- github.com
|
||||||
|
Test:
|
||||||
|
files:
|
||||||
|
- $test
|
||||||
|
allow:
|
||||||
|
- $gostd
|
||||||
|
- gitea.thegux.fr
|
||||||
|
- github.com
|
||||||
|
dupl:
|
||||||
|
threshold: 100
|
||||||
|
funlen:
|
||||||
|
lines: 100
|
||||||
|
statements: 50
|
||||||
|
gci:
|
||||||
|
sections:
|
||||||
|
- "standard"
|
||||||
|
- "default"
|
||||||
|
- "blank"
|
||||||
|
- "dot"
|
||||||
|
# - "alias"
|
||||||
|
- "prefix(gitea.thegux.fr)"
|
||||||
|
goconst:
|
||||||
|
min-len: 2
|
||||||
|
min-occurrences: 2
|
||||||
|
gocritic:
|
||||||
|
enabled-tags:
|
||||||
|
- diagnostic
|
||||||
|
- experimental
|
||||||
|
- opinionated
|
||||||
|
- performance
|
||||||
|
- style
|
||||||
|
disabled-checks:
|
||||||
|
- dupImport # https://github.com/go-critic/go-critic/issues/845
|
||||||
|
- ifElseChain
|
||||||
|
- octalLiteral
|
||||||
|
# - whyNoLint
|
||||||
|
- wrapperFunc
|
||||||
|
gocyclo:
|
||||||
|
min-complexity: 15
|
||||||
|
goimports:
|
||||||
|
local-prefixes: gitea.thegux.fr
|
||||||
|
mnd:
|
||||||
|
checks:
|
||||||
|
- argument
|
||||||
|
- case
|
||||||
|
- condition
|
||||||
|
- return
|
||||||
|
govet:
|
||||||
|
disable:
|
||||||
|
- fieldalignment
|
||||||
|
lll:
|
||||||
|
line-length: 200
|
||||||
|
misspell:
|
||||||
|
locale: US
|
||||||
|
nolintlint:
|
||||||
|
allow-unused: false # report any unused nolint directives
|
||||||
|
require-explanation: false # don't require an explanation for nolint directives
|
||||||
|
require-specific: false # don't require nolint directives to be specific about which linter is being skipped
|
||||||
|
errcheck:
|
||||||
|
check-blank: true
|
||||||
|
exclude-functions:
|
||||||
|
- '(*github.com/gin-gonic/gin.Error).SetType'
|
||||||
|
- '(*github.com/gin-gonic/gin.Context).Error'
|
||||||
|
|
||||||
|
linters:
|
||||||
|
disable-all: true
|
||||||
|
enable:
|
||||||
|
- bodyclose
|
||||||
|
- depguard
|
||||||
|
- dogsled
|
||||||
|
- dupl
|
||||||
|
- errcheck
|
||||||
|
- exhaustive
|
||||||
|
- exportloopref
|
||||||
|
- funlen
|
||||||
|
- gochecknoinits
|
||||||
|
- goconst
|
||||||
|
- gocritic
|
||||||
|
- gocyclo
|
||||||
|
- gofmt
|
||||||
|
- goimports
|
||||||
|
# - mnd
|
||||||
|
- goprintffuncname
|
||||||
|
- gosec
|
||||||
|
- gosimple
|
||||||
|
- govet
|
||||||
|
# - inamedparam
|
||||||
|
- ineffassign
|
||||||
|
- lll
|
||||||
|
- misspell
|
||||||
|
- nakedret
|
||||||
|
- noctx
|
||||||
|
- nolintlint
|
||||||
|
# - perfsprint
|
||||||
|
- rowserrcheck
|
||||||
|
# - sloglint
|
||||||
|
- staticcheck
|
||||||
|
- stylecheck
|
||||||
|
- typecheck
|
||||||
|
- unconvert
|
||||||
|
- unparam
|
||||||
|
- unused
|
||||||
|
- whitespace
|
||||||
|
# - gochecknoglobals # too many global in ds9
|
||||||
|
|
||||||
|
# don't enable:
|
||||||
|
# - asciicheck
|
||||||
|
# - scopelint
|
||||||
|
# - gocognit
|
||||||
|
# - godot
|
||||||
|
# - godox
|
||||||
|
# - goerr113
|
||||||
|
# - interfacer
|
||||||
|
# - maligned
|
||||||
|
# - nestif
|
||||||
|
# - prealloc
|
||||||
|
# - testpackage
|
||||||
|
# - revive
|
||||||
|
# - wsl
|
||||||
|
|
||||||
|
# issues:
|
||||||
|
# Excluding configuration per-path, per-linter, per-text and per-source
|
||||||
|
# fix: true
|
||||||
|
|
||||||
|
issues:
|
||||||
|
exclude-rules:
|
||||||
|
- path: '(.+)_test\.go'
|
||||||
|
linters:
|
||||||
|
- funlen
|
||||||
|
- goconst
|
||||||
|
- dupl
|
||||||
|
exclude-dirs:
|
||||||
|
- ..
|
||||||
|
|
||||||
|
run:
|
||||||
|
timeout: 5m
|
||||||
8
Makefile
Normal file
8
Makefile
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
run: lint
|
||||||
|
go run main.go
|
||||||
|
|
||||||
|
build: lint
|
||||||
|
go build -o hmdeploy main.go
|
||||||
|
|
||||||
|
lint:
|
||||||
|
golangci-lint run ./...
|
||||||
8
connection/connection.go
Normal file
8
connection/connection.go
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
package connection
|
||||||
|
|
||||||
|
type IConnection interface {
|
||||||
|
Execute(string) (string, error)
|
||||||
|
CopyFile(src, dest string) error
|
||||||
|
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
132
connection/ssh_connection.go
Normal file
132
connection/ssh_connection.go
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
package connection
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SSHConn struct {
|
||||||
|
addr string
|
||||||
|
client *ssh.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ IConnection = (*SSHConn)(nil)
|
||||||
|
|
||||||
|
func NewSSHConn(addr, user string, port int, privkey string) (SSHConn, error) {
|
||||||
|
var newconn SSHConn
|
||||||
|
|
||||||
|
sshAddr := addr + ":" + strconv.Itoa(port)
|
||||||
|
newconn.addr = sshAddr
|
||||||
|
|
||||||
|
conn, err := net.Dial("tcp", sshAddr)
|
||||||
|
if err != nil {
|
||||||
|
log.Err(err).Str("addr", addr).Msg("unable to dial ssh addr")
|
||||||
|
return newconn, err
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := os.ReadFile(privkey)
|
||||||
|
if err != nil {
|
||||||
|
log.Err(err).Str("private key", privkey).Msg("unable to read ssh private key")
|
||||||
|
return newconn, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sshPrivKey, err := ssh.ParsePrivateKey(c)
|
||||||
|
if err != nil {
|
||||||
|
log.Err(err).Str("private key", privkey).Msg("unable to parse ssh private key")
|
||||||
|
return newconn, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sshConfig := ssh.ClientConfig{
|
||||||
|
User: user,
|
||||||
|
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||||
|
Auth: []ssh.AuthMethod{
|
||||||
|
ssh.PublicKeys(sshPrivKey),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
sshConn, chNewChannel, chReq, err := ssh.NewClientConn(conn, sshAddr, &sshConfig)
|
||||||
|
if err != nil {
|
||||||
|
log.Err(err).Str("addr", sshAddr).Msg("unable to establish a new connection to the swarm")
|
||||||
|
return newconn, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sshClient := ssh.NewClient(sshConn, chNewChannel, chReq)
|
||||||
|
newconn.client = sshClient
|
||||||
|
|
||||||
|
log.Info().Str("addr", addr).Int("port", port).Msg("ssh connection sucessfully initialized")
|
||||||
|
return newconn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *SSHConn) Close() error {
|
||||||
|
return c.client.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *SSHConn) CopyFile(src, dest string) error {
|
||||||
|
sshSession, err := c.client.NewSession()
|
||||||
|
if err != nil {
|
||||||
|
log.Err(err).Str("addr", c.addr).Msg("unable to open an ssh session")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer sshSession.Close()
|
||||||
|
|
||||||
|
fileInfo, err := os.Stat(src)
|
||||||
|
if err != nil {
|
||||||
|
log.Err(err).Str("file", src).Msg("unable to stat scp source file")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Open(src)
|
||||||
|
if err != nil {
|
||||||
|
log.Err(err).Str("file", src).Msg("unable to open scp source file")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
w, _ := sshSession.StdinPipe()
|
||||||
|
defer w.Close()
|
||||||
|
|
||||||
|
fmt.Fprintf(w, "C0644 %d %s\n", fileInfo.Size(), filepath.Base(dest))
|
||||||
|
|
||||||
|
if _, err := io.Copy(w, file); err != nil {
|
||||||
|
log.Err(err).Str("src", src).Str("dest", dest).Msg("unable to scp src to dest")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprint(w, "\x00")
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := sshSession.Run(fmt.Sprintf("scp -t %s", dest)); err != nil {
|
||||||
|
log.Err(err).Str("addr", c.addr).Str("dest", dest).Msg("unable to run scp command")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info().Str("src", src).Str("dest", dest).Msg("file successfully uploaded")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *SSHConn) Execute(cmd string) (string, error) {
|
||||||
|
sshSession, err := c.client.NewSession()
|
||||||
|
if err != nil {
|
||||||
|
log.Err(err).Str("addr", c.addr).Msg("unable to open an ssh session")
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer sshSession.Close()
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
sshSession.Stdout = &buf
|
||||||
|
if err := sshSession.Run(cmd); err != nil {
|
||||||
|
log.Err(err).Str("addr", c.addr).Str("command", cmd).Msg("unable to execute an ssh command")
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
6
deployers/commons.go
Normal file
6
deployers/commons.go
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package deployers
|
||||||
|
|
||||||
|
type IDeployer interface {
|
||||||
|
Deploy() error
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
96
deployers/swarm.go
Normal file
96
deployers/swarm.go
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
package deployers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
|
"gitea.thegux.fr/hmdeploy/connection"
|
||||||
|
"gitea.thegux.fr/hmdeploy/docker"
|
||||||
|
"gitea.thegux.fr/hmdeploy/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SwarmDeployer struct {
|
||||||
|
ctx context.Context
|
||||||
|
fnCancel context.CancelFunc
|
||||||
|
|
||||||
|
conn connection.IConnection
|
||||||
|
dcli docker.IClient
|
||||||
|
|
||||||
|
project *models.Project
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ IDeployer = (*SwarmDeployer)(nil)
|
||||||
|
|
||||||
|
func NewSwarmDeployer(ctx context.Context, dockerClient docker.IClient, netInfo *models.HMNetInfo, project *models.Project) (SwarmDeployer, error) {
|
||||||
|
var sm SwarmDeployer
|
||||||
|
|
||||||
|
conn, err := connection.NewSSHConn(netInfo.IP.String(), netInfo.SSH.User, netInfo.SSH.Port, netInfo.SSH.PrivKey)
|
||||||
|
if err != nil {
|
||||||
|
return sm, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctxChild, fnCancel := context.WithCancel(ctx)
|
||||||
|
|
||||||
|
sm.ctx = ctxChild
|
||||||
|
sm.fnCancel = fnCancel
|
||||||
|
|
||||||
|
sm.conn = &conn
|
||||||
|
sm.dcli = dockerClient
|
||||||
|
sm.project = project
|
||||||
|
|
||||||
|
return sm, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sd *SwarmDeployer) Close() error {
|
||||||
|
return sd.conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sd *SwarmDeployer) clean() (err error) {
|
||||||
|
_, err = sd.conn.Execute(fmt.Sprintf("rm -f %s %s", models.ComposeFile, models.EnvFile))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sd *SwarmDeployer) Deploy() error {
|
||||||
|
defer sd.clean()
|
||||||
|
|
||||||
|
if sd.project.ImageName != "" {
|
||||||
|
tarFile, err := sd.dcli.Save(sd.project.ImageName, sd.project.Dir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer os.Remove(tarFile)
|
||||||
|
|
||||||
|
tarFileBase := filepath.Base(tarFile)
|
||||||
|
if err := sd.conn.CopyFile(tarFile, tarFileBase); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := sd.conn.Execute(fmt.Sprintf("docker load -i %s && rm %s", tarFileBase, tarFileBase)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if envFilePath := sd.project.Deps.EnvFile; envFilePath != "" {
|
||||||
|
envFileBase := filepath.Base(envFilePath)
|
||||||
|
if err := sd.conn.CopyFile(filepath.Join(sd.project.Dir, envFileBase), envFileBase); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
composeFileBase := filepath.Base(sd.project.Deps.ComposeFile)
|
||||||
|
if err := sd.conn.CopyFile(filepath.Join(sd.project.Dir, composeFileBase), composeFileBase); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := sd.conn.Execute(fmt.Sprintf("docker stack deploy -c %s %s", composeFileBase, sd.project.Name)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info().Msg("project deployed successfully on swarm")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
46
docker/client.go
Normal file
46
docker/client.go
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IClient interface {
|
||||||
|
Save(imageName, dest string) (string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Client struct{}
|
||||||
|
|
||||||
|
var _ IClient = (*Client)(nil)
|
||||||
|
|
||||||
|
func NewClient() Client {
|
||||||
|
return Client{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Save(imageName, dest string) (string, error) {
|
||||||
|
destInfo, err := os.Stat(dest)
|
||||||
|
if err != nil {
|
||||||
|
log.Err(err).Str("dest", dest).Msg("unable to stat dest directory")
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !destInfo.IsDir() {
|
||||||
|
log.Err(err).Str("dest", dest).Msg("dest directory must be a directory")
|
||||||
|
}
|
||||||
|
|
||||||
|
tarFile := fmt.Sprintf("%s.tar", imageName)
|
||||||
|
|
||||||
|
cmd := exec.Command("docker", "save", "-o", tarFile, imageName)
|
||||||
|
cmd.Dir = dest
|
||||||
|
if _, err := cmd.Output(); err != nil {
|
||||||
|
log.Err(err).Str("image", imageName).Str("dest", dest).Msg("unable to save image into tar")
|
||||||
|
return dest, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info().Str("image", imageName).Str("dest", dest).Msg("image successfully saved")
|
||||||
|
return filepath.Join(dest, tarFile), nil
|
||||||
|
}
|
||||||
16
go.mod
Normal file
16
go.mod
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
module gitea.thegux.fr/hmdeploy
|
||||||
|
|
||||||
|
go 1.23.0
|
||||||
|
|
||||||
|
toolchain go1.23.7
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/rs/zerolog v1.34.0
|
||||||
|
golang.org/x/crypto v0.36.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||||
|
golang.org/x/sys v0.31.0 // indirect
|
||||||
|
)
|
||||||
20
go.sum
Normal file
20
go.sum
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||||
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||||
|
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||||
|
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||||
|
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||||
|
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||||
|
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
|
||||||
|
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
|
||||||
75
main.go
Normal file
75
main.go
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
|
"gitea.thegux.fr/hmdeploy/deployers"
|
||||||
|
"gitea.thegux.fr/hmdeploy/docker"
|
||||||
|
"gitea.thegux.fr/hmdeploy/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
const HMDEPLOY_DIRNAME = ".homeserver"
|
||||||
|
const NETWORK_FILENAME = "map.json"
|
||||||
|
|
||||||
|
var HOME_PATH = os.Getenv("HOME")
|
||||||
|
|
||||||
|
func initLogger() {
|
||||||
|
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
|
||||||
|
log.Logger = log.With().Caller().Logger().Output(zerolog.ConsoleWriter{Out: os.Stderr})
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
ctx, stop := signal.NotifyContext(
|
||||||
|
context.Background(),
|
||||||
|
os.Interrupt,
|
||||||
|
os.Kill,
|
||||||
|
)
|
||||||
|
defer stop()
|
||||||
|
|
||||||
|
initLogger()
|
||||||
|
log.Info().Msg("hmdeploy started")
|
||||||
|
|
||||||
|
projectDir := flag.String("path", ".", "define the .homeserver project root dir")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
hmmap_path := path.Join(HOME_PATH, HMDEPLOY_DIRNAME, NETWORK_FILENAME)
|
||||||
|
c, err := os.ReadFile(hmmap_path)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err).Str("conf", hmmap_path).Msg("unable to load configuration")
|
||||||
|
}
|
||||||
|
|
||||||
|
var hmmap models.HMMap
|
||||||
|
if err := json.Unmarshal(c, &hmmap); err != nil {
|
||||||
|
log.Fatal().Err(err).Str("conf", hmmap_path).Msg("unable to parse configuration")
|
||||||
|
}
|
||||||
|
log.Info().Str("conf", hmmap_path).Msg("hmmap load successfully")
|
||||||
|
|
||||||
|
swarmNet := hmmap.GetSwarmNetInfo()
|
||||||
|
if swarmNet == nil {
|
||||||
|
log.Fatal().Err(err).Msg("unable to get swarm net info, does not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
project, err := models.ProjectFromDir(*projectDir)
|
||||||
|
if err != nil {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
dcli := docker.NewClient()
|
||||||
|
|
||||||
|
sd, err := deployers.NewSwarmDeployer(ctx, &dcli, swarmNet, &project)
|
||||||
|
if err != nil {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := sd.Deploy(); err != nil {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
36
models/hm.go
Normal file
36
models/hm.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HMNetInfo struct {
|
||||||
|
IP net.IP `json:"ip"`
|
||||||
|
WebURL string `json:"web_url,omitempty"`
|
||||||
|
SSH struct {
|
||||||
|
User string `json:"user"`
|
||||||
|
PrivKey string `json:"privkey"`
|
||||||
|
Port int `json:"port"`
|
||||||
|
} `json:"ssh,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HMVM map[string]*HMNetInfo
|
||||||
|
type HMLXC map[string]*HMNetInfo
|
||||||
|
|
||||||
|
type HMMap struct {
|
||||||
|
*HMNetInfo
|
||||||
|
VM HMVM `json:"vm,omitempty"`
|
||||||
|
LXC HMLXC `json:"lxc,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hm *HMMap) GetSwarmNetInfo() *HMNetInfo {
|
||||||
|
data, ok := hm.VM["swarm"]
|
||||||
|
if !ok {
|
||||||
|
log.Error().Msg("unable to get swarm net info, check your configuration")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
114
models/project.go
Normal file
114
models/project.go
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
MainDir string = ".homeserver"
|
||||||
|
|
||||||
|
ComposeFile string = "docker-compose.deploy.yml"
|
||||||
|
EnvFile = ".env"
|
||||||
|
NginxFile = "nginx.conf"
|
||||||
|
|
||||||
|
ConfFile = "hmdeploy.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidEnvFile = errors.New("unable to stat .env file")
|
||||||
|
ErrInvalidComposeFile = errors.New("unable to stat compose file")
|
||||||
|
ErrInvalidNginxFile = errors.New("unable to stat nginx file")
|
||||||
|
)
|
||||||
|
|
||||||
|
func getFileInfo(baseDir, filePath string) (fs.FileInfo, error) {
|
||||||
|
var fInf fs.FileInfo
|
||||||
|
|
||||||
|
filePath = filepath.Clean(filePath)
|
||||||
|
filePath = filepath.Join(baseDir, filePath)
|
||||||
|
|
||||||
|
composePath, err := filepath.Abs(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return fInf, fmt.Errorf("%w, %v", ErrInvalidComposeFile, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fInf, err = os.Stat(composePath)
|
||||||
|
if err != nil {
|
||||||
|
return fInf, fmt.Errorf("%w, %v", ErrInvalidComposeFile, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fInf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Project struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Dir string
|
||||||
|
ImageName string `json:"image"`
|
||||||
|
Deps struct {
|
||||||
|
EnvFile string `json:"env"`
|
||||||
|
EnvFileInfo fs.FileInfo
|
||||||
|
|
||||||
|
ComposeFile string `json:"compose"`
|
||||||
|
ComposeFileInfo fs.FileInfo
|
||||||
|
|
||||||
|
NginxFile string `json:"nginx"`
|
||||||
|
NginxFileInfo fs.FileInfo
|
||||||
|
} `json:"dependencies"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Project) validate() error {
|
||||||
|
cfs, err := getFileInfo(p.Dir, p.Deps.ComposeFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%w, %v", ErrInvalidComposeFile, err)
|
||||||
|
}
|
||||||
|
p.Deps.ComposeFileInfo = cfs
|
||||||
|
|
||||||
|
if p.Deps.EnvFile != "" {
|
||||||
|
efs, err := getFileInfo(p.Dir, p.Deps.EnvFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%w, %v", ErrInvalidEnvFile, err)
|
||||||
|
}
|
||||||
|
p.Deps.EnvFileInfo = efs
|
||||||
|
} else {
|
||||||
|
log.Warn().Msg("no .env file provided, hoping one is set elsewhere...")
|
||||||
|
}
|
||||||
|
|
||||||
|
nfs, err := getFileInfo(p.Dir, p.Deps.EnvFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%w, %v", ErrInvalidNginxFile, err)
|
||||||
|
}
|
||||||
|
p.Deps.NginxFileInfo = nfs
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ProjectFromDir(dir string) (Project, error) {
|
||||||
|
var p Project
|
||||||
|
|
||||||
|
dir = filepath.Join(dir, MainDir)
|
||||||
|
p.Dir = dir
|
||||||
|
|
||||||
|
content, err := os.ReadFile(filepath.Join(dir, ConfFile))
|
||||||
|
if err != nil {
|
||||||
|
log.Err(err).Str("dir", dir).Str("conf", ConfFile).Msg("unable to read conf file")
|
||||||
|
return p, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(content, &p); err != nil {
|
||||||
|
log.Err(err).Str("dir", dir).Str("conf", ConfFile).Msg("unable to parse conf file")
|
||||||
|
return p, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.validate(); err != nil {
|
||||||
|
log.Err(err).Str("dir", dir).Msg("unable to validate project")
|
||||||
|
return p, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user