-
Notifications
You must be signed in to change notification settings - Fork 234
/
laravel.go
286 lines (248 loc) · 7.45 KB
/
laravel.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
package scanner
import (
"bufio"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io/fs"
"log"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"github.com/blang/semver"
"github.com/superfly/flyctl/helpers"
"github.com/superfly/flyctl/internal/command/launch/plan"
)
// setup Laravel with a sqlite database
func configureLaravel(sourceDir string, config *ScannerConfig) (*SourceInfo, error) {
// Laravel projects contain the `artisan` command
if !checksPass(sourceDir, fileExists("artisan")) {
return nil, nil
}
s := &SourceInfo{
Env: map[string]string{
"APP_ENV": "production",
"LOG_CHANNEL": "stderr",
"LOG_LEVEL": "info",
"LOG_STDERR_FORMATTER": "Monolog\\Formatter\\JsonFormatter",
"SESSION_DRIVER": "cookie",
"SESSION_SECURE_COOKIE": "true",
},
Family: "Laravel",
Port: 8080,
Secrets: []Secret{
{
Key: "APP_KEY",
Help: "Laravel needs a unique application key.",
Generate: func() (string, error) {
// Method used in RandBytes never returns an error
r, _ := helpers.RandBytes(32)
return "base64:" + base64.StdEncoding.EncodeToString(r), nil
},
},
},
SkipDatabase: true,
ConsoleCommand: "php /var/www/html/artisan tinker",
}
// Min PHP version to use generator
minVersion, err := semver.Make("8.1.0")
if err != nil {
panic(err)
}
// The detected PHP version
phpVersion, err := extractPhpVersion()
if err != nil || phpVersion == "" {
// Fallback to 8.0, which has
// the broadest compatibility
phpVersion = "8.0"
}
s.BuildArgs = map[string]string{
"PHP_VERSION": phpVersion,
"NODE_VERSION": "18",
}
// Use default scanner templates if < min version(8.1.0)
phpNVersion, err := semver.Make(phpVersion + ".0")
if err != nil || phpNVersion.LT(minVersion) {
s.Files = templates("templates/laravel")
} else {
// Else use dockerfile-laravel generator
s.Callback = LaravelCallback
}
// Extract DB, Redis config from dotenv
db, redis, skipDB := extractConnections(".env")
s.SkipDatabase = skipDB
s.RedisDesired = redis
if db != 0 {
s.DatabaseDesired = db
}
return s, nil
}
func LaravelCallback(appName string, srcInfo *SourceInfo, plan *plan.LaunchPlan, flags []string) error {
// create temporary fly.toml for merge purposes
flyToml := "fly.toml"
_, err := os.Stat(flyToml)
if os.IsNotExist(err) {
// create a fly.toml consisting only of an app name
contents := fmt.Sprintf("app = \"%s\"\n", appName)
err := os.WriteFile(flyToml, []byte(contents), 0644)
if err != nil {
log.Fatal(err)
}
// inform caller of the presence of this file
srcInfo.MergeConfig = &MergeConfigStruct{
Name: flyToml,
Temporary: true,
}
}
// generate Dockerfile if it doesn't already exist
dockerfileExists := true
_, err = os.Stat("Dockerfile")
if errors.Is(err, fs.ErrNotExist) {
dockerfileExists = false
}
// check first to see if the package is already installed
installed := false
data, err := os.ReadFile("composer.json")
if err == nil {
var composerJson map[string]interface{}
err = json.Unmarshal(data, &composerJson)
if err == nil {
// check for the package in the composer.json
require, ok := composerJson["require"].(map[string]interface{})
if ok && require["fly-apps/dockerfile-laravel"] != nil {
installed = true
}
requireDev, ok := composerJson["require-dev"].(map[string]interface{})
if ok && requireDev["fly-apps/dockerfile-laravel"] != nil {
installed = true
}
}
}
// check if executable is available
vendorPath := filepath.Join("vendor", "bin", "dockerfile-laravel")
_, err = os.Stat(vendorPath)
if os.IsNotExist(err) {
installed = false
}
// install fly-apps/dockerfile-laravel if it's not already installed
if !installed {
args := []string{"composer", "require", "--dev", "fly-apps/dockerfile-laravel"}
fmt.Printf("installing: %s\n", strings.Join(args, " "))
cmd := exec.Command(args[0], args[1:]...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil && !dockerfileExists {
return fmt.Errorf("Dockerfile doesn't exist and failed to install fly-apps/dockerfile-laravel: %w", err)
}
}
args := []string{vendorPath, "generate"}
if dockerfileExists {
args = append(args, "--skip")
}
// add additional flags from launch command
if len(flags) > 0 {
args = append(args, flags...)
}
fmt.Printf("Running: %s\n", strings.Join(args, " "))
cmd := exec.Command(args[0], args[1:]...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to generate Dockerfile: %w", err)
}
// provide some advice
srcInfo.DeployDocs += fmt.Sprintf(`
If you need custom packages installed, or have problems with your deployment
build, you may need to edit the Dockerfile for app-specific changes. If you
need help, please post on https://community.fly.io.
Now: run 'fly deploy' to deploy your %s app.
`, srcInfo.Family)
return nil
}
func extractPhpVersion() (string, error) {
/* Example Output:
PHP 8.1.8 (cli) (built: Jul 8 2022 10:58:31) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.1.8, Copyright (c) Zend Technologies
with Zend OPcache v8.1.8, Copyright (c), by Zend Technologies
*/
cmd := exec.Command("php", "-v")
out, err := cmd.CombinedOutput()
if err != nil {
return "", err
}
// Capture major/minor version (leaving out revision version)
re := regexp.MustCompile(`PHP ([0-9]+\.[0-9]+)\.[0-9]`)
match := re.FindStringSubmatch(string(out))
if len(match) > 1 {
// If the PHP version is below 7.4, we won't have a
// container for it, so we'll use PHP 7.4
if match[1][0:1] == "7" {
vers, err := strconv.ParseFloat(match[1], 32)
if err != nil {
return "7.4", nil
}
if vers < 7.4 {
return "7.4", nil
}
}
return match[1], nil
}
return "", fmt.Errorf("could not find php version")
}
var dbRegStr = "^ *(DB_CONNECTION|DATABASE_URL) *= *(\"|')? *[a-zA-Z]+(\"|')?"
var redisRegStr = "^[^#]*redis"
// extractConnections detects the database connection of a laravel fly app
// by checking the .env file in the project's base directory for connection keywords.
// This ignores commented out lines and prioritizes the first connection occurance over others.
//
// Returns three variables:
//
// db - DatabaseKind of the connection extracted
// redis - reports whether redis was detected
// skipDb - reports whether a connection or redis was detected
func extractConnections(path string) (db DatabaseKind, redis bool, skipDb bool) {
// Get File Content
file, err := os.Open(path)
if err != nil {
return 0, false, true
}
defer file.Close() //skipcq: GO-S2307
// Set up Regex to match
// -not commented out, with DB_CONNECTION
dbReg := regexp.MustCompile(dbRegStr)
// -not commented out with redis keyword
redisReg := regexp.MustCompile(redisRegStr)
// Default Return Variables
db = 0
redis = false
skipDb = true
// Check each line for
// match on redis or db regex
scanner := bufio.NewScanner(file)
for scanner.Scan() {
text := scanner.Text()
if redisReg.MatchString(text) {
redis = true
skipDb = false
} else if db == 0 && dbReg.MatchString(text) {
if strings.Contains(text, "mysql") {
db = DatabaseKindMySQL
skipDb = false
} else if strings.Contains(text, "pgsql") || strings.Contains(text, "postgres") {
db = DatabaseKindPostgres
skipDb = false
} else if strings.Contains(text, "sqlite") {
db = DatabaseKindSqlite
skipDb = false
}
}
}
return db, redis, skipDb
}