-
Notifications
You must be signed in to change notification settings - Fork 224
/
django.go
247 lines (219 loc) · 9.73 KB
/
django.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
package scanner
import (
"fmt"
"os/exec"
"path"
"regexp"
"strings"
"github.com/blang/semver"
"github.com/logrusorgru/aurora"
"github.com/mattn/go-zglob"
"github.com/superfly/flyctl/helpers"
)
// setup django with a postgres database
func configureDjango(sourceDir string, config *ScannerConfig) (*SourceInfo, error) {
if !checksPass(sourceDir, dirContains("requirements.txt", "(?i)Django")) && !checksPass(sourceDir, dirContains("Pipfile", "(?i)Django")) && !checksPass(sourceDir, dirContains("pyproject.toml", "(?i)Django")) {
return nil, nil
}
s := &SourceInfo{
Family: "Django",
Port: 8000,
Env: map[string]string{
"PORT": "8000",
},
Secrets: []Secret{
{
Key: "SECRET_KEY",
Help: "Django needs a random, secret key. Use the random default we've generated, or generate your own.",
Generate: func() (string, error) {
return helpers.RandString(64)
},
},
},
Statics: []Static{
{
GuestPath: "/code/static",
UrlPrefix: "/static/",
},
},
SkipDeploy: true,
ConsoleCommand: "/code/manage.py shell",
}
vars := make(map[string]interface{})
// keep `pythonLatestSupported` up to date: https://devguide.python.org/versions/#supported-versions
// Keep the default `pythonVersion` as "3.11"
pythonLatestSupported := "3.8.0"
pythonVersion := "3.11"
pythonFullVersion, pinned, err := extractPythonVersion()
if err == nil && pythonFullVersion != "" {
if pinned {
// We pin versions if they're beta or RC and, as such, don't have a
// minor version equivalent Docker tag.
pythonVersion = pythonFullVersion
s.Notice += fmt.Sprintf(`%s It looks like you have Python %s installed, which is not an official release. This version is being explicitly pinned in the generated Dockerfile, and should be changed to an official release before deploying to production.`, aurora.Yellow("[WARNING]"), pythonFullVersion)
} else {
userVersion, userErr := semver.ParseTolerant(pythonFullVersion)
supportedVersion, supportedErr := semver.ParseTolerant(pythonLatestSupported)
if userErr == nil && supportedErr == nil {
// if Python version is below 3.8.0, use Python 3.11 (default)
// it is required to have Major, Minor and Patch (e.g. 3.11.2) to be able to use GT
// but only Major and Minor (e.g. 3.11) is used in the Dockerfile
if userVersion.GTE(supportedVersion) {
v, err := semver.Parse(pythonFullVersion)
if err == nil {
pythonVersion = fmt.Sprintf("%d.%d", v.Major, v.Minor)
}
s.Notice += fmt.Sprintf(`
%s Python %s was detected. 'python:%s-slim-bullseye' image will be set in the Dockerfile.
`, aurora.Faint("[INFO]"), pythonFullVersion, pythonVersion)
} else {
s.Notice += fmt.Sprintf(`
%s It looks like you have Python %s installed, but it has reached its end of support. Using Python %s to build your image instead.
Make sure to update the Dockerfile to use an image that is compatible with the Python version you are using.
%s We highly recommend that you update your application to use Python %s or newer. (https://devguide.python.org/versions/#supported-versions)
`, aurora.Yellow("[WARNING]"), pythonFullVersion, pythonVersion, aurora.Yellow("[WARNING]"), pythonLatestSupported)
}
}
}
} else {
s.Notice += fmt.Sprintf(`
%s Python version was not detected. Using Python %s to build your image instead.
Make sure to update the Dockerfile to use an image that is compatible with the Python version you are using.
%s We highly recommend that you update your application to use Python %s or newer. (https://devguide.python.org/versions/#supported-versions)
`, aurora.Yellow("[WARNING]"), pythonVersion, aurora.Yellow("[WARNING]"), pythonLatestSupported)
}
vars["pythonVersion"] = pythonVersion
vars["pinnedPythonVersion"] = pinned
if checksPass(sourceDir, fileExists("Pipfile")) {
vars["pipenv"] = true
} else if checksPass(sourceDir, fileExists("pyproject.toml")) {
vars["poetry"] = true
} else if checksPass(sourceDir, fileExists("requirements.txt")) {
vars["venv"] = true
}
wsgiFiles, err := zglob.Glob(`./**/wsgi.py`)
if err == nil && len(wsgiFiles) > 0 {
var wsgiFilesProject []string
for _, wsgiPath := range wsgiFiles {
// when using a virtual environment to manage the dependencies (e.g. venv), the 'site-packages/' folder is created within the virtual environment folder
// This folder contains all the (dependencies) packages installed within the virtual environment
// exclude dependencies matches that contain 'site-packages' in the path (e.g. .venv/lib/python3.11/site-packages/django/core/handlers/wsgi.py)
if !strings.Contains(wsgiPath, "site-packages") {
wsgiFilesProject = append(wsgiFilesProject, wsgiPath)
}
}
if len(wsgiFilesProject) > 0 {
wsgiFilesLen := len(wsgiFilesProject)
dirPath, _ := path.Split(wsgiFilesProject[wsgiFilesLen-1])
dirName := path.Base(dirPath)
vars["wsgiName"] = dirName
vars["wsgiFound"] = true
if wsgiFilesLen > 1 {
// warning: multiple wsgi.py files found
s.DeployDocs = s.DeployDocs + fmt.Sprintf(`
Multiple wsgi.py files were found in your Django application:
[%s]
Before proceeding, make sure '%s' is the module containing a WSGI application object named 'application'. If not, update your Dockefile.
This module is used on Dockerfile to start the Gunicorn server process.
`, strings.Join(wsgiFilesProject, ", "), dirPath)
}
}
}
// check if settings.py file exists
settingsFiles, err := zglob.Glob(`./**/settings.py`)
if err == nil && len(settingsFiles) == 0 {
// if no settings.py files are found, check if any *prod*.py (e.g. production.py, prod.py, settings_prod.py) exists in 'settings/' folder
settingsFiles, err = zglob.Glob(`./**/settings/*prod*.py`)
}
if err == nil && len(settingsFiles) > 0 {
settingsFilesLen := len(settingsFiles)
// check if multiple settings.py files were found; warn the user it's not recommended and what to do instead
if settingsFilesLen > 1 {
// warning: multiple settings.py files found
s.DeployDocs = s.DeployDocs + fmt.Sprintf(`
Multiple 'settings.py' files were found in your Django application:
[%s]
It's not recommended to have multiple 'settings.py' files.
Instead, you can have a 'settings/' folder with the settings files according to the different environments (e.g., local.py, staging.py, production.py).
In this case, you can specify which settings file to use when running the Django application by setting the 'DJANGO_SETTINGS_MODULE' environment variable to the corresponding settings file.
`, strings.Join(settingsFiles, ", "))
}
// check if STATIC_ROOT setting is set in ANY of the settings.py files
for _, settingsPath := range settingsFiles {
// in production, you must define a STATIC_ROOT directory where collectstatic will copy them.
if checksPass(sourceDir, dirContains(settingsPath, "STATIC_ROOT")) {
vars["collectStatic"] = true
s.DeployDocs = s.DeployDocs + fmt.Sprintf(`
'STATIC_ROOT' setting was detected in '%s'!
Static files will be collected during build time by running 'python manage.py collectstatic' on Dockerfile.
`, settingsPath)
// check if django.core.management.utils.get_random_secret_key() is used to set a default secret key
// if not found, set a random SECRET_KEY for building purposes
if checksPass(sourceDir, dirContains(settingsPath, "default=get_random_secret_key()")) {
vars["hasRandomSecretKey"] = true
} else {
// generate a random 50 character random string usable as a SECRET_KEY setting value on Dockerfile
// based on https://github.com/django/django/blob/main/django/core/management/utils.py#L79
randomSecretKey, err := helpers.RandString(50)
if err == nil {
vars["randomSecretKey"] = randomSecretKey
s.DeployDocs = s.DeployDocs + fmt.Sprintf(`
A default SECRET_KEY was not detected in '%s'!
A generated SECRET_KEY "%s" was set on Dockerfile for building purposes.
Optionally, you can use django.core.management.utils.get_random_secret_key() to set the SECRET_KEY default value in your %s.
`, settingsPath, randomSecretKey, settingsPath)
}
}
break
}
}
}
// check if project has a postgres dependency
if checksPass(sourceDir, dirContains("requirements.txt", "psycopg")) ||
checksPass(sourceDir, dirContains("Pipfile", "psycopg")) ||
checksPass(sourceDir, dirContains("pyproject.toml", "psycopg")) {
vars["hasPostgres"] = true
s.ReleaseCmd = "python manage.py migrate"
if !checksPass(sourceDir, dirContains("requirements.txt", "django-environ", "dj-database-url")) {
s.DeployDocs = s.DeployDocs + `
Your Django app is almost ready to deploy!
We recommend using the django-environ(pip install django-environ) or dj-database-url(pip install dj-database-url) to parse the DATABASE_URL from os.environ['DATABASE_URL']
For detailed documentation, see https://fly.dev/docs/django/
`
} else {
s.DeployDocs = s.DeployDocs + `
Your Django app is ready to deploy!
For detailed documentation, see https://fly.dev/docs/django/
`
}
}
s.Files = templatesExecute("templates/django", vars)
return s, nil
}
func extractPythonVersion() (string, bool, error) {
/* Example Output:
Python 3.11.2
Python 3.12.0b4
*/
pythonVersionOutput := "Python 3.11.0" // Fallback to 3.11
cmd := exec.Command("python3", "--version")
out, err := cmd.CombinedOutput()
if err == nil {
pythonVersionOutput = string(out)
} else {
cmd := exec.Command("python", "--version")
out, err := cmd.CombinedOutput()
if err == nil {
pythonVersionOutput = string(out)
}
}
re := regexp.MustCompile(`Python ([0-9]+\.[0-9]+\.[0-9]+(?:[a-zA-Z]+[0-9]+)?)`)
match := re.FindStringSubmatch(pythonVersionOutput)
if len(match) > 1 {
version := match[1]
nonNumericRegex := regexp.MustCompile(`[^0-9.]`)
pinned := nonNumericRegex.MatchString(version)
return version, pinned, nil
}
return "", false, fmt.Errorf("Could not find Python version")
}