Skip to content

Commit 8107c53

Browse files
author
AlexVakhovski
committed
Optmize folders, upd readme
1 parent f9a6fe9 commit 8107c53

File tree

7 files changed

+379
-0
lines changed

7 files changed

+379
-0
lines changed

src/build.bat

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
@echo off
2+
echo Initializing Go Linux build...
3+
4+
REM Set GOOS and GOARCH
5+
set GOOS=linux
6+
set GOARCH=amd64
7+
8+
REM Create dist folder if it doesn't exist
9+
if not exist "..\dist" (
10+
mkdir "..\dist"
11+
)
12+
13+
REM Build the binary into ../dist
14+
go build -o ..\dist\dockdev ./cmd
15+
16+
REM Check result
17+
if exist ..\dist\dockdev (
18+
echo Build successful: ..\dist\dockdev
19+
) else (
20+
echo Build failed! No output binary found.
21+
exit /b 1
22+
)

src/cmd/main.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package main
2+
3+
import (
4+
"bufio"
5+
"fmt"
6+
"os"
7+
"strings"
8+
"generator/internal"
9+
)
10+
11+
func main() {
12+
args := os.Args
13+
14+
if len(args) == 3 && args[1] == "rm" {
15+
internal.DeleteProject(args[2])
16+
return
17+
}
18+
19+
var domain string
20+
if len(args) == 2 {
21+
domain = args[1]
22+
} else {
23+
fmt.Print("Enter project domain (e.g. app.test): ")
24+
reader := bufio.NewReader(os.Stdin)
25+
d, err := reader.ReadString('\n')
26+
if err != nil {
27+
fmt.Println("Failed to read input:", err)
28+
os.Exit(1)
29+
}
30+
domain = strings.TrimSpace(d)
31+
}
32+
33+
if domain == "" {
34+
fmt.Println("Domain cannot be empty.")
35+
os.Exit(1)
36+
}
37+
38+
if err := internal.GenerateProject(domain); err != nil {
39+
fmt.Println("Error:", err)
40+
os.Exit(1)
41+
}
42+
}

src/go.mod

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module generator
2+
3+
go 1.24.2
4+
5+
require github.com/joho/godotenv v1.5.1 // indirect

src/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
2+
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=

src/internal/generator.go

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
package internal
2+
3+
import (
4+
"bufio"
5+
"fmt"
6+
"os"
7+
"os/exec"
8+
"path/filepath"
9+
"strings"
10+
"github.com/joho/godotenv"
11+
)
12+
13+
type TemplateData struct {
14+
Domain string
15+
Prefix string
16+
ProjectIP string
17+
NetworkName string
18+
}
19+
20+
func GenerateProject(domain string) error {
21+
if err := godotenv.Load(".env"); err != nil {
22+
return fmt.Errorf("error loading .env file: %w", err)
23+
}
24+
25+
network := os.Getenv("NETWORK_NAME")
26+
baseIP := os.Getenv("PROJECT_START_IP")
27+
ipmapPath := ".ipmap.env"
28+
templateDir := "templates"
29+
projectDir := filepath.Join("domains", domain)
30+
prefix := strings.Split(domain, ".")[0]
31+
hostsPath := "/mnt/c/Windows/System32/drivers/etc/hosts"
32+
reverseProxyDir := "shared-services"
33+
reverseProxyName := "nginx-reverse-proxy"
34+
35+
if _, err := os.Stat(projectDir); !os.IsNotExist(err) {
36+
return fmt.Errorf("project already exists: %s", projectDir)
37+
}
38+
if err := os.MkdirAll(projectDir, 0755); err != nil {
39+
return err
40+
}
41+
42+
usedIPs, err := LoadUsedIPs(ipmapPath)
43+
if err != nil {
44+
return err
45+
}
46+
projectIP, err := FindNextFreeIP(baseIP, usedIPs)
47+
if err != nil {
48+
return err
49+
}
50+
if err := AppendIPMapping(ipmapPath, domain, projectIP); err != nil {
51+
return err
52+
}
53+
54+
data := TemplateData{
55+
Domain: domain, Prefix: prefix, ProjectIP: projectIP, NetworkName: network,
56+
}
57+
58+
if err := RenderTemplate(
59+
filepath.Join(templateDir, "docker-compose.yml.tmpl"),
60+
filepath.Join(projectDir, "docker-compose.yml"),
61+
data,
62+
); err != nil {
63+
return err
64+
}
65+
66+
confDir := filepath.Join(projectDir, "conf", "nginx")
67+
if err := os.MkdirAll(confDir, 0755); err != nil {
68+
return err
69+
}
70+
if err := RenderTemplate(
71+
filepath.Join(templateDir, "nginx.conf.tmpl"),
72+
filepath.Join(confDir, "default.conf"),
73+
data,
74+
); err != nil {
75+
return err
76+
}
77+
78+
appDstDir := filepath.Join(projectDir, "app")
79+
if err := os.MkdirAll(appDstDir, 0755); err != nil {
80+
return err
81+
}
82+
if err := RenderTemplate(
83+
filepath.Join(templateDir, "app", "index.html"),
84+
filepath.Join(appDstDir, "index.html"),
85+
data,
86+
); err != nil {
87+
return err
88+
}
89+
90+
sitesDir := filepath.Join(reverseProxyDir, "sites")
91+
if err := os.MkdirAll(sitesDir, 0755); err != nil {
92+
return err
93+
}
94+
siteConf := filepath.Join(sitesDir, domain+".conf")
95+
if _, err := os.Stat(siteConf); os.IsNotExist(err) {
96+
if err := RenderTemplate(
97+
filepath.Join(templateDir, "site.conf.tmpl"),
98+
siteConf,
99+
data,
100+
); err != nil {
101+
return err
102+
}
103+
fmt.Println("Created reverse proxy config:", siteConf)
104+
} else {
105+
fmt.Println("Reverse proxy config already exists:", siteConf)
106+
}
107+
108+
fmt.Println("Starting shared-services...")
109+
if err := runDockerComposeUp(reverseProxyDir); err != nil {
110+
return err
111+
}
112+
113+
fmt.Println("Starting project containers...")
114+
if err := runDockerComposeUp(projectDir); err != nil {
115+
return err
116+
}
117+
118+
fmt.Println("Reloading reverse proxy config...")
119+
err = exec.Command("docker", "exec", reverseProxyName, "nginx", "-s", "reload").Run()
120+
if err != nil {
121+
fmt.Println("Reload failed, restarting container...")
122+
err = exec.Command("docker", "restart", reverseProxyName).Run()
123+
if err != nil {
124+
return fmt.Errorf("failed to reload or restart reverse proxy: %w", err)
125+
}
126+
}
127+
128+
if err := updateWindowsHosts(domain, hostsPath); err != nil {
129+
return err
130+
}
131+
132+
return nil
133+
}
134+
135+
func DeleteProject(domain string) {
136+
reader := bufio.NewReader(os.Stdin)
137+
fmt.Printf("Are you sure you want to delete domain '%s'? [y/N]: ", domain)
138+
answer, _ := reader.ReadString('\n')
139+
answer = strings.ToLower(strings.TrimSpace(answer))
140+
if answer != "y" {
141+
fmt.Println("Aborted.")
142+
return
143+
}
144+
145+
projectPath := filepath.Join("domains", domain)
146+
composeFile := filepath.Join(projectPath, "docker-compose.yml")
147+
148+
if _, err := os.Stat(composeFile); err == nil {
149+
fmt.Println("Stopping containers for", domain, "...")
150+
stopCmd := exec.Command("docker", "compose", "down")
151+
stopCmd.Dir = projectPath
152+
stopCmd.Stdout = os.Stdout
153+
stopCmd.Stderr = os.Stderr
154+
if err := stopCmd.Run(); err != nil {
155+
fmt.Println("Warning: failed to stop containers:", err)
156+
}
157+
}
158+
159+
if err := os.RemoveAll(projectPath); err != nil {
160+
fmt.Println("Failed to remove domain folder:", err)
161+
} else {
162+
fmt.Println("Deleted folder:", projectPath)
163+
}
164+
165+
ipmap := ".ipmap.env"
166+
lines, err := os.ReadFile(ipmap)
167+
if err == nil {
168+
var kept []string
169+
for _, line := range strings.Split(string(lines), "\n") {
170+
if !strings.HasPrefix(line, domain+"=") {
171+
kept = append(kept, line)
172+
}
173+
}
174+
os.WriteFile(ipmap, []byte(strings.Join(kept, "\n")), 0644)
175+
fmt.Println("Updated:", ipmap)
176+
}
177+
178+
sitePath := filepath.Join("shared-services", "sites", domain+".conf")
179+
if err := os.Remove(sitePath); err == nil {
180+
fmt.Println("Removed reverse proxy config:", sitePath)
181+
}
182+
183+
hosts := "/mnt/c/Windows/System32/drivers/etc/hosts"
184+
hfile, err := os.ReadFile(hosts)
185+
if err == nil {
186+
var out []string
187+
for _, line := range strings.Split(string(hfile), "\n") {
188+
if !strings.Contains(line, domain) {
189+
out = append(out, line)
190+
}
191+
}
192+
os.WriteFile(hosts, []byte(strings.Join(out, "\n")), 0644)
193+
fmt.Println("Updated Windows hosts file.")
194+
}
195+
196+
fmt.Println("Domain", domain, "was successfully deleted.")
197+
}
198+
199+
func runDockerComposeUp(dir string) error {
200+
cmd := exec.Command("docker", "compose", "up", "-d")
201+
cmd.Dir = dir
202+
cmd.Stdout = os.Stdout
203+
cmd.Stderr = os.Stderr
204+
return cmd.Run()
205+
}
206+
207+
func updateWindowsHosts(domain, path string) error {
208+
hostsEntry := fmt.Sprintf("127.0.0.1 %s", domain)
209+
210+
file, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0644)
211+
if err != nil {
212+
return err
213+
}
214+
defer file.Close()
215+
216+
scanner := bufio.NewScanner(file)
217+
for scanner.Scan() {
218+
if strings.Contains(scanner.Text(), domain) {
219+
fmt.Println("Hosts entry already exists.")
220+
return nil
221+
}
222+
}
223+
224+
if _, err := file.WriteString("\n" + hostsEntry + "\n"); err != nil {
225+
return err
226+
}
227+
228+
fmt.Println("Domain added to Windows hosts file.")
229+
return nil
230+
}

src/internal/ipallocator.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package internal
2+
3+
import (
4+
"bufio"
5+
"fmt"
6+
"os"
7+
"strconv"
8+
"strings"
9+
)
10+
11+
func LoadUsedIPs(ipmapPath string) (map[string]bool, error) {
12+
used := make(map[string]bool)
13+
14+
file, err := os.Open(ipmapPath)
15+
if err != nil {
16+
if os.IsNotExist(err) {
17+
return used, nil // empty map if file doesn't exist
18+
}
19+
return nil, err
20+
}
21+
defer file.Close()
22+
23+
scanner := bufio.NewScanner(file)
24+
for scanner.Scan() {
25+
line := scanner.Text()
26+
parts := strings.SplitN(line, "=", 2)
27+
if len(parts) == 2 {
28+
used[parts[1]] = true
29+
}
30+
}
31+
32+
return used, scanner.Err()
33+
}
34+
35+
func FindNextFreeIP(base string, used map[string]bool) (string, error) {
36+
prefix := base[:strings.LastIndex(base, ".")]
37+
start, _ := strconv.Atoi(base[strings.LastIndex(base, ".")+1:])
38+
39+
for i := start; i < 255; i++ {
40+
candidate := fmt.Sprintf("%s.%d", prefix, i)
41+
if !used[candidate] {
42+
return candidate, nil
43+
}
44+
}
45+
46+
return "", fmt.Errorf("no available IPs in subnet")
47+
}
48+
49+
func AppendIPMapping(ipmapPath, domain, ip string) error {
50+
f, err := os.OpenFile(ipmapPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
51+
if err != nil {
52+
return err
53+
}
54+
defer f.Close()
55+
_, err = f.WriteString(fmt.Sprintf("%s=%s\n", domain, ip))
56+
return err
57+
}

src/internal/template.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package internal
2+
3+
import (
4+
"os"
5+
"text/template"
6+
)
7+
8+
func RenderTemplate(templatePath string, destPath string, data interface{}) error {
9+
tmpl, err := template.ParseFiles(templatePath)
10+
if err != nil {
11+
return err
12+
}
13+
14+
out, err := os.Create(destPath)
15+
if err != nil {
16+
return err
17+
}
18+
defer out.Close()
19+
20+
return tmpl.Execute(out, data)
21+
}

0 commit comments

Comments
 (0)