dsd-upsun 1.2.1__py3-none-any.whl

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.
dsd_upsun/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ from .deploy import dsd_get_plugin_config
2
+ from .deploy import dsd_deploy
dsd_upsun/deploy.py ADDED
@@ -0,0 +1,34 @@
1
+ """Manages all Upsun-specific aspects of the deployment process."""
2
+
3
+ import django_simple_deploy
4
+
5
+ from dsd_upsun.platform_deployer import PlatformDeployer
6
+ from .plugin_config import PluginConfig
7
+ from . import utils as upsun_utils
8
+
9
+
10
+ @django_simple_deploy.hookimpl
11
+ def dsd_get_plugin_config():
12
+ """Get platform-specific attributes needed by core."""
13
+ plugin_config = PluginConfig()
14
+ return plugin_config
15
+
16
+
17
+ @django_simple_deploy.hookimpl
18
+ def dsd_pre_inspect():
19
+ """Do some work before core inspects the user's project."""
20
+ # There's an apparent bug in the Upsun CLI that causes the .upsun/local/
21
+ # dir to not be ignored on Windows like it is on other OSes. We can fix
22
+ # that ourselves until Upsun updates their CLI.
23
+ # In the configuration-only approach, the user has already run
24
+ # `upsun create`, so we'll try that fix here. This avoids the user running
25
+ # `manage.py deploy` with an unclean Git status.
26
+ if (msg_fixed := upsun_utils.fix_git_exclude_bug()):
27
+ return msg_fixed
28
+
29
+
30
+ @django_simple_deploy.hookimpl
31
+ def dsd_deploy():
32
+ """Carry out platform-specific deployment steps."""
33
+ platform_deployer = PlatformDeployer()
34
+ platform_deployer.deploy()
@@ -0,0 +1,204 @@
1
+ """A collection of messages used in platform_deployer.py."""
2
+
3
+ # For conventions, see documentation in deploy_messages.py
4
+
5
+ from textwrap import dedent
6
+
7
+ from django.conf import settings
8
+
9
+
10
+ confirm_automate_all = """
11
+ The --automate-all flag means the deploy command will:
12
+ - Run `upsun create` for you, to create an empty Upsun project.
13
+ - This will create a project in the us-3.platform.sh region. If you wish
14
+ to use a different region, cancel this operation and use the --region flag.
15
+ - You can see a list of all regions at:
16
+ https://docs.upsun.com/development/regions.html#region-location
17
+ - Commit all changes to your project that are necessary for deployment.
18
+ - Push these changes to Upsun.
19
+ - Open your deployed project in a new browser tab.
20
+ """
21
+
22
+ cancel_upsun = """
23
+ Okay, cancelling Upsun deployment.
24
+ """
25
+
26
+ cli_not_installed = """
27
+ In order to deploy to Upsun, you need to install the Upsun CLI.
28
+ See here: https://docs.upsun.com/administration/cli.html
29
+ After installing the CLI, you can run the deploy command again.
30
+ """
31
+
32
+ cli_logged_out = """
33
+ You are currently logged out of the Upsun CLI. Please log in,
34
+ and then run the deploy command again.
35
+ You can log in from the command line:
36
+ $ upsun login
37
+ """
38
+
39
+ upsun_settings_found = """
40
+ There is already an Upsun-specific settings block in settings.py. Is it okay to
41
+ overwrite this block, and everything that follows in settings.py?
42
+ """
43
+
44
+ cant_overwrite_settings = """
45
+ In order to configure the project for deployment, we need to write an Upsun-specific
46
+ settings block. Please remove the current Upsun-specific settings, and then run
47
+ the deploy command again.
48
+ """
49
+
50
+ no_project_name = """
51
+ An Upsun project name could not be found.
52
+
53
+ The deploy command expects that you've already run `upsun create`, or
54
+ associated the local project with an existing project on Upsun.
55
+
56
+ If you haven't done so, run the `upsun create` command and then run
57
+ the deploy command again. You can override this warning by using
58
+ the `--deployed-project-name` flag to specify the name you want to use for the
59
+ project. This must match the name of your Upsun project.
60
+ """
61
+
62
+ org_not_found = """
63
+ An Upsun organization name could not be found.
64
+
65
+ You may have created an Upsun account, but not created an organization.
66
+ The Upsun CLI requires an organization name when creating a new project.
67
+
68
+ Please visit the Upsun console and make sure you have created an organization.
69
+ You can also do this through the CLI using the `upsun organization:create` command.
70
+ For help, run `upsun help organization:create`.
71
+ """
72
+
73
+ no_org_available = """
74
+ An Upsun org must be used to make a deployment. Please identify or create the org
75
+ you'd like to use, and then try again.
76
+ """
77
+
78
+ login_required = """
79
+ You appear to be logged out of the Upsun CLI. Please run the
80
+ command `upsun login`, and then run the deploy command again.
81
+
82
+ You may be able to override this error by passing the `--deployed-project-name`
83
+ flag.
84
+ """
85
+
86
+ unknown_error = """
87
+ An unknown error has occurred. Do you have the Upsun CLI installed?
88
+ """
89
+
90
+ may_configure = """
91
+ You may want to re-run the deploy command without the --automate-all flag.
92
+
93
+ You will have to create the Upsun project yourself, but django-simple-deploy
94
+ will do all the necessary configuration for deployment.
95
+ """
96
+
97
+
98
+ # --- Dynamic strings ---
99
+ # These need to be generated in functions, to display information that's
100
+ # determined as the script runs.
101
+
102
+
103
+ def confirm_use_org(org_name):
104
+ """Confirm use of this org to create a new project."""
105
+
106
+ msg = dedent(
107
+ f"""
108
+ --- The Upsun CLI requires an organization name when creating a new project. ---
109
+ When using --automate-all, a project will be created on your behalf. The following
110
+ organization was found: {org_name}
111
+
112
+ This organization will be used to create a new project. If this is not okay,
113
+ enter n to cancel this operation.
114
+ """
115
+ )
116
+
117
+ return msg
118
+
119
+
120
+ def unknown_create_error(e):
121
+ """Process a non-specific error when running `upsun create`
122
+ while using automate_all. This is most likely an issue with the user
123
+ not having permission to create a new project, for example because they
124
+ are on a trial plan and have already created too many projects.
125
+ """
126
+
127
+ msg = dedent(
128
+ f"""
129
+ --- An error has occurred when trying to create a new Upsun project. ---
130
+
131
+ While running `upsun create`, an error has occurred. You should check
132
+ the Upsun console to see if a project was partially created.
133
+
134
+ The error messages that Upsun provides, both through the CLI and
135
+ the console, are not always specific enough to be helpful.
136
+
137
+ The following output may help diagnose the error:
138
+ ***** output of `upsun create` *****
139
+
140
+ {e.stderr.decode()}
141
+
142
+ ***** end output *****
143
+ """
144
+ )
145
+
146
+ return msg
147
+
148
+
149
+ def success_msg(log_output=""):
150
+ """Success message, for configuration-only run."""
151
+
152
+ msg = dedent(
153
+ f"""
154
+ --- Your project is now configured for deployment on Upsun. ---
155
+
156
+ To deploy your project, you will need to:
157
+ - Commit the changes made in the configuration process.
158
+ $ git status
159
+ $ git add .
160
+ $ git commit -am "Configured project for deployment."
161
+ - Push your project to Upsun's servers:
162
+ $ upsun push
163
+ - Open your project:
164
+ $ upsun url
165
+ - As you develop your project further:
166
+ - Make local changes
167
+ - Commit your local changes
168
+ - Run `upsun push`
169
+ """
170
+ )
171
+
172
+ if log_output:
173
+ msg += dedent(
174
+ f"""
175
+ - You can find a full record of this configuration in the dsd_logs directory.
176
+ """
177
+ )
178
+
179
+ return msg
180
+
181
+
182
+ def success_msg_automate_all(deployed_url):
183
+ """Success message, when using --automate-all."""
184
+
185
+ msg = dedent(
186
+ f"""
187
+
188
+ --- Your project should now be deployed on Upsun. ---
189
+
190
+ It should have opened up in a new browser tab.
191
+ - You can also visit your project at {deployed_url}
192
+
193
+ If you make further changes and want to push them to Upsun,
194
+ commit your changes and then run `upsun push`.
195
+
196
+ Also, if you haven't already done so you should review the
197
+ documentation for Python deployments on Upsun at:
198
+ - https://fixed.docs.upsun.com/languages/python.html
199
+ - This documentation will help you understand how to maintain
200
+ your deployment.
201
+
202
+ """
203
+ )
204
+ return msg
@@ -0,0 +1,427 @@
1
+ """Manages all Upsun-specific aspects of the deployment process."""
2
+
3
+ import sys, os, subprocess, time
4
+ from pathlib import Path
5
+
6
+ from django.conf import settings
7
+ from django.core.management.utils import get_random_secret_key
8
+ from django.utils.crypto import get_random_string
9
+ from django.utils.safestring import mark_safe
10
+
11
+ from django_simple_deploy.management.commands.utils import plugin_utils
12
+ from django_simple_deploy.management.commands.utils.plugin_utils import dsd_config
13
+ from django_simple_deploy.management.commands.utils.command_errors import (
14
+ DSDCommandError,
15
+ )
16
+
17
+ from . import deploy_messages as upsun_msgs
18
+ from . import utils as upsun_utils
19
+
20
+
21
+ class PlatformDeployer:
22
+ """Perform the initial deployment to Upsun.
23
+
24
+ If --automate-all is used, carry out an actual deployment.
25
+ If not, do all configuration work so the user only has to commit changes, and call
26
+ `upsun push`.
27
+ """
28
+
29
+ def __init__(self):
30
+ self.templates_path = Path(__file__).parent / "templates"
31
+
32
+ # --- Public methods ---
33
+
34
+ def deploy(self, *args, **options):
35
+ """Coordinate the overall configuration and deployment."""
36
+ plugin_utils.write_output("\nConfiguring project for deployment to Upsun...")
37
+
38
+ self._validate_platform()
39
+
40
+ self._prep_automate_all()
41
+ self._modify_settings()
42
+ self._add_requirements()
43
+ self._add_platform_app_yaml()
44
+ self._add_platform_dir()
45
+ self._add_services_yaml()
46
+ self._settings_env_var()
47
+
48
+ self._conclude_automate_all()
49
+ self._show_success_message()
50
+
51
+ # --- Helper methods for deploy() ---
52
+
53
+ def _validate_platform(self):
54
+ """Make sure the local environment and project supports deployment to
55
+ Upsun.
56
+
57
+ Make sure CLI is installed, and user is authenticated. Make sure necessary
58
+ resources have been created and identified, and that we have the user's
59
+ permission to use those resources.
60
+
61
+ Returns:
62
+ None
63
+
64
+ Raises:
65
+ DSDCommandError: If we find any reason deployment won't work.
66
+ """
67
+ if dsd_config.unit_testing:
68
+ # Unit tests don't use the CLI. Use the deployed project name that was
69
+ # passed to the simple_deploy CLI.
70
+ self.deployed_project_name = dsd_config.deployed_project_name
71
+ plugin_utils.log_info(
72
+ f"Deployed project name: {self.deployed_project_name}"
73
+ )
74
+ return
75
+
76
+ self._check_upsun_settings()
77
+ self._validate_cli()
78
+
79
+ self.deployed_project_name = self._get_upsun_project_name()
80
+ plugin_utils.log_info(f"Deployed project name: {self.deployed_project_name}")
81
+
82
+ self.org_name = self._get_org_name()
83
+ plugin_utils.log_info(f"\nOrg name: {self.org_name}")
84
+
85
+ def _prep_automate_all(self):
86
+ """Intial work for automating entire process.
87
+
88
+ Returns:
89
+ None: If creation of new project was successful.
90
+
91
+ Raises:
92
+ DSDCommandError: If create command fails.
93
+
94
+ Note: create command outputs project id to stdout if known, all other
95
+ output goes to stderr.
96
+ """
97
+ if not dsd_config.automate_all:
98
+ return
99
+
100
+ plugin_utils.write_output(" Running `upsun create`...")
101
+ plugin_utils.write_output(
102
+ " (Please be patient, this can take a few minutes."
103
+ )
104
+ cmd = f"upsun create --title { self.deployed_project_name } --org {self.org_name} --region {dsd_config.region} --yes"
105
+
106
+ try:
107
+ # Note: if user can't create a project the returncode will be 6, not 1.
108
+ # This may affect whether a CompletedProcess is returned, or an Exception
109
+ # is raised.
110
+ # Also, create command outputs project id to stdout if known, all other
111
+ # output goes to stderr.
112
+ plugin_utils.run_slow_command(cmd)
113
+ except subprocess.CalledProcessError as e:
114
+ error_msg = upsun_msgs.unknown_create_error(e)
115
+ raise DSDCommandError(error_msg)
116
+
117
+ # Fix bug ignoring .upsun/local on Windows.
118
+ if (msg_fixed := upsun_utils.fix_git_exclude_bug()):
119
+ plugin_utils.write_output(msg_fixed)
120
+
121
+ def _modify_settings(self):
122
+ """Add upsun-specific settings.
123
+
124
+ This settings block is currently the same for all users. The ALLOWED_HOSTS
125
+ setting should be customized.
126
+ """
127
+ template_path = self.templates_path / "settings.py"
128
+ plugin_utils.modify_settings_file(template_path)
129
+
130
+ def _add_platform_app_yaml(self):
131
+ """Add a .platform.app.yaml file."""
132
+
133
+ # Build contents from template.
134
+ if dsd_config.pkg_manager == "poetry":
135
+ template_path = "poetry.platform.app.yaml"
136
+ elif dsd_config.pkg_manager == "pipenv":
137
+ template_path = "pipenv.platform.app.yaml"
138
+ else:
139
+ template_path = "platform.app.yaml"
140
+ template_path = self.templates_path / template_path
141
+
142
+ context = {
143
+ "project_name": dsd_config.local_project_name,
144
+ "deployed_project_name": self.deployed_project_name,
145
+ }
146
+
147
+ contents = plugin_utils.get_template_string(template_path, context)
148
+
149
+ # Write file to project.
150
+ path = dsd_config.project_root / ".platform.app.yaml"
151
+ plugin_utils.add_file(path, contents)
152
+
153
+ def _add_requirements(self):
154
+ """Add requirements for Upsun."""
155
+ requirements = ["platformshconfig", "gunicorn", "psycopg2"]
156
+ plugin_utils.add_packages(requirements)
157
+
158
+ def _add_platform_dir(self):
159
+ """Add a .platform directory, if it doesn't already exist."""
160
+ self.platform_dir_path = dsd_config.project_root / ".platform"
161
+ plugin_utils.add_dir(self.platform_dir_path)
162
+
163
+ def _add_services_yaml(self):
164
+ """Add the .platform/services.yaml file."""
165
+
166
+ template_path = self.templates_path / "services.yaml"
167
+ contents = plugin_utils.get_template_string(template_path, context=None)
168
+
169
+ path = self.platform_dir_path / "services.yaml"
170
+ plugin_utils.add_file(path, contents)
171
+
172
+ def _settings_env_var(self):
173
+ """Set the DJANGO_SETTINGS_MODULE env var, if needed."""
174
+ # This is primarily for Wagtail projects, as signified by a settings/production.py file.
175
+ if dsd_config.settings_path.parts[-2:] == ("settings", "production.py"):
176
+ plugin_utils.write_output(
177
+ " Setting DJANGO_SETTINGS_MODULE environment variable..."
178
+ )
179
+
180
+ # Need form mysite.settings.production
181
+ dotted_settings_path = ".".join(
182
+ dsd_config.settings_path.parts[-3:]
183
+ ).removesuffix(".py")
184
+
185
+ cmd = f"upsun variable:create --level environment --environment main --name DJANGO_SETTINGS_MODULE --value {dotted_settings_path} --no-interaction --visible-build true --prefix env"
186
+ output = plugin_utils.run_quick_command(cmd)
187
+ plugin_utils.write_output(output)
188
+
189
+ def _conclude_automate_all(self):
190
+ """Finish automating the push to Upsun.
191
+
192
+ - Commit all changes.
193
+ - Call `upsun push`.
194
+ - Open project.
195
+ """
196
+ # Making this check here lets deploy() be cleaner.
197
+ if not dsd_config.automate_all:
198
+ return
199
+
200
+ plugin_utils.commit_changes()
201
+
202
+ # Push project.
203
+ plugin_utils.write_output(" Pushing to Upsun...")
204
+
205
+ # Pause to make sure project that was just created can be used.
206
+ plugin_utils.write_output(
207
+ " Pausing 10s to make sure project is ready to use..."
208
+ )
209
+ time.sleep(10)
210
+
211
+ # Use run_slow_command(), to stream output as it runs.
212
+ cmd = "upsun push --yes"
213
+ plugin_utils.run_slow_command(cmd)
214
+
215
+ # Open project.
216
+ plugin_utils.write_output(" Opening deployed app in a new browser tab...")
217
+ cmd = "upsun url --yes"
218
+ output = plugin_utils.run_quick_command(cmd)
219
+ plugin_utils.write_output(output)
220
+
221
+ # Get url of deployed project.
222
+ # This can be done with an re, but there's one line of output with
223
+ # a url, so finding that line is simpler.
224
+ # DEV: Move this to a utility, and write a test against standard Upsun
225
+ # output.
226
+ self.deployed_url = ""
227
+ for line in output.stdout.decode().split("\n"):
228
+ if "https" in line:
229
+ self.deployed_url = line.strip()
230
+
231
+ def _show_success_message(self):
232
+ """After a successful run, show a message about what to do next."""
233
+
234
+ # DEV:
235
+ # - Mention that this script should not need to be run again unless creating
236
+ # a new deployment.
237
+ # - Describe ongoing approach of commit, push, migrate. Lots to consider
238
+ # when doing this on production app with users, make sure you learn.
239
+
240
+ if dsd_config.automate_all:
241
+ msg = upsun_msgs.success_msg_automate_all(self.deployed_url)
242
+ plugin_utils.write_output(msg)
243
+ else:
244
+ msg = upsun_msgs.success_msg(dsd_config.log_output)
245
+ plugin_utils.write_output(msg)
246
+
247
+ # --- Helper methods for methods called from deploy.py ---
248
+
249
+ def _check_upsun_settings(self):
250
+ """Check to see if an Upsun settings block already exists."""
251
+ start_line = "# Upsun settings."
252
+ plugin_utils.check_settings(
253
+ "Upsun",
254
+ start_line,
255
+ upsun_msgs.upsun_settings_found,
256
+ upsun_msgs.cant_overwrite_settings,
257
+ )
258
+
259
+ def _validate_cli(self):
260
+ """Make sure the Upsun CLI is installed, and user is authenticated."""
261
+ cmd = "upsun --version"
262
+
263
+ # This generates a FileNotFoundError on macOS and Linux if the CLI is not installed.
264
+ try:
265
+ output_obj = plugin_utils.run_quick_command(cmd)
266
+ except FileNotFoundError:
267
+ raise DSDCommandError(upsun_msgs.cli_not_installed)
268
+
269
+ plugin_utils.log_info(output_obj)
270
+ if sys.platform == "win32":
271
+ stderr = output_obj.stderr.decode(encoding="utf-8")
272
+ if "'upsun' is not recognized as an internal or external command" in stderr:
273
+ raise DSDCommandError(upsun_msgs.cli_not_installed)
274
+
275
+ # Check that the user is authenticated.
276
+ cmd = "upsun auth:info --no-interaction"
277
+ output_obj = plugin_utils.run_quick_command(cmd)
278
+
279
+ if "Authentication is required." in output_obj.stderr.decode():
280
+ raise DSDCommandError(upsun_msgs.cli_logged_out)
281
+
282
+ def _get_upsun_project_name(self):
283
+ """Get the deployed project name.
284
+
285
+ If using automate_all, we'll set this. Otherwise, we're looking for the name
286
+ that was given in the `upsun create` command.
287
+ - Try to get this from `project:info`.
288
+ - If can't get project name:
289
+ - Exit with warning, and inform user of --deployed-project-name
290
+ flag to override this error.
291
+
292
+ Retuns:
293
+ str: The deployed project name.
294
+ Raises:
295
+ DSDCommandError: If deployed project name can't be found.
296
+ """
297
+ # If we're creating the project, we'll just use the startproject name.
298
+ if dsd_config.automate_all:
299
+ return dsd_config.local_project_name
300
+
301
+ # Use the provided name if --deployed-project-name specified.
302
+ if dsd_config.deployed_project_name:
303
+ return dsd_config.deployed_project_name
304
+
305
+ # Use --yes flag to avoid interactive prompt hanging in background
306
+ # if the user is not currently logged in to the CLI.
307
+ cmd = "upsun project:info --yes --format csv"
308
+ output_obj = plugin_utils.run_quick_command(cmd)
309
+ output_str = output_obj.stdout.decode()
310
+
311
+ # Log cmd, but don't log the output of `project:info`. It contains identifying
312
+ # information about the user and project, including client_ssh_key.
313
+ plugin_utils.log_info(cmd)
314
+
315
+ # If there's no stdout, the user is probably logged out, hasn't called
316
+ # create, or doesn't have the CLI installed.
317
+ # Also, I've seen both ProjectNotFoundException and RootNotFoundException
318
+ # raised when no project has been created.
319
+ if not output_str:
320
+ output_str = output_obj.stderr.decode()
321
+ if "LoginRequiredException" in output_str:
322
+ raise DSDCommandError(upsun_msgs.login_required)
323
+ elif "ProjectNotFoundException" in output_str:
324
+ raise DSDCommandError(upsun_msgs.no_project_name)
325
+ elif "RootNotFoundException" in output_str:
326
+ raise DSDCommandError(upsun_msgs.no_project_name)
327
+ else:
328
+ error_msg = upsun_msgs.unknown_error
329
+ error_msg += upsun_msgs.cli_not_installed
330
+ raise DSDCommandError(error_msg)
331
+
332
+ # Pull deployed project name from output.
333
+ lines = output_str.splitlines()
334
+ title_line = [line for line in lines if "title," in line][0]
335
+ # Assume first project is one to use.
336
+ project_name = title_line.split(",")[1].strip()
337
+ project_name = upsun_utils.get_project_name(output_str)
338
+
339
+ # Project names can only have lowercase alphanumeric characters.
340
+ # See: https://github.com/ehmatthes/django-simple-deploy/issues/323
341
+ if " " in project_name:
342
+ project_name = project_name.replace(" ", "_").lower()
343
+ if project_name:
344
+ return project_name
345
+
346
+ # Couldn't find a project name. Warn user, and tell them about override flag.
347
+ raise DSDCommandError(upsun_msgs.no_project_name)
348
+
349
+ def _get_org_name(self):
350
+ """Get the organization name associated with the user's Upsun account.
351
+
352
+ This is needed for creating a project using automate_all.
353
+ Confirm that it's okay to use this org.
354
+
355
+ Note: In the csv output, Upsun refers to the alphanumeric ID as a Name,
356
+ and the user-provided name as a Label. This gets confusing. :/
357
+
358
+ Returns:
359
+ str: org name (id)
360
+ None: if not using automate-all
361
+ Raises:
362
+ DSDCommandError:
363
+ - if org name found, but not confirmed.
364
+ - if org name not found
365
+ """
366
+ if not dsd_config.automate_all:
367
+ return
368
+
369
+ cmd = "upsun organization:list --yes --format csv"
370
+ output_obj = plugin_utils.run_quick_command(cmd)
371
+ output_str = output_obj.stdout.decode()
372
+ plugin_utils.log_info(output_str)
373
+
374
+ org_ids, org_names = upsun_utils.get_org_ids_names(output_str)
375
+
376
+ if not org_names:
377
+ raise DSDCommandError(upsun_msgs.org_not_found)
378
+
379
+ if len(org_names) == 1:
380
+ # Get permission to use this org.
381
+ org_name = org_names[0]
382
+ if self._confirm_use_org(org_name):
383
+ # Return the corresponding ID, not the name.
384
+ return org_ids[0]
385
+
386
+ # Show all org names, ask user to make selection.
387
+ prompt = "\n*** Found multiple organizations on Upsun. ***\n"
388
+ for index, name in enumerate(org_names):
389
+ prompt += f"\n {index}: {name}"
390
+ prompt += "\n\nWhich organization would you like to use? "
391
+
392
+ valid_choices = [i for i in range(len(org_names))]
393
+
394
+ # Confirm selection, because we do *not* want to deploy using the wrong org.
395
+ confirmed = False
396
+ while not confirmed:
397
+ selection = plugin_utils.get_numbered_choice(
398
+ prompt, valid_choices, upsun_msgs.no_org_available
399
+ )
400
+ selected_org = org_names[selection]
401
+
402
+ confirm_prompt = f"You have selected {selected_org}."
403
+ confirm_prompt += " Is that correct?"
404
+ confirmed = plugin_utils.get_confirmation(confirm_prompt)
405
+
406
+ # Return corresponding ID, not the name.
407
+ return org_ids[selection]
408
+
409
+ def _confirm_use_org(self, org_name):
410
+ """Confirm that it's okay to use the org that was found.
411
+
412
+ Returns:
413
+ True: if confirmed
414
+ DSDCommandError: if not confirmed
415
+ """
416
+
417
+ dsd_config.stdout.write(upsun_msgs.confirm_use_org(org_name))
418
+ confirmed = plugin_utils.get_confirmation(skip_logging=True)
419
+
420
+ if confirmed:
421
+ dsd_config.stdout.write(" Okay, continuing with deployment.")
422
+ return True
423
+ else:
424
+ # Exit, with a message that configuration is still an option.
425
+ msg = upsun_msgs.cancel_upsun
426
+ msg += upsun_msgs.may_configure
427
+ raise DSDCommandError(msg)
@@ -0,0 +1,27 @@
1
+ """Config class for plugin information shared with core."""
2
+
3
+ from . import deploy_messages as upsun_msgs
4
+
5
+
6
+ class PluginConfig:
7
+ """Class for managing attributes that need to be shared with core.
8
+
9
+ This is similar to the class DSDConfig in core's dsd_config.py.
10
+
11
+ This should future-proof plugins somewhat, in that if more information needs
12
+ to be shared back to core, it can be added here without breaking changes to the
13
+ core-plugin interface.
14
+
15
+ Get plugin-specific attributes required by core.
16
+
17
+ Required:
18
+ - automate_all_supported
19
+ - platform_name
20
+ Optional:
21
+ - confirm_automate_all_msg (required if automate_all_supported is True)
22
+ """
23
+
24
+ def __init__(self):
25
+ self.automate_all_supported = True
26
+ self.confirm_automate_all_msg = upsun_msgs.confirm_automate_all
27
+ self.platform_name = "Upsun"
@@ -0,0 +1,62 @@
1
+ # This file describes an application. You can have multiple applications
2
+ # in the same project.
3
+ #
4
+ # See https://fixed.docs.upsun.com/create-apps.html
5
+
6
+ # The name of this app. Must be unique within a project.
7
+ name: '{{ deployed_project_name }}'
8
+
9
+ # The runtime the application uses.
10
+ type: 'python:3.10'
11
+
12
+ # The relationships of the application with services or other applications.
13
+ #
14
+ # The left-hand side is the name of the relationship as it will be exposed
15
+ # to the application in the PLATFORM_RELATIONSHIPS variable. The right-hand
16
+ # side is in the form `<service name>:<endpoint name>`.
17
+ relationships:
18
+ database: "db:postgresql"
19
+
20
+ # The configuration of app when it is exposed to the web.
21
+ web:
22
+ # Whether your app should speak to the webserver via TCP or Unix socket
23
+ # https://fixed.docs.upsun.com/guides/django/deploy/configure.html#how-to-start-your-app
24
+ upstream:
25
+ socket_family: unix
26
+ # Commands are run once after deployment to start the application process.
27
+ commands:
28
+ start: "pipenv run gunicorn -w 4 -b unix:$SOCKET {{ project_name }}.wsgi:application"
29
+ locations:
30
+ "/":
31
+ passthru: true
32
+ "/static":
33
+ root: "static"
34
+ expires: 1h
35
+ allow: true
36
+
37
+ # The size of the persistent disk of the application (in MB).
38
+ disk: 512
39
+
40
+ # Set a local R/W mount for logs
41
+ mounts:
42
+ 'logs':
43
+ source: local
44
+ source_path: logs
45
+
46
+ # The hooks executed at various points in the lifecycle of the application.
47
+ hooks:
48
+ # The build hook runs before the application is deployed, and is useful for
49
+ # assembling the codebase.
50
+ # DEV: Why remove logs right after making them?
51
+ # Also, may want to split requirements into requirements.txt and requirements_remote.txt.
52
+ build: |
53
+ pip install --upgrade pip
54
+ pip install pipenv
55
+ pipenv install
56
+
57
+ mkdir logs
58
+ pipenv run python manage.py collectstatic
59
+ rm -rf logs
60
+ deploy: |
61
+ pipenv run python manage.py migrate
62
+
@@ -0,0 +1,60 @@
1
+ # This file describes an application. You can have multiple applications
2
+ # in the same project.
3
+ #
4
+ # See https://fixed.docs.upsun.com/create-apps.html
5
+
6
+ # The name of this app. Must be unique within a project.
7
+ name: '{{ deployed_project_name }}'
8
+
9
+ # The runtime the application uses.
10
+ type: 'python:3.12'
11
+
12
+ # The relationships of the application with services or other applications.
13
+ #
14
+ # The left-hand side is the name of the relationship as it will be exposed
15
+ # to the application in the PLATFORM_RELATIONSHIPS variable. The right-hand
16
+ # side is in the form `<service name>:<endpoint name>`.
17
+ relationships:
18
+ database: "db:postgresql"
19
+
20
+ # The configuration of app when it is exposed to the web.
21
+ web:
22
+ # Whether your app should speak to the webserver via TCP or Unix socket
23
+ # https://fixed.docs.upsun.com/guides/django/deploy/configure.html#how-to-start-your-app
24
+ upstream:
25
+ socket_family: unix
26
+ # Commands are run once after deployment to start the application process.
27
+ commands:
28
+ start: "gunicorn -w 4 -b unix:$SOCKET {{ project_name }}.wsgi:application"
29
+ locations:
30
+ "/":
31
+ passthru: true
32
+ "/static":
33
+ root: "static"
34
+ expires: 1h
35
+ allow: true
36
+
37
+ # The size of the persistent disk of the application (in MB).
38
+ disk: 512
39
+
40
+ # Set a local R/W mount for logs
41
+ mounts:
42
+ 'logs':
43
+ source: local
44
+ source_path: logs
45
+
46
+ # The hooks executed at various points in the lifecycle of the application.
47
+ hooks:
48
+ # The build hook runs before the application is deployed, and is useful for
49
+ # assembling the codebase.
50
+ # DEV: Why remove logs right after making them?
51
+ # Also, may want to split requirements into requirements.txt and requirements_remote.txt.
52
+ build: |
53
+ pip install --upgrade pip
54
+ pip install -r requirements.txt
55
+
56
+ mkdir logs
57
+ python manage.py collectstatic
58
+ rm -rf logs
59
+ deploy: |
60
+ python manage.py migrate
@@ -0,0 +1,74 @@
1
+ # This file describes an application. You can have multiple applications
2
+ # in the same project.
3
+ #
4
+ # See https://fixed.docs.upsun.com/create-apps.html
5
+
6
+ # The name of this app. Must be unique within a project.
7
+ name: '{{ deployed_project_name }}'
8
+
9
+ # The runtime the application uses.
10
+ type: 'python:3.10'
11
+
12
+ # Set properties for poetry.
13
+ variables:
14
+ env:
15
+ POETRY_VERSION: '1.3.1'
16
+ POETRY_VIRTUALENVS_IN_PROJECT: true
17
+ POETRY_VIRTUALENVS_CREATE: false
18
+
19
+ # The relationships of the application with services or other applications.
20
+ #
21
+ # The left-hand side is the name of the relationship as it will be exposed
22
+ # to the application in the PLATFORM_RELATIONSHIPS variable. The right-hand
23
+ # side is in the form `<service name>:<endpoint name>`.
24
+ relationships:
25
+ database: "db:postgresql"
26
+
27
+ # The configuration of app when it is exposed to the web.
28
+ web:
29
+ # Whether your app should speak to the webserver via TCP or Unix socket
30
+ # https://fixed.docs.upsun.com/guides/django/deploy/configure.html#how-to-start-your-app
31
+ upstream:
32
+ socket_family: unix
33
+ # Commands are run once after deployment to start the application process.
34
+ commands:
35
+ start: "/app/.local/bin/poetry run gunicorn -w 4 -b unix:$SOCKET {{ project_name }}.wsgi:application"
36
+ locations:
37
+ "/":
38
+ passthru: true
39
+ "/static":
40
+ root: "static"
41
+ expires: 1h
42
+ allow: true
43
+
44
+ # The size of the persistent disk of the application (in MB).
45
+ disk: 512
46
+
47
+ # Set a local R/W mount for logs
48
+ mounts:
49
+ 'logs':
50
+ source: local
51
+ source_path: logs
52
+
53
+ # The hooks executed at various points in the lifecycle of the application.
54
+ hooks:
55
+ # The build hook runs before the application is deployed, and is useful for
56
+ # assembling the codebase.
57
+ # DEV: Why remove logs right after making them?
58
+ # Also, may want to split requirements into requirements.txt and requirements_remote.txt.
59
+ build: |
60
+ set -e
61
+ pip install --upgrade pip
62
+ export PIP_USER=false
63
+ curl -sSL https://install.python-poetry.org | python3 - --version $POETRY_VERSION
64
+ export PIP_USER=true
65
+ /app/.local/bin/poetry lock
66
+ /app/.local/bin/poetry export -f requirements.txt --output requirements.txt --without-hashes --with deploy
67
+ pip install -r requirements.txt
68
+
69
+ mkdir logs
70
+ python manage.py collectstatic
71
+ rm -rf logs
72
+ deploy: |
73
+ python manage.py migrate
74
+
@@ -0,0 +1,11 @@
1
+
2
+ # The services of the project.
3
+ #
4
+ # Each service listed will be deployed in its own container as part of your
5
+ # Platform.sh project.
6
+ #
7
+ # See https://fixed.docs.upsun.com/add-services.html
8
+
9
+ db:
10
+ type: postgresql:16
11
+ disk: 1024
@@ -0,0 +1,41 @@
1
+ {{current_settings}}
2
+
3
+ # Upsun settings.
4
+ import os
5
+
6
+ if os.environ.get("PLATFORM_APPLICATION_NAME"):
7
+ # Import some Upsun settings from the environment.
8
+ from platformshconfig import Config
9
+
10
+ config = Config()
11
+
12
+ try:
13
+ ALLOWED_HOSTS.append("*")
14
+ except NameError:
15
+ ALLOWED_HOSTS = ["*"]
16
+
17
+ DEBUG = False
18
+
19
+ STATIC_URL = "/static/"
20
+
21
+ if config.appDir:
22
+ STATIC_ROOT = os.path.join(config.appDir, "static")
23
+ if config.projectEntropy:
24
+ SECRET_KEY = config.projectEntropy
25
+
26
+ if not config.in_build():
27
+ db_settings = config.credentials("database")
28
+ DATABASES = {
29
+ "default": {
30
+ "ENGINE": "django.db.backends.postgresql",
31
+ "NAME": db_settings["path"],
32
+ "USER": db_settings["username"],
33
+ "PASSWORD": db_settings["password"],
34
+ "HOST": db_settings["host"],
35
+ "PORT": db_settings["port"],
36
+ },
37
+ "sqlite": {
38
+ "ENGINE": "django.db.backends.sqlite3",
39
+ "NAME": os.path.join(BASE_DIR, "db.sqlite3"),
40
+ },
41
+ }
dsd_upsun/utils.py ADDED
@@ -0,0 +1,77 @@
1
+ """Utilities specific to Upsun deployments."""
2
+
3
+ from pathlib import Path
4
+ import sys
5
+
6
+
7
+ def get_project_name(output_str):
8
+ """Get the project name from the output of `upsun project:info`.
9
+
10
+ Command is run with `--format csv` flag.
11
+
12
+ Returns:
13
+ str: project name
14
+ """
15
+ lines = output_str.splitlines()
16
+ title_line = [line for line in lines if "title," in line][0]
17
+ # Assume first project is one to use.
18
+ project_name = title_line.split(",")[1].strip()
19
+
20
+ return project_name
21
+
22
+
23
+ def get_org_ids_names(output_str):
24
+ """Get org ids and names from output of `upsun organization:list --yes --format csv`.
25
+
26
+ Sample input:
27
+ Name,Label,Owner email
28
+ <org-name>,<org-label>,<org-owner@example.com>
29
+ <org-name-2>,<org-label-2>,<org-owner-2@example.com>
30
+
31
+ Returns:
32
+ tuple: list, list
33
+ None: If user has no organizations.
34
+ """
35
+ if "No organizations found." in output_str:
36
+ return None
37
+
38
+ lines = output_str.split("\n")[1:]
39
+
40
+ org_ids = [line.split(",")[0] for line in lines if line]
41
+
42
+ # Build descriptive names like this: "<org-id> <org-label> (<flexible|fixed>)"
43
+ org_names = [
44
+ f"{line.split(",")[0]} {line.split(",")[1]} ({line.split(",")[2]})"
45
+ for line in lines
46
+ if line
47
+ ]
48
+
49
+ return org_ids, org_names
50
+
51
+
52
+ def fix_git_exclude_bug():
53
+ """Fix the bug where .upsun/local/ is not ignored on Windows.
54
+
55
+ See: https://github.com/platformsh/cli/issues/286
56
+
57
+ Returns:
58
+ Str: confirmation message if bug was fixed
59
+ None: if no changes were made
60
+ """
61
+ if sys.platform != "win32":
62
+ return
63
+
64
+ path_upsun_local = Path(".upsun") / "local"
65
+ if not path_upsun_local.exists():
66
+ return
67
+
68
+ path_exclude = Path(".git") / "info" / "exclude"
69
+ if not path_exclude.exists():
70
+ return
71
+
72
+ exclude_text = path_exclude.read_text()
73
+ exclude_text_fixed = exclude_text.replace(r"/.upsun\local", r"/.upsun/local")
74
+
75
+ if exclude_text_fixed != exclude_text:
76
+ path_exclude.write_text(exclude_text_fixed)
77
+ return "Fixed /.upsun/local entry in .git/info/exclude file."
@@ -0,0 +1,70 @@
1
+ Metadata-Version: 2.4
2
+ Name: dsd-upsun
3
+ Version: 1.2.1
4
+ Summary: A plugin for django-simple-deploy, supporting deployments to Upsun.
5
+ Author-email: Eric Matthes <ehmatthes@gmail.com>
6
+ Project-URL: Documentation, https://github.com/django-simple-deploy/dsd-upsun
7
+ Project-URL: GitHub, https://github.com/django-simple-deploy/dsd-upsun
8
+ Project-URL: Changelog, https://github.com/django-simple-deploy/dsd-upsun/blob/main/CHANGELOG.md
9
+ Keywords: django,deployment
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Framework :: Django :: 4.2
12
+ Classifier: Framework :: Django :: 5.1
13
+ Classifier: Framework :: Django :: 5.2
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: BSD License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Programming Language :: Python :: 3.14
24
+ Requires-Python: >=3.9
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Requires-Dist: django>=4.2
28
+ Requires-Dist: pluggy>=1.5.0
29
+ Requires-Dist: toml>=0.10.2
30
+ Requires-Dist: requests>=2.32.2
31
+ Requires-Dist: django-simple-deploy>=1.3.0
32
+ Provides-Extra: dev
33
+ Requires-Dist: black>=24.1.0; extra == "dev"
34
+ Requires-Dist: build>=1.2.1; extra == "dev"
35
+ Requires-Dist: pytest>=8.3.0; extra == "dev"
36
+ Requires-Dist: twine>=5.1.1; extra == "dev"
37
+ Dynamic: license-file
38
+
39
+ dsd-upsun
40
+ ===
41
+
42
+ dsd-upsun is a plugin for deploying Django projects to [Upsun](https://upsun.com) (formerly Platform.sh), using django-simple-deploy.
43
+
44
+ This is an officially supported plugin, so the full documentation for dsd-upsun is included in the [django-simple-deploy](https://django-simple-deploy.readthedocs.io/en/latest/) docs. The Quick Start page for Upsun is [here](https://django-simple-deploy.readthedocs.io/en/latest/quick_starts/quick_start_upsun/).
45
+
46
+ Upsun documentation
47
+ ---
48
+
49
+ The home page for the [Upsun docs](https://devcenter.upsun.com) is a good starting point, but it can be hard to know where to look for Django-specific information. Here are some helpful links to be aware of, whether you're deploying a project to Upsun or helping maintain this plugin.
50
+
51
+ Upsun has two approaches to deployment: Fixed and Flex. With a Fixed plan, you specify the size of the resources you want to use for your project. When your project grows, you'll need to upgrade to a higher Fixed plan. With a Flex plan, you can select from a wider variety of specific resource sizes. When your project grows you can scale individual resources yourself, or you can let Upsun scale resources for you.
52
+
53
+ Currently, dsd-upsun only supports Fixed deployments. The roadmap includes a plan to add support for both Fixed and Flex deployments.
54
+
55
+ ### Fixed docs
56
+
57
+
58
+ The main documentation page for Fixed deployments is [here](https://fixed.docs.upsun.com). Other relevant pages include:
59
+
60
+ - [Fixed philosophy](https://fixed.docs.upsun.com/learn/overview/philosophy.html)
61
+ - [Python on Upsun Fixed](https://fixed.docs.upsun.com/languages/python.html)
62
+ - [Django on Upsun Fixed](https://fixed.docs.upsun.com/guides/django.html)
63
+ - [Deploy Django on Upsun Fixed](https://fixed.docs.upsun.com/guides/django/deploy.html)
64
+ - [Django configuration](https://fixed.docs.upsun.com/guides/django/deploy/customize.html#django-configuration)
65
+ - [Configure Django for Upsun Fixed](https://fixed.docs.upsun.com/guides/django/deploy/configure.html)
66
+
67
+ The main page for Flex deployments is [here](https://docs.upsun.com). Other relevant pages include:
68
+
69
+ - [Operational maturity for Django](https://upsun.com/django/)
70
+ - [Deploy Django on Upsun Flex](https://docs.upsun.com/get-started/stacks/django.html)
@@ -0,0 +1,16 @@
1
+ dsd_upsun/__init__.py,sha256=Hhn4V_hCKQV525mBTRr7oHtiSeaJOGrxuI00HABDffQ,73
2
+ dsd_upsun/deploy.py,sha256=lRwJ4xhm_SmP8mxaDZaIZwAYkIMpTOEIJP8Etowx_Fw,1198
3
+ dsd_upsun/deploy_messages.py,sha256=Ay5SOKzgXvpOFMbKLlmqrtmQDTpbMIsmqcHdOtQG_bM,6444
4
+ dsd_upsun/platform_deployer.py,sha256=ksG5Qi-myIKj6sNi2Fe06n61WQinrUeepZOH7zyYMfg,16550
5
+ dsd_upsun/plugin_config.py,sha256=QAhymAPun38GSm7_yTKXVUf_kPJD8nt2psfMFJQuLBE,853
6
+ dsd_upsun/utils.py,sha256=x2TivWZpwid-AqZbvVzBftNVTHWgPQRHtD0TxyapRvQ,2129
7
+ dsd_upsun/templates/pipenv.platform.app.yaml,sha256=jeIR5PhPka5Y8kW3hBmJ1yp1PqNyKCBBW4mH0T8pc_k,1973
8
+ dsd_upsun/templates/platform.app.yaml,sha256=N2mq6nlJRb7HCPO9_I4F9jcmNJNgpg5tO_5m3pS5QG4,1930
9
+ dsd_upsun/templates/poetry.platform.app.yaml,sha256=OrU6FJa-kzwTK-VT5414X09ionkARWjIQpD0w3KKqO0,2419
10
+ dsd_upsun/templates/services.yaml,sha256=eLN8qe9VZqMt9o9DKlA11vRGYiO1Qg1qBw-yWqaKXLA,231
11
+ dsd_upsun/templates/settings.py,sha256=wechLsIYyhiGmKxJ9i6C8XWEETCynHZW_GPwHOJZ7Bk,1126
12
+ dsd_upsun-1.2.1.dist-info/licenses/LICENSE,sha256=eE0PSpGK-C_T72lmuE6kQ_Am4fOlJympaeS_aItmiws,1484
13
+ dsd_upsun-1.2.1.dist-info/METADATA,sha256=N4CAaf4hh_4pUOvi0JElN4mnLoFN5sFwWDBjbgzt4wg,3838
14
+ dsd_upsun-1.2.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
15
+ dsd_upsun-1.2.1.dist-info/top_level.txt,sha256=3WChrd4xa_Bh0gQG6SKGhSuKEfYgWRxWZrqAcdUh4W0,10
16
+ dsd_upsun-1.2.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,11 @@
1
+ Copyright (c) Eric Matthes and individual contributors.
2
+
3
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
4
+
5
+ 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
6
+
7
+ 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
8
+
9
+ 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
10
+
11
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1 @@
1
+ dsd_upsun