The 0.74.0 release introduces a new hook
to package authors: the install
hook. The install
hook runs when a package is first installed. This hook is unique in that it runs outside of a service or census context. With all other hooks, like init
and run
just to name a couple, the owning package must be loaded as a Supervisor service in order to execute the hook’s behavior. The install
hook can be packaged with any package regardless of whether it will run as a service and will execute when the package is installed. This installation may occur completely separate from any running Supervisor. For instance it could be triggered by a hab pkg install
command but may also be triggered by a Supervisor’s hab svc load
if the package has not yet been installed. Further, it could be a dependency of another package being installed. Because the package does not need to run as a service, any uninstalled package with an install
hook in the dependency tree of an installing package will have this hook executed as part of the parent installation.
Why a new hook?
Sometimes installing software involves much more than simply laying files on disk. In many cases, simply extracting the files packaged in a hart
file to the /hab/pkgs
folders and ensuring that some environment variables are set at runtime are all that is needed for an application to behave. However in some cases, there may be required system API calls needed to ensure that software components are correctly registered with certain OS sub systems. This is often the case with Windows applications which may require registry keys to be set, Windows features to be enabled or complicated install scripts and binaries to run before the software can be considered installed and ready to use.
Today we manage these kinds of applications in Habitat by including this installation behavior inside of an init
or run
hook. This has worked but what if the application is a command line tool, a library, or a stand alone application that runs outside of a Supervisor? In such cases, you must deploy the application as a service and include a run
hook that simply spins and sleeps. Even if the application is a service, what if it has dependencies like IIS or COM components that require the execution of scripts to fully register and configure those key dependencies. Well we either bundle those dependencies with the parent app or include their installation scripts with the parent package requiring the parent application to “know” intimate details about its dependencies.
With the install
hook we can now break these applications up so that any installation behavior needed in a dependant package can be managed by that package. This also means that one can install Habitat packages that run as stand-alone applications and not be required to run them as services in order to execute their installation scripts.
An example install hook scenario
To illustrate where an install
hook may be convenient, I’ll use a legacy Windows web application that we use in some Habitat demonstrations. This application requires IIS and ASP.Net 3.5 as well as a dependency on a COM library. Before leveraging the new install
hook, its pkg_deps
looked like:
$pkg_deps=@("core/dsc-core", "core/sql-dmo")
Its init
hook registered the COM component packaged in the sql-dmo
package:
."$env:SystemRoot\SysWow64\regsvr32.exe" /s "{{pkgPathFor "core/sql-dmo"}}\Program Files (x86)\Microsoft SQL Server\80\Tools\Binn\sqldmo.dll"
So my web application needs to know that its sql-dmo
dependency is COM based and must be aware of how such dependencies are registered as well as the exact files in the dependent package that must be referenced.
It’s run
hook uses the dsc-core
dependency to execute the following DSC configuration:
WindowsFeature netfx3 { Ensure = "Present" Name = "NET-Framework-Core" {{#if cfg.netfx3_source}} Source = "{{cfg.netfx3_source}}" {{/if}} } WindowsFeature ASP { Ensure = "Present" Name = "WEB-ASP-NET" } WindowsFeature static_content { Ensure = "Present" Name = "WEB-STATIC-CONTENT" }
This makes sure that the .NET 3.5 runtime is installed, IIS is enabled, and the ASP.Net ISAPI filters are configured and that additionally the WEB-STATIC-CONTENT
feature is enabled so that IIS can serve images, stylesheets and static html
files.
What if I wanted to install several applications that depended on the same .NET 3.5 framework or leveraged IIS or ASP.Net? Each application would need to include this same DSC (or equivelent) behavior.
Let’s see how this works differently with the install
hook.
In this case my web application does not need an install
hook, but if I could have dependencies with install
hooks, my pkg_deps
looks like this:
$pkg_deps=@("core/dsc-core", "core/sql-dmo", "core/iis-webserverrole", "core/dotnet-35sp1-runtime", "core/iis-aspnet35")
I still need dsc-core
to configure my app_pool with IIS, but that is fine since it is an application specific concern. I’m using a new sql-dmo
release that has an install
hook which can register itself:
."$env:SystemRoot\SysWow64\regsvr32.exe" /s "{{pkg.path}}\Program Files (x86)\Microsoft SQL Server\80\Tools\Binn\sqldmo.dll"
Now my application does not need to concern itself at all with this detail.
Additionally, I now have dependencies for IIS, the .Net 3.5 framework and a package that enables ASP.Net to be configured into IIS. Each of these dependencies have an install
hook that enables the appropriate Windows feature(s). As an example, here is the install
hook for the iis-webserverrole
package:
function Test-Feature { Write-Host "Check if IIS-WebServerRole is enabled..." $(dism /online /get-featureinfo /featurename:IIS-WebServerRole) -contains "State : Enabled" } if (!(Test-Feature)) { Write-Host "Enabling IIS-WebServerRole..." dism /online /enable-feature /featurename:IIS-WebServerRole if (!(Test-Feature)) { Write-Host "IIS-WebServerRole was not enabled!" exit 1 } }
This uses dism
to check and enable our Windows feature. Not as elegant as DSC but I know this will work back to a vanilla 2008 R2 or Windows 7 OS. If for some reason, enabling the feature is not succesful, I make sure that the hook exits with a non 0
exit code. Depending on what went wrong, dism
will often exit succesfully even if the feature enablement was not succesful. If the install
hook returns a non 0
exit code, Habitat considers the install
hook execution to be a failure and will retry whenever attempting to load the package or any other package that declares a dependency on this package.
Templating considerations
One can use handlebar templating syntax in an install
hook just like any other hook. However because the hook compilation occurs outside of any running Supervisor’s census ring, the install
hook and any configuration templates they reference will not have access to package binds
or the sys
namespace.
Another important difference with the install
hook is that it should not reference configuration files located in {{pkg.svc_config_path}}
– those rendered from a plan’s config
folder. Instead, the install
hook has access to a new pkg
property: {{pkg.svc_config_install_path}}
which is the destination of rendered templates located in a plan’s config_install
folder. This ensures that templates that are rerendered due to configuration changes in a running Supervisor will not trigger a service restart if they only impact the installation of a package.
Core plans supporting install hooks
The following plans have been added that leverage an install
hook:
core/dotnet-35sp1-runtime
core/dotnet-472-runtime
core/iis-aspnet35
core/iis-aspnet4
core/iis-webserverrole
These plans have been modified to support an install
hook:
core/sql-dmo
core/sqlserver
core/sqlserver2005
The sql-dmo
plan now adds its own COM registration as mentioned above and the sqlserver
and sqlserver2005
plans provide install hooks so that a hab pkg export docker
can install SQL Server during the Docker image build as discussed below.
New windows-service package running .NET Core
To support the installation of .NET 4.x runtimes in an install
hook where you are running the Habitat Supervisor as a Windows service using the windows-service
package, we have migrated this package to run on .NET Core. At the time it was initially written, .NET Core did not support Windows services. Running as a .NET 4 Full Framework application, .NET 4 runtimes cannot be updated while the service is running without requiring a reboot. Migrating to .NET Core allows .NET 4 runtime packages to install without this issue.
In case you edit the service’s configuration to adjust logging or debug settings, be aware that the logging configurations have been moved to c:\hab\svc\windows-service\log4net.xml
and all other settings are now in c:\hab\svc\windows-service\HabService.dll.config
.
Docker exporter behavior
One other behavior change that comes with the install
hook is that the Docker exporter will execute the install
hook as part of the Docker image build. This is especially helpful when a service has install behavior that takes a long time to run. Currently if this behavior is in an init
hook, the installation will occur on every docker run
of the image but with an install
hook it can be a one time operation that occurs during the export operation.
The new --memory
argument
We have added a new Docker exporter argument --memory
to support some common installation scenarios. The value passed to this argument is simply forwarded to docker build
. To illustrate when you would use the --memory
argument, imagine installing something like SQL Server. If you are using Docker from a Windows 10 environment, the default amount of memory allocated to a container is 1GB. However, installing SQL Server will likely fail with only a single GB of RAM. I can use the --memory
argument to set an alternate amount of memory like 2gb
which is adequate for installing SQL Server.
Manipulating configuration data during the image build
Sticking with our SQL Server scenario, the default service account configured to run SQL Server in the core/sqlserver
plan is NT AUTHORITY\Network Service
, which is absolutely the best default in a non-container environment. However, a container based SQL Server install should run under NT AUTHORITY\SYSTEM
. The best (and only) way to override configuration defaults during a docker export is to use a HAB_
environment variables because we cannot mount a user.toml
into the docker build. So if I set the HAB_SQLSERVER
environment variable just before runing hab pkg export docker
:
$env:HAB_SQLSERVER="{`"svc_account`":`"NT AUTHORITY\\SYSTEM`"}"
my export should override any settings in the plan’s default.toml
with the toml specified above.
Enabling the INSTALL_HOOK
Habitat feature
The install
hook is currently an “experimental” Habitat feature and must be enabled in order for any of the install
hook behavior to run. To enable this feature, set the HAB_FEAT_INSTALL_HOOK
environment variable to any value. This must be set in the following contexts:
- shell – to enable it for cli commands like
hab pkg install
andhab pkg export docker
- Supervisor service – to enable the execution of any
install
hooks when the Supervisor loads an uninstalled package - Studio – to ensure that configuration templates located in the
config_install
folder are included in a built package’shart
.
Enabling the INSTALL_HOOK
feature in a Windows Docker Studio
By setting HAB_FEAT_INSTALL_HOOK
before entering any Studio, that variable should be persisted to the new Studio. However, a Windows Docker Studio runs the Supervisor inside of a Windows service which will not have access to the variable in the local shell. The easiest way to ensure that the Supervisor inside of a Windows Docker Studio is running with the INSTALL_HOOK
feature enabled is to run the following immediately after entering the studio:
setx HAB_FEAT_INSTALL_HOOK ON /m Restart-Service Habitat
This will set the environment variable at the machine
scope and restart the Habitat Supervisor for the variable to take effect.