Monday 3 May 2021

Installing and running CFWheels in my Lucee Docker container

G'day:

2021-05-05

Some of the way I've needed to configure things to get CFWheels working across the board have been tweaked/augmented from what I have in this article. A stripped down version of the steps I currently have is at "Short version: getting CFWheels working outside the context of a web-browsable directory". I can't be arsed rewriting this every time I find something new I need to tweak, but I'll keep that other article up to date.

If you've been following along in the previous articles in this series (Lucee / CFWheels / Docker), You'll know I have a set of Docker containers running Nginx, MariaDB and Lucee. I have been configuring this lot using a TDD approach, so I have a suite of tests that demonstrate everything is integrated nicely. Now it's time to install CFWheels as well, and have a look at it. I will be continuing my TDD approach to doing this work, so will list the cases I'm addressing at the heading of each step. Update, two weeks later: no I won't. This took so bloody long and was such a pain in the arse, I'm not gonna bother with that.

Overall aim: "It serves the CFWheels welcome page"

Reading ahead through the CFWheels › Installation › Test it docs, I see I should expect a welcome page when I browse to the site, all going well. So I will write a test to expect this (CFWheelsTest.cfc):

component extends=testbox.system.BaseSpec {

    function run() {
        describe("Tests CFWheels app installation", () => {
            it("serves the CFWheels welcome page after installation", () => {
                http url="http://cfml-in-docker.frontend/" result="response";

                expect(response.status_code).toBe( 200, "HTTP status code incorrect")
                expect(response.fileContent.trim()).toBe( "NOT SURE YET", "Response body incorrect")
            })
        })
    }
}

I dunno what form this welcome will take as yet, so I've just got some text that definitely won't be right, and will start with that. I'd improve this later when I know what to check for.

Attempting (spoilers: and failing) to install using CFWheels CLI

Looking at the CFWheels installation docs, I should use CommandBox to do the installation from ForgeBox, so I'll give that a go. I've already got CommandBox installed to run my tests. One thing I've noticed from reading ahead, the default location the files for CFWheels get put in a manual install is just slapped in the web root. That's… erm… "less good than it could be", and I won't be doing this. I've already gone through this with TestBox, so you can read why in that earlier article: "Adding TestBox, some tests and CFConfig into my Lucee container › It needs to only expose public stuff to the public"

I can tell CommandBox where to install things easily enough:

root@efe37f109b87:/var/www# box install id=cfwheels-cli directory=vendor
√ | Installing package [forgebox:cfwheels-cli]
root@efe37f109b87:/var/www#

And this has updated my box.json for me:

root@efe37f109b87:/var/www# cat box.json
{
    "dependencies":{
        "cfwheels-cli":"^0.4.0"
    },
    "devDependencies":{
        "testbox":"^4.2.1+400"
    },
    "installPaths":{
        "cfwheels-cli":"vendor/cfwheels-cli/",
        "testbox":"vendor/testbox/"
    },
    "testbox":{
        "runner":"http://localhost:8888/test/runTests.cfm"
    }
}
root@efe37f109b87:/var/www#

So far… so good.

The way the docs are recommending I set up my new CFWheels app is through this CLI thing I just installed, by going box wheels new. It sounds to me like this is going to assume it can do whatever it likes in its install directory, so to start with I'm just going to do the install in a temp directory and have a look at what it does, and whether it will collide with my existing work. It's nae bother if it does, I'll move my stuff around, but I just want to know what I'm getting myself into first.

root@efe37f109b87:/var/tmp/cfwheels# pwd
/var/tmp/cfwheels
root@efe37f109b87:/var/tmp/cfwheels# box wheels new
ERROR (5.2.1+00295)

Command "wheels new" cannot be resolved.

Please type "help" for assistance.

root@efe37f109b87:/var/tmp/cfwheels#

OK. Not off to a good start. I gulped and wondered whether this needs to be run from within the directory I did the install, so I switched over to /var/www/vendor/cfwheels-cli and tried there: same result. I also tried doing it from within CommandBox's special wee shell, but the results were the same.

I tried to see what help was on offer for CFWheels in general:

CommandBox> wheels help

**************************************************
* CommandBox Help for wheels
**************************************************


Command [wheels] not found.

Fine. OK, look: I don't know what's going on here; and to be completely honest, I don't care. The manual installation process is "stick the files in a directory", and I can pretty much manage that by myself (cough: don't speak too soon, Cameron). I don't think I need the Wheels CLI to do this for me. Or, as is the case here: not do it for me. De-installing that.

Installing just CFWheels

I'll still use ForgeBox to install the app though:

root@efe37f109b87:/var/www# box install id=cfwheels directory=vendor/cfwheels
√ | Installing package [forgebox:cfwheels]
root@efe37f109b87:/var/www#

One thing to note here is that I needed to specify the installation subdirectory for CFWheels too, ie: directory=vendor/cfwheels. When I installed Testbox, I only had to specify the base directory, ie: directory=vendor, and the testbox subdirectory was created for me within that. Now I don't mind whether I need to not specify the directory the installation actually goes in, or whether I don't need to specify it. But it should be one or the other. I'm not sure what's going on here.

Anyway, having done this, I see all the files in a subdirectory how one might want. Kinda:

root@efe37f109b87:/var/www# ll vendor/cfwheels/
total 88
drwxr-xr-x 16 root root 4096 Apr 25 18:35 ./
drwxr-xr-x  1 root root 4096 Apr 25 18:35 ../
-rwxr--r--  1 root root   67 Nov 22 09:57 Application.cfc*
-rwxr--r--  1 root root  975 Apr 25 18:10 box.json*
drwxr-xr-x  6 root root 4096 Apr 25 18:35 config/
drwxr-xr-x  2 root root 4096 Apr 25 18:35 controllers/
drwxr-xr-x  2 root root 4096 Apr 25 18:35 events/
drwxr-xr-x  2 root root 4096 Apr 25 18:35 files/
drwxr-xr-x  2 root root 4096 Apr 25 18:35 global/
drwxr-xr-x  2 root root 4096 Apr 25 18:35 images/
-rwxr--r--  1 root root   52 Nov 22 09:57 index.cfm*
drwxr-xr-x  2 root root 4096 Apr 25 18:35 javascripts/
drwxr-xr-x  2 root root 4096 Apr 25 18:35 miscellaneous/
drwxr-xr-x  2 root root 4096 Apr 25 18:35 models/
drwxr-xr-x  2 root root 4096 Apr 25 18:35 plugins/
-rwxr--r--  1 root root   52 Nov 22 09:57 rewrite.cfm*
-rwxr--r--  1 root root  162 Nov 22 09:29 root.cfm*
drwxr-xr-x  2 root root 4096 Apr 25 18:35 stylesheets/
drwxr-xr-x  4 root root 4096 Apr 25 18:35 tests/
drwxr-xr-x  2 root root 4096 Apr 25 18:35 views/
drwxr-xr-x 14 root root 4096 Apr 25 18:35 wheels/
root@efe37f109b87:/var/www#

Most of that stuff is stub files for my Wheels application; except for the wheels directory which is the actual CFWheels application. I expected this, because the default approach to installing this thing is - remember - "slap it all in the web root". I'll deal with this next, but first I see a problem with box.json:

root@efe37f109b87:/var/www# cat box.json
{
    "dependencies":{
        "cfwheels":"^2.2.0"
    },
    "devDependencies":{
        "testbox":"^4.2.1+400"
    },
    "installPaths":{
        "testbox":"vendor/testbox/"
    },
    "testbox":{
        "runner":"http://localhost:8888/test/runTests.cfm"
    }
}
root@efe37f109b87:/var/www#

It's not put the installPath in for CFWheels. This is no good when I come to use this for a Dockerised deployment. I will quickly manually edit this file and do a rebuild and see what happens. Back in a few min.

(Oh I also note that CommandBox has written "vendor/testbox/" in there, despite me saying just "vendor". I mean… fine…: but why did it not do the same for CFWheels?)

Cool so I did the rebuild, and having put the installation path for CFWheels into box.json manually, it all seemed to work:

root@78cb5ad3bf2c:/var/www# ll vendor
total 16
drwxr-xr-x  1 root root 4096 Apr 25 18:47 ./
drwxr-xr-x  1 root root 4096 Apr 25 18:47 ../
drwxr-xr-x 16 root root 4096 Apr 25 18:47 cfwheels/
drwxr-xr-x  9 root root 4096 Apr 25 18:47 testbox/
root@78cb5ad3bf2c:/var/www# ll vendor/cfwheels/
total 84
drwxr-xr-x 16 root root 4096 Apr 25 18:47 ./
drwxr-xr-x  1 root root 4096 Apr 25 18:47 ../
-rwxr--r--  1 root root   67 Nov 22 09:57 Application.cfc*
-rwxr--r--  1 root root  975 Apr 25 18:47 box.json*
drwxr-xr-x  6 root root 4096 Apr 25 18:47 config/
drwxr-xr-x  2 root root 4096 Apr 25 18:47 controllers/
drwxr-xr-x  2 root root 4096 Apr 25 18:47 events/
drwxr-xr-x  2 root root 4096 Apr 25 18:47 files/
drwxr-xr-x  2 root root 4096 Apr 25 18:47 global/
drwxr-xr-x  2 root root 4096 Apr 25 18:47 images/
-rwxr--r--  1 root root   52 Nov 22 09:57 index.cfm*
drwxr-xr-x  2 root root 4096 Apr 25 18:47 javascripts/
drwxr-xr-x  2 root root 4096 Apr 25 18:47 miscellaneous/
drwxr-xr-x  2 root root 4096 Apr 25 18:47 models/
drwxr-xr-x  2 root root 4096 Apr 25 18:47 plugins/
-rwxr--r--  1 root root   52 Nov 22 09:57 rewrite.cfm*
-rwxr--r--  1 root root  162 Nov 22 09:29 root.cfm*
drwxr-xr-x  2 root root 4096 Apr 25 18:47 stylesheets/
drwxr-xr-x  4 root root 4096 Apr 25 18:47 tests/
drwxr-xr-x  2 root root 4096 Apr 25 18:47 views/
drwxr-xr-x 14 root root 4096 Apr 25 18:47 wheels/
root@78cb5ad3bf2c:/var/www#

Now my challenges start.

Reorganising the file structure

I have to take that listing above, and reorganise it as follows:

adam@DESKTOP-QV1A45U:/mnt/c/src/cfml-in-docker$ tree . --dirsfirst
.
├── public
│   ├── images
│   ├── javascripts
│   ├── stylesheets
│   ├── Application.cfc
│   ├── index.cfm
│   └── rewrite.cfm
├── src
│   ├── config
│   ├── controllers
│   ├── events
│   ├── files
│   ├── global
│   ├── miscellaneous
│   ├── models
│   ├── plugins
│   ├── views
│   ├── Application.cfc
│   └── root.cfm
├── tests
│   ├── functions
│   │   └── Example.cfc
│   ├── requests
│   │   └── Example.cfc
│   └── Test.cfc
└── vendor
   └── cfwheels
       └── wheels


Getting CFWheels to work with that file structure

A couple of week has passed since I wrote the preceding paragraph. In the interim I've written a couple of articles about some side challenges I have had as I progressed this work. They weren't directly-related enough to what I'm talking about here to include them, hence I've published them separately:

Along the way I've enhanced the testing I've had to do to ensure my tinkerings worked and didn't break anything. As it currently stands I have a CFWheels-driven site "working": I have created some test endpoints that behave as I expect, and I can run its internal test suite. That's a pretty light definition of "working", I know.

I'll continue to detail the CFWheels config changes I needed to make to get everything working…

After doing the work for that "How TDD and automated testing helped me…" article above, I had a decent battery of tests that acted as a safety net for expectations I had for my site URLs:

I'm showing you the status as they are now, after I've done the work; obviously as the work was under way they weren't all a) there; b) passing. I just can't be bothered rolling my code back to show them that way, and in a way this is a more useful listing anyhow.

Oh and ignore the skipped ones. There's a bug in CFWheels that prevents those from working properly: the 404 page in dev more returns a 200-OK. I've left the tests skipped as a reminder I need to revisit them once the bug is fixed. I actually note it's been fixed (nice work!), but the version it's in ain't up on ForgeBox yet.

So… how did I get the thing to work?

Well. Firstly. How did I not get it to work. My expectations were that any well-designed third-party app would not make assumptions about where it was in the file system; or where any files implementing it happened to be. All I expected to have to do here is to set a mapping for CFWheels, and a mapping for my app / implementation code, tell my code where the CFWheels application is and then it'd just work. However I'm afraid CFWheels is not a very well-designed application, so that was a non-starter.

Core CFWheels code has been implemented so that it will only work if the wheels directory is right slap-bang in the middle of my implementation code. Primarily this line of code in wheels/events/onapplicationstart.cfm:

$include(template = "config/settings.cfm");

And that weirdo (name and implementation) $include function does this to it:

<cfinclude template="../../#LCase(arguments.template)#">

Neat. Let's dispense with our carefully-cased file names, cos we'll enforce lower-case on them all. That's definitely the job of a function called "include". Sorry "$include". And we'll just force the all to be two driectories up from wheels/global because isn't that where everybody puts their files?

I'm going to need to put that wheels directory into the middle of my source-controlled application code files, are't I? Sigh. I'm not checking it in to source control though: that would be too far beyond the pale. I'm installing it to where it belongs: vendor, and then getting Docker to copy it into my source-controlled directory once it's there:

RUN git clone git@github.com:adamcameron/cfml-in-docker.git .

# irrelevant stuff snipped

RUN mkdir -p vendor
RUN box install

RUN cp -R /var/www/vendor/cfwheels/wheels /var/www/src/wheels

I'm also .gitignore-ing /src/wheels/ so it doesn't accidentally end up in source control.

I know this is a really jerry-built, but… well: my hands are kind of tied.

The next step can be sorted out via config settings CFWheels exposes via config/settings.cfm:

srcPrefix = "../src"

set(eventPath = "#srcPrefix#/events")
set(filePath = "#srcPrefix#/files")
set(modelPath = "#srcPrefix#/models")
set(modelComponentPath = "#srcPrefix#/models")
set(pluginPath = "#srcPrefix#/plugins")
set(pluginComponentPath = "#srcPrefix#/plugins")
set(viewPath = "#srcPrefix#/views")
set(controllerPath = "#srcPrefix#/controllers")

set(webPath = "")
set(imagePath = "images")
set(javascriptPath = "javascripts")
set(stylesheetPath = "stylesheets")

set(wheelsComponentPath = "cfmlInDocker.wheels")

This is almost OK. Except for the headless-function use of set, which appears in here as if by magic; and for some unfathomable reason that it's implemented in a .CFM file not in a method of maybe a Settings.cfc file or something a bit more OOP. The fact that this .CFM file is ultimately included into a .CFC file makes the situation worse, not better, IMO. This is endemic in the application design of CFWheels btw.

Getting the testing framework to work

The other thing I needed to do is to add some mappings so the inbuilt CFWheels testing "framework" can actually run tests. This is all web-driven, btw. These had to go into src/Application.cfc

thisDirectory = getDirectoryFromPath(getCurrentTemplatePath())
this.mappings["/public/wheels"] =  getCanonicalPath("#thisDirectory#wheels")
this.mappings["/public/tests"] = getCanonicalPath("#thisDirectory#../tests")
this.mappings["/app/tests"] = getCanonicalPath("#thisDirectory#../tests")

You might as why I need to put these into my application's Application.cfc, and I couldn't put them in a tests/Application.cfc that happened to extend the app's one. Yes, this was my expectation as well: this crazy notion of OOP and separation of concerns. However CFWheels apparently runs its tests via a controller in your web app, then there isn't really a tests/Application.cfc that I can leverage.

After that the UI in the test framework was still breaking, creating links incorrectly. This ended up being because I needed to tell CFWheels that I am rewriting URLs. Not sure why, but there you go (Tom King set me straight on this, on the CFWheels subchannel of the CFML Slack channel: he is a star and has been a big help to me in all of this). The settings are also in config/settings.cfm:

set(URLRewriting="On")
set(rewriteFile="index.cfm")

And after doing that lot: all my TestBox tests were green! And there's even a test in there that tests that CFWheels' tests are working, which they are. I say "all of them", that just means the coupla stub tests they include as examples, and two wee tests I stuck in as proof-of-working. I'm not doing anything major in there. But it's green:

I'll not go into the implementation of those tests here. I'll look at that particular horror story in a separate article.

And the site

Tests are one thing, but I better demonstrate to you that the CFWheels-driven site is up and running too:



Conclusion

Detailing how to get this working took a lot less time to write (and for you to read) than it did for me to work out. It was not helped by my various Nginx-config-shortcomings; but I did have to dive into CFWheels's code more than I would have liked, and that was an exercise in frustration in itself as the way it's been implemented is… "obstructive". As I mentioned above Tom King patiently answered a bunch of questions for me, which was also a big help.

However here I am. I've got my app working. It would not surprise me if there were some other config tweaks I need to make down the track, but I can start doing some exercises of adding some of me own routes, controller, models, views in here now. I've worked through the CFWHeels tutorials and a bunch of their screencasts, and it seems easy enough to throw simple things together. I have my suspicions that as soon as requirements are no longer simplistic then the way CFWheels is designed / implemented might start getting in its own way, but I'll give it the benefit of the doubt for now.

Righto.

--
Adam

 

 

 

 

But wait! There's more!

Just after pressing "Publish" on this, I foolishly clicked about on the UI of the site home page. And I clicked on "Docs":

And the thanks I got for doing that was this:

I looked through the CFWheels code to see what it was ass-u-me`ing, and it's here in wheels/functions.cfm:

this.wheels.rootPath = GetDirectoryFromPath(GetBaseTemplatePath());

// ...

// Add mapping to the root of the site (e.g. C:\inetpub\wwwroot\, C:\inetpub\wwwroot\appfolder\).
// This is useful when extending controllers and models in parent folders (e.g. extends="app.controllers.Controller").
this.mappings["/app"] = this.wheels.rootPath;

And in this case, GetBaseTemplatePath() is the index.cfm file in the public, not the app root. So that won't do. I thought this might be a show stopper because I can't override that because of the way CFWheels is architected. However all was not lost because as far as I can tell that base /app mapping is never used by itself, it's only ever used as the base for app.controllers, app.models, and app.wheels. I can just make the correct mappings to those, in my Application.cfc. But first a quick test:

it("serves the Docs page", () => {
    http url="http://cfml-in-docker.frontend/wheels/docs" result="response";

    expect(response.status_code).toBe(200, "HTTP status code incorrect")
    expect(response.fileContent).toInclude("<title>Docs | CFWheels</title>")
})

Now we can fix it:

thisDirectory = getDirectoryFromPath(getCurrentTemplatePath())
this.mappings["/public/wheels"] =  getCanonicalPath("#thisDirectory#wheels")
this.mappings["/public/tests"] = getCanonicalPath("#thisDirectory#../tests")
this.mappings["/app/tests"] = getCanonicalPath("#thisDirectory#../tests")

this.mappings["/app/controllers"] = getCanonicalPath("#thisDirectory#controllers")
this.mappings["/app/models"] = getCanonicalPath("#thisDirectory#models")
this.mappings["/app/wheels"] = getCanonicalPath("#thisDirectory#wheels")

And that sorted it. I wonder what the next glitch is gonna be. But I'm gonna stop looking. For now…

RightoAgain.

--
AdamAgain