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

A question of configuration #79

Open
Jaykul opened this issue Dec 27, 2016 · 6 comments
Open

A question of configuration #79

Jaykul opened this issue Dec 27, 2016 · 6 comments

Comments

@Jaykul
Copy link
Member

Jaykul commented Dec 27, 2016

Does anyone else use (external) configuration files for their modules?
How do you wish PowerShell supported that?

I looked at a bunch of modules which need configuration (like PSReadLine, PSCX, AzureRM, PowerShellGet, PSScriptAnalyzer) and every one of them is using a different way to deal with it. Most of them require you to configure them with their own commands, so you can't configure them until after you import them (and configuring them in your Profile.ps1 would force you to import them). A few use environment variables, global variables, or XML or JSON configuration files.

It seems to me that we can do better: there's an opportunity here for an RFC to the PowerShell Core project to either add a setting, or an event, or a set of configuration commands.

The idea is to create a simple common convention for a way to customize modules on import (or run initialization code after import). The requirements are:

  • The initialization must always get run, regardless of how the module is imported.
  • Configuration must not inadvertently import the module.

Here are my two main ideas, along with an old suggestion I was reminded of earlier today... what do you all think? Is there a better way?

  1. A Module Initialization Script

Several modules (notably AzureRM.Profile) use a "startup" script which is run by the module (within the module scope?) when the module is being loaded. Others don't have startup scripts, but still rely on cmdlets for all their customization (notable PSReadLine).

Azure is using the IModuleAssemblyInitializer interface and subtly abusing it to run a local script file for binary modules. However, we could run it any number of ways, and we could establish a pattern for the file name like "$PSHome\modulename-profile.ps1" ...

In an ideal world, we would add a setting for the module manifest, and PowerShell would automatically run that script at import time (as though it were dot-sourced at the end of the module psm1?).

  1. Module configuration

I wrote the Configuration Module to allow modules to store their settings in layered (i.e. default + machine + user) PSD1 data files that are stored in your AppData folders. This allows configuration files to be manipulated using commands from the Configuration module without worrying about importing the module you're configuring (potentially before you even have it installed).

I had to write some code to make serialization to and from psd1 work (we could use JSON, but I want more type safety and such). and I also made some assumptions about what people want/need (layering, file storage in AppData, etc).

In an ideal world, we would have these commands built-in to PowerShell, and the configuration would be automatically imported and exported on module load/unload, and would populate a magic variable $PSModuleConfig

  1. Module Load Event

This would not work "down level" but hypothetically, we could add a "ModuleImported" event to PowerShell 6 that would trigger (with a module name) do that someone could just write (in their main profile script) something like this:

Register-EngineEvent ModuleImported {
    switch($_) {
        "PSReadLine" { <# customize module #> }
    }
}

This would be interesting because it allows you do any sort of logic you want, and would therefore be backward compatible to old modules that have existing configuration or setting commands, even though it couldn't ever work with old versions of PowerShell.

The down side, of course, would be the lack of discoverability, and the fact that it wouldn't force authors to think about how they want configuration to work ;-)

Thoughts, comments, rejections?

@michaeltlombardi
Copy link

Personally, I've started using option 2 and I prefer it - backwards compatibility, sane places for configs to go, standardized way of writing configurations that the community is already familiar with for specifying settings in DSC, the layering has been useful, probably more things I can't remember at this point.

@indented-automation
Copy link
Contributor

Mostly 1 with the added reliance on commands within the module for customization. Where configuration is persistent, it was stored in XML and more recently JSON formatted files.

Configuration in my case is normally drawn in to a script level variable on module load (my a module init script).

I don't personally like the idea of polluting the users session with variables so I've avoided using Global variables. The same applies to environment variables. I felt hiding the configuration variable(s) simplified the amount of validation needed in the relatively unlikely event someone was fiddling with things (or I made a mistake and wrote a conflicting change into a different module).

I've used your Configuration module for changing module manifests (lately, at build time) but little beyond that so far. The module I'm updating now will use the Configuration module instead of my older approaches.

TL;DR: Yes I use external configuration. I would be very happy with 2.

@mattmcnabb
Copy link
Contributor

Could this work just like format files (without the xml, of course)? A module could contain a default configuration, which could be added to with a command similar to Update-FormatData, maybe Update-ConfigData?

@rkeithhill
Copy link

For PSCX we created a provider for configuration such that you have a PSCX drive to tweak configuration.

One issue if you create a permanent storage location for configurations is to make sure it is easily findable so when you set up a new machine, you can copy the configuration files to the new machine. For this reason, I'd rather see the user's configuration file stored in the user's $PROFILE dir. And shouldn't the default configuration reside in the module's install dir? Would machine config go in \Users\Public\Documents\WindowsPowerShell?

@kilasuit
Copy link

We already have the functionality in 1) and specifically

In an ideal world, we would add a setting for the module manifest, and PowerShell would automatically run that script at import time (as though it were dot-sourced at the end of the module psm1?).

If you place a ps1 in the module manifest, like so
NestedModules = @('Initialize.ps1')
then at load time this loads this initialization script in the module scope just like dot sourcing would do if it is in the psm1. It will run the RootModule/ModuleToProcess first and then run, in the order that is specified, anything in the NestedModules property. So you could have 2/3/4 etc scripts in there if you wanted.

Whereas if you had the ps1 in the module manifest like this
ScriptsToProcess= @('Initialize.ps1')
then this runs in the caller scope prior to running the import of the psm1 or dll listed in the RootModule/ModuleToProcess property in the manifest. Which is exactly what we dont want.

This is documented (albeit not very well) via the NestedModules Parameter in the help for New-ModuleManifest which is at
https://msdn.microsoft.com/powershell/reference/5.1/microsoft.powershell.core/New-ModuleManifest

When we think of personalisation of modules and how I wish I could easily manipulate how they would function, I do quite like the Ideals behind how @Jaykul has implemented it in the Configuration module as personally I prefer to see any machine level information in the C:\ProgramData folder than in the Public User location. Though this is just a preference of mine. In regards to @rkeithhill point on the using the $Profile directory, I think that it could be quite useful for those in the enterprises that have mapped home drives and move between systems regularly.

So in a way if you combined 1 & 2 a little you would already have all the functionality that you are looking for and although I do think the ModuleLoad Event could be interesting and useful however as noted it's not backwards compatible.

Only other thing would be that I'd be suggestive of dropping the need of CompanyName from the Configuration Module and keep the paths to match up with how modules are currently stored (v5 & above) so

  • $env:LocalAppData\WindowsPowerShell\Modules\<Module Name>\<Version>\Configuration.psd1
  • $env:AppData\WindowsPowerShell\Modules\<Module Name>\<Version>\Configuration.psd1
  • $env:ProgramData\WindowsPowerShell\Modules\<Module Name>\<Version>\Configuration.psd1

However there is 1 small snag and that is that in theory you'd need to also have the following for use in PowerShell Core

  • $env:LocalAppData\PowerShell\Modules\<Module Name>\<Version>\Configuration.psd1
  • $env:AppData\PowerShell\Modules\<Module Name>\<Version>\Configuration.psd1
  • $env:ProgramData\PowerShell\Modules\<Module Name>\<Version>\Configuration.psd1

and potentially the following for v4 and below

  • $env:LocalAppData\WindowsPowerShell\Modules\<Module Name>\Configuration.psd1
  • $env:AppData\WindowsPowerShell\Modules\<Module Name>\Configuration.psd1
  • $env:ProgramData\WindowsPowerShell\Modules\<Module Name>\Configuration.psd1

BUT

I like the idea and I've been spending time overhauling PowerShellGet partially due to a similar reason and as linked to I think this would make a good way forward to helping solve some of the PowerShellGet issues that have been considerably irritating over time.

@Jaykul
Copy link
Member Author

Jaykul commented Mar 13, 2017

@kilasuit Apart from scoping and loading order weirdness, there are two problems with solving the 1) Module initialization script by using nested modules:

  1. If a nested module doesn't exist, your module fails to load
  2. Configuration needs to be outside your module folder

One or the other of those I can solve:

  1. I could ship a default config file
  2. I could use NestedModules = @("$env:LocalAppData\PowerShell\Modules\<Module Name>\<Version>\Settings.ps1")

But the solution to each one makes the other one intractable....

For what it's worth, in principle, I agree about leaving the company out of the module paths, but I was nervous about the fact that it's possible to work around module name collisions by installing one as "current user" and one as "all users" ... so I chose to go with something I was more confident of. In retrospect, it makes the configuration files harder to find, so I'd probably be better off using Modules\ModuleName\GUID\Version if necessary...

However, I'm not sure about the location. Technically Microsoft PowerShell owns that, and could blow it all away... <AppData>\<CompanyName>\<ProductName>\PowerShellModule\<Version>\ is technically a more correct location, but again, even harder for a user to guess at?

Of course, the exact path doesn't matter if this is built in via dependence on PowerShell or a 3rd party module, because people will learn use the cmdlets to discover it.

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

No branches or pull requests

6 participants