farmer icon indicating copy to clipboard operation
farmer copied to clipboard

Support threading arbitrary arguments through to IBuilder.

Open isaacabraham opened this issue 4 years ago • 8 comments

Currently, IBuilder only takes in the Location as an argument. I proposed adding a Context object of some kind that can be used to thread global (immutable!) state through to all builders. This would permit scenarios as follows:

let myBuilder = foo {
    doSomething // can access "name" set below with value "dave"
}

let template = arm {
    set_context "name" "dave"
}

Linked to #592 - this would allow us to globally turn on or off default switches such as default identity etc.

Thoughts @ninjarobot ?

isaacabraham avatar Apr 26 '21 22:04 isaacabraham

Also adding @MNie @MoeJw @TheRSP - would you find this capability useful?

isaacabraham avatar Apr 26 '21 22:04 isaacabraham

I've bashed together a prototype - here's how you could consume it to create customised behaviours (of course, specific builders would need to support it, at least for right now - we could look at a more fully featured extensibility model as well that could allow custom "modifiers" to be applied through the Farmer pipeline to any resource):

// A custom keyword that wraps a generic statebag method
type ArmBuilder with
    [<CustomOperation "environment_name">]
    member this.EnvironmentName (state, value) = this.AddCustomState(state, "postfix", value)   

let storageAccount = storageAccount { name "prashlovesfarmer" }
let wa = webApp { name "blaha" }

let deployment = arm {
    location Location.NorthEurope
    environment_name "dev"
    add_resources [ storageAccount; wa ]
}

and in the web app:

        member this.BuildResources ctx = [
            // shadow this with a new Name, based on some custom logic to set the based name for the builder based on the context.
            let this =
                { this with
                    Name =
                        match postfix ctx.State with
                        | Some postfix -> this.Name-postfix
                        | None -> this.Name }

In the JSON:

      "apiVersion": "2020-06-01",
      "dependsOn": [
        "[resourceId('Microsoft.Insights/components', 'blaha-dev-ai')]",
        "[resourceId('Microsoft.Web/serverfarms', 'blaha-dev-farm')]"
      ],
      "identity": {
        "type": "None"
      },
      "kind": "app",
      "location": "northeurope",
      "name": "blaha-dev",
      "properties": {
        "httpsOnly": false,
        "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', 'blaha-dev-farm')]",
        "siteConfig": {
          "alwaysOn": false,
          "appSettings": [
            {
              "name": "APPINSIGHTS_INSTRUMENTATIONKEY",
              "value": "[reference(resourceId('Microsoft.Insights/components', 'blaha-dev-ai'), '2014-04-01').InstrumentationKey]"
            },
            {
              "name": "APPINSIGHTS_PROFILERFEATURE_VERSION",

etc.

isaacabraham avatar Apr 26 '21 23:04 isaacabraham

@isaacabraham I think this would be useful. We currently have env parameter which we pass through to all builders. One thing I don't quite understand is how we would make use of the parameter. Would we need to copy the whole BuildResources method?

r30e avatar Apr 27 '21 06:04 r30e

I think this might be also handy when configuring Common things between resources like Diagnostic -setting and alerts etc. enable them for all resources we deploy instead of just creating one for every resource.. but this might not be an easy thing to do

MoeJw avatar Apr 27 '21 12:04 MoeJw

@TheRSP I have no idea. At the moment, the prototype I have isn't truly extensible - the BuildResources method would need to be rewritten, as you say. A truly extensible option would be to allow you to provide a plugin - some kind of interface - which gets called after each builder with the context and all the resources that it generated; you could then do whatever you wanted to the ARM resources.

It would be fairly low-level though - you'd be dealing with IArmResource and things like that.

isaacabraham avatar Apr 27 '21 20:04 isaacabraham

The idea is nice, as Moe and Richard mentioned it could be pretty handy when doing some repeatable configuration for some resources.

As I understand it's gonna work as some kind of "applier" on IArmResource. The only concern I have right now is. What if we would want to extend only some parts of resources. I mean for example add function "a" to function/service bus/documentdb but not to the web app, etc. Is it a valid use case, or we don't want to extend it in such way?

MNie avatar Apr 28 '21 20:04 MNie

@MNie The way it would probably end up looking like would be that you simply get a collection of IArmTemplates (maybe with an Resource ID or similar) and then have to pattern match over them to "pick" the ones that you want - and then do whatever you want with them.

Alternatively, perhaps we could inject it earlier in the pipeline i.e. before the BuildResource method runs.

isaacabraham avatar Apr 28 '21 22:04 isaacabraham

I haven't thought this through fully so may be completely infeasible but could it be done such that you pass in a function to a builder saying how to use the context?

let storage = storageAccount{
  name "storage"
  use_context (fun res ctx -> {res with Name = ctx.ResourcePrefix + res.Name})    
}

arm {
  set_context {| ResourcePrefix = "companydev" |}
  add_resource storage
}

r30e avatar Apr 30 '21 12:04 r30e