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

Allow proxying arbitrary ports and starting daemons, more explicit nodejs support, fixes #3456 #4057

Merged
merged 26 commits into from
Aug 2, 2022

Conversation

rfay
Copy link
Member

@rfay rfay commented Jul 29, 2022

The Problem/Issue/Bug:

Manual Testing Instructions:

Try out all the things in the doc

Automated Testing Overview:

Added TestExtraPortExpose

@rfay
Copy link
Member Author

rfay commented Jul 29, 2022

Please take a look and see what you think @tyler36 @torenware @hchonov @davereid-pfg and anybody else interested in nodejs support.

@github-actions
Copy link

github-actions bot commented Jul 30, 2022

@torenware
Copy link
Collaborator

I've just read the updated docs, which look good so far. A couple of general question about how this will work:

  1. How would node_modules get populated? The contents are platform specific, it turns out, and in general you don't want to assume that the node_modules directory you get from the host is compatible with the Linux container it will be running on.
  2. Related to the previous: to prevent platform related problems, you probably want to run something like npm install or (better) pnpm install. This makes sure that that the contents of node_modules is compatible with Linux and of the right architecture.
  3. PHP projects and NodeJS project tend to have similar directory structures, with source in an src/ directory. So having the JavaScript files in your project root will be a problem. Best practice is putting the JavaScript in a sub-folder. I tend to use a frontend/ directory for this purpose; I'd be curious what the Laravel people do as well for this. This needs to be a settable parameter with a reasonable default. And if people really want to put their files in at the root, well, let them, but make them ask for it explicitly.

I'm going to try to get this to work with a trivial install of something, but that's what I have for the first pass.

@rfay
Copy link
Member Author

rfay commented Jul 30, 2022

Thanks @torenware

  1. How would node_modules get populated? That's a feature of the project, and has to be done by the project. It's the project code, and ddev npm install
  2. "to prevent platform related problems", yup, like you said. People may want a npm install in their post-start hooks.
  3. "PHP projects and NodeJS project tend to have similar directory structures" - People get to set up their projects the way they want.

@torenware
Copy link
Collaborator

1. How would node_modules get populated? That's a feature of the project, and has to be done by the project. It's the project code, and `ddev npm install`

IIRC ddev npm install assumes that node_modules is in the project root. You may want to make that behavior configurable, to allow people to use the npm command. The default behavior of ddev commands to set the current directory to project root may not be the right behavior.

3. "PHP projects and NodeJS project tend to have similar directory structures" - People get to set up their projects the way they want.

True.

@torenware
Copy link
Collaborator

torenware commented Jul 30, 2022

I'm starting up with these additions to config.yaml:

# 4057 tests
web_extra_daemons:
  - name: "http-1"
    command: "node_modules/.bin/vite"
    directory: /var/www/html/frontend
web_extra_exposed_ports:
  - name: node-vite
    container_port: 5173
    http_port: 9998
    https_port: 5147
hooks:
  post-start:
    - exec: "cd frontend; npm install"

First issue: I set web_extra_daemons.directory to /var/www/html/frontend, but ddev gets angry about that. From ddev logs I see:

2022-07-30 00:40:38,162 INFO success: nginx entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
2022-07-30 00:40:38,164 INFO spawnerr: can't find command 'node_modules/.bin/vite'
2022-07-30 00:40:38,164 INFO success: mailhog entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
2022-07-30 00:40:40,172 INFO spawnerr: can't find command 'node_modules/.bin/vite'
2022-07-30 00:40:43,183 INFO spawnerr: can't find command 'node_modules/.bin/vite'
2022-07-30 00:40:47,194 INFO spawnerr: can't find command 'node_modules/.bin/vite'
2022-07-30 00:40:52,217 INFO spawnerr: can't find command 'node_modules/.bin/vite'
2022-07-30 00:40:58,246 INFO spawnerr: can't find command 'node_modules/.bin/vite'
2022-07-30 00:41:05,278 INFO spawnerr: can't find command 'node_modules/.bin/vite'
2022-07-30 00:41:13,310 INFO spawnerr: can't find command 'node_modules/.bin/vite'
2022-07-30 00:41:22,339 INFO spawnerr: can't find command 'node_modules/.bin/vite'
2022-07-30 00:41:32,378 INFO spawnerr: can't find command 'node_modules/.bin/vite'
2022-07-30 00:41:32,379 INFO gave up: http-1 entered FATAL state, too many start retries too quickly
2022-07-30 00:41:33,383 WARN child_exit_monitor: bad result line: 'This line kills supervisor: ver:3.0 server:supervisor serial:0 pool:child_exit_monitor poolserial:0 eventname:PROCESS_STATE_FATAL len:54'
2022-07-30 00:41:33,384 WARN child_exit_monitor: has entered the UNKNOWN state and will no longer receive events, this usually indicates the process violated the eventlistener protocol
2022-07-30 00:41:33,386 WARN received SIGQUIT indicating exit request

So ddev falls over.

I change the first portion of the config to:

web_extra_daemons:
  - name: "http-1"
    command: "cd frontend; node_modules/.bin/vite"
    directory: /var/www/html

This also kills ddev good and dead:

2022-07-30 00:53:09,407 INFO spawnerr: can't find command 'cd'
2022-07-30 00:53:12,422 INFO spawnerr: can't find command 'cd'
2022-07-30 00:53:16,440 INFO spawnerr: can't find command 'cd'
2022-07-30 00:53:21,473 INFO spawnerr: can't find command 'cd'
2022-07-30 00:53:27,508 INFO spawnerr: can't find command 'cd'
2022-07-30 00:53:34,541 INFO spawnerr: can't find command 'cd'
2022-07-30 00:53:42,588 INFO spawnerr: can't find command 'cd'
2022-07-30 00:53:51,631 INFO spawnerr: can't find command 'cd'
2022-07-30 00:54:01,685 INFO spawnerr: can't find command 'cd'
2022-07-30 00:54:01,685 INFO gave up: http-1 entered FATAL state, too many start retries too quickly
2022-07-30 00:54:02,689 WARN child_exit_monitor: bad result line: 'This line kills supervisor: ver:3.0 server:supervisor serial:0 pool:child_exit_monitor poolserial:0 eventname:PROCESS_STATE_FATAL len:54'
2022-07-30 00:54:02,690 WARN child_exit_monitor: has entered the UNKNOWN state and will no longer receive events, this usually indicates the process violated the eventlistener protocol
2022-07-30 00:54:02,691 WARN received SIGQUIT indicating exit request

Sounds like the command must be something that can be exec'd. I'll write a short script and see if I can get around this.

Which works.

@torenware
Copy link
Collaborator

Working config:

# 4057 tests
web_extra_daemons:
  - name: "http-1"
    command: "./launch-vite"
    directory: /var/www/html
web_extra_exposed_ports:
  - name: node-vite
    container_port: 5173
    http_port: 9998
    https_port: 5173
web_environment:
  # Used by my simple PHP page:
  - VITE_PRIMARY_PORT=5173
  - VITE_PROJECT_DIR=frontend

hooks:
  post-start:
    - exec: "cd frontend; npm install"
    - exec: "frontend/node_modules/.bin/vite -v"

This required an "adapter script" so it could be exec'd, which contains:

#! /usr/bin/env bash

cd frontend
exec node_modules/.bin/vite "$@"

My canary script at public/index.php prints out:

Test Vite Server
Vite Project Found: frontend

Vite appears to be listening...

as it should.

So, issues found:

  • Either I don't get what web_extra_daemons.directory does, or it doesn't work right. I expected it to change the current directory. It does not.
  • You can't run from a sub-directory without some trickery, as you see above. Best to support running from a sub-directory in a simpler way.
  • If the config is a bit off, ddev falls over beyond the power of any Dr. McCoy to do anything beyond saying "It's dead, Jim". Some kind of pre-flighting might make it less scary, so the container can fail w/o taking down ddev entirely.

I'll post my test project to GitHub so folks can use it during the test of this PR. But vite is a fainting goat of a program, and is a fair test of the feature, I think.

@torenware
Copy link
Collaborator

Test program up on GitHub. Simple "proof of life" test: checks for an install directory for the JavaScript app, and whether the expected port is actually listening. Nothing really Vite specific done at all, so you should be able to test about any container that gets started up with a bit of fiddling.

@rfay
Copy link
Member Author

rfay commented Jul 30, 2022

Thanks so much for looking at this so carefully!

web_extra_daemons:
  - name: "http-1"
    command: "cd frontend; node_modules/.bin/vite"
    directory: /var/www/html

instead of the more natural

web_extra_daemons:
  - name: "http-1"
    command: "node_modules/.bin/vite"
    directory: /var/www/html/frontend

Please try some simple things like running php -S 0.0.0.0:<port> and seeing what happens and what directory it serves.

@torenware
Copy link
Collaborator

* I don't understand why you would do
web_extra_daemons:
  - name: "http-1"
    command: "cd frontend; node_modules/.bin/vite"
    directory: /var/www/html

instead of the more natural

web_extra_daemons:
  - name: "http-1"
    command: "node_modules/.bin/vite"
    directory: /var/www/html/frontend

Please try some simple things like running php -S 0.0.0.0:<port> and seeing what happens and what directory it serves.

That's what I thought too. BECAUSE IT TRIED IT AND IT DIDN'T WORK ;-) Yeah, that might be the reason.

Using the syntax from the docs, I got the errors:

2022-07-30 00:40:40,172 INFO spawnerr: can't find command 'node_modules/.bin/vite'
2022-07-30 00:40:43,183 INFO spawnerr: can't find command 'node_modules/.bin/vite'
2022-07-30 00:40:47,194 INFO spawnerr: can't find command 'node_modules/.bin/vite'
2022-07-30 00:40:52,217 INFO spawnerr: can't find command 'node_modules/.bin/vite'
2022-07-30 00:40:58,246 INFO spawnerr: can't find command 'node_modules/.bin/vite'
2022-07-30 00:41:05,278 INFO spawnerr: can't find command 'node_modules/.bin/vite'
2022-07-30 00:41:13,310 INFO spawnerr: can't find command 'node_modules/.bin/vite'
2022-07-30 00:41:22,339 INFO spawnerr: can't find command 'node_modules/.bin/vite'
2022-07-30 00:41:32,378 INFO spawnerr: can't find command 'node_modules/.bin/vite'
2022-07-30 00:41:32,379 INFO gave up: http-1 entered FATAL state, too many start retries too quickly
2022-07-30 00:41:33,383 WARN child_exit_monitor: bad result line: 'This line kills supervisor: ver:3.0 server:supervisor serial:0 pool:child_exit_monitor poolserial:0 eventname:PROCESS_STATE_FATAL len:54'
2022-07-30 00:41:33,384 WARN child_exit_monitor: has entered the UNKNOWN state and will no longer receive events, this usually indicates the process violated the eventlistener protocol
2022-07-30 00:41:33,386 WARN received SIGQUIT indicating exit request

Not sure how you tested it, but the code as it's currently written definitely set the working directory wrong. You have my test program; try changing my config to the original, and watch the smoke emerge.

@rfay
Copy link
Member Author

rfay commented Jul 30, 2022

Thanks for making https://github.com/torenware/ddev-pr-4057 - I clicked through to it earlier, but it doesn't say how you should set it up, what the expectations are, how you'd test it, where npm install or whatever should be done, none of those things. Maybe you could add just a little to the README to guide me. Please say what the problem is when you do whatever one should do.

BTW, you can't do things like command: "cd frontend; node_modules/.bin/vite" in supervisord config (or plenty of other places) because that's a bash command, and supervisord is not running commands under bash. You could conceivably do command: bash -c "cd frontend && node_modules/.bin/vite" but it wouldn't be nearly as good as just using directory and using a relevant relative command.

You can validate the configurations used for supervisord by ddev ssh and look at /etc/supervisord/*.conf - you'll see what gets generated, and maybe that will help sort things.

Again, it probably makes sense to start with simple things that don't have dependencies like php -S 0.0.0.0:<port> in various directories that have random things in them so you can see what's running. The automated test just has a couple of directories with a file in each.

@torenware
Copy link
Collaborator

torenware commented Jul 30, 2022

-I do see why your test passed however. The data-

	app.WebExtraDaemons = []WebExtraDaemon{
		{Name: "FirstDaemon", Command: "php -S 0.0.0.0:3000", Directory: "/var/www/html"},
		{Name: "SecondDaemon", Command: "php -S 0.0.0.0:4000", Directory: "/var/www/html/sub"},
	}

Looks like the tests initially crashed because node_modules ran in the wrong dir. If it runs first, then:

web_extra_daemons:
  - name: "http-1"
    command: "node_modules/.bin/vite"
    directory: /var/www/html/frontend

does run correctly. So I stand corrected.

Note that if node_modules isn't populated, you do get the full crash. Since you're not enforcing npm install, it's something to consider, because you're going to see that problem in the wild.

@torenware
Copy link
Collaborator

Updated the test program. There really is nothing to do except run ddev launch, since the index.php page itself is doing a connectivity check. A version of that page is what I used to verify ddev-viteserve a couple of days ago. Trust mocks, but verify, like they say.

@rfay
Copy link
Member Author

rfay commented Jul 30, 2022

Thanks so much for your engagement on this @torenware .

Note that if node_modules isn't populated, you do get the full crash. Since you're not enforcing npm install, it's something to consider, because you're going to see that problem in the wild.

Do you have suggestions on how to get people to do the npm install or yarn install? Most people have it in a post-start hook, I don't know how else it would be.

@torenware
Copy link
Collaborator

torenware commented Jul 30, 2022

Some ramblings for what to do around this.

The biggest stability issue right now is how the app responds when web_extra_daemons[].command does not exist, or is not something that can be run by your supervisord daemonizer. Right now ddev has an unhandled panic, and the app goes down. So the first thing to do is to make sure that this does not happen, and the app gives off a useful error. Right now, you only know what happened by looking at the logs, which is likely to translate into a support issue.

Beyond that:

  • A bit of preflighting code to weed out anything that looks invalid for the command setting. I'm getting that it needs to be something "exec-able" -- a Unix command with its arguments. Something using type or command maybe?
    • Look for characters like ; or & and flag them?
    • OR: maybe wrap weird looking input into a wrapper script, and exec that instead???
  • Better trap any error when supervisord kicks over.
  • Give feedback as to why command looks bad.

Probably you can restrict parse out ^\S+ and make sure it exists and is executable. I can't think of anything else that would fail this and still be appropriate. Looking for anything that would require a shell to run them and refusing to run it sounds like a win as well. Consider adding unit tests that are passed bad input here to make sure you're checking for this stuff.

Then you get to node_modules specifically, since the most likely reason the binary or script is not found is due to an install script not having run. The next most likely is that a non-native binary is sitting at the end of command because some * install routine ran on the host, likely also a crasher.

  • Consider adding pnpm to the base container and assigning a global command to it, since pnpm seems to be better at handling the corner cases you'd have with npm install or yarn install. In particular, it's faster and quieter than if * install has already been called. The node community seems to be moving in that direction as well.
  • Make sure whatever you have for * install, it handles subdirectories easily and that the argument for the sub-dir is well documented. You seem to be planning to do this already, but yeah, you need that.
  • Consider looking for a package.json in some steps and just calling * install? Not sure how this would work.
  • OR: if the binary or script in command wasn't found, emit an error that mentions calling * install.
  • Casually make sure that the docs show calling * install in a pre-start hook just to encourage people to infer it's required.

I'm having difficulty thinking of a scenario where a node-based daemon would not have a package.json file deployed with it. Maybe check for it in the current directory and warn if it's absent? There may be some new-fangled package managers for JS that don't use one, but it's very uncommon. Even package managers that support this (e.g., pnpm IIRC) create a package.json file for backwards compatibility.

@rfay
Copy link
Member Author

rfay commented Jul 30, 2022

The problem you had was that supervisord apparently doesn't change the directory until after it finds the binary, so an absolute path to the binary is required. I updated docs (will push shortly) and did a PR for your test repo.

I do note that you have the npm install in post-start hook in your demo repo, and supervisord is trying to start vite long before post-start hooks run. So that's a problem for first run for sure, we'll want to think about that.

Thanks for the good thinking/rambling about node usage.

  • I don't object to adding pnpm, but there seem to be an infinite number of these things.
  • People can obviously run a daemon like they always have with a post-start hook. I added that to docs as well.
  • For now, this is advanced configuration. While I agree we want to avoid support issues, it's not possible to do these kinds of things perfectly. Note that ddev is just creating a standard config for supervisord, and supervisord does its thing. We could make it so supervisord didn't bring the system down if the daemon didn't run.
  • Added more clarity about required settings, and examples have absolute paths.
  • This is a first stab at expanded support for nodejs stuff, it's by no means the last, people will want more and we'll be able to give it.

Copy link
Collaborator

@torenware torenware left a comment

Choose a reason for hiding this comment

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

Misc. comments on patch.

```yaml
hooks:
post-start:
- exec: "nohup /var/www/html/frontend/node_modules/.bin/vite &"
Copy link
Collaborator

Choose a reason for hiding this comment

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

Probably not the example to use. I'm reasonably sure this would not work; it's something I tried a few weeks back for ddev-viteserve


For example, you could use this configuration to run two instances of the nodejs http-server serving different directories:

```yaml
web_extra_daemons:
- name: "http-1"
command: "node_modules/.bin/http-server -p 3000"
command: "/var/www/html/node_modules/.bin/http-server -p 3000"
Copy link
Collaborator

Choose a reason for hiding this comment

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

Better to allow relative commands, and check the existence of a relative path before you pass it to supervisord for processing. Also: you can use ddev w/o being aware that the host's dev directory is mapped to /var/www/html.

directory: /var/www/html
- name: "http-2"
command: "node_modules/.bin/http-server /var/www/html/sub -p 3000"
command: "/var/www/html/node_modules/.bin/http-server /var/www/html/sub -p 3000"
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm not sure I've seen anybody use http_server like this in the wild. Odd example.

site := TestSites[0]

testcommon.ClearDockerEnv()
app := new(DdevApp)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Legal, but not really idiomatic Go. Better &DdevApp{}, especially if you're going to set anything in the structure immediately anyway.

return errors.Errorf("web container not found")
return fmt.Errorf("web container for %s not found", app.Name)
}
if container.State != "running" {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Good this. Since containers will fail to start on occasion, and you want that output where users will immediately see it.

@torenware
Copy link
Collaborator

torenware commented Jul 30, 2022

  • I don't object to adding pnpm, but there seem to be an infinite number of these things.

Welcome to JavaScript world. It's Dante-esqe "Abandon all hope all ye that enter here".

The problem is that npm is slow, and that yarn 1.x is not really getting maintained, so the community is moving off the later, and looking for something better for the niche. pnpm seems to be the most mature and the best maintained. But yeah, just heard the developer of bun speak Thursday night in SF, so Keep Watching This Space.

  • People can obviously run a daemon like they always have with a post-start hook. I added that to docs as well.

Actually harder than it looks. The vite example currently in the draft docs actually won't work; I've tried doing exactly that.

  • For now, this is advanced configuration. While I agree we want to avoid support issues, it's not possible to do these kinds of things perfectly. Note that ddev is just creating a standard config for supervisord, and supervisord does its thing. We could make it so supervisord didn't bring the system down if the daemon didn't run.

Supervisord is, remember, an implementation detail. It's clearly subject to problems if it is given bad input. Worth filtering at least a little before giving it control.

  • Added more clarity about required settings, and examples have absolute paths.

Better to support relative paths and pre-flight them. Remember the case of not running npm install; the path will not exist, and Supervisord will get pissy with you. So absolute paths is not the fix, because the lack is not the problem. The absolute path in this case would also be invalid.

  • This is a first stab at expanded support for nodejs stuff, it's by no means the last, people will want more and we'll be able to give it.

True.

@rfay
Copy link
Member Author

rfay commented Jul 30, 2022

Hmm, I tested the post-start hook on your repo and it worked... That's where it came from.

@torenware
Copy link
Collaborator

Hmm, I tested the post-start hook on your repo and it worked... That's where it came from.

Best test more; it doesn't stay up. It falls over if it actually does anything, which was hell to debug. Also, shutting it down would be a PITA if you started it that way. In ddev-viteserve, there's a reason I launch it in a tmux window, which is damonization lite. I can easily find the tmux detached process and kill it. Which I do.

I don't know if the problem is specific to vite (or even vite 2), or if more scripts would also die with only nohup and a ham sandwich. But I know vite is picker than that.

@rfay
Copy link
Member Author

rfay commented Jul 30, 2022

I'll use a different example.

@rfay rfay force-pushed the 20220728_proxy_arbitrary_ports branch from 04b9ed8 to cdf879e Compare July 30, 2022 19:45
@torenware
Copy link
Collaborator

torenware commented Jul 30, 2022

If you do get frustrated with supervisord, tmux is actually a good substitute. Here's my vite-serve command from that repo. Key part, where I kill the process if it's already running, and restart it in the tmux detached process:

  # create a background tmux session and tell it to run
  # our vite script
  tmux kill-session -t vite-sess 2>/dev/null
  tmux new -s vite-sess -d
  tmux send "node node_modules/vite/bin/vite.js --port $VITE_PRIMARY_PORT --host" C-m
  echo "Vite now serving $VITE_DIR"

Nice thing is it fails gracefully, and if you need to debug it, you reattach the window and see what happened. No smoke and no fire, just heat.

@rfay
Copy link
Member Author

rfay commented Jul 30, 2022

I imagine tmux is a great way to manage separate sessions; supervisord has been working fine for years and I probably won't mess with it. However, it may not be the perfect solution for adding user-daemons. I see you install tmux during your install in that repo.

@torenware
Copy link
Collaborator

torenware commented Jul 30, 2022

I install both tmux and pnpm, yeah. tmux is fairly light-weight, and it allows you to run scripts or binaries that don't tolerate what you need to do in order to daemonize a process (disconnecting the process from any terminal and exec'ing away). The process sees a realistic using pseudo terminal, and just runs as if it was in the foreground.

It also doesn't require you to save away the PID anywhere, since tmux will let you address a session by name.

Both tools have their place. My usage is slightly "off label", but was a good fit for a script that likes working under terminal.

@rfay
Copy link
Member Author

rfay commented Jul 30, 2022

Tests uncovered an interesting chicken-egg problem in TestExtraPortExpose when using mutagen. The test creates a subdirectory "sub" on the host and then starts the app; daemon requires the directory "sub" per configuration. But mutagen can't sync and create that until the container comes up. But oops, the container can't come up until it finds that "sub" dir.

Not sure what the solution is to that. The current behavior of supervisord of killing everything off if one thing can't come up may just not be useful. I know I put that in there years ago, probably for a reason. But not sure it's a good thing. I think healthchecks came along after that.

@rfay
Copy link
Member Author

rfay commented Jul 30, 2022

Added a delay between starting daemon and next retry if it fails, and added a little grace period in the script if running mutagen. Seems to sort this out. Also, bash is now used in running the command so that would have prevented some of the trouble you had @torenware .

@torenware
Copy link
Collaborator

torenware commented Jul 31, 2022 via email

@rfay
Copy link
Member Author

rfay commented Aug 1, 2022

This is passing tests now with mutagen and without. I hope a couple of people can try it out.

Copy link
Member Author

@rfay rfay left a comment

Choose a reason for hiding this comment

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

I went through this and think everything's OK, except that the comments for config.yaml in templates.go don't include web_extra_daemons, so that should be added on. I'll probably add that in a later PR.

@rfay rfay merged commit 98dd2d7 into ddev:master Aug 2, 2022
@rfay rfay deleted the 20220728_proxy_arbitrary_ports branch August 2, 2022 00:40
@hchonov
Copy link

hchonov commented Sep 16, 2022

Unfortunately, I was able to first test today and for me nginx does not forward the requests properly for my nextjs app. I've opened #4208 for this.

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

Successfully merging this pull request may close these issues.

None yet

3 participants