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:
Viktor Barzin 2026-06-09 08:45:33 +00:00
parent 6d224861c4
commit fd0f4a0365
1166 changed files with 358546 additions and 0 deletions

8
cli/Dockerfile Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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)
}

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

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