nvoi 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +19 -0
  3. data/Gemfile +9 -0
  4. data/Gemfile.lock +151 -0
  5. data/Makefile +26 -0
  6. data/Rakefile +16 -0
  7. data/doc/config-schema.yaml +357 -0
  8. data/examples/apex-wildcard/deploy.yml +68 -0
  9. data/examples/golang/.gitignore +19 -0
  10. data/examples/golang/Dockerfile +43 -0
  11. data/examples/golang/README.md +59 -0
  12. data/examples/golang/deploy.enc +0 -0
  13. data/examples/golang/deploy.yml +54 -0
  14. data/examples/golang/go.mod +39 -0
  15. data/examples/golang/go.sum +96 -0
  16. data/examples/golang/main.go +177 -0
  17. data/examples/golang/models/user.go +17 -0
  18. data/examples/golang-postgres-multi/.gitignore +18 -0
  19. data/examples/golang-postgres-multi/Dockerfile +39 -0
  20. data/examples/golang-postgres-multi/README.md +211 -0
  21. data/examples/golang-postgres-multi/deploy.yml +67 -0
  22. data/examples/golang-postgres-multi/go.mod +45 -0
  23. data/examples/golang-postgres-multi/go.sum +108 -0
  24. data/examples/golang-postgres-multi/main.go +197 -0
  25. data/examples/golang-postgres-multi/models/user.go +17 -0
  26. data/examples/postgres-multi/.env.production.example +11 -0
  27. data/examples/postgres-multi/README.md +112 -0
  28. data/examples/postgres-multi/deploy.yml +74 -0
  29. data/examples/postgres-single/.env.production.example +11 -0
  30. data/examples/postgres-single/.gitignore +15 -0
  31. data/examples/postgres-single/Dockerfile +35 -0
  32. data/examples/postgres-single/README.md +76 -0
  33. data/examples/postgres-single/deploy.yml +56 -0
  34. data/examples/postgres-single/go.mod +45 -0
  35. data/examples/postgres-single/go.sum +108 -0
  36. data/examples/postgres-single/main.go +184 -0
  37. data/examples/rails-single/.dockerignore +51 -0
  38. data/examples/rails-single/.env.production.example +11 -0
  39. data/examples/rails-single/.github/dependabot.yml +12 -0
  40. data/examples/rails-single/.github/workflows/ci.yml +39 -0
  41. data/examples/rails-single/.gitignore +20 -0
  42. data/examples/rails-single/.node-version +1 -0
  43. data/examples/rails-single/.rubocop.yml +8 -0
  44. data/examples/rails-single/.ruby-version +1 -0
  45. data/examples/rails-single/Dockerfile +86 -0
  46. data/examples/rails-single/Gemfile +56 -0
  47. data/examples/rails-single/Gemfile.lock +350 -0
  48. data/examples/rails-single/Procfile.dev +3 -0
  49. data/examples/rails-single/README.md +17 -0
  50. data/examples/rails-single/Rakefile +6 -0
  51. data/examples/rails-single/app/assets/builds/.keep +0 -0
  52. data/examples/rails-single/app/assets/images/.keep +0 -0
  53. data/examples/rails-single/app/assets/stylesheets/application.tailwind.css +1 -0
  54. data/examples/rails-single/app/controllers/application_controller.rb +4 -0
  55. data/examples/rails-single/app/controllers/concerns/.keep +0 -0
  56. data/examples/rails-single/app/controllers/users_controller.rb +19 -0
  57. data/examples/rails-single/app/helpers/application_helper.rb +2 -0
  58. data/examples/rails-single/app/javascript/application.js +3 -0
  59. data/examples/rails-single/app/javascript/controllers/application.js +9 -0
  60. data/examples/rails-single/app/javascript/controllers/hello_controller.js +7 -0
  61. data/examples/rails-single/app/javascript/controllers/index.js +8 -0
  62. data/examples/rails-single/app/jobs/application_job.rb +7 -0
  63. data/examples/rails-single/app/mailers/application_mailer.rb +4 -0
  64. data/examples/rails-single/app/models/application_record.rb +3 -0
  65. data/examples/rails-single/app/models/concerns/.keep +0 -0
  66. data/examples/rails-single/app/models/user.rb +2 -0
  67. data/examples/rails-single/app/views/layouts/application.html.erb +28 -0
  68. data/examples/rails-single/app/views/layouts/mailer.html.erb +13 -0
  69. data/examples/rails-single/app/views/layouts/mailer.text.erb +1 -0
  70. data/examples/rails-single/app/views/pwa/manifest.json.erb +22 -0
  71. data/examples/rails-single/app/views/pwa/service-worker.js +26 -0
  72. data/examples/rails-single/app/views/users/index.html.erb +38 -0
  73. data/examples/rails-single/bin/brakeman +7 -0
  74. data/examples/rails-single/bin/bundle +109 -0
  75. data/examples/rails-single/bin/dev +11 -0
  76. data/examples/rails-single/bin/docker-entrypoint +14 -0
  77. data/examples/rails-single/bin/jobs +6 -0
  78. data/examples/rails-single/bin/kamal +27 -0
  79. data/examples/rails-single/bin/rails +4 -0
  80. data/examples/rails-single/bin/rake +4 -0
  81. data/examples/rails-single/bin/rubocop +8 -0
  82. data/examples/rails-single/bin/setup +37 -0
  83. data/examples/rails-single/bin/thrust +5 -0
  84. data/examples/rails-single/bun.lock +224 -0
  85. data/examples/rails-single/config/application.rb +42 -0
  86. data/examples/rails-single/config/boot.rb +4 -0
  87. data/examples/rails-single/config/cable.yml +17 -0
  88. data/examples/rails-single/config/cache.yml +16 -0
  89. data/examples/rails-single/config/credentials.yml.enc +1 -0
  90. data/examples/rails-single/config/database.yml +100 -0
  91. data/examples/rails-single/config/environment.rb +5 -0
  92. data/examples/rails-single/config/environments/development.rb +69 -0
  93. data/examples/rails-single/config/environments/production.rb +87 -0
  94. data/examples/rails-single/config/environments/test.rb +50 -0
  95. data/examples/rails-single/config/initializers/assets.rb +7 -0
  96. data/examples/rails-single/config/initializers/content_security_policy.rb +25 -0
  97. data/examples/rails-single/config/initializers/filter_parameter_logging.rb +8 -0
  98. data/examples/rails-single/config/initializers/inflections.rb +16 -0
  99. data/examples/rails-single/config/locales/en.yml +31 -0
  100. data/examples/rails-single/config/puma.rb +41 -0
  101. data/examples/rails-single/config/queue.yml +18 -0
  102. data/examples/rails-single/config/recurring.yml +15 -0
  103. data/examples/rails-single/config/routes.rb +4 -0
  104. data/examples/rails-single/config.ru +6 -0
  105. data/examples/rails-single/db/cable_schema.rb +11 -0
  106. data/examples/rails-single/db/cache_schema.rb +12 -0
  107. data/examples/rails-single/db/migrate/20251123095526_create_users.rb +10 -0
  108. data/examples/rails-single/db/queue_schema.rb +129 -0
  109. data/examples/rails-single/db/seeds.rb +9 -0
  110. data/examples/rails-single/deploy.yml +57 -0
  111. data/examples/rails-single/lib/tasks/.keep +0 -0
  112. data/examples/rails-single/log/.keep +0 -0
  113. data/examples/rails-single/package.json +17 -0
  114. data/examples/rails-single/public/400.html +114 -0
  115. data/examples/rails-single/public/404.html +114 -0
  116. data/examples/rails-single/public/406-unsupported-browser.html +114 -0
  117. data/examples/rails-single/public/422.html +114 -0
  118. data/examples/rails-single/public/500.html +114 -0
  119. data/examples/rails-single/public/icon.png +0 -0
  120. data/examples/rails-single/public/icon.svg +3 -0
  121. data/examples/rails-single/public/robots.txt +1 -0
  122. data/examples/rails-single/script/.keep +0 -0
  123. data/examples/rails-single/vendor/.keep +0 -0
  124. data/examples/rails-single/yarn.lock +188 -0
  125. data/exe/nvoi +6 -0
  126. data/lib/nvoi/cli.rb +190 -0
  127. data/lib/nvoi/cloudflare/client.rb +287 -0
  128. data/lib/nvoi/config/config.rb +248 -0
  129. data/lib/nvoi/config/env_resolver.rb +63 -0
  130. data/lib/nvoi/config/loader.rb +102 -0
  131. data/lib/nvoi/config/naming.rb +196 -0
  132. data/lib/nvoi/config/ssh_keys.rb +82 -0
  133. data/lib/nvoi/config/types.rb +274 -0
  134. data/lib/nvoi/constants.rb +59 -0
  135. data/lib/nvoi/credentials/crypto.rb +88 -0
  136. data/lib/nvoi/credentials/editor.rb +272 -0
  137. data/lib/nvoi/credentials/manager.rb +173 -0
  138. data/lib/nvoi/deployer/cleaner.rb +36 -0
  139. data/lib/nvoi/deployer/image_builder.rb +23 -0
  140. data/lib/nvoi/deployer/infrastructure.rb +126 -0
  141. data/lib/nvoi/deployer/orchestrator.rb +146 -0
  142. data/lib/nvoi/deployer/retry.rb +67 -0
  143. data/lib/nvoi/deployer/service_deployer.rb +311 -0
  144. data/lib/nvoi/deployer/tunnel_manager.rb +57 -0
  145. data/lib/nvoi/deployer/types.rb +8 -0
  146. data/lib/nvoi/errors.rb +67 -0
  147. data/lib/nvoi/k8s/renderer.rb +44 -0
  148. data/lib/nvoi/k8s/templates.rb +29 -0
  149. data/lib/nvoi/logger.rb +72 -0
  150. data/lib/nvoi/providers/aws.rb +403 -0
  151. data/lib/nvoi/providers/base.rb +111 -0
  152. data/lib/nvoi/providers/hetzner.rb +288 -0
  153. data/lib/nvoi/providers/hetzner_client.rb +170 -0
  154. data/lib/nvoi/remote/docker_manager.rb +203 -0
  155. data/lib/nvoi/remote/ssh_executor.rb +72 -0
  156. data/lib/nvoi/remote/volume_manager.rb +103 -0
  157. data/lib/nvoi/service/delete.rb +234 -0
  158. data/lib/nvoi/service/deploy.rb +80 -0
  159. data/lib/nvoi/service/exec.rb +144 -0
  160. data/lib/nvoi/service/provider.rb +36 -0
  161. data/lib/nvoi/steps/application_deployer.rb +26 -0
  162. data/lib/nvoi/steps/database_provisioner.rb +60 -0
  163. data/lib/nvoi/steps/k3s_cluster_setup.rb +105 -0
  164. data/lib/nvoi/steps/k3s_provisioner.rb +351 -0
  165. data/lib/nvoi/steps/server_provisioner.rb +43 -0
  166. data/lib/nvoi/steps/services_provisioner.rb +29 -0
  167. data/lib/nvoi/steps/tunnel_configurator.rb +66 -0
  168. data/lib/nvoi/steps/volume_provisioner.rb +154 -0
  169. data/lib/nvoi/version.rb +5 -0
  170. data/lib/nvoi.rb +79 -0
  171. data/templates/app-deployment.yaml.erb +102 -0
  172. data/templates/app-ingress.yaml.erb +20 -0
  173. data/templates/app-secret.yaml.erb +10 -0
  174. data/templates/app-service.yaml.erb +12 -0
  175. data/templates/db-statefulset.yaml.erb +76 -0
  176. data/templates/service-deployment.yaml.erb +91 -0
  177. data/templates/worker-deployment.yaml.erb +50 -0
  178. metadata +361 -0
@@ -0,0 +1,43 @@
1
+ # Multi-stage build for optimized image size
2
+
3
+ # Stage 1: Build
4
+ FROM golang:1.21-alpine AS builder
5
+
6
+ # Install build dependencies
7
+ RUN apk add --no-cache gcc musl-dev sqlite-dev
8
+
9
+ WORKDIR /app
10
+
11
+ # Copy dependency files
12
+ COPY go.mod go.sum ./
13
+
14
+ # Download dependencies
15
+ RUN go mod download
16
+
17
+ # Copy source code
18
+ COPY . .
19
+
20
+ # Build the application
21
+ # CGO_ENABLED=1 is required for SQLite
22
+ # Set CGO_CFLAGS for musl compatibility
23
+ RUN CGO_ENABLED=1 CGO_CFLAGS="-D_LARGEFILE64_SOURCE" GOOS=linux go build -o app .
24
+
25
+ # Stage 2: Runtime
26
+ FROM alpine:latest
27
+
28
+ # Install runtime dependencies
29
+ RUN apk --no-cache add ca-certificates sqlite-libs
30
+
31
+ WORKDIR /app
32
+
33
+ # Copy binary from builder
34
+ COPY --from=builder /app/app .
35
+
36
+ # Create data directory
37
+ RUN mkdir -p /app/data
38
+
39
+ # Expose port
40
+ EXPOSE 3000
41
+
42
+ # Run the application
43
+ CMD ["./app"]
@@ -0,0 +1,59 @@
1
+ # Go + GORM + SQLite Example
2
+
3
+ Minimal example demonstrating NVOI deployment.
4
+
5
+ ## What It Does
6
+
7
+ - Visit `/` → Creates random user → Returns all users
8
+ - Visit `/health` → Health check
9
+ - Database persists across deployments
10
+
11
+ ## Prerequisites
12
+
13
+ 1. Own a domain and add it to Cloudflare DNS
14
+ 2. Hetzner Cloud account
15
+ 3. Run `make build` from project root
16
+
17
+ ## Deploy
18
+
19
+ ```bash
20
+ # Setup
21
+ cp .env.example .env
22
+ vim .env # Add your tokens
23
+
24
+ # Update domain in deploy.yml
25
+ vim deploy.yml # Set domain/subdomain
26
+
27
+ # Deploy
28
+ make example-deploy
29
+ ```
30
+
31
+ ## Test
32
+
33
+ ```bash
34
+ # After deployment
35
+ curl https://yoursubdomain.yourdomain.com/
36
+ curl https://yoursubdomain.yourdomain.com/health
37
+ ```
38
+
39
+ ## Local Test
40
+
41
+ ```bash
42
+ make example-run
43
+ curl http://localhost:3000/
44
+ ```
45
+
46
+ ## Response Example
47
+
48
+ ```json
49
+ {
50
+ "message": "User created on this visit!",
51
+ "new_user": {
52
+ "id": 1,
53
+ "name": "Alice Smith",
54
+ "email": "user1699999999@example.com"
55
+ },
56
+ "total_users": 1,
57
+ "all_users": [...]
58
+ }
59
+ ```
Binary file
@@ -0,0 +1,54 @@
1
+ application:
2
+ name: example-golang
3
+ environment: production
4
+
5
+ # Provider configuration
6
+ domain_provider:
7
+ cloudflare:
8
+ api_token: $CLOUDFLARE_API_TOKEN
9
+ account_id: $CLOUDFLARE_ACCOUNT_ID
10
+
11
+ compute_provider:
12
+ hetzner:
13
+ api_token: $HETZNER_API_TOKEN
14
+ server_type: cx22 # Explicit server type
15
+ server_location: fsn1 # Explicit location
16
+
17
+ # Server configuration (single server, master: true is implicit)
18
+ servers:
19
+ master:
20
+ type: cx22
21
+ location: fsn1
22
+
23
+ # Container retention
24
+ keep_count: 2
25
+
26
+ app:
27
+ web:
28
+ servers: [master]
29
+ domain: rb.run # Your domain (must be on Cloudflare)
30
+ subdomain: golang # Subdomain for this app
31
+ port: 3000
32
+
33
+ # Nested health check configuration
34
+ healthcheck:
35
+ type: http
36
+ path: /health
37
+ port: 3000
38
+ interval: 10s
39
+ timeout: 5s
40
+ retries: 3
41
+
42
+ # Volume mounts for SQLite data persistence
43
+ volumes:
44
+ data: /app/data
45
+
46
+ # SQLite database (adapter specified, volume configured at service level)
47
+ database:
48
+ servers: [master]
49
+ adapter: sqlite3
50
+
51
+ env:
52
+ APP_NAME: nvoi-example-app
53
+ LOG_LEVEL: info
54
+ GIN_MODE: release
@@ -0,0 +1,39 @@
1
+ module github.com/nvoi/example-app
2
+
3
+ go 1.21
4
+
5
+ require (
6
+ github.com/gin-gonic/gin v1.9.1
7
+ gorm.io/driver/sqlite v1.5.4
8
+ gorm.io/gorm v1.25.5
9
+ )
10
+
11
+ require (
12
+ github.com/bytedance/sonic v1.9.1 // indirect
13
+ github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
14
+ github.com/gabriel-vasile/mimetype v1.4.2 // indirect
15
+ github.com/gin-contrib/sse v0.1.0 // indirect
16
+ github.com/go-playground/locales v0.14.1 // indirect
17
+ github.com/go-playground/universal-translator v0.18.1 // indirect
18
+ github.com/go-playground/validator/v10 v10.14.0 // indirect
19
+ github.com/goccy/go-json v0.10.2 // indirect
20
+ github.com/jinzhu/inflection v1.0.0 // indirect
21
+ github.com/jinzhu/now v1.1.5 // indirect
22
+ github.com/json-iterator/go v1.1.12 // indirect
23
+ github.com/klauspost/cpuid/v2 v2.2.4 // indirect
24
+ github.com/leodido/go-urn v1.2.4 // indirect
25
+ github.com/mattn/go-isatty v0.0.19 // indirect
26
+ github.com/mattn/go-sqlite3 v1.14.18 // indirect
27
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
28
+ github.com/modern-go/reflect2 v1.0.2 // indirect
29
+ github.com/pelletier/go-toml/v2 v2.0.8 // indirect
30
+ github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
31
+ github.com/ugorji/go/codec v1.2.11 // indirect
32
+ golang.org/x/arch v0.3.0 // indirect
33
+ golang.org/x/crypto v0.9.0 // indirect
34
+ golang.org/x/net v0.10.0 // indirect
35
+ golang.org/x/sys v0.8.0 // indirect
36
+ golang.org/x/text v0.9.0 // indirect
37
+ google.golang.org/protobuf v1.30.0 // indirect
38
+ gopkg.in/yaml.v3 v3.0.1 // indirect
39
+ )
@@ -0,0 +1,96 @@
1
+ github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
2
+ github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
3
+ github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
4
+ github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
5
+ github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
6
+ github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
7
+ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
8
+ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
9
+ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
10
+ github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
11
+ github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
12
+ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
13
+ github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
14
+ github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
15
+ github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
16
+ github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
17
+ github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
18
+ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
19
+ github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
20
+ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
21
+ github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
22
+ github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
23
+ github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
24
+ github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
25
+ github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
26
+ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
27
+ github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
28
+ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
29
+ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
30
+ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
31
+ github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
32
+ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
33
+ github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
34
+ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
35
+ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
36
+ github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
37
+ github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
38
+ github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
39
+ github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
40
+ github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
41
+ github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
42
+ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
43
+ github.com/mattn/go-sqlite3 v1.14.18 h1:JL0eqdCOq6DJVNPSvArO/bIV9/P7fbGrV00LZHc+5aI=
44
+ github.com/mattn/go-sqlite3 v1.14.18/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
45
+ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
46
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
47
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
48
+ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
49
+ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
50
+ github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
51
+ github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
52
+ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
53
+ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
54
+ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
55
+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
56
+ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
57
+ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
58
+ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
59
+ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
60
+ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
61
+ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
62
+ github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
63
+ github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
64
+ github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
65
+ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
66
+ github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
67
+ github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
68
+ github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
69
+ golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
70
+ golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
71
+ golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
72
+ golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
73
+ golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
74
+ golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
75
+ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
76
+ golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
77
+ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
78
+ golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
79
+ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
80
+ golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
81
+ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
82
+ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
83
+ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
84
+ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
85
+ google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
86
+ google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
87
+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
88
+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
89
+ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
90
+ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
91
+ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
92
+ gorm.io/driver/sqlite v1.5.4 h1:IqXwXi8M/ZlPzH/947tn5uik3aYQslP9BVveoax0nV0=
93
+ gorm.io/driver/sqlite v1.5.4/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4=
94
+ gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
95
+ gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
96
+ rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
@@ -0,0 +1,177 @@
1
+ package main
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
6
+ "log"
7
+ "math/rand"
8
+ "net/http"
9
+ "os"
10
+ "os/signal"
11
+ "syscall"
12
+ "time"
13
+
14
+ "github.com/gin-gonic/gin"
15
+ "github.com/nvoi/example-app/models"
16
+ "gorm.io/driver/sqlite"
17
+ "gorm.io/gorm"
18
+ )
19
+
20
+ var db *gorm.DB
21
+
22
+ func main() {
23
+ // Initialize database
24
+ var err error
25
+ db, err = initDB()
26
+ if err != nil {
27
+ log.Fatalf("Failed to initialize database: %v", err)
28
+ }
29
+
30
+ // Auto-migrate database schema
31
+ if err := db.AutoMigrate(&models.User{}); err != nil {
32
+ log.Fatalf("Failed to migrate database: %v", err)
33
+ }
34
+
35
+ // Setup router
36
+ router := setupRouter()
37
+
38
+ // Configure server
39
+ srv := &http.Server{
40
+ Addr: ":3000",
41
+ Handler: router,
42
+ }
43
+
44
+ // Start server in goroutine
45
+ go func() {
46
+ log.Println("Starting server on :3000")
47
+ if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
48
+ log.Fatalf("Server failed to start: %v", err)
49
+ }
50
+ }()
51
+
52
+ // Wait for interrupt signal for graceful shutdown
53
+ quit := make(chan os.Signal, 1)
54
+ signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
55
+ <-quit
56
+
57
+ log.Println("Shutting down server...")
58
+
59
+ // Graceful shutdown with 5 second timeout
60
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
61
+ defer cancel()
62
+
63
+ if err := srv.Shutdown(ctx); err != nil {
64
+ log.Fatalf("Server forced to shutdown: %v", err)
65
+ }
66
+
67
+ log.Println("Server exited")
68
+ }
69
+
70
+ func initDB() (*gorm.DB, error) {
71
+ // Ensure data directory exists
72
+ if err := os.MkdirAll("data", 0755); err != nil {
73
+ return nil, fmt.Errorf("failed to create data directory: %w", err)
74
+ }
75
+
76
+ // Open SQLite database
77
+ db, err := gorm.Open(sqlite.Open("data/app.db"), &gorm.Config{})
78
+ if err != nil {
79
+ return nil, fmt.Errorf("failed to connect to database: %w", err)
80
+ }
81
+
82
+ log.Println("Database connected successfully")
83
+ return db, nil
84
+ }
85
+
86
+ func setupRouter() *gin.Engine {
87
+ // Set Gin mode from environment
88
+ if mode := os.Getenv("GIN_MODE"); mode != "" {
89
+ gin.SetMode(mode)
90
+ }
91
+
92
+ router := gin.Default()
93
+
94
+ // Health check endpoint (required for deployment)
95
+ router.GET("/health", healthCheck)
96
+
97
+ // Main endpoint: creates user on every visit, returns all users
98
+ router.GET("/", handleVisit)
99
+
100
+ return router
101
+ }
102
+
103
+ // Health check handler (required for zero-downtime deployments)
104
+ func healthCheck(c *gin.Context) {
105
+ // Check database connectivity
106
+ sqlDB, err := db.DB()
107
+ if err != nil {
108
+ c.JSON(http.StatusServiceUnavailable, gin.H{
109
+ "status": "unhealthy",
110
+ "error": "database connection failed",
111
+ })
112
+ return
113
+ }
114
+
115
+ if err := sqlDB.Ping(); err != nil {
116
+ c.JSON(http.StatusServiceUnavailable, gin.H{
117
+ "status": "unhealthy",
118
+ "error": "database ping failed",
119
+ })
120
+ return
121
+ }
122
+
123
+ c.JSON(http.StatusOK, gin.H{
124
+ "status": "healthy",
125
+ "service": "nvoi-example-app",
126
+ "time": time.Now().Format(time.RFC3339),
127
+ })
128
+ }
129
+
130
+ // Main handler: creates a new user on every visit and returns all users
131
+ func handleVisit(c *gin.Context) {
132
+ // Create a new user with random name
133
+ newUser := models.User{
134
+ Name: generateRandomName(),
135
+ Email: fmt.Sprintf("user-xxx%d@example.com", time.Now().UnixNano()),
136
+ }
137
+
138
+ if err := db.Create(&newUser).Error; err != nil {
139
+ c.JSON(http.StatusInternalServerError, gin.H{
140
+ "error": "Failed to create user",
141
+ })
142
+ return
143
+ }
144
+
145
+ // Fetch all users
146
+ var users []models.User
147
+ if err := db.Find(&users).Error; err != nil {
148
+ c.JSON(http.StatusInternalServerError, gin.H{
149
+ "error": "Failed to fetch users",
150
+ })
151
+ return
152
+ }
153
+
154
+ // Get deployment info
155
+ hostname, _ := os.Hostname()
156
+
157
+ // Return response
158
+ c.JSON(http.StatusOK, gin.H{
159
+ "hostname": hostname,
160
+ "message": "User created on this visit!",
161
+ "new_user": newUser,
162
+ "total_users": len(users),
163
+ "all_users": users,
164
+ })
165
+ }
166
+
167
+ // Generate random name for demo purposes
168
+ func generateRandomName() string {
169
+ firstNames := []string{"Alice", "Bob", "Charlie", "Diana", "Eve", "Frank", "Grace", "Henry", "Ivy", "Jack"}
170
+ lastNames := []string{"Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis", "Rodriguez", "Martinez"}
171
+
172
+ rand.Seed(time.Now().UnixNano())
173
+ firstName := firstNames[rand.Intn(len(firstNames))]
174
+ lastName := lastNames[rand.Intn(len(lastNames))]
175
+
176
+ return fmt.Sprintf("%s %s", firstName, lastName)
177
+ }
@@ -0,0 +1,17 @@
1
+ package models
2
+
3
+ import (
4
+ "time"
5
+
6
+ "gorm.io/gorm"
7
+ )
8
+
9
+ // User represents a user in the system
10
+ type User struct {
11
+ ID uint `gorm:"primarykey" json:"id"`
12
+ Name string `gorm:"not null" json:"name" binding:"required"`
13
+ Email string `gorm:"uniqueIndex;not null" json:"email" binding:"required,email"`
14
+ CreatedAt time.Time `json:"created_at"`
15
+ UpdatedAt time.Time `json:"updated_at"`
16
+ DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
17
+ }
@@ -0,0 +1,18 @@
1
+ # Binaries
2
+ app
3
+ *.exe
4
+ *.dll
5
+ *.so
6
+ *.dylib
7
+
8
+ # Test binary
9
+ *.test
10
+
11
+ # Output of the go coverage tool
12
+ *.out
13
+
14
+ # Dependency directories
15
+ vendor/
16
+
17
+ # Go workspace file
18
+ go.work
@@ -0,0 +1,39 @@
1
+ # Multi-stage build for optimized image size
2
+
3
+ # Stage 1: Build
4
+ FROM golang:1.23-alpine AS builder
5
+
6
+ # Install build dependencies (no SQLite needed for Postgres)
7
+ RUN apk add --no-cache gcc musl-dev
8
+
9
+ WORKDIR /app
10
+
11
+ # Copy dependency files
12
+ COPY go.mod go.sum ./
13
+
14
+ # Download dependencies
15
+ RUN go mod download
16
+
17
+ # Copy source code
18
+ COPY . .
19
+
20
+ # Build the application
21
+ # CGO_ENABLED=1 required for some postgres driver features
22
+ RUN CGO_ENABLED=1 GOOS=linux go build -o app .
23
+
24
+ # Stage 2: Runtime
25
+ FROM alpine:latest
26
+
27
+ # Install runtime dependencies
28
+ RUN apk --no-cache add ca-certificates
29
+
30
+ WORKDIR /app
31
+
32
+ # Copy binary from builder
33
+ COPY --from=builder /app/app .
34
+
35
+ # Expose port
36
+ EXPOSE 3000
37
+
38
+ # Run the application
39
+ CMD ["./app"]