Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add django-dotenv, and compensate for the removal of drama-free-django runtime features #5133

Closed
wants to merge 21 commits into from

Conversation

rosskarchner
Copy link
Member

@rosskarchner rosskarchner commented Jul 23, 2019

These changes compensate for the removal of the "runtime" customizations in Drama Free Django, proposed here. This means that cfgov-refresh needs to do a little more work:

  • dfd will no longer populate environment variables from a JSON file. To deal with that, this PR adds django-dotenv and directly supports the old 'environment.json' files, so that they continue to work while we make the transition.
  • dfd will no longer add 'static.in' subdirectories to the STATICFILES_DIRS list, so that functionality has been added to cfgov.settings.base, triggered by a 'DJANGO_STATICFILES_IN' environment variable. You can see that in action in docker/drama-free-django/_build.sh
  • the scripts in docker/drama-free-django/ have been adapted to these changes.

Additions

  • django-dotenv
  • DJANGO_STATICFILES_IN functionality in cfgov.settings.base

Testing

  1. docker/drama-free-django/build.sh && docker/drama-free-django/test.sh should work as expected

Todos

  • Merge Remove runtime customizations drama-free-django#25
  • docker/drama-free-django/_build.sh is currently set to install drama-free-django's magic-removal-branch. That should be fixed before merging.
  • The ansible side of this effort is not quite done yet.

Checklist

  • PR has an informative and human-readable title
  • Changes are limited to a single goal (no scope creep)
  • Code can be automatically merged (no conflicts)
  • Code follows the standards laid out in the CFPB development guidelines
  • Passes all existing automated tests
  • Any change in functionality is tested
  • New functions are documented (with a description, list of inputs, and expected output)
  • Placeholder code is flagged / future todos are captured in comments
  • Visually tested in supported browsers and devices (see checklist below 👇)
  • Project documentation has been updated
  • Reviewers requested with the Reviewers tool ➡️

Testing checklist

Browsers

  • Chrome on desktop
  • Firefox
  • Safari on macOS
  • Edge
  • Internet Explorer 9, 10, and 11
  • Safari on iOS
  • Chrome on Android

Accessibility

  • Keyboard friendly
  • Screen reader friendly

Other

  • Is useable without CSS
  • Is useable without JS
  • Flexible from small to large screens
  • No linting errors or warnings
  • JavaScript tests are passing

Copy link
Member

@hkeeler hkeeler left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall looks good. Just a few questions/thoughts on environment_candidates.



def initialize_environment():
environment_candidates = [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need these 3 possible options? Seems like a chance to have a .env in more than one place, and mistake which one is actually being used. Seems worth of a comment and/or some docs describing when to use these different locations.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, it'd be nice to standardize on one location for .env. Can we standardize on using the repo root (../.env), which would also work out to the DFD root during deployments?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense to me.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it now only looks for '.env' as a sibling of 'cfgov'

]

for candidate in environment_candidates:
if os.path.exists(candidate):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about when none of the candidates are found? Is that OK? In a Docker world, I'm guessing you wouldn't use a .env file at all, and would just pass in envvars right to the container?

Perhaps a log statement saying which .env file is being used, or none at all?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now, instead of searching for candidates, it tries to read ../../env. If it isn't there, the dotenv library will issue a warning

cfgov/manage.py Outdated
this_dir = os.path.dirname(os.path.abspath(__file__))


def initialize_environment():
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same questions as in wsgi.py.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressed above

@@ -58,7 +60,8 @@ grep -Eho \
| xargs touch

# Now that we're sure that we have webfont files, we can test collectstatic.
./venv/bin/django-admin collectstatic
#./venv/bin/django-admin collectstatic
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably don't need this commented out code here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed!

docker/drama-free-django/_build.sh Outdated Show resolved Hide resolved
@@ -264,6 +265,11 @@
PROJECT_ROOT.child('templates', 'wagtailadmin')
]

if 'DJANGO_STATICFILES_IN' in os.environ:
pattern = os.path.join(
os.environ['DJANGO_STATICFILES_IN'],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the argument over making this configurable vs. just hardcoding use of our current static.in as the one place we always place these files? We already document this location, for example here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right now, I don't think the cfgov-refresh version of static.in gets included in the build artifact?

I guess we could make it relative, though-- it will always be a sibling to the cfgov directory.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any reason why not to modify the Jenkins build to use static.in? In my branch modifying the Jenkins job to use the Docker-based build (docker-dfd-build), I use that and it works well. We don't use it in the non-Docker-based build currently.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see a reason not to do that-- I think they just came about at different times, and for different reasons, and nobody ever connected the dots.



def initialize_environment():
environment_candidates = [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, it'd be nice to standardize on one location for .env. Can we standardize on using the repo root (../.env), which would also work out to the DFD root during deployments?


for candidate in environment_candidates:
if os.path.exists(candidate):
dotenv.read_dotenv(candidate)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
dotenv.read_dotenv(candidate)
dotenv.read_dotenv(candidate)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've removed that whole function

for candidate in environment_candidates:
if os.path.exists(candidate):
dotenv.read_dotenv(candidate)
return
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return
return

@chosak
Copy link
Member

chosak commented Jul 24, 2019

I note your comment:

The ansible side of this effort is not quite done yet.

Would it make sense to push back loading of .env and instead alter this PR to load from environment.json, essentially pulling in this code from DFD?

This would accomplish "stop using DFD at runtime" and allow us to proceed more incrementally, working as-is with the current Ansible code that generates environment.json files. In a subsequent PR (or multiple PRs) we could then further alter cfgov-refresh and the Ansible code to instead generate .env files.

@rosskarchner
Copy link
Member Author

This would accomplish "stop using DFD at runtime" and allow us to proceed more incrementally, working as-is with the current Ansible code that generates environment.json files. In a subsequent PR (or multiple PRs) we could then further alter cfgov-refresh and the Ansible code to instead generate .env files.

That's an interesting idea-- I've been pondering a different approach to breaking the dependencies (having ansible deploy both the json and the .env)-- but since this exists now and the ansible changes don't yet, your approach makes more sense.

@rosskarchner
Copy link
Member Author

@chosak I just pushed an implementation of that

@chosak
Copy link
Member

chosak commented Jul 25, 2019

@rosskarchner, thanks, I'll take another look at this PR.

One thing: don't we need to make at least some Ansible changes before this can go live, e.g. setting the DJANGO_STATIC_ROOT environment variable to the right location?

@rosskarchner
Copy link
Member Author

@chosak what do you think about changing this default, which I don't think we actually use anywhere (outside of Docker experiments, maybe) https://github.com/cfpb/cfgov-refresh/blob/master/cfgov/cfgov/settings/base.py#L244

@chosak
Copy link
Member

chosak commented Jul 25, 2019

@rosskarchner that seems sensible to me, you mean, to /srv/cfgov/static? That seems like it'd be good to add to this PR.

@rosskarchner
Copy link
Member Author

Yeah, that's what I meant

Copy link
Member

@chosak chosak left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I notice that this logic in the sample .env file causes these warning messages to appear whenever you run a management command:

$ cfgov/manage.py shell
/Users/chosaka/.virtualenvs/cfgov-refresh/lib/python2.7/site-packages/dotenv.py:115: SyntaxWarning: Line 'if [ $(uname -s) = "Darwin" ]; then' doesn't match format
  SyntaxWarning
/Users/chosaka/.virtualenvs/cfgov-refresh/lib/python2.7/site-packages/dotenv.py:115: SyntaxWarning: Line '  export DATABASE_URL=postgres://cfpb@localhost/cfgov' doesn't match format
  SyntaxWarning
/Users/chosaka/.virtualenvs/cfgov-refresh/lib/python2.7/site-packages/dotenv.py:115: SyntaxWarning: Line '  export ES_HOST=localhost' doesn't match format
  SyntaxWarning
/Users/chosaka/.virtualenvs/cfgov-refresh/lib/python2.7/site-packages/dotenv.py:115: SyntaxWarning: Line 'fi' doesn't match format
  SyntaxWarning

This is only an annoyance but it sure feels like it could be a real annoyance for people running management commands constantly. Is there anything we can do about this?

@@ -264,6 +265,8 @@
PROJECT_ROOT.child('templates', 'wagtailadmin')
]

static_in = REPOSITORY_ROOT.child('static.in')
STATICFILES_DIRS += [str(d) for d in static_in.listdir(filter=DIRS)]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you add this logic into the base settings, can you also remove this duplicative line from local settings?

cfgov/cfgov/settings/base.py Outdated Show resolved Hide resolved
environmentdotjson_path = os.path.join(this_dir, '../../environment.json')

if os.path.exists(envfile_path):
dotenv.read_dotenv(envfile_path)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to use override=True here (see docs)? That would make the .env file override any existing variables already in the environment. I think we do want that behavior, and that's how environment.json works now.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd say I'm mildly uncomfortable with that-- when I read about it, it seemed like an elegant way to recognize that if a variable has been set through some other mechanism, it reflects somebody's intent.

That said, I don't feel too strongly. I'll go ahead and make the change.

Copy link
Member Author

@rosskarchner rosskarchner Jul 30, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I found the reason not to use override=True: if .env takes precedence, then we can't set environment variables in docker-compose.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I found the reason not to use override=True: if .env takes precedence, then we can't set environment variables in docker-compose.

docker-compose itself has an env_file attribute, and it supports the same KEY=value format as django-dotenv. So, maybe the better option for compose, is to not load the .env file in-container, and let compose inject those envvars itself?

We'll have to do something similar in a production environment. We don't want to bake in env-specific .env files at image build time, and we won't be able to map in a .env at runtime, so the app will have to support just having those envvars already there at container start. I think that's fine in the current logic, since it doesn't fail if neither .env nor environment.json are present.

Also, is the plan to update .env_SAMPLE to the KEY=value style syntax? Or do we need it to be an actual shell script for other purposes?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've removed override=True, so variables set via Docker, Linux, Compose, or tox will always take precedence.

cfgov/manage.py Outdated
envfile_path = os.path.join(this_dir, '../.env')
environmentdotjson_path = os.path.join(this_dir, '../environment.json')
if os.path.exists(envfile_path):
dotenv.read_dotenv(envfile_path)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above comment re override=True.

@@ -45,6 +45,8 @@ export DJANGO_SETTINGS_MODULE=cfgov.settings.production
# commands do nothing.
mkdir -p static.in/0/fonts

export DJANGO_STATICFILES_IN=/tmp/current/static.in
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a legacy addition that can now be removed since we're using local static.in?

@chosak
Copy link
Member

chosak commented Jul 25, 2019

One other thought about this PR: if you were to remove these changes in _build.sh, and these changes in _test.sh, could this PR be merged as-is and have things continue to work using the existing master version of drama-free-django?

Essentially, the variables would get loaded again, with no change. That'd let us move ahead with this change even while the DFD changes are still under review.

Another thing: the docs [currently suggest using autoenv to load the variables, here and here. Can we change that if we want to move to auto-loading?

@rosskarchner
Copy link
Member Author

This is only an annoyance but it sure feels like it could be a real annoyance for people running management commands constantly. Is there anything we can do about this?

Do you know what that's for?

We could:

  • find a way to eliminate that logic -- django-dotenv doesn't interpret bash files, it expects every line to just be a variable.
  • preprocess the .env file, reading it into memory, stripping out the invalid lines, and then using parse_dotenv instead of read_dotenv.

@chosak
Copy link
Member

chosak commented Jul 25, 2019

Do you know what that's for?

Yes. It's because when running using a local virtualenv, the database host and Elasticsearch host are both localhost, but in the existing docker-compose setup, these are postgres and elasticsearch, respectively. Perhaps there's some better way of eliminating that logic, like defaulting them to localhost but letting docker-compose override them?

@wpears
Copy link
Member

wpears commented Jul 25, 2019

Those commands (specifically activate-virtualenv.sh) could just be moved into a separate file that gets invoked. .env was just being overloaded and I agree that it's probably better to keep it as a pure envvar file.

I agree with andy that defaulting them to localhost is probably the right path

@rosskarchner
Copy link
Member Author

One other thought about this PR: if you were to remove these changes in _build.sh, and these changes in _test.sh, could this PR be merged as-is and have things continue to work using the existing master version of drama-free-django?

Essentially, the variables would get loaded again, with no change. That'd let us move ahead with this change even while the DFD changes are still under review.

One wrinkle: these changes don't help with /venv/bin/django-admin collectstatic

That's kind of the reason my initial inclination was a seperate DFD pull request, just for adding the manage.py wrapper (that PR would need to be updated to use the runpy.run_path approach)

Another thing: the docs [currently suggest using autoenv to load the variables, here and here. Can we change that if we want to move to auto-loading?

I think so?

@chosak
Copy link
Member

chosak commented Jul 25, 2019

One wrinkle: these changes don't help with /venv/bin/django-admin collectstatic

Hm. Well, frankly, the original thought behind adding the collectstatic call to _test.sh was because I wanted to test collectstatic on Travis (#5110). But because of some bugginess of that approach, and a hesitancy to add it as a potential blocker to all cfgov-refresh PRs, that got rolled back (#5119). So that code isn't being invoked by anything right now.

So if it would make things simpler to just remove that call to collectstatic, maybe just for now, we could just do that here. I'll also take a look at your open DFD PR as well, of course.

@rosskarchner
Copy link
Member Author

While chipping away at getting this working under mod_wsgi, I discovered that variables set via Apache's SetEnv were not available to the wsgi.py script. After tinkering with some work-arounds, I decide that a simpler solution was to just add a new, production WSGI file, that unambiguously sets DJANGO_SETTINGS_MODULE to cfgov.settings.production. Deployment will need to be updated to use cfgov/cfgov/production_wsgi.py. We are already working on changing that, since DFD will no longer include the current /current/wsgi.py

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants