-
Notifications
You must be signed in to change notification settings - Fork 181
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
[devbox] Make stack-examples using Postgres actually work #874
Conversation
Current dependencies on/for this PR:
This comment was auto-generated by Graphite. |
0557e19
to
dcec06f
Compare
bbb30db
to
8014da0
Compare
785cb3c
to
f04d04e
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This feels overly complex and outside of the realm of what devbox should care about. I'm all for making sure our tests can run, but this:
- Introduces significant complexity
- Generates different symlinks based on the depth of the project in the filesystem. (this in particular fells bad). It creates some branching that I'm afraid could cause issues in the future.
- Can generate different sym links depending on package name.
Another alternative:
- Have
CreateFilesAndShowReadme
create a symlink to[project]/.devbox/virtenv
. Keep this simple and deterministic (so don't change methods depending on length). - Replace existing
virtenv
variable in the templates with[symlink]/[pgk]
- If
[symlink]/[pgk]
is longer than 80 chars, show a warning. This should be incredibly rare. I'm not even sure we should do this, since this is an issue every developer has to deal with and is independent of devbox.
internal/plugin/plugin.go
Outdated
func ensureVirtenvPath(projectDir string) (string, error) { | ||
path := filepath.Join(projectDir, VirtenvPath) | ||
if err := createDir(path); err != nil { | ||
return "", err | ||
} | ||
return path, nil | ||
} | ||
|
||
func ensureVirtenvPathForPackage(projectDir, pkg string) (string, error) { | ||
virtenvPath, err := ensureVirtenvPath(projectDir) | ||
if err != nil { | ||
return "", err | ||
} | ||
path := filepath.Join(virtenvPath, pkg) | ||
if err := createDir(path); err != nil { | ||
return "", err | ||
} | ||
return path, nil | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does CreateFilesAndShowReadme
not already do this?
https://github.com/jetpack-io/devbox/blob/main/internal/plugin/plugin.go#L62-L66
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I ran into some failure cases. It seems this function is called from other callpaths as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
its possible, this was a false impression I got. I can remove these two ensure
functions.
internal/plugin/plugin.go
Outdated
"Virtenv": virtenvPathForPackage, | ||
"VirtenvRuntimePath": filepath.Join(virtenvXdgRuntimePath, pkg), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would just use Virtenv
and have the symlink in there. So Virtenv
becomes:
~/.local/run/[project-hash]/[pkg]
instead of having both (Virtenv and VirtenvRuntimePath)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah, okay, I can do that.
internal/plugin/files.go
Outdated
xdgRuntimePath, err := setupXdgRuntimePath(projectDir) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure this is the best place to do this. We will run this on every config we load. (so we're just running this multiple times unnecessarily).
CreateFilesAndShowReadme
which is the main API for plugins could ensure the symlink is created
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ack, on this being sub-optimal. I have a TODO below in line 65 to improve this.
getConfigIfAny
is used in quite a few places:
❯ git grep -n getConfigIfAny
internal/plugin/files.go:17:func getConfigIfAny(pkg, projectDir string) (*config, error) {
internal/plugin/hooks.go:6: c, err := getConfigIfAny(pkg, projectDir)
internal/plugin/info.go:12: cfg, err := getConfigIfAny(pkg, projectDir)
internal/plugin/plugin.go:54: cfg, err := getConfigIfAny(pkg, projectDir)
internal/plugin/plugin.go:136: cfg, err := getConfigIfAny(pkg, projectDir)
internal/plugin/services.go:10: conf, err := getConfigIfAny(pkg, projectDir)
So, not just CreateFilesAndShowReadme
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
moved TODO to this callsite.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getConfigIfAny
is a bit of a general load function for a single plugin, while the symlink is per-project. So IMO it makes sense to create in the same place we ensure virtenv directory is created (since that is what the symlink is supposed to point to)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can create it there, but I still need to pass it in. GetConfigIfAny
is called from many different code paths. This is why I have added a TODO to improve the code more holistically in this respect.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CreateFilesAndShowReadme
gets called on every ensurePackagesAreInstalled
so it's where you want this.
It's not the best API, but I'm confident you will get the symlink you want.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CreateFilesAndShowReadme
creates the virtenv
directory. It creates any support files needed for plugins (e.g configs, etc). Services cannot run without these files which is why I think it makes the most sense for us to create the symlink there.
The symlink path is computed deterministically using projectDir
which means GetConfigIfAny
can just get the Symlink
path without having to try and re-create it.
internal/plugin/files.go
Outdated
if len([]byte(xdgRuntimeDir)) < maxDirLen { | ||
if err := os.MkdirAll(xdgRuntimeDir, 0700); err != nil { | ||
return "", errors.WithStack(err) | ||
} | ||
return xdgRuntimeDir, nil | ||
} | ||
|
||
// TODO hook up to correct io.writer | ||
//ux.Fwarning("xdgRuntimeDir is too long for unix-socket paths, "+ | ||
// "falling back to /tmp/user-<uid>. xdgRuntime: %s\n", xdgRuntimeDir) | ||
|
||
// Fallback to /tmp/user-<uid>/devbox/ dir | ||
// We do not use os.TempDir() since it can be pretty long in MacOS. | ||
// /tmp is posix-compliant and expected to exist, and is short. | ||
dir := filepath.Join(fmt.Sprintf("/tmp/user-%d", os.Getuid()), "devbox") | ||
if err := os.MkdirAll(dir, 0700); err != nil { | ||
return "", errors.WithStack(err) | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure this complexity is really worth it. If the user nests their project so deep that sockets are invalid, it's not really up to devbox to fix it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I disagree here. Devbox is in charge of deciding the path to where the socket lives, so it is up to devbox to fix it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm with you on providing a symlink, but not on splitting the implementation. It's code that almost certainly will never get exercised.
if a users ~[HOME]/.local/run/v-12345/postgres/socket.12435 is more than 100 characters it means their username is 50 characters. The maximum on linux os 32.
internal/plugin/files.go
Outdated
deriveLinkName := func(projectDir string) (string, error) { | ||
h := fnv.New32a() | ||
_, err := h.Write([]byte(projectDir)) | ||
if err != nil { | ||
return "", errors.WithStack(err) | ||
} | ||
hashed := strconv.FormatUint(uint64(h.Sum32()), 10) | ||
if len(hashed) > virtenvPathHashLength { // the length is expected to be 10 but being defensive. | ||
hashed = hashed[:virtenvPathHashLength] | ||
} | ||
linkName := fmt.Sprintf("virtenv-%s", hashed) | ||
return linkName, nil | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not just use md5? Also use base64 to make it smaller, 5 chars is more than enough entropy.
Finally you can name it v-[hash]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wanted a non-crypto hash so its faster. Not sure md5 provides any meaningful benefit here.
other points are 👍🏾 on 5 chars and v-hash
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I disagree on your assessment that its adding complexity for a scenario that we need not support. This scenario happens in both real-world situations and tests (which are also real-world situations: our users can be expected to write CICD tests for their own projects).
So I am going to update but preserve the fallback so that this scenario can be robust to failure.
Happy to discuss more.
internal/plugin/files.go
Outdated
xdgRuntimePath, err := setupXdgRuntimePath(projectDir) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ack, on this being sub-optimal. I have a TODO below in line 65 to improve this.
getConfigIfAny
is used in quite a few places:
❯ git grep -n getConfigIfAny
internal/plugin/files.go:17:func getConfigIfAny(pkg, projectDir string) (*config, error) {
internal/plugin/hooks.go:6: c, err := getConfigIfAny(pkg, projectDir)
internal/plugin/info.go:12: cfg, err := getConfigIfAny(pkg, projectDir)
internal/plugin/plugin.go:54: cfg, err := getConfigIfAny(pkg, projectDir)
internal/plugin/plugin.go:136: cfg, err := getConfigIfAny(pkg, projectDir)
internal/plugin/services.go:10: conf, err := getConfigIfAny(pkg, projectDir)
So, not just CreateFilesAndShowReadme
.
internal/plugin/files.go
Outdated
deriveLinkName := func(projectDir string) (string, error) { | ||
h := fnv.New32a() | ||
_, err := h.Write([]byte(projectDir)) | ||
if err != nil { | ||
return "", errors.WithStack(err) | ||
} | ||
hashed := strconv.FormatUint(uint64(h.Sum32()), 10) | ||
if len(hashed) > virtenvPathHashLength { // the length is expected to be 10 but being defensive. | ||
hashed = hashed[:virtenvPathHashLength] | ||
} | ||
linkName := fmt.Sprintf("virtenv-%s", hashed) | ||
return linkName, nil | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wanted a non-crypto hash so its faster. Not sure md5 provides any meaningful benefit here.
other points are 👍🏾 on 5 chars and v-hash
internal/plugin/files.go
Outdated
if len([]byte(xdgRuntimeDir)) < maxDirLen { | ||
if err := os.MkdirAll(xdgRuntimeDir, 0700); err != nil { | ||
return "", errors.WithStack(err) | ||
} | ||
return xdgRuntimeDir, nil | ||
} | ||
|
||
// TODO hook up to correct io.writer | ||
//ux.Fwarning("xdgRuntimeDir is too long for unix-socket paths, "+ | ||
// "falling back to /tmp/user-<uid>. xdgRuntime: %s\n", xdgRuntimeDir) | ||
|
||
// Fallback to /tmp/user-<uid>/devbox/ dir | ||
// We do not use os.TempDir() since it can be pretty long in MacOS. | ||
// /tmp is posix-compliant and expected to exist, and is short. | ||
dir := filepath.Join(fmt.Sprintf("/tmp/user-%d", os.Getuid()), "devbox") | ||
if err := os.MkdirAll(dir, 0700); err != nil { | ||
return "", errors.WithStack(err) | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I disagree here. Devbox is in charge of deciding the path to where the socket lives, so it is up to devbox to fix it.
internal/plugin/plugin.go
Outdated
"Virtenv": virtenvPathForPackage, | ||
"VirtenvRuntimePath": filepath.Join(virtenvXdgRuntimePath, pkg), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah, okay, I can do that.
internal/plugin/plugin.go
Outdated
func ensureVirtenvPath(projectDir string) (string, error) { | ||
path := filepath.Join(projectDir, VirtenvPath) | ||
if err := createDir(path); err != nil { | ||
return "", err | ||
} | ||
return path, nil | ||
} | ||
|
||
func ensureVirtenvPathForPackage(projectDir, pkg string) (string, error) { | ||
virtenvPath, err := ensureVirtenvPath(projectDir) | ||
if err != nil { | ||
return "", err | ||
} | ||
path := filepath.Join(virtenvPath, pkg) | ||
if err := createDir(path); err != nil { | ||
return "", err | ||
} | ||
return path, nil | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I ran into some failure cases. It seems this function is called from other callpaths as well.
internal/plugin/files.go
Outdated
// The symlink enables the path to be short so that unix sockets can be placed | ||
// in this directory. Unix sockets have legacy ~100 char limits for their paths. | ||
// | ||
// TODO savil. Calculate this once for each plugin, and reuse. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should be: once for each project
b60ab66
to
ccdce85
Compare
this is also not accurate. The symlinks are one per project. |
I think this is a nice thing to do for users, but like everything there are tradeoffs. I do think this going beyond what devbox needs to do (user could create symlink themselves), but given that we want to provide a good user experience I think creating the symlink is the way to go. That said, splitting the implementation feels like code that is (almost?) impossible to exercise at the expense of leaving code in that we might never feel confident removing (it's always harder to remove stuff like this). |
internal/plugin/files.go
Outdated
"go.jetpack.io/devbox/plugins" | ||
) | ||
|
||
func getConfigIfAny(pkg, projectDir string) (*config, error) { | ||
func getConfigIfAny(pkg, projectDir, xdgRuntimePath string) (*config, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
instead of passing xdgRuntimePath
here, what do you think of the following API:
plugin.VirtenvSymlinkPath(projectDir string) string
plugin.CreateVirtenvSymlink(projectDir string) string
CreateVirtenvSymlink
callsVirtenvSymlinkPath
to get path it needs to create.CreateFilesAndShowReadme
callsCreateVirtenvSymlink
to create the symlink if neededgetConfigIfAny
callsVirtenvSymlinkPath
to populatevirtenv
variable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like it +1
internal/plugin/files.go
Outdated
// | ||
// The symlink enables the path to be short so that unix sockets can be placed | ||
// in this directory. Unix sockets have legacy ~100 char limits for their paths. | ||
func setupXdgRuntimePath(projectDir string) (string, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit, rename to CreateVirtenvSymlink
internal/plugin/files.go
Outdated
|
||
// virtenvRuntimeLinkPath returns a path for the given project's virtenv's Runtime resources to live in. | ||
// It strives to be XDG compliant, but will diverge if needed to be short enough to be used in unix sockets. | ||
func virtenvRuntimeLinkPath(projectDir string) (string, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit rename to VirtenvSymlinkPath
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you put CreateVirtenvSymlink
and VirtenvSymlinkPath
into new file? (maybe symlink.go
)
@mikeland86 thanks for the after-hours review. Let me think about this over the weekend and get it to a place we're all happy with. |
@mikeland86 I took to heart your feedback about designing this properly, since it'll be hard to make changes later. Falling back to /tmp: lets drop it
For completeness, I'll add: the fallback path gets exercised in the unit-test, and in the examples testscripts. In our testscripts, we set That said, I agree that for realistic user scenarios having the XDG_RUNTIME_PATH is sufficient for postgres plugins. The fallback Lets drop the Symlink has problems Reading this PR again, I find linking The spec on XDG_RUNTIME_DIR states:
I don't think we need be concerned about managing the lifetime of the XDG_RUNTIME_DIR; that seems the OS's job, but likely best to not have a link pointing to all of Suggested fix:
My proposal to move forward is:
What do you think? |
Spoke to Mike. Instead of (1) in the proposal for per-plugin link from XDG_RUNTIME_DIR, we'll have: |
b3f465b
to
783be5c
Compare
@mikeland86 PTAL |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for fixing all this -- I know it hasn't been straightforward. You mentioned you're planning on changing the hash, so LGTM.
@@ -14,6 +18,8 @@ | |||
"psql devbox_lapp < setup_postgres_db.sql" | |||
], | |||
"run_test": [ | |||
"set -e", | |||
"rm -rf $PGDATA", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this dangerous? Could there ever be a bug where the user's normal $PGDATA is set and it deletes their database?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh I think I don't need this... I added it while debugging and forgot to remove I think.
…t path is too long" error
…sureVirtenvXYZ functions, use 5 length hash, and minor other changes
…kPath (3) override XDG_STATE_HOME for examples testscripts
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice!
783be5c
to
9497fff
Compare
## Summary Remove virtualenv data xdg directory symlinks. The current state is quite hard to reason about for the following reason: ### Diverging source of truth We have `.devbox/virtualenv` which supposedly should be the source of truth for plugins. However, upon inspection, it looks like the paths and variables set for `{{ .Virtualenv }}` is pointing at my XDG directory. Because of such divergence, any operations that directly modifies the XDG directory is not reflected in the `.devbox/virtualenv` directory. However, the rest of the devbox CLI regards `.devbox/virtualenv` as the source of truth. This is the root cause of quite a few plugin errors such as mariadb, drupal, and python venv. ### Obscure XDG path The reason we have a hash directory in our XDG path is to avoid path collision. But since I use devbox so often, my XDG paths become a clown show: <img width="920" alt="Screenshot 2023-06-13 at 12 18 10 PM" src="https://github.com/jetpack-io/devbox/assets/2292093/b539362e-bfd7-4594-a7da-6bf76ad2e11c"> This makes it impossible to debug plugins. To get around the paths too long error described in #874, I override the env variable that points to the `.sock` path in our `run_test` scripts. ### TODO - [x] Clean up symlink creation functions - [x] Remove symlink tests ## How was it tested? ``` devbox run build cd examples/databases/mariadb devbox install devbox services up ```
Summary
Added
set -e
indevbox.json
oflapp-stack
andlepp-stack
examples. This exposed failure scenarios indevbox run run_test
:curl
was missing from packages. Fix: add curl to packages.PGPORT
inenv
ofdevbox.json
, and updated other examples code that referenced the db-port.lepp-stack
, the nginx port of80
was not working in Github Actions. Fix: Changed to8089
.Fix for "unix socket path too long" error
Unix socket paths need to be less than approx. 100 chars long. This is a legacy limitation.
Postgres's socket path is determined by the
PGHOST
location.- in
plugins/postgresql.json
, we setPGHOST
toVirtenv
template data.- We make a symlink from
XDG_STATE_HOME/devbox/v-<projectDir hash>
to<project dir>/.devbox/virtenv
, and set the template'sVirtenv
data to be this symlink's path:XDG_STATE_HOME/devbox/v-<projectDir hash>/<plugin>
Test Plan
to run the
lapp-stack
andlepp-stack
:virtenvSymlinkPath
debugging pro-tip:
When there is a failure, there will often be active postgres processes. Trying to run
run_test
again may lead to issues, sopkill -f postgres
them first. This is because failures in run_test preventdevbox services stop
from cleaning these processes up.