fix: restore tree dropped by 6d224861; land stem95su gdrive-sync (10m) [ci skip]
6d224861 came from a --no-checkout worktree whose empty index made the
commit drop every file except two. This restores 05b50d2b's full tree and
correctly adds stacks/stem95su/gdrive-sync.tf + the service-catalog stem95su
entry. Forward-only (parent=6d224861, no force-push); [ci skip] since the
live infra was never applied from the broken commit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
6d224861c4
commit
fd0f4a0365
1166 changed files with 358546 additions and 0 deletions
8
cli/Dockerfile
Normal file
8
cli/Dockerfile
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
FROM golang:alpine
|
||||
RUN mkdir /app
|
||||
ADD . /app/
|
||||
WORKDIR /app
|
||||
RUN go build -o infra_cli .
|
||||
RUN adduser -S -D -H -h /app appuser
|
||||
USER appuser
|
||||
CMD ["./infra_cli", "-h"]
|
||||
2
cli/README.md
Normal file
2
cli/README.md
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
# What is this?
|
||||
This is a CLI to manipulate files in the terraform repo and commit and push them
|
||||
81
cli/email_alias.go
Normal file
81
cli/email_alias.go
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/badoux/checkmail"
|
||||
"github.com/brianvoe/gofakeit/v6"
|
||||
"github.com/golang/glog"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
addEmailAliasUseCase = "add-email-alias"
|
||||
emailAliasFlagName = "forward-to"
|
||||
fromEmailDomainFlagName = "from-domain"
|
||||
emailAliasesConfigFileRelative = "/modules/kubernetes/mailserver/extra/aliases.txt"
|
||||
)
|
||||
|
||||
func addEmailAlias(gitFs *GitFS, to, fromDomain string) (string, error) {
|
||||
if err := checkmail.ValidateFormat(to); err != nil {
|
||||
return "", errors.Wrapf(err, fmt.Sprintf("failed to create new email aliases because invalid input format: %s", to))
|
||||
}
|
||||
if err := checkmail.ValidateHost(to); err != nil {
|
||||
return "", errors.Wrapf(err, fmt.Sprintf("failed to create new email aliases because domain for %s does not exist", to))
|
||||
}
|
||||
aliasEmail := generateRandomEmail(fromDomain)
|
||||
glog.Infof("adding %s -> %s alias to %s", aliasEmail, to, emailAliasesConfigFileRelative)
|
||||
|
||||
// Read existing contents
|
||||
fRead, err := (*gitFs.fs).OpenFile(emailAliasesConfigFileRelative, os.O_RDONLY, 0644)
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "failed to open file where email aliases are recorded")
|
||||
}
|
||||
fileContentsBytes, err := ioutil.ReadAll(fRead)
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "failed to read existing aliases file")
|
||||
}
|
||||
glog.Infof("current aliases file contents: \n%s", string(fileContentsBytes))
|
||||
defer fRead.Close()
|
||||
|
||||
newContents := getAddedAliasContents(string(fileContentsBytes), aliasEmail, to)
|
||||
// Write new contents
|
||||
fWrite, err := (*gitFs.fs).OpenFile(emailAliasesConfigFileRelative, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "failed to open file where new email alias will be added")
|
||||
}
|
||||
glog.Infof("writing new contents to file: \n%s", newContents)
|
||||
if _, err = fWrite.Write([]byte(newContents)); err != nil {
|
||||
return "", errors.Wrapf(err, "failed to write config to file")
|
||||
}
|
||||
defer fWrite.Close()
|
||||
return aliasEmail, nil
|
||||
}
|
||||
|
||||
func generateRandomEmail(fromDomain string) string {
|
||||
return fmt.Sprintf("%s-%s-generated%s", strings.ToLower(gofakeit.Adverb()), strings.ToLower(gofakeit.FirstName()), fromDomain)
|
||||
}
|
||||
|
||||
func getPostfixAlias(from, to string) string {
|
||||
return fmt.Sprintf("%s %s", from, to)
|
||||
}
|
||||
|
||||
func getAddedAliasContents(currentContents, from, to string) string {
|
||||
lines := strings.Split(currentContents, "\n")
|
||||
newLines := []string{}
|
||||
for _, l := range lines {
|
||||
l = strings.TrimSpace(l)
|
||||
if l == "" {
|
||||
continue
|
||||
}
|
||||
if strings.HasSuffix(l, to) {
|
||||
continue
|
||||
}
|
||||
newLines = append(newLines, l)
|
||||
}
|
||||
newLines = append(newLines, getPostfixAlias(from, to))
|
||||
return strings.Join(newLines, "\n") + "\n"
|
||||
}
|
||||
51
cli/git.go
Normal file
51
cli/git.go
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/go-git/go-billy/v5"
|
||||
"github.com/go-git/go-billy/v5/memfs"
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing/transport/http"
|
||||
memory "github.com/go-git/go-git/v5/storage/memory"
|
||||
"github.com/golang/glog"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
repository = "https://github.com/ViktorBarzin/infra"
|
||||
)
|
||||
|
||||
var (
|
||||
gitUser = os.Getenv("GIT_USER")
|
||||
gitToken = os.Getenv("GIT_TOKEN")
|
||||
)
|
||||
|
||||
type GitFS struct {
|
||||
repo *git.Repository
|
||||
fs *billy.Filesystem
|
||||
auth *http.BasicAuth
|
||||
}
|
||||
|
||||
func NewGitFS(repoURL string) (*GitFS, error) {
|
||||
glog.Infof("initializing new git fs from repo url: %s", repoURL)
|
||||
auth := &http.BasicAuth{
|
||||
Username: gitUser,
|
||||
Password: gitToken,
|
||||
}
|
||||
storer := memory.NewStorage()
|
||||
fs := memfs.New()
|
||||
|
||||
r, err := git.Clone(storer, fs, &git.CloneOptions{
|
||||
URL: repository,
|
||||
Auth: auth,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to clone repo from repo url '%s'", repoURL)
|
||||
}
|
||||
return &GitFS{repo: r, fs: &fs, auth: auth}, nil
|
||||
}
|
||||
|
||||
func (g *GitFS) Push() error {
|
||||
return g.repo.Push(&git.PushOptions{Auth: g.auth})
|
||||
}
|
||||
13
cli/go.mod
Normal file
13
cli/go.mod
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
module viktorbarzin/infra/cli
|
||||
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
github.com/badoux/checkmail v1.2.1 // indirect
|
||||
github.com/brianvoe/gofakeit/v6 v6.3.0 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.1.0 // indirect
|
||||
github.com/go-git/go-git/v5 v5.3.0 // indirect
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 // indirect
|
||||
)
|
||||
106
cli/go.sum
Normal file
106
cli/go.sum
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
|
||||
github.com/Microsoft/go-winio v0.4.16 h1:FtSW/jqD+l4ba5iPBj9CODVtgfYAD8w2wS923g/cFDk=
|
||||
github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=
|
||||
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/badoux/checkmail v1.2.1 h1:TzwYx5pnsV6anJweMx2auXdekBwGr/yt1GgalIx9nBQ=
|
||||
github.com/badoux/checkmail v1.2.1/go.mod h1:XroCOBU5zzZJcLvgwU15I+2xXyCdTWXyR9MGfRhBYy0=
|
||||
github.com/brianvoe/gofakeit/v6 v6.3.0 h1:h1M5XPubl81K+41Ry0g5P4Q9a7OCM8FgFf2Heey5j24=
|
||||
github.com/brianvoe/gofakeit/v6 v6.3.0/go.mod h1:palrJUk4Fyw38zIFB/uBZqsgzW5VsNllhHKKwAebzew=
|
||||
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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
|
||||
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4=
|
||||
github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E=
|
||||
github.com/go-git/go-billy/v5 v5.0.0 h1:7NQHvd9FVid8VL4qVUMm8XifBK+2xCoZ2lSk0agRrHM=
|
||||
github.com/go-git/go-billy/v5 v5.0.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
|
||||
github.com/go-git/go-billy/v5 v5.1.0 h1:4pl5BV4o7ZG/lterP4S6WzJ6xr49Ba5ET9ygheTYahk=
|
||||
github.com/go-git/go-billy/v5 v5.1.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw=
|
||||
github.com/go-git/go-git/v5 v5.2.0 h1:YPBLG/3UK1we1ohRkncLjaXWLW+HKp5QNM/jTli2JgI=
|
||||
github.com/go-git/go-git/v5 v5.2.0/go.mod h1:kh02eMX+wdqqxgNMEyq8YgwlIOsDOa9homkUq1PoTMs=
|
||||
github.com/go-git/go-git/v5 v5.3.0 h1:8WKMtJR2j8RntEXR/uvTKagfEt4GYlwQ7mntE4+0GWc=
|
||||
github.com/go-git/go-git/v5 v5.3.0/go.mod h1:xdX4bWJ48aOrdhnl2XqHYstHbbp6+LFS4r4X+lNVprw=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/imdario/mergo v0.3.9 h1:UauaLniWCFHWd+Jp9oCEkTBj8VO/9DKg3PV3VCNMDIg=
|
||||
github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
|
||||
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
|
||||
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY=
|
||||
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 h1:DowS9hvgyYSX4TO5NpyC606/Z4SxnNYbT+WX27or6Ck=
|
||||
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
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/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/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70=
|
||||
github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
|
||||
github.com/xanzy/ssh-agent v0.3.0 h1:wUMzuKtKilRgBAD1sUb8gOwwRr2FGoBVumcjoOACClI=
|
||||
github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0=
|
||||
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM=
|
||||
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210326060303-6b1517762897 h1:KrsHThm5nFk34YtATK1LsThyGhGbGe1olrte/HInHvs=
|
||||
golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 h1:uYVVQ9WP/Ds2ROhcaGPeIdVq0RIXVLwsHlnvJ+cT1So=
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210324051608-47abb6519492 h1:Paq34FxTluEPvVyayQqMPgHm+vTOrIifmcYxFBx9TLg=
|
||||
golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
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-20190902080502-41f04d3bba15/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/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
231
cli/main.go
Normal file
231
cli/main.go
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
"github.com/golang/glog"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
useCaseFlagName = "use-case"
|
||||
repoRootFlagName = "repo-root"
|
||||
printResultOnlyFlagName = "result-only"
|
||||
dynamicDnsDomainDefault = "viktorbarzin.ddns.net"
|
||||
publicDomainDefault = "viktorbarzin.me"
|
||||
)
|
||||
|
||||
var (
|
||||
validUseCases = []string{vpnUseCaseFlagName, setupOpenWRTDNSFlagName, addEmailAliasUseCase}
|
||||
)
|
||||
|
||||
func main() {
|
||||
err := run()
|
||||
if err != nil {
|
||||
glog.Errorf("run failed: %s", err.Error())
|
||||
os.Exit(255)
|
||||
}
|
||||
}
|
||||
|
||||
func run() error {
|
||||
useCase := flag.String(useCaseFlagName, "", fmt.Sprintf("Use case to run. Available use cases are: %+v", validUseCases))
|
||||
printResultOnly := flag.Bool(printResultOnlyFlagName, false, "Whether or not to print only the result (allocated ip) or print full command logging")
|
||||
// repoRootParam := flag.String(repoRootFlagName, "", fmt.Sprintf("Path to the root of the infra repository."))
|
||||
|
||||
// VPN flags
|
||||
vpnClientName := flag.String(vpnClientNameFlagName, "", fmt.Sprintf("Friendly VPN user name."))
|
||||
vpnClientPubKey := flag.String(vpnClientPubKeyFlagName, "", fmt.Sprintf("VPN client public key."))
|
||||
|
||||
// OpenWRT DNS flags
|
||||
openWRTNewDNS := flag.String(setupOpenWRTNewDNSFlagName, "", fmt.Sprintf("New DNS server to set."))
|
||||
|
||||
// add email alias flags
|
||||
emailToForwardTo := flag.String(emailAliasFlagName, "", "Email which is used to forward emails to.")
|
||||
fromDomain := flag.String(fromEmailDomainFlagName, "@viktorbarzin.me", "Domain name which will receive emails. Example @viktorbarzin.me")
|
||||
|
||||
// settings for updating the main domain using the dyndns domain
|
||||
dynDnsDomain := flag.String(dynDnsDomainFlagName, dynamicDnsDomainDefault, "Dynamic DNS domain to check against - used to update the main domain")
|
||||
publicDomain := flag.String(publicDomainFlagName, publicDomainDefault, "Public domain to update")
|
||||
|
||||
// Flag definitions above!
|
||||
flag.Parse()
|
||||
|
||||
if !*printResultOnly {
|
||||
flag.Set("logtostderr", "true")
|
||||
flag.Set("stderrthreshold", "WARNING")
|
||||
flag.Set("v", "2")
|
||||
}
|
||||
|
||||
// if *repoRootParam == "" {
|
||||
// return fmt.Errorf("'-%s' flag must not be empty", repoRootFlagName)
|
||||
// }
|
||||
if *useCase == "" {
|
||||
return fmt.Errorf("'-%s' flag must not be empty", useCaseFlagName)
|
||||
}
|
||||
// repoRoot, err := filepath.Abs(*repoRootParam)
|
||||
// if err != nil {
|
||||
// return errors.Wrapf(err, "failed to create absolute path from %s", repoRoot)
|
||||
// }
|
||||
|
||||
glog.Infof("Use case is: %s", *useCase)
|
||||
// glog.Infof("Repo root is: %s", repoRoot)
|
||||
var err error
|
||||
|
||||
switch *useCase {
|
||||
case vpnUseCaseFlagName:
|
||||
gitFs, err := NewGitFS(repository)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to initialize git fs")
|
||||
}
|
||||
worktree, err := gitFs.repo.Worktree()
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to get worktree")
|
||||
}
|
||||
|
||||
// get last used ip and increment
|
||||
ip, err := getAndUpdateIP(gitFs, vpnLastIPConfFileRelative)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to get valid last ip from file %s", vpnLastIPConfFileRelative)
|
||||
}
|
||||
// insert new vpn client config
|
||||
err = addVPNClient(gitFs, *vpnClientName, *vpnClientPubKey, vpnClientsConfFileRelative, ip)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to add vpn client")
|
||||
}
|
||||
// commit changes
|
||||
if _, err = worktree.Commit("Added new VPN client config", &git.CommitOptions{All: true, Author: &object.Signature{Name: "Webhook Handler Bot"}}); err != nil {
|
||||
return errors.Wrapf(err, "failed to commit")
|
||||
}
|
||||
if *printResultOnly {
|
||||
println(ip)
|
||||
}
|
||||
if err = gitFs.Push(); err != nil {
|
||||
return errors.Wrapf(err, "failed to push changes")
|
||||
}
|
||||
case setupOpenWRTDNSFlagName:
|
||||
if *openWRTNewDNS == "" {
|
||||
return fmt.Errorf("New DNS cannot be empty")
|
||||
}
|
||||
if sshKeyPath == "" {
|
||||
return fmt.Errorf("Env variable %s must be set to the location of the private key to use", sshKeyPath)
|
||||
}
|
||||
key, err := ioutil.ReadFile(sshKeyPath)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "unable to read private key")
|
||||
}
|
||||
output, err := SetOpenWRTDNS(key, *openWRTNewDNS)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, fmt.Sprintf("cmd output: %s", output))
|
||||
}
|
||||
if *printResultOnly {
|
||||
println(fmt.Sprintf("Successfully set DNS server to '%s'", *openWRTNewDNS))
|
||||
}
|
||||
case addEmailAliasUseCase:
|
||||
if *emailToForwardTo == "" {
|
||||
return fmt.Errorf("%s must not be empty when using %s use case", emailAliasFlagName, addEmailAliasUseCase)
|
||||
}
|
||||
glog.Infof("Trying to add %s email alias", *emailToForwardTo)
|
||||
gitFs, err := NewGitFS(repository)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to initialize git fs")
|
||||
}
|
||||
worktree, err := gitFs.repo.Worktree()
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to get worktree")
|
||||
}
|
||||
emailAlias, err := addEmailAlias(gitFs, *emailToForwardTo, *fromDomain)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to add email alias")
|
||||
}
|
||||
glog.Infof("generated %s email alias", emailAlias)
|
||||
// commit changes
|
||||
if _, err = worktree.Commit("Added new email alias", &git.CommitOptions{All: true, Author: &object.Signature{Name: "Webhook Handler Bot"}}); err != nil {
|
||||
return errors.Wrapf(err, "failed to commit")
|
||||
}
|
||||
if *printResultOnly {
|
||||
fmt.Printf("Successfully created '%s' -> '%s' forwarding", emailAlias, *emailToForwardTo)
|
||||
// println(ip)
|
||||
}
|
||||
if err = gitFs.Push(); err != nil {
|
||||
return errors.Wrapf(err, "failed to push changes")
|
||||
}
|
||||
glog.Infof("successfully added %s -> %s email aliasing", emailAlias, *emailToForwardTo)
|
||||
case updatePublicIPUseCaseFlagName:
|
||||
// Resolve the dynamic dns record
|
||||
publicDNSIps, err := net.LookupIP(*publicDomain)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to resolve IP addresses")
|
||||
}
|
||||
if len(publicDNSIps) < 1 {
|
||||
return fmt.Errorf("no ips found for %s", *dynDnsDomain)
|
||||
}
|
||||
var publicDNSIp net.IP = nil
|
||||
for _, ip := range publicDNSIps {
|
||||
if ip.To4() != nil {
|
||||
publicDNSIp = ip
|
||||
}
|
||||
}
|
||||
if publicDNSIp == nil {
|
||||
return errors.Wrapf(err, "failed to resolve IPv4 address for dyndns")
|
||||
}
|
||||
|
||||
// Resolve the dynamic dns record
|
||||
dynamicDNSIps, err := net.LookupIP(*dynDnsDomain)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to resolve IP addresses")
|
||||
}
|
||||
if len(dynamicDNSIps) < 1 {
|
||||
return fmt.Errorf("no ips found for %s", *dynDnsDomain)
|
||||
}
|
||||
var dynamicDNSIp net.IP
|
||||
for _, ip := range dynamicDNSIps {
|
||||
if ip.To4() != nil {
|
||||
dynamicDNSIp = ip
|
||||
}
|
||||
}
|
||||
|
||||
if publicDNSIp.Equal(dynamicDNSIp) {
|
||||
glog.Infof("IPs of dyndns and current ip match, nothing to do: current=%s, dyndns=%s", publicDNSIp, dynamicDNSIp)
|
||||
return nil
|
||||
}
|
||||
// Send notification as glue records can't be modified programatically for godaddy :/
|
||||
defer notifyForIPChange(publicDNSIp, dynamicDNSIp)
|
||||
// setup git repo
|
||||
// Old, code-as-infra based approach
|
||||
// gitFs, err := NewGitFS(repository)
|
||||
// if err != nil {
|
||||
// return errors.Wrapf(err, "failed to initialize git fs")
|
||||
// }
|
||||
// worktree, err := gitFs.repo.Worktree()
|
||||
// if err != nil {
|
||||
// return errors.Wrapf(err, "failed to get worktree")
|
||||
// }
|
||||
// err = updatePublicIP(gitFs, publicDNSIp, dynamicDNSIp)
|
||||
// if err != nil {
|
||||
// return fmt.Errorf("failed to update public ip: %w", err)
|
||||
// }
|
||||
// // // commit changes
|
||||
// if _, err = worktree.Commit("Update public ip and ns records", &git.CommitOptions{All: true, Author: &object.Signature{Name: "Webhook Handler Bot"}}); err != nil {
|
||||
// return errors.Wrapf(err, "failed to commit")
|
||||
// }
|
||||
// if err = gitFs.Push(); err != nil {
|
||||
// return errors.Wrapf(err, "failed to push changes")
|
||||
// }
|
||||
username := os.Getenv("TECHNITIUM_USERNAME")
|
||||
password := os.Getenv("TECHNITIUM_PASSWORD")
|
||||
// dynamicDNSIp = net.ParseIP("6.9.6.9")
|
||||
return UpdatePublicIPViaTechnitiumAPI(dynamicDNSIp, username, password)
|
||||
default:
|
||||
err = errors.New(fmt.Sprintf("unsupported use case: %s", *useCase))
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
63
cli/openwrt_dns.go
Normal file
63
cli/openwrt_dns.go
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
const (
|
||||
sshKeyPathEnvVarName = "SSH_KEY"
|
||||
setupOpenWRTDNSFlagName = "setup-openwrt-dns"
|
||||
setupOpenWRTNewDNSFlagName = "new-dns"
|
||||
|
||||
openWRTUser = "root"
|
||||
openWRTHost = "192.168.1.1:22" // Using IP because assuming DNS is down
|
||||
)
|
||||
|
||||
var (
|
||||
sshKeyPath, _ = os.LookupEnv(sshKeyPathEnvVarName)
|
||||
)
|
||||
|
||||
// SetOpenWRTDNS ssh-es into `host` and sets `dns` as it's primary dns for dnsmasq
|
||||
func SetOpenWRTDNS(privateKey []byte, dns string) (string, error) {
|
||||
signer, err := ssh.ParsePrivateKey(privateKey)
|
||||
if err != nil {
|
||||
log.Fatalf("unable to parse private key: %v", err)
|
||||
}
|
||||
|
||||
config := &ssh.ClientConfig{
|
||||
User: openWRTUser,
|
||||
Auth: []ssh.AuthMethod{
|
||||
ssh.PublicKeys(signer),
|
||||
},
|
||||
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||
}
|
||||
client, err := ssh.Dial("tcp", openWRTHost, config)
|
||||
if err != nil {
|
||||
log.Fatal("Failed to dial: ", err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
session, err := client.NewSession()
|
||||
if err != nil {
|
||||
log.Fatal("Failed to create session: ", err)
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
cmd := openwrtDNSUpdateCmd(dns)
|
||||
var b bytes.Buffer
|
||||
session.Stdout = &b
|
||||
if err := session.Run(cmd); err != nil {
|
||||
log.Fatal("Failed to run: " + err.Error())
|
||||
}
|
||||
fmt.Println(b.String())
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func openwrtDNSUpdateCmd(newDNS string) string {
|
||||
return fmt.Sprintf("sed -i \"s/\\slist server.*/ list server '%s'/\" /etc/config/dhcp && /etc/init.d/dnsmasq reload", newDNS)
|
||||
}
|
||||
108
cli/update_viktorbarzin_me.go
Normal file
108
cli/update_viktorbarzin_me.go
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/golang/glog"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
dynDnsDomainFlagName = "dynamic-domain"
|
||||
publicDomainFlagName = "public-domain"
|
||||
updatePublicIPUseCaseFlagName = "update-public-ip"
|
||||
|
||||
maintfFileRelative = "/main.tf"
|
||||
)
|
||||
|
||||
func updatePublicIP(gitFs *GitFS, currIp, newIp net.IP) error {
|
||||
/* Steps to update:
|
||||
1. Read main.tf where we update the bind config with the public ip (replace all occurrences of the public ip)
|
||||
1.1) read the line where the variable is specified i.e
|
||||
bind_db_viktorbarzin_me = replace(var.bind_db_viktorbarzin_me, "<current_ip>", "<new_ip>")
|
||||
1.2) switch <new_ip> and <currenct_ip>
|
||||
1.3) replace second ip (<new_ip> or after the switch <current_ip>) with the new_ip
|
||||
2. Update godaddy glue record
|
||||
|
||||
*/
|
||||
newMainTfContents, err := getNewContent(gitFs, currIp, newIp)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to get updated main.tf contents")
|
||||
}
|
||||
f, err := (*gitFs.fs).OpenFile(maintfFileRelative, os.O_WRONLY|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to open file %s for writing", maintfFileRelative)
|
||||
}
|
||||
if _, err = f.Write([]byte(newMainTfContents)); err != nil {
|
||||
return errors.Wrapf(err, "failed to write back new contents to %s:\n %s", maintfFileRelative, newMainTfContents)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get updated contents of main.tf
|
||||
func getNewContent(gitFs *GitFS, currIp, newIp net.IP) (string, error) {
|
||||
f, err := (*gitFs.fs).OpenFile(maintfFileRelative, os.O_RDONLY, 0644)
|
||||
defer f.Close()
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "failed to open tfvars file: %s", maintfFileRelative)
|
||||
}
|
||||
bytes, err := ioutil.ReadAll(f)
|
||||
contents := string(bytes)
|
||||
|
||||
newLines := []string{}
|
||||
for _, line := range strings.Split(contents, "\n") {
|
||||
lineToAdd := line
|
||||
// if line is the one that sets un the bind config
|
||||
if strings.HasPrefix(line, " bind_db_viktorbarzin_me") {
|
||||
// extract old and new ip
|
||||
// line example:
|
||||
// bind_db_viktorbarzin_me = replace(var.bind_db_viktorbarzin_me, "<current_ip>", "<new_ip>")
|
||||
// lineToAdd = strings.Replace(lineToAdd, "\"", "", -1) // remove all quotes
|
||||
// lineToAdd = strings.Replace(lineToAdd, ")", "", -1) // remove the trailing closing bracket
|
||||
// splitByComma := strings.Split(lineToAdd, ",")
|
||||
// if len(splitByComma) != 3 {
|
||||
// return "", fmt.Errorf("invalid line; got: %s", line)
|
||||
// }
|
||||
// newIpStr := strings.ReplaceAll(splitByComma[2], " ", "")
|
||||
// lineToAdd = fmt.Sprintf(" bind_db_viktorbarzin_me = replace(var.bind_db_viktorbarzin_me, \"%s\", \"%s\")", newIpStr, newIp.String())
|
||||
|
||||
// Since we're not changing tfvars, only update the replacement value
|
||||
lineToAdd = fmt.Sprintf(" bind_db_viktorbarzin_me = replace(var.bind_db_viktorbarzin_me, \"85.130.108.6\", \"%s\")", newIp.String())
|
||||
}
|
||||
newLines = append(newLines, lineToAdd)
|
||||
}
|
||||
return strings.Join(newLines, "\n"), nil
|
||||
}
|
||||
|
||||
func notifyForIPChange(oldIP, newIP net.IP) error {
|
||||
// Notify if dyndns ip is different to public
|
||||
// Currently send a message to Viktor via the webhook handler
|
||||
const url = "https://webhook.viktorbarzin.me/fb/message-viktor"
|
||||
body := []byte(fmt.Sprintf("Public IP (%s) is different than dynamic dns IP (%s). Job is running to update infra bind. As it stands Spaceship.com does not provide an API to update glue records so please manually update the hostnames in the Spaceship.com UI to use the new IP: %s", oldIP.String(), newIP.String(), newIP.String()))
|
||||
|
||||
// Send the HTTP request
|
||||
resp, err := http.Post(url, "application/json", bytes.NewBuffer(body))
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "Error sending request")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Check the response status code
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("Request failed. Status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// Read the response body
|
||||
responseBody, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "Error reading response")
|
||||
}
|
||||
glog.Infof("Response:", string(responseBody))
|
||||
return nil
|
||||
}
|
||||
199
cli/update_viktorbarzin_me_technitium.go
Normal file
199
cli/update_viktorbarzin_me_technitium.go
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type CreateTokenResponse struct {
|
||||
Username string `json:"username"`
|
||||
TokenName string `json:"tokenName"`
|
||||
Token string `json:"token"`
|
||||
Status string `json:"status"`
|
||||
ErrorMessage string `json:"errorMessage"`
|
||||
}
|
||||
|
||||
type GetRecordsResponse struct {
|
||||
Response struct {
|
||||
Zone struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Internal bool `json:"internal"`
|
||||
DnssecStatus string `json:"dnssecStatus"`
|
||||
Disabled bool `json:"disabled"`
|
||||
} `json:"zone"`
|
||||
Records []struct {
|
||||
Disabled bool `json:"disabled"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Ttl int64 `json:"ttl"`
|
||||
RData struct {
|
||||
IpAddress string `json:"ipAddress"`
|
||||
// there's more fields that we don't use atm
|
||||
} `json:"rData"`
|
||||
// RData interface{} `json:"rData"`
|
||||
DnsSecStatus string `json:"dnsSecStatus"`
|
||||
} `json:"records"`
|
||||
} `json:"response"`
|
||||
}
|
||||
type UpdateRecordResponse struct {
|
||||
Status string `json:"status"`
|
||||
ErrorMessage string `json:"errorMessage"`
|
||||
}
|
||||
|
||||
const TECHNITIUM_HOST = "technitium-web.technitium"
|
||||
|
||||
// const TECHNITIUM_HOST = "localhost"
|
||||
|
||||
func UpdatePublicIPViaTechnitiumAPI(newIp net.IP, username string, password string) error {
|
||||
token, err := createTechnitiumToken(username, password)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to get technitium token")
|
||||
}
|
||||
for _, ns := range []string{"ns1", "ns2", "@"} {
|
||||
nsRecordName := ""
|
||||
if ns == "@" {
|
||||
nsRecordName = "viktorbarzin.me."
|
||||
} else {
|
||||
nsRecordName = ns + ".viktorbarzin.me"
|
||||
}
|
||||
currIpStr, err := getRecordValue(token, nsRecordName, "A")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to get A record for ns server")
|
||||
}
|
||||
currIp := net.ParseIP(currIpStr)
|
||||
fmt.Printf("updating A record %s to %s\n", nsRecordName, newIp.String())
|
||||
err = UpdateTechnitiumNSRecord(token, nsRecordName, "A", currIp, newIp)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to update NS A record")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func UpdatePublicIPv6ViaTechnitiumAPI(newIp net.IP, username string, password string) error {
|
||||
token, err := createTechnitiumToken(username, password)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to get technitium token")
|
||||
}
|
||||
for _, ns := range []string{"ns1", "ns2", "@"} {
|
||||
nsRecordName := ""
|
||||
if ns == "@" {
|
||||
nsRecordName = "viktorbarzin.me."
|
||||
} else {
|
||||
nsRecordName = ns + ".viktorbarzin.me"
|
||||
}
|
||||
currIpStr, err := getRecordValue(token, nsRecordName, "AAAA")
|
||||
if err != nil {
|
||||
fmt.Printf("no existing AAAA record for %s, skipping\n", nsRecordName)
|
||||
continue
|
||||
}
|
||||
currIp := net.ParseIP(currIpStr)
|
||||
fmt.Printf("updating AAAA record %s to %s\n", nsRecordName, newIp.String())
|
||||
err = UpdateTechnitiumNSRecord(token, nsRecordName, "AAAA", currIp, newIp)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to update NS AAAA record")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func UpdateTechnitiumNSRecord(token, domain, recordType string, currIp, newIp net.IP) error {
|
||||
baseURL := fmt.Sprintf("http://%s:5380/api/zones/records/update", TECHNITIUM_HOST)
|
||||
params := map[string]string{
|
||||
"token": token,
|
||||
"domain": domain,
|
||||
"type": recordType,
|
||||
"newIpAddress": newIp.String(),
|
||||
"ipAddress": currIp.String(),
|
||||
}
|
||||
resp, err := sendTechnitiumAPIRequest(baseURL, params)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to update record")
|
||||
}
|
||||
var parsedResponse UpdateRecordResponse
|
||||
err = json.NewDecoder(strings.NewReader(resp)).Decode(&parsedResponse)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to decode json response when updating record")
|
||||
}
|
||||
if parsedResponse.Status == "error" {
|
||||
return fmt.Errorf("received error status when updating record: %s", parsedResponse.ErrorMessage)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func createTechnitiumToken(username string, password string) (string, error) {
|
||||
baseURL := fmt.Sprintf("http://%s:5380/api/user/createToken", TECHNITIUM_HOST)
|
||||
params := map[string]string{
|
||||
"user": username,
|
||||
"pass": password,
|
||||
"tokenName": "infra-cli-token",
|
||||
}
|
||||
resp, err := sendTechnitiumAPIRequest(baseURL, params)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to fetch token")
|
||||
}
|
||||
var tokenResponse CreateTokenResponse
|
||||
// println(resp)
|
||||
err = json.NewDecoder(strings.NewReader(resp)).Decode(&tokenResponse)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to decode json response")
|
||||
}
|
||||
if tokenResponse.Status != "ok" {
|
||||
return "", fmt.Errorf("received error status when fetching token: %s, error: %s", tokenResponse.Status, tokenResponse.ErrorMessage)
|
||||
}
|
||||
return tokenResponse.Token, nil
|
||||
}
|
||||
|
||||
func getRecordValue(token, domain, recordType string) (string, error) {
|
||||
baseURL := fmt.Sprintf("http://%s:5380/api/zones/records/get", TECHNITIUM_HOST)
|
||||
params := map[string]string{
|
||||
"token": token,
|
||||
"domain": domain,
|
||||
}
|
||||
resp, err := sendTechnitiumAPIRequest(baseURL, params)
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "failed to fetch record values for domain %s", domain)
|
||||
}
|
||||
|
||||
var response GetRecordsResponse
|
||||
err = json.NewDecoder(strings.NewReader(resp)).Decode(&response)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to decode json response when getting all zone records")
|
||||
}
|
||||
for _, record := range response.Response.Records {
|
||||
if record.Type == recordType {
|
||||
return record.RData.IpAddress, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("failed to find record for name %s and type %s", domain, recordType)
|
||||
}
|
||||
|
||||
func sendTechnitiumAPIRequest(baseURL string, params map[string]string) (string, error) {
|
||||
url, err := url.Parse(baseURL)
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "failed to create base url")
|
||||
}
|
||||
// Encode the URL parameters
|
||||
query := url.Query()
|
||||
for key, value := range params {
|
||||
query.Add(key, value)
|
||||
}
|
||||
url.RawQuery = query.Encode()
|
||||
|
||||
resp, err := http.Get(url.String())
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to create token")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return string(body), err
|
||||
}
|
||||
113
cli/vpn.go
Normal file
113
cli/vpn.go
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/golang/glog"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
vpnUseCaseFlagName = "vpn"
|
||||
vpnClientNameFlagName = "vpn-client-name"
|
||||
vpnClientPubKeyFlagName = "vpn-pub-key"
|
||||
vpnClientsConfFileRelative = "/modules/kubernetes/wireguard/extra/clients.conf"
|
||||
vpnLastIPConfFileRelative = "/modules/kubernetes/wireguard/extra/last_ip.txt"
|
||||
)
|
||||
|
||||
var (
|
||||
allowedClientName = regexp.MustCompile(`^[a-zA-Z0-9 ]+$`)
|
||||
allowedPubKey = regexp.MustCompile(`^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$`)
|
||||
)
|
||||
|
||||
// addVPNClient inserts new client config
|
||||
func addVPNClient(gitFs *GitFS, clientName, publicKey, clientsConfPath, ip string) error {
|
||||
if clientName == "" {
|
||||
return fmt.Errorf("client name must not be empty when creating a new vpn config")
|
||||
}
|
||||
if publicKey == "" {
|
||||
return fmt.Errorf("public key cannot be empty when creating new vpn config")
|
||||
}
|
||||
if !allowedClientName.Match([]byte(clientName)) {
|
||||
return fmt.Errorf("client key must match '%s', got %s", allowedClientName.String(), clientName)
|
||||
}
|
||||
if !allowedPubKey.Match([]byte(publicKey)) {
|
||||
return fmt.Errorf("client public key must match '%s', got '%s'", allowedPubKey.String(), publicKey)
|
||||
}
|
||||
|
||||
contents := "[Peer]\n# friendly_name = " + clientName + "\nPublicKey = " + publicKey + "\nAllowedIPs = " + ip + "\n\n"
|
||||
glog.Infof("adding the following config: \n%s", contents)
|
||||
f, err := (*gitFs.fs).OpenFile(clientsConfPath, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to open client configs file to add new vpn client")
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if _, err = f.Write([]byte(contents)); err != nil {
|
||||
return errors.Wrapf(err, "failed to write config to file")
|
||||
}
|
||||
|
||||
glog.Infof("successfully added new vpn client config for %s with interface ip %s", clientName, ip)
|
||||
return nil
|
||||
}
|
||||
|
||||
func incrementIP(origIP, cidr string) (string, error) {
|
||||
ip := net.ParseIP(origIP)
|
||||
_, ipNet, err := net.ParseCIDR(cidr)
|
||||
if err != nil {
|
||||
return origIP, err
|
||||
}
|
||||
for i := len(ip) - 1; i >= 0; i-- {
|
||||
ip[i]++
|
||||
if ip[i] != 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if !ipNet.Contains(ip) {
|
||||
return origIP, errors.New("overflowed CIDR while incrementing IP")
|
||||
}
|
||||
return ip.String(), nil
|
||||
}
|
||||
|
||||
// getAndUpdateIP Reads `fileName`, tries to get the ip, increments it, tries to write it back and returns the new address
|
||||
func getAndUpdateIP(gitFs *GitFS, fileName string) (string, error) {
|
||||
f, err := (*gitFs.fs).Open(fileName)
|
||||
bytes, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "filed to read file %s", fileName)
|
||||
}
|
||||
errPrefix := "file has incorrect format: "
|
||||
content := strings.TrimSpace(string(bytes))
|
||||
lines := strings.Split(content, "\n")
|
||||
if len(lines) != 1 {
|
||||
return "", fmt.Errorf(errPrefix + fmt.Sprintf("expected 1 line got %d", len(lines)))
|
||||
}
|
||||
lineSplit := strings.Split(lines[0], " ")
|
||||
if len(lineSplit) < 1 {
|
||||
return "", fmt.Errorf("expected non empty line")
|
||||
}
|
||||
ipcidr := strings.Split(lineSplit[len(lineSplit)-1], "/")
|
||||
ipAddr := ipcidr[0]
|
||||
cidr := ipcidr[1]
|
||||
incrementedIP, err := incrementIP(ipAddr, strings.Join(ipcidr, "/"))
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "failed to increment ip for string '%s'", ipcidr)
|
||||
}
|
||||
|
||||
// Write back updated ip
|
||||
fileContents := fmt.Sprintf("# DO NOT MANUALLY EDIT THIS LINE. Last IP: %s", incrementedIP+"/"+cidr)
|
||||
f, err = (*gitFs.fs).OpenFile(fileName, os.O_WRONLY|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "failed to open file %s for writing", fileName)
|
||||
}
|
||||
if _, err = f.Write([]byte(fileContents)); err != nil {
|
||||
return "", errors.Wrapf(err, "failed to write back new ip to file %s contents %s", fileName, fileContents)
|
||||
}
|
||||
glog.Infof("new ip: %s", incrementedIP)
|
||||
return incrementedIP + "/32", nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue