In this exercise, we’ll migrate our code from the previous step directly into the Farmer codebase.
Now that you know that your resource model produces the correct Json value when passed into Farmer, we can now create a proper type that contains the “parameterised” parts of the above function, such as name
, sku
and adminUserEnabled
, and properly takes part in the Farmer pipeline, by implementing the IArmResource
interface. This type is normally a full Record and should use types as required to capture e.g. SKUs or other elements that would benefit from typing (in the example above, sku
is a string, but we will shortly replace that with a union type).
// src/Farmer/Arm/ContainerRegistry.fs
[<AutoOpen>]
module Farmer.Arm.ContainerRegistry
open Farmer
// Create a reference to the full ARM registries type and version.
let registries = ResourceType ("Microsoft.ContainerRegistry/registries", "2019-05-01")
// Temporarily define the SKU and other types alongside the IArmResource.
type Sku =
| Basic
| Standard
| Premium
type Registries =
{ Name : ResourceName
Location : Location
Sku : Sku
AdminUserEnabled : bool }
interface IArmResource with
member this.ResourceId = registries.resourceId this.Name
member this.JsonModel =
{| name = this.Name.Value
``type`` = "Microsoft.ContainerRegistry/registries"
apiVersion = "2019-05-01"
sku = {| name = this.Sku.ToString() |}
location = this.Location.ArmValue
tags = {||}
properties = {| adminUserEnabled = this.AdminUserEnabled |}
|} :> _ // upcast to obj
Notice how we perform simple “serialization” of elements such as the SKU, but otherwise most fields are just copied across.
The biggest difference is that we have now also introduced the notion of ResourceType
and ResourceId
. A ResourceType
allows you to specify the versioned ARM type that your resource implements; a ResourceId
represents the qualified path to a specific, named resource. It contains at least the resource’s type and its name, but can also optionally include e.g. the resource group that the resource belongs to.
Because most ARM resources have a set of fields that are commonly used e.g. name, location etc., Farmer comes with a helper factory function to construct ARM JSON objects quickly and easily. Here’s a shortened version of JsonModel
above:
{| registries.Create(this.Name, this.Location) with
sku = {| name = this.Sku.ToString() |}
properties = {| adminUserEnabled = this.AdminUserEnabled |}
|} :> _ // upcast to obj
Now, the common fields will be generated for us through the registies.Create
function; any custom fields (such as sku
and properties
) are then applied on top.
You can skip this step if you’re just experimenting in e.g. a script.
For now, we’ve created any associated types such as Sku
directly above the file, but you’ll want to migrate these to a Farmer.ContainerRegistry
module in Common.fs
afterwards e.g.
// src/Farmer/Common.fs
namespace Farmer
module ContainerRegistry =
type Sku =
| Basic
| Standard
| Premium
You can test this again easily by passing an instance into a Farmer deployment like we did in the previous step:
open Farmer.Arm.ContainerRegistry
let registries =
{ Name = ResourceName "my-registry"
Location = Location.WestEurope
Sku = ContainerRegistry.Basic
AdminUserEnabled = true }
let deployment = arm {
location Location.NorthEurope
add_resource registries
}
deployment
|> Writer.quickWrite "output"
Note that F# records must be completely filled at creation, so you must provide values for all four fields.
You could now write a test to assert the Json structure. Most tests in the project though tend to test from the Farmer builders, which we will get to soon. You can stop right here if you want - what you’ve done so far allows you to create IArmResource
objects which can be added to the Farmer pipeline. However, we will go further in the next exericse and make it even easier to create Container Registries by creating an IBuilder.