From add39ddeaef3a48e04a719d7f6e63b9e7cdb2157 Mon Sep 17 00:00:00 2001 From: "NORTHAMERICA\\dramuy" Date: Wed, 4 Apr 2018 13:19:08 -0700 Subject: [PATCH] Initial commit --- cli/content/vm_alisases.json | 87 +++++ cli/vmss/module.libsonnet | 93 +++++ cli/vmss/moduleroot.jsonnet | 13 + cli/vmss/parameters.libsonnet | 118 +++++++ cli/vmss/tests/complex.json | 60 ++++ cli/vmss/tests/complex.jsonnet | 11 + cli/vmss/tests/simple.json | 22 ++ cli/vmss/tests/simple.jsonnet | 8 + .../VirtualMachineScaleSet/module.libsonnet | 78 +++++ .../VirtualMachineScaleSet/tests/simple.json | 25 ++ .../tests/simple.libsonnet | 6 + .../virtualMachineScaleSet.libsonnet | 317 ++++++++++++++++++ core/module.libsonnet | 85 +++++ core/moduledef.libsonnet | 20 ++ core/resource.libsonnet | 23 ++ examples/simplified_vmss/module.libsonnet | 85 +++++ module.libsonnet | 93 +++++ moduleroot.jsonnet | 13 + network/LoadBalancer/loadBalancer.libsonnet | 115 +++++++ network/LoadBalancer/module.libsonnet | 44 +++ network/LoadBalancer/tests/simple.libsonnet | 21 ++ network/NetworkSecurityGroup/module.libsonnet | 26 ++ .../networkSecurityGroup.libsonnet | 65 ++++ .../NetworkSecurityGroup/tests/simple.json | 25 ++ .../tests/simple.libsonnet | 8 + network/PublicIpAddress/module.libsonnet | 33 ++ .../PublicIpAddress/publicIpAddress.libsonnet | 24 ++ network/PublicIpAddress/tests/simple.json | 26 ++ .../PublicIpAddress/tests/simple.libsonnet | 14 + network/VirtualNetwork/module.libsonnet | 69 ++++ network/VirtualNetwork/tests/complex.jsonnet | 61 ++++ network/VirtualNetwork/tests/simple.json | 25 ++ network/VirtualNetwork/tests/simple.libsonnet | 6 + .../VirtualNetwork/virtualNetwork.libsonnet | 79 +++++ parameters.libsonnet | 118 +++++++ readme.md | 61 ++++ 36 files changed, 1977 insertions(+) create mode 100644 cli/content/vm_alisases.json create mode 100644 cli/vmss/module.libsonnet create mode 100644 cli/vmss/moduleroot.jsonnet create mode 100644 cli/vmss/parameters.libsonnet create mode 100644 cli/vmss/tests/complex.json create mode 100644 cli/vmss/tests/complex.jsonnet create mode 100644 cli/vmss/tests/simple.json create mode 100644 cli/vmss/tests/simple.jsonnet create mode 100644 compute/VirtualMachineScaleSet/module.libsonnet create mode 100644 compute/VirtualMachineScaleSet/tests/simple.json create mode 100644 compute/VirtualMachineScaleSet/tests/simple.libsonnet create mode 100644 compute/VirtualMachineScaleSet/virtualMachineScaleSet.libsonnet create mode 100644 core/module.libsonnet create mode 100644 core/moduledef.libsonnet create mode 100644 core/resource.libsonnet create mode 100644 examples/simplified_vmss/module.libsonnet create mode 100644 module.libsonnet create mode 100644 moduleroot.jsonnet create mode 100644 network/LoadBalancer/loadBalancer.libsonnet create mode 100644 network/LoadBalancer/module.libsonnet create mode 100644 network/LoadBalancer/tests/simple.libsonnet create mode 100644 network/NetworkSecurityGroup/module.libsonnet create mode 100644 network/NetworkSecurityGroup/networkSecurityGroup.libsonnet create mode 100644 network/NetworkSecurityGroup/tests/simple.json create mode 100644 network/NetworkSecurityGroup/tests/simple.libsonnet create mode 100644 network/PublicIpAddress/module.libsonnet create mode 100644 network/PublicIpAddress/publicIpAddress.libsonnet create mode 100644 network/PublicIpAddress/tests/simple.json create mode 100644 network/PublicIpAddress/tests/simple.libsonnet create mode 100644 network/VirtualNetwork/module.libsonnet create mode 100644 network/VirtualNetwork/tests/complex.jsonnet create mode 100644 network/VirtualNetwork/tests/simple.json create mode 100644 network/VirtualNetwork/tests/simple.libsonnet create mode 100644 network/VirtualNetwork/virtualNetwork.libsonnet create mode 100644 parameters.libsonnet create mode 100644 readme.md diff --git a/cli/content/vm_alisases.json b/cli/content/vm_alisases.json new file mode 100644 index 0000000..d682485 --- /dev/null +++ b/cli/content/vm_alisases.json @@ -0,0 +1,87 @@ +{ + "$schema":"http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json", + "contentVersion":"1.0.0.0", + "parameters":{}, + "variables":{}, + "resources":[], + + "outputs":{ + "aliases":{ + "type":"object", + "value":{ + + "Linux":{ + "CentOS":{ + "publisher":"OpenLogic", + "offer":"CentOS", + "sku":"7.3", + "version":"latest" + }, + "CoreOS":{ + "publisher":"CoreOS", + "offer":"CoreOS", + "sku":"Stable", + "version":"latest" + }, + "Debian":{ + "publisher":"credativ", + "offer":"Debian", + "sku":"8", + "version":"latest" + }, + "openSUSE-Leap": { + "publisher":"SUSE", + "offer":"openSUSE-Leap", + "sku":"42.2", + "version": "latest" + }, + "RHEL":{ + "publisher":"RedHat", + "offer":"RHEL", + "sku":"7.3", + "version":"latest" + }, + "SLES":{ + "publisher":"SUSE", + "offer":"SLES", + "sku":"12-SP2", + "version":"latest" + }, + "UbuntuLTS":{ + "publisher":"Canonical", + "offer":"UbuntuServer", + "sku":"16.04-LTS", + "version":"latest" + } + }, + + "Windows":{ + "Win2016Datacenter":{ + "publisher":"MicrosoftWindowsServer", + "offer":"WindowsServer", + "sku":"2016-Datacenter", + "version":"latest" + }, + "Win2012R2Datacenter":{ + "publisher":"MicrosoftWindowsServer", + "offer":"WindowsServer", + "sku":"2012-R2-Datacenter", + "version":"latest" + }, + "Win2012Datacenter":{ + "publisher":"MicrosoftWindowsServer", + "offer":"WindowsServer", + "sku":"2012-Datacenter", + "version":"latest" + }, + "Win2008R2SP1":{ + "publisher":"MicrosoftWindowsServer", + "offer":"WindowsServer", + "sku":"2008-R2-SP1", + "version":"latest" + } + } + } + } + } +} diff --git a/cli/vmss/module.libsonnet b/cli/vmss/module.libsonnet new file mode 100644 index 0000000..73a3f97 --- /dev/null +++ b/cli/vmss/module.libsonnet @@ -0,0 +1,93 @@ +local network = { + LoadBalancer: import 'network/LoadBalancer/loadBalancer.libsonnet', + PublicIpAddress: import 'network/PublicIpAddress/module.libsonnet', + VirtualNetwork: import 'network/VirtualNetwork/module.libsonnet', +}; +local compute = { + VirtualMachineScaleSet: import 'compute/VirtualMachineScaleSet/virtualMachineScaleSet.libsonnet' +}; + +local core = import 'core/module.libsonnet'; +local Module = import 'core/moduledef.libsonnet'; + +Module { + + parameterMetadata:: import 'parameters.libsonnet', + + imageFromAlias(aliasOrImage):: + // + // extract display name from the content/vm_aliases.json file. The content/vm_aliases file + // can be found at: + // https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/arm-compute/quickstart-templates/aliases.json + // + assert std.type(aliasOrImage) == 'object' || std.type(aliasOrImage) == 'string' : "imageFromAlias parameter expected 'object' or 'string' type parameter - got %s" % [ std.type(aliasOrImage) ]; + local vmImages = (import 'cli/content/vm_alisases.json').outputs.aliases.value; + if std.type(aliasOrImage) == 'string' && std.objectHas(vmImages.Linux, aliasOrImage) then + vmImages.Linux[aliasOrImage] + else if std.type(aliasOrImage) == 'string' && std.objectHas(vmImages.Windows, aliasOrImage) then + vmImages.Windows[aliasOrImage] + else + assert std.type(aliasOrImage) == 'object' : "Unable to find image '%s'" % [ aliasOrImage ]; + aliasOrImage, + + // Build a virtual network if one is not provided. + virtualNetwork:: + // Optionally build a virtual network. + local shouldCreateVnet = $.arguments.virtualNetwork == '[new]'; + if shouldCreateVnet then + network.VirtualNetwork.new( + '%sVNET' % [ $.arguments.name ], + addressPrefix='10.0.0.0/16') + .withSubnet('%sSubnet' % [ $.arguments.name ], addressPrefix='10.0.0.0/24') + else + $.arguments.virtualNetwork, + + // Build a public IP Address unless on is provided, or the parameters + // indicate that one is not wanted... + publicIpAddress:: + // Optionally build a public ip address + local shouldCreatePublicIpAddress = $.arguments.publicIpAddress == null; + if shouldCreatePublicIpAddress then + network.PublicIpAddress.new('%sLBPublicIP' % [ $.arguments.name ]) + .withAllocationMethod($.arguments.publicIpAllocationMethod) + else + $.arguments.publicIpAddress, + + // Build a front facing load balancer unless on is provided, or the + // parameters indicate that one is not wanted... + loadBalancer:: + local shouldCreateLoadBalancer = $.arguments.loadBalancer == null; + if shouldCreateLoadBalancer then + network.LoadBalancer.new('%sLB' % [ $.arguments.name ], sku= $.arguments.loadBalancerSku) + .withIpConfiguration('loadBalancerFrontEnd') + .withPublicIpAddress($.publicIpAddress) + .withBackendAddressPool($.arguments.backendPoolName) + .withNatRule($.arguments.authenticationType), + + // Build all resources created by the module... + virtualMachineScaleSet:: + compute.VirtualMachineScaleSet.new( + name= $.arguments.name, + osType = 'linux', // UNDONE - support windows again + imageReference = self.imageFromAlias($.arguments.imageReference), + overProvision = ! $.arguments.disableOverprovision, + capacity = $.arguments.instanceCount, + upgradePolicy = $.arguments.upgradePolicy, + skuName = $.arguments.vmSku + ) + .withAuth($.arguments.adminUserName, $.arguments.adminPassword, $.arguments.sshPublicKeys) + .withIdentity($.arguments.identity) + .behindLoadBalancer($.loadBalancer, $.virtualNetwork, $.arguments.subnet) + .withDataDisks($.arguments.dataDiskSizes, $.arguments.dataDiskCaching), + + outputs: { + virtualMachineScaleSet: { + type: 'string', + value: $.virtualMachineScaleSet.id + }, + [if $.virtualNetwork != null then 'virtualNetwork']: { + type: 'string', + value: core.resourceId($.virtualNetwork) + } + } +} \ No newline at end of file diff --git a/cli/vmss/moduleroot.jsonnet b/cli/vmss/moduleroot.jsonnet new file mode 100644 index 0000000..2ddc3ea --- /dev/null +++ b/cli/vmss/moduleroot.jsonnet @@ -0,0 +1,13 @@ +local m = import './module.libsonnet'; + +function(params) + m { + parameters: { + [k]: ((params.parameters)[k]).value + for k in std.objectFieldsAll(params.parameters) + }, + } { + '$schema': 'https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#', + contentVersion: '0.0.0.1' + } + diff --git a/cli/vmss/parameters.libsonnet b/cli/vmss/parameters.libsonnet new file mode 100644 index 0000000..d940515 --- /dev/null +++ b/cli/vmss/parameters.libsonnet @@ -0,0 +1,118 @@ +{ + name: { + type: 'string' + }, + imageReference: { + type: ['string', 'object'], + }, + adminUserName: { + type: 'string' + }, + location: { + type: 'string' + }, + disableOverprovision: { + type: 'boolean', + defaultValue: false, + }, + instanceCount: { + type: 'integer', + defaultValue: 2, + }, + upgradePolicy: { + type: 'string', + defaultValue: 'manual', + }, + adminPassword: { + type: 'string', + defaultValue: null + }, + authenticationType: { + type: 'string', + defaultValue: if $.osType == 'Windows' then 'rdp' else 'ssh', + }, + vmSku: { + type: 'string', + defaultValue: 'Standard_D1_v2', + }, + sshPublicKeys: { + type: 'string', + defaultValue: [], + }, + loadBalancer: { + type: ['object', 'string'], + defaultValue: null, // { sku: null }, + }, + loadBalancerSku: { + type: 'string', + defaultValue: null, + }, + virtualNetwork: { + type: [ 'object', 'string' ], + defaultValue: '[new]', + }, + backendPoolName: { + type: 'string', + defaultValue: null, + }, + natPoolName: { + type: 'string', + defaultValue: null, + }, + backendPort: { + type: 'string', + defaultValue: null, + }, + publicIpAllocationMethod: { + type: 'string', + defaultValue: 'dynamic', + }, + dataDiskSizes: { + type: ['array', 'number' ], + defaultValue: [], + }, + osType: { + type: 'string', + defaultValue: null, // error "'osType' is a required parameters", + }, + subnet: { + type: [ 'string', 'object' ], + defaultValue: null, + }, + customData: { + type: 'string', + defaultValue: null, + }, + licenseType: { + type: 'string', + defaultValue: null, + }, + singlePlacementGroup: { + type: 'boolean', + defaultValue: $.instanceCount <= 100, + }, + publicIpAddress: { + type: [ 'string', 'object' ], + defaultValue: null, + }, + publicIpAddressDnsName: { + type: 'string', + defaultValue: null, + }, + publicIpAddressPerVm: { + type: 'boolean', + defaultValue: false, + }, + zones: { + type: 'array', + defaultValue: [], + }, + dataDiskCaching: { + type: 'string', + defaultValue: null, + }, + identity: { + type: [ 'array', 'string' ], + defaultValue:null + } +} \ No newline at end of file diff --git a/cli/vmss/tests/complex.json b/cli/vmss/tests/complex.json new file mode 100644 index 0000000..edb8036 --- /dev/null +++ b/cli/vmss/tests/complex.json @@ -0,0 +1,60 @@ +{ + "$schema":"http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json", + "contentVersion":"1.0.0.0", + "parameters":{}, + "variables":{}, + "resources":[ + { + "name": "TheVnet", + "type": "Microsoft.JohanSte/modules", + "apiVersion": "1971-11--01", + "properties": { + "module": "network/VirtualNetwork/module.libsonnet", + "name": "vnet" + } + }, + { + "name": "PublicIpAddress", + "type": "Microsoft.JohanSte/modules", + "apiVersion": "1971-11-01", + "properties": { + "name": "pip", + "module": "network/PublicIpAddress/module.libsonnet" + } + }, + { + "name": "FirstVmssModule", + "type": "Microsoft.JohanSte/modules", + "apiVersion": "1971-11-01", + "properties": { + "module": "cli/vmss/module.libsonnet", + "name": "thisIsSoMuchSimpler", + "adminUserName": "johanste", + "imageReference": "UbuntuLTS", + "sshPublicKeys": [ "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCXwbIlLS57sL6S7LKplMtT1UJLZXkKNaWmLum1r+MMqncIFVdWbqsjSB1hHYxjFP5VxR/cK5Kq1jmAq57S6JeOlC1JC86Ka+S5EHrboZo1t/zNbAFRcQXxdLgqSB3767Q24W48fhhKngCKuVJ8bvvwTC0WskgY2ePlwlG1Erfzc0twnVkHOYISM0zFGEdKcjqYR1PaYXmaPzaZsFMQAv3ymUd1hM4mj3ZfHm34M4rxjlUaTrhxVdN5z2TBHFjJa6YLulmex9g4MRaaHQU9xDL5BXpWHRmyepQ+P1KBjOd9VUam789+BCYaQ5ZC/9XaPDVvwOUkQaF7PinNgI98Nx+T JohanSte@Johans-MacBook-Pro.local\n" ], + "dataDiskSizes": 1023, + "identity": "[system]", + "publicIpAddress": "[reference('PublicIpAddress').outputs.id.value]" + } + }, + { + "name": "SecondVmssModule", + "type": "Microsoft.JohanSte/modules", + "apiVersion": "1971-11-01", + "dependsOn": ["TheVnet"], + "properties": { + "module": "cli/vmss/module.libsonnet", + "name": "othevmms", + "adminUserName": "johanste", + "imageReference": "UbuntuLTS", + "sshPublicKeys": [ "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCXwbIlLS57sL6S7LKplMtT1UJLZXkKNaWmLum1r+MMqncIFVdWbqsjSB1hHYxjFP5VxR/cK5Kq1jmAq57S6JeOlC1JC86Ka+S5EHrboZo1t/zNbAFRcQXxdLgqSB3767Q24W48fhhKngCKuVJ8bvvwTC0WskgY2ePlwlG1Erfzc0twnVkHOYISM0zFGEdKcjqYR1PaYXmaPzaZsFMQAv3ymUd1hM4mj3ZfHm34M4rxjlUaTrhxVdN5z2TBHFjJa6YLulmex9g4MRaaHQU9xDL5BXpWHRmyepQ+P1KBjOd9VUam789+BCYaQ5ZC/9XaPDVvwOUkQaF7PinNgI98Nx+T JohanSte@Johans-MacBook-Pro.local\n" ], + "dataDiskSizes": 1023, + "identity": "[system]", + "virtualNetwork": "vnet", + "subnet": "default" + } + } + ], + + "outputs":{} +} \ No newline at end of file diff --git a/cli/vmss/tests/complex.jsonnet b/cli/vmss/tests/complex.jsonnet new file mode 100644 index 0000000..915409c --- /dev/null +++ b/cli/vmss/tests/complex.jsonnet @@ -0,0 +1,11 @@ +(import 'cli/vmss/module.libsonnet') + { + parameters:: { + name: 'complex', + adminUserName: 'johanste', + imageReference: 'UbuntuLTS', + adminPassword: 'super$ecret3611', + dataDiskSizes: [1024, 2024, 2048], + publicIpAddress: null, + } + } \ No newline at end of file diff --git a/cli/vmss/tests/simple.json b/cli/vmss/tests/simple.json new file mode 100644 index 0000000..04d9232 --- /dev/null +++ b/cli/vmss/tests/simple.json @@ -0,0 +1,22 @@ +{ + "$schema":"http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json", + "contentVersion":"1.0.0.0", + "parameters":{}, + "variables":{}, + "resources":[ + { + "name": "TheModule", + "type": "Microsoft.JohanSte/modules", + "apiVersion": "1971-11-01", + "properties": { + "module": "cli/vmss/module.libsonnet", + "name": "simples_ubuntu", + "adminUserName": "johanste", + "imageReference": "UbuntuLTS", + "sshPublicKeys": [ "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCXwbIlLS57sL6S7LKplMtT1UJLZXkKNaWmLum1r+MMqncIFVdWbqsjSB1hHYxjFP5VxR/cK5Kq1jmAq57S6JeOlC1JC86Ka+S5EHrboZo1t/zNbAFRcQXxdLgqSB3767Q24W48fhhKngCKuVJ8bvvwTC0WskgY2ePlwlG1Erfzc0twnVkHOYISM0zFGEdKcjqYR1PaYXmaPzaZsFMQAv3ymUd1hM4mj3ZfHm34M4rxjlUaTrhxVdN5z2TBHFjJa6YLulmex9g4MRaaHQU9xDL5BXpWHRmyepQ+P1KBjOd9VUam789+BCYaQ5ZC/9XaPDVvwOUkQaF7PinNgI98Nx+T JohanSte@Johans-MacBook-Pro.local\n" ] + } + } + ], + + "outputs":{} +} \ No newline at end of file diff --git a/cli/vmss/tests/simple.jsonnet b/cli/vmss/tests/simple.jsonnet new file mode 100644 index 0000000..531d1f1 --- /dev/null +++ b/cli/vmss/tests/simple.jsonnet @@ -0,0 +1,8 @@ +(import '../module.libsonnet') { + parameters:: { + name: 'funky', + adminUserName: 'johanste', + imageReference: 'UbuntuLTS', + sshPublicKeys: [ "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCXwbIlLS57sL6S7LKplMtT1UJLZXkKNaWmLum1r+MMqncIFVdWbqsjSB1hHYxjFP5VxR/cK5Kq1jmAq57S6JeOlC1JC86Ka+S5EHrboZo1t/zNbAFRcQXxdLgqSB3767Q24W48fhhKngCKuVJ8bvvwTC0WskgY2ePlwlG1Erfzc0twnVkHOYISM0zFGEdKcjqYR1PaYXmaPzaZsFMQAv3ymUd1hM4mj3ZfHm34M4rxjlUaTrhxVdN5z2TBHFjJa6YLulmex9g4MRaaHQU9xDL5BXpWHRmyepQ+P1KBjOd9VUam789+BCYaQ5ZC/9XaPDVvwOUkQaF7PinNgI98Nx+T JohanSte@Johans-MacBook-Pro.local\n" ] + } +} \ No newline at end of file diff --git a/compute/VirtualMachineScaleSet/module.libsonnet b/compute/VirtualMachineScaleSet/module.libsonnet new file mode 100644 index 0000000..4c6c7a5 --- /dev/null +++ b/compute/VirtualMachineScaleSet/module.libsonnet @@ -0,0 +1,78 @@ +local Module = import '../../core/moduledef.libsonnet'; +local VirtualMachineScaleSet = import 'virtualMachineScaleSet.libsonnet'; + +Module { + + id:: $.resource.id, + + parameterMetadata:: { + name: { + type: 'string', + }, + osType: { + type: 'string', + defaultValue: 'linux', + }, + skuName: { + type: 'string', + defaultValue: 'standard_d1_v2', + }, + imageReference: { + type: 'object', + }, + virtualNetwork: { + type: 'object', + defaultValue: null + }, + subnet: { + type: 'string', + defaultValue: null, + }, + capacity: { + type: 'number', + defaultValue: 3 + }, + overProvision: { + type: 'boolean', + defaultValue: null, + }, + platformFaultDomainCount: { + type: 'number', + defaultValue: null, + }, + singlePlacementGroup: { + type: 'number', + defaultValue: null, + }, + upgradePolicy: { + type: 'string', + defaultValue: 'manual', + }, + zoneBalance: { + type: 'boolean', + defaultValue: null + } + }, + + resource:: VirtualMachineScaleSet.new ( + name = $.arguments.name, + imageReference = $.arguments.imageReference, + virtualNetwork = $.arguments.virtualNetwork, + subnet = $.arguments.subnet, + osType = $.arguments.osType, + skuName = $.arguments.skuName, + capacity = $.arguments.capacity, + overProvision = $.arguments.overProvision, + platformFaultDomainCount = $.arguments.platformFaultDomainCount, + singlePlacementGroup = $.arguments.singlePlacementGroup, + upgradePolicy = $.arguments.upgradePolicy, + zoneBalance = $.arguments.zoneBalance + ), + + outputs: { + id: { + type: 'string', + value: $.resource.id + } + } +} \ No newline at end of file diff --git a/compute/VirtualMachineScaleSet/tests/simple.json b/compute/VirtualMachineScaleSet/tests/simple.json new file mode 100644 index 0000000..96a4596 --- /dev/null +++ b/compute/VirtualMachineScaleSet/tests/simple.json @@ -0,0 +1,25 @@ +{ + "$schema":"http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json", + "contentVersion":"1.0.0.0", + "parameters":{}, + "variables":{}, + "resources":[ + { + "name": "TestVirtualMachineScaleSet", + "type": "Microsoft.JohanSte/modules", + "apiVersion": "1971-11-01", + "properties": { + "module": "compute/VirtualMachineScaleSet/module.libsonnet", + "name": "vmss" + } + } + ], + + "outputs": { + "virtualMachineScaleSet": { + "type": "string", + "value": "[reference('TestVirtualMachineScaleSet').outputs.id.value]" + } + + } +} \ No newline at end of file diff --git a/compute/VirtualMachineScaleSet/tests/simple.libsonnet b/compute/VirtualMachineScaleSet/tests/simple.libsonnet new file mode 100644 index 0000000..23f4d48 --- /dev/null +++ b/compute/VirtualMachineScaleSet/tests/simple.libsonnet @@ -0,0 +1,6 @@ +(import '../module.libsonnet') { + parameters:: { + name: 'doesitwork', + imageReference: 'UbuntuLTS', + } +} \ No newline at end of file diff --git a/compute/VirtualMachineScaleSet/virtualMachineScaleSet.libsonnet b/compute/VirtualMachineScaleSet/virtualMachineScaleSet.libsonnet new file mode 100644 index 0000000..e0b9089 --- /dev/null +++ b/compute/VirtualMachineScaleSet/virtualMachineScaleSet.libsonnet @@ -0,0 +1,317 @@ +local armmodule = import '../../core/module.libsonnet'; + +local LinuxOsMixin = { + + _IdentityExtensionName:: 'ManagedIdentityExtensionForLinux', + + withAuth(userName, password=null, sshKeys=null):: + + self { + properties +: { + virtualMachineProfile +: { + osProfile +: { + adminUserName: userName, + [if password != null then 'adminPassword']: password, + [if sshKeys != null then'linuxConfiguration']: { + disablePasswordAuthentication: password == null, + ssh: { + publicKeys: [ + { + keyData: sshKey, + path: "/home/%s/.ssh/authorized_keys" % [ userName ], + } + for sshKey in sshKeys + ], + }, + }, + }, + }, + }, + }, + }; + +local WindowsOsMixin = { + + _IdentityExtensionName:: 'ManagedIdentityExtensionForWindows', + + withAuth(userName, password):: + assert password != null : 'Windows Virtual Machine needs a (valid) password'; + self { + properties +: { + virtualMachineProfile +: { + osProfile +: { + adminUserName: userName, + adminPassword: password, + }, + }, + }, + }, + }; + +local ManagedPlatformImageOsDiskMixin = { + fromImage(image, storageAccountType=null):: + self { + properties +: { + virtualMachineProfile +: { + storageProfile +: { + imageReference: image, + osDisk: { + caching: 'ReadWrite', + managedDisk: { + storageAccountType: storageAccountType + }, + createOption: 'FromImage', + }, + }, + }, + }, + }, + }; + +local NativePlatformImageOsDiskMixin = { + fromImage(image, osVhdUri, name, storageAccountType=null):: + self { + properties +: { + virtualMachineProfile +: { + storageProfile +: { + osDisk: { + name: name, + imageReference: image, + caching: 'ReadWrite', + createOption: 'FromImage', + osType: self.osType, + image: image, + vhd: { uri: osVhdUri }, + }, + }, + }, + }, + }, + }; + + +armmodule.Resource { + apiVersion: '2017-12-01', + type: 'Microsoft.Compute/virtualMachineScaleSets', + + new(name, + osType, + imageReference, + skuName='standard_d1_v2', + capacity=3, + overProvision=null, + platformFaultDomainCount=null, + singlePlacementGroup=null, + upgradePolicy='manual', + zoneBalance=null, + virtualNetwork=null, + subnet=null):: + + local osMixin = if osType == 'windows' then + WindowsOsMixin + else + LinuxOsMixin; + + (self + osMixin + ManagedPlatformImageOsDiskMixin { + name: name, + sku: { + capacity: capacity, + name: skuName, + }, + properties: { + [if overProvision != null then 'overProvision']: overProvision, + [if platformFaultDomainCount != null then 'platformFaultDomainCount']: platformFaultDomainCount, + [if singlePlacementGroup != null then 'singlePlacementGroup']: singlePlacementGroup, + upgradePolicy: { + mode: upgradePolicy + }, + [if zoneBalance != null then 'zoneBalance']: zoneBalance, + virtualMachineProfile: { + osProfile: { + computerNamePrefix: $.makeComputerNamePrefix(name) + }, + storageProfile: { + dataDisks: [], + }, + }, + }, + }).onSubnet(virtualNetwork, subnet).fromImage(imageReference), + + makeComputerNamePrefix(name):: + local computerNamePrefix = armmodule.stdex.strReplace(name, '_', ''); + computerNamePrefix + std.md5(computerNamePrefix)[0:5], + + withIdentity(nameOrId):: + if nameOrId == null then + self + else + assert nameOrId == '[system]' : "Only system assigned identity supported right now (expected '[system]', got '%s')" % [ nameOrId ]; + self { + [if nameOrId == '[system]' then 'identity']: { + type: "SystemAssigned" + }, + properties +: { + virtualMachineProfile +: { + extensionProfile +: { + extensions +: [ + { + name: $._IdentityExtensionName, + properties +: { + autoUpgradeMinorVersion: true, + publisher: "Microsoft.ManagedIdentity", + settings: { + port: 50342 + }, + type: $._IdentityExtensionName, + typeHandlerVersion: "1.0" + } + } + ] + }, + } + }, + }, + + withSku(name, tier='standard', capacity=3):: + self { + sku: { + tier: tier, + capacity: capacity, + name: name, + }, + + // We can tell the user that they called the same function more than once + // by replacing the method. TODO: Is this a good idea? + withSku(name, tier='standard', capacity=3):: + error 'withSku called multiple times - a mistake has been made!', + + }, + + + behindLoadBalancer(loadBalancer, vnet, subnet=null, backendAddressPool=null, natPool=null):: + if loadBalancer == null then + self + else + local lbName = armmodule.resourceName(loadBalancer); + local vnetName = armmodule.resourceName(vnet); + local backendAddressPoolName = if backendAddressPool != null then backendAddressPool else loadBalancer.properties.backendAddressPools[0].name; + local natPoolName = if natPool != null then natPool else if std.length(loadBalancer.properties.inboundNatPools) > 0 then loadBalancer.properties.inboundNatPools[0].name else null; + local subnetName = if subnet != null then subnet else vnet.properties.subnets[0].name; + local ipConfigurationName = '%sIPConfig' % [ lbName ]; + + self.withDependency(loadBalancer).withDependency(vnet) { + properties +: { + virtualMachineProfile +: { + networkProfile +: { + local isPrimary = std.length(self.networkInterfaceConfigurations) == 1, + + networkInterfaceConfigurations +: [ + { + name: '%sNic' % [ $.name ], + properties: { + primary: isPrimary, + ipConfigurations: [ + { + name: ipConfigurationName, + properties: { + loadBalancerBackendAddressPools: [ + { + id: "[concat(resourceId('Microsoft.Network/loadBalancers', '%s'), '/backendAddressPools/%s')]" % [ lbName, backendAddressPoolName ], + } + ], + subnet: { + id: "[concat(resourceId('Microsoft.Network/virtualNetworks', '%s'), '/subnets/%s')]" % [ vnetName, subnetName ], + }, + [if natPoolName != null then 'loadBalancerInboundNatPools']: [ + { + id: "[concat(resourceId('Microsoft.Network/loadBalancers', '%s'), '/inboundNatPools/%s')]" % [ lbName, natPoolName ], + }, + ], + }, + } + ] + } + } + ] + }, + }, + }, + }, + + withDataDisks(dataDisks, caching=null, storageAccountType=null):: + assert std.type(dataDisks) == 'array' || std.type(dataDisks) == 'number' : "Expected type of dataDisks to be one of ('array', 'number), got '%s'" % [ std.type(dataDisks)]; + local diskArray = if std.type(dataDisks) == 'number' then [ dataDisks ] else dataDisks; + local lun = std.length($.properties.virtualMachineProfile.storageProfile.dataDisks) + 1; + local numDisksToAdd = std.length(diskArray); + if numDisksToAdd == 0 then + self + else + self { + properties +: { + virtualMachineProfile +: { + storageProfile +: { + dataDisks +: [ + { + lun: lun + index - 1, + createOption: 'empty', + caching: caching, + diskSizeGB: diskArray[index], + managedDisk: { + storageAccountType: null + }, + }, + for index in std.range(0, numDisksToAdd - 1) + ], + }, + }, + }, + }, + + + _onSubnetById(subnet):: + local parts = std.split(subnet, '/'); + local vnetName = parts[8]; + local subnetName = parts[10]; + + self._onSubnetCore(vnetName, subnetName), + + _onSubnetCore(vnetName, subnetName):: + self { + properties +: { + virtualMachineProfile +: { + networkProfile +: { + local isPrimary = std.length(self.networkInterfaceConfigurations) == 1, + + networkInterfaceConfigurations +: [ + { + name: '%s' % [vnetName], + properties: { + primary: isPrimary, + ipConfigurations: [ + { + name: '%s-%s' % [vnetName, subnetName], + properties: { + subnet: { + id: "[concat(resourceId('Microsoft.Network/virtualNetworks', '%s'), '/subnets/%s')]" % [ vnetName, subnetName ], + }, + }, + } + ] + } + } + ] + }, + }, + }, + }, + + onSubnet(vnet, subnet=null):: + if vnet == null && subnet == null then + self + else if vnet == null && armmodule.isValidResourceId(subnet) then + self._onSubnetById(subnet) + else if armmodule.isResource(vnet) then + self.withDependency(vnet)._onSubnetCore(vnet.name, subnet) + else + self, +} \ No newline at end of file diff --git a/core/module.libsonnet b/core/module.libsonnet new file mode 100644 index 0000000..d732ebc --- /dev/null +++ b/core/module.libsonnet @@ -0,0 +1,85 @@ +{ + Resource:: import './resource.libsonnet', + + isValidResourceId(value):: + if std.type(value) != 'string' then false + else value[0] == '[' || std.startsWith(value, '/subscriptions'), + + isResource(resource):: + $.stdex.isObject(resource) && $.stdex.get(resource, 'type', null) != null, + + isModule(object):: + $.stdex.isObject(object) && $.stdex.get(object, 'resources', null) != null, + + resourceId(instanceOrString):: + if $.stdex.isString(instanceOrString) then instanceOrString + else if 'id' in instanceOrString then instanceOrString.id + else error "'Tried to get id from object that didn't have an id - '%s'" % [ instanceOrString ], + + resourceName(instanceOrName):: + if std.type(instanceOrName) == 'string' then + assert instanceOrName[0] != '[' : "TODO: Computed names not supported yet..."; + local parts = std.split(instanceOrName, '/'); + if std.length(parts) >= 8 then + // The 7th segment of an id is the name... + parts[7] + else + instanceOrName + else instanceOrName.name, + + stdex:: { + + isString(val):: + std.type(val) == 'string', + + isObject(val):: + std.type(val) == 'object', + + isArray(val):: + std.type(val) == 'array', + + mergeParameters(parameters, metadata, acceptUnknownParameters = false):: + // UNDONE: Check type + validate value + local unknownParameters = [ + parameterName + for parameterName in std.objectFieldsAll(parameters) + if !(parameterName in metadata) + ]; + assert acceptUnknownParameters || std.length(unknownParameters) == 0 : "Unexpected parameters '%s' received. Expected one of '%s'" % [ unknownParameters, std.objectFieldsAll(metadata) ]; + + local mergedParameters = parameters { + local validator = + if 'validate' in metadata[k] then + metadata[k].validate + else function(val) val, + + [k] : if k in super then validator(super[k]) else if 'defaultValue' in metadata[k] then metadata[k].defaultValue else error "Missing parameter '%s'" % [ k ] + for k in std.set(std.objectFieldsAll(parameters) + std.objectFieldsAll(metadata)) + }; + mergedParameters, + + + get(instance, member, default):: + assert self.isObject(instance) : "Incorrect type for parameter 'instance' - expected 'object', got '%s'" % [ std.type(instance) ]; + assert self.isString(member) : "Incorrect type parameter 'member' - expected 'string', got '%s'" % [ std.type(member) ]; + + if std.objectHasAll(instance, member) then + instance[member] + else + default, + + last(arr):: + assert std.type(arr) == 'array' : "parameter 'arr' must be of type 'array' - got " % [ std.type(arr) ]; + local len = std.length(arr); + if len == 0 then + null + else + arr[std.length(arr) - 1], + + strReplace(str, from, to):: + if str == '' then '' + else if std.startsWith(str, from) then to + self.strReplace(std.substr(str, std.length(from), std.length(str) - std.length(from)), from, to) + else std.substr(str, 0, 1) + self.strReplace(std.substr(str, 1, std.length(str) - 1), from, to), + }, +} + diff --git a/core/moduledef.libsonnet b/core/moduledef.libsonnet new file mode 100644 index 0000000..39a2f29 --- /dev/null +++ b/core/moduledef.libsonnet @@ -0,0 +1,20 @@ +local module = import './module.libsonnet'; +local stdex = module.stdex; + + +{ + parameters:: std.extVar('parameters'), + parameterMetadata:: error "A module must declare it's parameters and associated metadata", + + id:: $.outputs, + + arguments:: + stdex.mergeParameters($.parameters, $.parameterMetadata), + + resources:std.flattenArrays( + [ + if module.isResource(self[key]) then [ self[key] ] else self[key].resources, + for key in std.objectFieldsAll(self) if key != 'resources' && (module.isResource(self[key]) || module.isModule(self[key])) + ]), + outputs: {}, +} \ No newline at end of file diff --git a/core/resource.libsonnet b/core/resource.libsonnet new file mode 100644 index 0000000..9f8156c --- /dev/null +++ b/core/resource.libsonnet @@ -0,0 +1,23 @@ +local core = import 'module.libsonnet'; + +{ + id:: "[resourceId('%s', '%s')]" % [ self.type, self.name ], + type: error "resource must have a 'type' property", + apiVersion: error "resource must have a 'apiVersion' property", + location: "[resourceGroup().location]", + name: error "'name' is a required property for resources", + + dependsOn:[], + tags: {}, + withDependency(resourceOrId):: + if core.isResource(resourceOrId) || core.isModule(resourceOrId) then + self { + dependsOn +: [ resourceOrId.id ] + } + else + self, + + properties: { + + }, +} \ No newline at end of file diff --git a/examples/simplified_vmss/module.libsonnet b/examples/simplified_vmss/module.libsonnet new file mode 100644 index 0000000..6655355 --- /dev/null +++ b/examples/simplified_vmss/module.libsonnet @@ -0,0 +1,85 @@ +local Module = import 'core/moduledef.libsonnet'; + +// Resolution of modules can be made simpler (or at least different) by having a custom resolver +// or by having a top-level ARM (or platform) module from which one can access individual modules +// by "dotting" into the name (i.e. local arm = import 'arm.libsonnet'; arm.compute.VirtualNetwork { +// bla bla}) +// +// For expedience, just use the standard import to allow for use of vanilla +// import statements. +local VirtualNetwork = import 'network/VirtualNetwork/module.libsonnet'; +local NetworkSecurityGroup = import 'network/NetworkSecurityGroup/module.libsonnet'; +local VirtualMachineScaleSet = import 'compute/VirtualMachineScaleSet/module.libsonnet'; + +// Technically, there is nothing that *requires* a module to extend the core Module. Anything +// looking enough like a module (i.e. manifests a resources array, and output dictionary and optionally +// a parameterMetadata dictionary and accepts a 'parameters' dictionary with arguments works just as well) +// However, using Module as base gives you basic parameter validation and collection of all generated resources +// into the resources array for free... +Module { + + // Parameter metadata allows for introspection of the module to understand what + // parameters can be passed to the module as well as other metadata such as + // default values and parameter types. + parameterMetadata: { + name: { + type: 'string', + defaultValue: 'vmss' + }, + size: { + type: 'string', + defaultValue: 'Standard_DS1_V2' + }, + allowPublicAccess: { + type: ['boolean', 'string', 'object' ], + defaultValue: true + }, + subnet: { + type: 'string', + defaultValue: null + }, + }, + + // It is easy to conditionally create a resource. + // The arguments member was populated automagically by the base module based on + // parameters passed in to the module and default values for the module parameters + networkSecurityGroup:: if $.arguments.allowPublicAccess then + NetworkSecurityGroup { + parameters: { + name: $.arguments.name + 'nsg', + rule: 'ssh' + } + }, + + + virtualNetwork:: if $.arguments.subnet == null then + VirtualNetwork { + parameters: { + name: $.arguments.name + 'vnet', + addressPrefix: if $.arguments.size == 'small' then '10.0.0.0/24' else '10.0.0.0/16', + subnet: 'frontend', + networkSecurityGroup: $.networkSecurityGroup + } + }, + + virtualMachineScaleSet:: VirtualMachineScaleSet { + parameters: { + name: $.arguments.name, + imageReference: 'ubuntuLTS', // TODO: Move the simple/commonly used image names from the CLI VMSS module to the platform VMSS module + skuName: $.arguments.size, + // Passing a resource as an argument to another module sets up a dependency automagically + [if $.arguments.subnet == null then 'virtualNetwork']: $.virtualNetwork.virtualNetwork, // ISSUE - getting 'root' instance from a module is ugly here + [if $.arguments.subnet != null then 'subnet']: $.arguments.subnet + }, + }, + +} +// Parameters below are not part of the actual module - they are here to allow for quick testing +// of the module. +{ + parameters:: { + size: 'Standard_A0', + allowPublicAccess: true, + subnet: '/subscriptions/43fb366f-2061-4600-9d5d-795a6a0bba4c/resourceGroups/cli/providers/Microsoft.Network/virtualNetworks/vnet/subnets/default' + } +} \ No newline at end of file diff --git a/module.libsonnet b/module.libsonnet new file mode 100644 index 0000000..73a3f97 --- /dev/null +++ b/module.libsonnet @@ -0,0 +1,93 @@ +local network = { + LoadBalancer: import 'network/LoadBalancer/loadBalancer.libsonnet', + PublicIpAddress: import 'network/PublicIpAddress/module.libsonnet', + VirtualNetwork: import 'network/VirtualNetwork/module.libsonnet', +}; +local compute = { + VirtualMachineScaleSet: import 'compute/VirtualMachineScaleSet/virtualMachineScaleSet.libsonnet' +}; + +local core = import 'core/module.libsonnet'; +local Module = import 'core/moduledef.libsonnet'; + +Module { + + parameterMetadata:: import 'parameters.libsonnet', + + imageFromAlias(aliasOrImage):: + // + // extract display name from the content/vm_aliases.json file. The content/vm_aliases file + // can be found at: + // https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/arm-compute/quickstart-templates/aliases.json + // + assert std.type(aliasOrImage) == 'object' || std.type(aliasOrImage) == 'string' : "imageFromAlias parameter expected 'object' or 'string' type parameter - got %s" % [ std.type(aliasOrImage) ]; + local vmImages = (import 'cli/content/vm_alisases.json').outputs.aliases.value; + if std.type(aliasOrImage) == 'string' && std.objectHas(vmImages.Linux, aliasOrImage) then + vmImages.Linux[aliasOrImage] + else if std.type(aliasOrImage) == 'string' && std.objectHas(vmImages.Windows, aliasOrImage) then + vmImages.Windows[aliasOrImage] + else + assert std.type(aliasOrImage) == 'object' : "Unable to find image '%s'" % [ aliasOrImage ]; + aliasOrImage, + + // Build a virtual network if one is not provided. + virtualNetwork:: + // Optionally build a virtual network. + local shouldCreateVnet = $.arguments.virtualNetwork == '[new]'; + if shouldCreateVnet then + network.VirtualNetwork.new( + '%sVNET' % [ $.arguments.name ], + addressPrefix='10.0.0.0/16') + .withSubnet('%sSubnet' % [ $.arguments.name ], addressPrefix='10.0.0.0/24') + else + $.arguments.virtualNetwork, + + // Build a public IP Address unless on is provided, or the parameters + // indicate that one is not wanted... + publicIpAddress:: + // Optionally build a public ip address + local shouldCreatePublicIpAddress = $.arguments.publicIpAddress == null; + if shouldCreatePublicIpAddress then + network.PublicIpAddress.new('%sLBPublicIP' % [ $.arguments.name ]) + .withAllocationMethod($.arguments.publicIpAllocationMethod) + else + $.arguments.publicIpAddress, + + // Build a front facing load balancer unless on is provided, or the + // parameters indicate that one is not wanted... + loadBalancer:: + local shouldCreateLoadBalancer = $.arguments.loadBalancer == null; + if shouldCreateLoadBalancer then + network.LoadBalancer.new('%sLB' % [ $.arguments.name ], sku= $.arguments.loadBalancerSku) + .withIpConfiguration('loadBalancerFrontEnd') + .withPublicIpAddress($.publicIpAddress) + .withBackendAddressPool($.arguments.backendPoolName) + .withNatRule($.arguments.authenticationType), + + // Build all resources created by the module... + virtualMachineScaleSet:: + compute.VirtualMachineScaleSet.new( + name= $.arguments.name, + osType = 'linux', // UNDONE - support windows again + imageReference = self.imageFromAlias($.arguments.imageReference), + overProvision = ! $.arguments.disableOverprovision, + capacity = $.arguments.instanceCount, + upgradePolicy = $.arguments.upgradePolicy, + skuName = $.arguments.vmSku + ) + .withAuth($.arguments.adminUserName, $.arguments.adminPassword, $.arguments.sshPublicKeys) + .withIdentity($.arguments.identity) + .behindLoadBalancer($.loadBalancer, $.virtualNetwork, $.arguments.subnet) + .withDataDisks($.arguments.dataDiskSizes, $.arguments.dataDiskCaching), + + outputs: { + virtualMachineScaleSet: { + type: 'string', + value: $.virtualMachineScaleSet.id + }, + [if $.virtualNetwork != null then 'virtualNetwork']: { + type: 'string', + value: core.resourceId($.virtualNetwork) + } + } +} \ No newline at end of file diff --git a/moduleroot.jsonnet b/moduleroot.jsonnet new file mode 100644 index 0000000..2ddc3ea --- /dev/null +++ b/moduleroot.jsonnet @@ -0,0 +1,13 @@ +local m = import './module.libsonnet'; + +function(params) + m { + parameters: { + [k]: ((params.parameters)[k]).value + for k in std.objectFieldsAll(params.parameters) + }, + } { + '$schema': 'https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#', + contentVersion: '0.0.0.1' + } + diff --git a/network/LoadBalancer/loadBalancer.libsonnet b/network/LoadBalancer/loadBalancer.libsonnet new file mode 100644 index 0000000..75160d9 --- /dev/null +++ b/network/LoadBalancer/loadBalancer.libsonnet @@ -0,0 +1,115 @@ +local armmodule = import '../../core/module.libsonnet'; + +armmodule.Resource { + + type: 'Microsoft.Network/loadBalancers', + apiVersion: '2017-11-01', + + new(name, sku=null):: + self { + name: name, + [if sku != null then 'sku']: { + name: sku + }, + }, + + withIpConfiguration(name):: + self { + properties +: { + frontendIpConfigurations +: [ + { + name: name + } + ], + }, + }, + + withPublicIpAddress(publicIpAddress):: + if publicIpAddress == null then self else + local publicIpAddressId = armmodule.resourceId(publicIpAddress); + local existingIpConfigurations = self.properties.frontendIpConfigurations; + self.withDependency(publicIpAddress) { + properties +: { + frontendIpConfigurations: [ + ipconf + for ipconf in existingIpConfigurations[0:std.length(existingIpConfigurations) - 1]] + + [ + existingIpConfigurations[std.length(existingIpConfigurations) - 1] { + properties +: { + publicIpAddress +: { + id: publicIpAddressId, + }, + } + } + ] + }, + }, + + onSubnet(vnet, subnet=null, ipConfigurationName=null):: + local vnetName = armmodule.resourceName(vnet); + local subnetName = if subnet != null then subnet else vnet.properties.subnets[0].name; + local existingIpConfigurations = self.properties.frontendIpConfigurations; + self.withDependency(vnet) { + properties +: { + frontendIpConfigurations: [ + ipconf + for ipconf in existingIpConfigurations[0:std.length(existingIpConfigurations) - 1]] + + [ + existingIpConfigurations[std.length(existingIpConfigurations) - 1] { + properties +: { + subnet: { + id: "[concat(resourceId('Microsoft.Network/virtualNetworks', '%s'), '/subnets/%s')]" % [ vnetName, subnetName ], + }, + } + }, + ], + }, + }, + + + withBackendAddressPool(name):: + self { + properties +: { + backendAddressPools +: [ + { + name: if name != null then name else '%sBEPool' % [ $.name ], + } + ], + }, + }, + + withNatRule(ruleOrName):: + local name = "%sNatPool" % [ $.name ]; + local lastFrontendIpConfiguration = armmodule.stdex.last($.properties.frontendIpConfigurations); + local frontendIpConfigurationId = "[concat(resourceId('Microsoft.Network/loadBalancers', '%s'), '/frontendIPConfigurations/', '%s')]" % [ $.name, lastFrontendIpConfiguration.name ]; + + local builtInRules = { + ssh: { + backendPort: 22, + frontendPortRangeEnd: "50119", + frontendPortRangeStart: "50000", + protocol: "tcp" + } + }; + + local settings = { + frontendIPConfiguration: { + id: frontendIpConfigurationId, + } + } + if std.type(ruleOrName) == 'string' then + builtInRules[ruleOrName] + else + ruleOrName; + + self + { + properties +: { + inboundNatPools +: + [ + { + name: name, + properties: settings + } + ] + } + }, +} \ No newline at end of file diff --git a/network/LoadBalancer/module.libsonnet b/network/LoadBalancer/module.libsonnet new file mode 100644 index 0000000..d91f99f --- /dev/null +++ b/network/LoadBalancer/module.libsonnet @@ -0,0 +1,44 @@ +local Module = import '../../core/moduledef.libsonnet'; +local LoadBalancer = import 'loadBalancer.libsonnet'; + +Module { + + parameterMetadata:: { + name: { + type: 'string' + }, + sku: { + type: 'string', + defaultValue: null + }, + publicIpAddress: { + type: [ 'string', 'boolean', 'object' ], + defaultValue: null, + }, + ipConfiguration: { + type: 'string', + defaultValue: 'ipconfig' + }, + virtualNetwork: { + type: 'object', + defaultValue: null, + }, + subnet: { + type: 'string', + defaultValue: null, + }, + }, + + resource:: + local raw = LoadBalancer.new( + name = $.arguments.name, + sku = $.arguments.sku + ).withIpConfiguration($.arguments.ipConfiguration); + local addIp = if $.arguments.publicIpAddress != false then raw.withPublicIpAddress($.arguments.publicIpAddress) else raw; + addIp.onSubnet($.arguments.virtualNetwork, $.arguments.subnet), + + + outputs: { + id: $.resource.id + }, +} \ No newline at end of file diff --git a/network/LoadBalancer/tests/simple.libsonnet b/network/LoadBalancer/tests/simple.libsonnet new file mode 100644 index 0000000..41e5854 --- /dev/null +++ b/network/LoadBalancer/tests/simple.libsonnet @@ -0,0 +1,21 @@ +local LoadBalancer = import '../module.libsonnet'; +local PublicIp = import 'network/PublicIpAddress/module.libsonnet'; +local Vnet = import 'network/VirtualNetwork/module.libsonnet'; + +local vnet = Vnet { + parameters: { + name: 'thevnet', + subnet: 'subnet' + }, +}; + +LoadBalancer { + parameters:: { + name: 'lb', + sku: 'basic', + ipConfiguration: 'first', + publicIpAddress: false, + virtualNetwork: vnet.virtualNetwork, + subnet: vnet.outputs.subnet.value[0], + }, +} diff --git a/network/NetworkSecurityGroup/module.libsonnet b/network/NetworkSecurityGroup/module.libsonnet new file mode 100644 index 0000000..9f43cd6 --- /dev/null +++ b/network/NetworkSecurityGroup/module.libsonnet @@ -0,0 +1,26 @@ +local Module = import '../../core/moduledef.libsonnet'; + +Module { + + id:: $.resource.id, + + parameterMetadata:: { + name: { + type: 'string' + }, + rule: { + type: 'string', + defaultValue: null, + }, + }, + + resource:: + (import 'networkSecurityGroup.libsonnet').new($.arguments.name).withRule($.arguments.rule), + + outputs: { + id: { + type: 'string', + value: $.id + } + } +} \ No newline at end of file diff --git a/network/NetworkSecurityGroup/networkSecurityGroup.libsonnet b/network/NetworkSecurityGroup/networkSecurityGroup.libsonnet new file mode 100644 index 0000000..36d7087 --- /dev/null +++ b/network/NetworkSecurityGroup/networkSecurityGroup.libsonnet @@ -0,0 +1,65 @@ +local armmodule = import '../../core/module.libsonnet'; + +armmodule.Resource { + + apiVersion: '2017-11-01', + type: 'Microsoft.Network/networkSecurityGroups', + location: "[resourceGroup().location]", + + wellKnownRules:: { + ssh: { + name: 'IncomingSSH', + properties: { + access: 'Allow', + direction: "inbound", + protocol: 'tcp', + sourcePortRange: 22, + destinationPortRange: 22, + sourceAddressPrefix: '*', + destinationAddressPrefix: '*', + priority: 1000, + } + } + }, + + new( + name + ):: + self { + name: name, + properties: { + securityRules: [], + }, + }, + + withRule(rule, + access=null, + direction=null, + priority=null, + protocol=null, + sourcePortRange=null, + destinationPortRange=null):: + if rule == null then + self + else + local ruleDefinition = if std.type(rule) == 'string' && std.objectHas($.wellKnownRules, rule) then $.wellKnownRules[rule] else rule; + local overrides = std.prune({ + [if std.type(ruleDefinition) == 'string' then 'name']: ruleDefinition, + properties: { + access: access, + direction: direction, + priority: priority, + protocol: protocol, + sourcePortRange: sourcePortRange, + destinationPortRange: destinationPortRange, + }, + }); + + self { + properties +: { + securityRules +: [ + std.mergePatch(ruleDefinition,overrides) + ] + }, + }, +} \ No newline at end of file diff --git a/network/NetworkSecurityGroup/tests/simple.json b/network/NetworkSecurityGroup/tests/simple.json new file mode 100644 index 0000000..e09e13a --- /dev/null +++ b/network/NetworkSecurityGroup/tests/simple.json @@ -0,0 +1,25 @@ +{ + "$schema":"http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json", + "contentVersion":"1.0.0.0", + "parameters":{}, + "variables":{}, + "resources":[ + { + "name": "NetworkSecurityGroup", + "type": "Microsoft.JohanSte/modules", + "apiVersion": "1971-11-01", + "properties": { + "module": "network/NetworkSecurityGroup/module.libsonnet", + "name": "nsg" + } + } + ], + + "outputs": { + "publicIpAddress": { + "type": "string", + "value": "[reference('NetworkSecurityGroup').outputs.id.value]" + } + + } +} \ No newline at end of file diff --git a/network/NetworkSecurityGroup/tests/simple.libsonnet b/network/NetworkSecurityGroup/tests/simple.libsonnet new file mode 100644 index 0000000..93d508f --- /dev/null +++ b/network/NetworkSecurityGroup/tests/simple.libsonnet @@ -0,0 +1,8 @@ +local Nsg = import '../module.libsonnet'; + +Nsg { + parameters: { + name: 'yeah', + rule: 'ssh' + }, +} \ No newline at end of file diff --git a/network/PublicIpAddress/module.libsonnet b/network/PublicIpAddress/module.libsonnet new file mode 100644 index 0000000..706feb8 --- /dev/null +++ b/network/PublicIpAddress/module.libsonnet @@ -0,0 +1,33 @@ +local core = import '../../core/module.libsonnet'; +local stdex = core.stdex; +local Module = import '../../core/moduledef.libsonnet'; + +Module { + parameterMetadata:: { + name: { + type: 'string' + }, + allocationMethod: { + type: 'string', + defaultValue: 'dynamic' + }, + }, + + resource:: $.new($.arguments.name, allocationMethod = $.arguments.allocationMethod), + + new(name, allocationMethod=null):: + assert stdex.isString(name) : "Incorrect type for parameter 'name' - expected 'string', got '%s'" % [ std.type(name)]; + assert allocationMethod == null || stdex.isString(allocationMethod) : "Incorrect type for parameter 'parameters' - expected 'object', got '%s'" % [ std.type(name)]; + + local resource = (import 'publicIpAddress.libsonnet').new( + name=name); + + resource.withAllocationMethod(allocationMethod), + + outputs: { + id: { + type: 'string', + value: $.resource.id + } + } +} \ No newline at end of file diff --git a/network/PublicIpAddress/publicIpAddress.libsonnet b/network/PublicIpAddress/publicIpAddress.libsonnet new file mode 100644 index 0000000..a1d825e --- /dev/null +++ b/network/PublicIpAddress/publicIpAddress.libsonnet @@ -0,0 +1,24 @@ +local armmodule = import '../../core/module.libsonnet'; + +{ + ResourceTemplate:: armmodule.Resource { + apiVersion: '2015-06-15', + type: 'Microsoft.Network/publicIpAddresses', + name: error "'name' is a required property", + + withAllocationMethod(allocationMethod):: + if allocationMethod == null then + self + else + self { + properties +: { + publicIPAllocationMethod: allocationMethod + }, + }, + }, + + new(name, allocationMethod=null):: + $.ResourceTemplate { + name: name + }.withAllocationMethod(allocationMethod) +} \ No newline at end of file diff --git a/network/PublicIpAddress/tests/simple.json b/network/PublicIpAddress/tests/simple.json new file mode 100644 index 0000000..0f6a108 --- /dev/null +++ b/network/PublicIpAddress/tests/simple.json @@ -0,0 +1,26 @@ +{ + "$schema":"http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json", + "contentVersion":"1.0.0.0", + "parameters":{}, + "variables":{}, + "resources":[ + { + "name": "MyLittlePublicIp", + "type": "Microsoft.JohanSte/modules", + "apiVersion": "1971-11-01", + "properties": { + "module": "network/PublicIpAddress/module.libsonnet", + "name": "pubip", + "allocationMethod": "dynamic" + } + } + ], + + "outputs": { + "publicIpAddress": { + "type": "string", + "value": "[reference('MyLittlePublicIp').outputs.id.value]" + } + + } +} \ No newline at end of file diff --git a/network/PublicIpAddress/tests/simple.libsonnet b/network/PublicIpAddress/tests/simple.libsonnet new file mode 100644 index 0000000..a5d3ed0 --- /dev/null +++ b/network/PublicIpAddress/tests/simple.libsonnet @@ -0,0 +1,14 @@ +local PublicIpAddress = import '../module.libsonnet'; + +local publicIp = PublicIpAddress.new(name='simplePublicIp', allocationMethod='static'); +local module = PublicIpAddress { + parameters:: { + name: 'simplePublicIp', + allocationMethod: 'static' + }, +}; + +{ + r: publicIp, + m: module, +} \ No newline at end of file diff --git a/network/VirtualNetwork/module.libsonnet b/network/VirtualNetwork/module.libsonnet new file mode 100644 index 0000000..3fc90f8 --- /dev/null +++ b/network/VirtualNetwork/module.libsonnet @@ -0,0 +1,69 @@ +local Module = import '../../core/moduledef.libsonnet'; +local NetworkSecurityGroup = import 'network/NetworkSecurityGroup/module.libsonnet'; + +Module { + parameterMetadata:: { + name: { + type: 'string' + }, + addressPrefix: { + type: 'string', + defaultValue: '10.0.0.0/16' + }, + subnet: { + type: 'string', + defaultValue: 'default' + }, + subnetAddressPrefix: { + type: 'string', + defaultValue: '10.0.0.0/24', + }, + networkSecurityGroup: { + type: [ 'string', 'object' ], + defaultValue: null + }, + }, + + // + // The instance member contains the resource as created "by default" from the + // given set of parameters. + // + virtualNetwork:: $.new( + name = $.arguments.name, + addressPrefix = $.arguments.addressPrefix + ).withSubnet( + name = $.arguments.subnet, + addressPrefix = $.arguments.subnetAddressPrefix + ).withNetworkSecurityGroup($.arguments.networkSecurityGroup), + + new(name, addressPrefix):: + (import 'virtualNetwork.libsonnet').new( + name=name, + addressPrefix=addressPrefix + ), + + withAccess(accessType):: + self { + networkSecurityGroup:: NetworkSecurityGroup { + parameters: { + name: $.arguments.name + 'NSG', + rule: accessType + }, + }, + + virtualNetwork:: + super.virtualNetwork.withNetworkSecurityGroup(self.networkSecurityGroup), + }, + + outputs: { + + id: { + type: 'string', + value: $.virtualNetwork.id + }, + subnet: { + type: 'array', + value: [subnet.name for subnet in $.virtualNetwork.properties.subnets], + }, + } +} \ No newline at end of file diff --git a/network/VirtualNetwork/tests/complex.jsonnet b/network/VirtualNetwork/tests/complex.jsonnet new file mode 100644 index 0000000..30fcdc0 --- /dev/null +++ b/network/VirtualNetwork/tests/complex.jsonnet @@ -0,0 +1,61 @@ +local Module = import 'core/moduledef.libsonnet'; + +local VirtualNetwork = import '../virtualNetwork.libsonnet'; +local NetworkSecurityGroup = import 'network/NetworkSecurityGroup/module.libsonnet'; +local PublicIpAddress = import 'network/PublicIpAddress/module.libsonnet'; + +// Test module definition that builds up a vnet with two subnets and +// respective NSGs +Module { + + parameterMetadata: { + name: { + type: 'string' + }, + frontendNsg: { + type: 'object', + defaultValue: { + name: '%sfrontendNsg' % [ $.arguments.name ], + rule: 'ssh' + }, + }, + backendNsg: { + type: 'object', + defaultValue: { + name: '%sbackendNsg' % [ $.arguments.name ], + }, + }, + }, + + frontendNsg:: NetworkSecurityGroup { + parameters: $.arguments.frontendNsg + }, + + backendNsg:: NetworkSecurityGroup { + parameters: $.arguments.backendNsg + }, + + publicIp:: PublicIpAddress{ + parameters: { + name: 'pip' + } + }, + + vnet: VirtualNetwork.new('davnet') + .withDnsServers(['10.0.0.1']) + .withSubnet('frontend', addressPrefix='10.0.0.0/24') + .withNetworkSecurityGroup($.frontendNsg) + .withSubnet('backend', addressPrefix='10.0.1.0/24') + .withNetworkSecurityGroup($.backendNsg), +} + +// Parameters passed in to module +{ + parameters: { + name: 'testingdav', + backendNsg: { + name: '%soverridden' % [ $.arguments.name ], + rule: 'ssh' + }, + } +} \ No newline at end of file diff --git a/network/VirtualNetwork/tests/simple.json b/network/VirtualNetwork/tests/simple.json new file mode 100644 index 0000000..0d6ad85 --- /dev/null +++ b/network/VirtualNetwork/tests/simple.json @@ -0,0 +1,25 @@ +{ + "$schema":"http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json", + "contentVersion":"1.0.0.0", + "parameters":{}, + "variables":{}, + "resources":[ + { + "name": "SampleVnetModule", + "type": "Microsoft.JohanSte/modules", + "apiVersion": "1971-11-01", + "properties": { + "module": "network/VirtualNetwork/module.libsonnet", + "name": "sampleVnet" + } + } + ], + + "outputs": { + "publicIpAddress": { + "type": "string", + "value": "[reference('SampleVnetModule').outputs.id.value]" + } + + } +} \ No newline at end of file diff --git a/network/VirtualNetwork/tests/simple.libsonnet b/network/VirtualNetwork/tests/simple.libsonnet new file mode 100644 index 0000000..ea1017a --- /dev/null +++ b/network/VirtualNetwork/tests/simple.libsonnet @@ -0,0 +1,6 @@ +(import '../module.libsonnet') { + parameters:: { + name: 'SampleVnet', + addressPrefix: '10.0.0.0/16', + }, +} \ No newline at end of file diff --git a/network/VirtualNetwork/virtualNetwork.libsonnet b/network/VirtualNetwork/virtualNetwork.libsonnet new file mode 100644 index 0000000..100b701 --- /dev/null +++ b/network/VirtualNetwork/virtualNetwork.libsonnet @@ -0,0 +1,79 @@ +local core = import '../../core/module.libsonnet'; +local stdex = core.stdex; + +core.Resource { + + apiVersion: '2017-11-01', + type: 'Microsoft.Network/virtualNetworks', + + new(name, subnet=null, subnetAddressPrefix=null, ddosProtection=null, enableVmProtection=null, addressPrefix=null):: + (self + { + name: name, + type: 'Microsoft.Network/virtualNetworks', + properties: { + [if ddosProtection != null then 'enableDdosProtection']: ddosProtection, + [if enableVmProtection != null then 'enableVmProtection']: enableVmProtection, + subnets: [], + }, + }).withAddressPrefix(addressPrefix), + + withAddressPrefix(addressPrefix):: + if addressPrefix == null then + self + else + assert stdex.isString(addressPrefix) : "Invalid type for parameter addressPrefix - expected 'string', got '%s'" % [ std.type(addressPrefix) ]; + self { + properties +: { + addressSpace: { + addressPrefixes +: [ addressPrefix ], + }, + }, + }, + + withDnsServers(dnsServers):: + assert stdex.isArray(dnsServers) : "Invalid type for parameter dnsServers - expected 'array', got '%s'" % [ std.type(dnsServers)]; + + self { + properties +: { + dhcpOptions +: { + dnsServers +: dnsServers + }, + }, + }, + + withNetworkSecurityGroup(networkSecurityGroup):: + if networkSecurityGroup == null then + self + else + local subnets = self.properties.subnets; + + self.withDependency(networkSecurityGroup) { + properties +: { + subnets: subnets[0:std.length(subnets) - 1] + + [ subnets[std.length(subnets) - 1 ] { + properties +: { + networkSecurityGroup: { + id: core.resourceId(networkSecurityGroup) + } + } + }], + }, + }, + + withSubnet(name =null, addressPrefix, serviceEndpoints=null):: + local subnetName = if name != null then name else 'subnet%s' % [ std.length(self.properties.subnets) + 1 ]; + + self { + properties +: { + subnets +: [ + { + name: subnetName, + properties: { + addressPrefix: addressPrefix, + [if serviceEndpoints != null then 'serviceEndpoints']: serviceEndpoints + } + }, + ], + }, + }, +} \ No newline at end of file diff --git a/parameters.libsonnet b/parameters.libsonnet new file mode 100644 index 0000000..d940515 --- /dev/null +++ b/parameters.libsonnet @@ -0,0 +1,118 @@ +{ + name: { + type: 'string' + }, + imageReference: { + type: ['string', 'object'], + }, + adminUserName: { + type: 'string' + }, + location: { + type: 'string' + }, + disableOverprovision: { + type: 'boolean', + defaultValue: false, + }, + instanceCount: { + type: 'integer', + defaultValue: 2, + }, + upgradePolicy: { + type: 'string', + defaultValue: 'manual', + }, + adminPassword: { + type: 'string', + defaultValue: null + }, + authenticationType: { + type: 'string', + defaultValue: if $.osType == 'Windows' then 'rdp' else 'ssh', + }, + vmSku: { + type: 'string', + defaultValue: 'Standard_D1_v2', + }, + sshPublicKeys: { + type: 'string', + defaultValue: [], + }, + loadBalancer: { + type: ['object', 'string'], + defaultValue: null, // { sku: null }, + }, + loadBalancerSku: { + type: 'string', + defaultValue: null, + }, + virtualNetwork: { + type: [ 'object', 'string' ], + defaultValue: '[new]', + }, + backendPoolName: { + type: 'string', + defaultValue: null, + }, + natPoolName: { + type: 'string', + defaultValue: null, + }, + backendPort: { + type: 'string', + defaultValue: null, + }, + publicIpAllocationMethod: { + type: 'string', + defaultValue: 'dynamic', + }, + dataDiskSizes: { + type: ['array', 'number' ], + defaultValue: [], + }, + osType: { + type: 'string', + defaultValue: null, // error "'osType' is a required parameters", + }, + subnet: { + type: [ 'string', 'object' ], + defaultValue: null, + }, + customData: { + type: 'string', + defaultValue: null, + }, + licenseType: { + type: 'string', + defaultValue: null, + }, + singlePlacementGroup: { + type: 'boolean', + defaultValue: $.instanceCount <= 100, + }, + publicIpAddress: { + type: [ 'string', 'object' ], + defaultValue: null, + }, + publicIpAddressDnsName: { + type: 'string', + defaultValue: null, + }, + publicIpAddressPerVm: { + type: 'boolean', + defaultValue: false, + }, + zones: { + type: 'array', + defaultValue: [], + }, + dataDiskCaching: { + type: 'string', + defaultValue: null, + }, + identity: { + type: [ 'array', 'string' ], + defaultValue:null + } +} \ No newline at end of file diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..dec7b9f --- /dev/null +++ b/readme.md @@ -0,0 +1,61 @@ +# Modules + +## Definition +A module is a jsonnet library that, when evaluated, yields an object conforming to the following subset of attributes of an ARM template: + +``` +{ + resources: [ + { } + ], + + outputs: { + ...: { + ... + } + ... + } +} +``` +*(In fact, an existing ARM template intended for inline use is a valid Module per this definition)* + +A module can be parameterized. Argument values are passed to the module by injecting a 'parameters' environment variable into the execution environment (equivaluent of passing a `ext-str parameters=...` to the [jsonnet cli](http://jsonnet.org/implementation/commandline.html)) + +## Conventions +In addition to the core definition of a module, there are several conventions that, when followed, provide surrounding tooling with additional information/capabilities. + +### Introspection/module metadata + +A module may declare parameters that it supports by providing a parmameterMetadata attribute. The parameterMetadata attribute includes the name, type and default value for parameters. *TODO: additional functionality can be added to parameterMetadata, including imperative validation, descriptions, enum values etc.)* + +Example: +``` +{ + parameterMetadata:: { + stringValue: { + type: 'string' + }, + optionalIntegerValue: { + type: 'number', + defaultValue: 7 + } + } +} +``` + +A caller may inspect what arguments a module provides by examining the *parameterMetadata* field of the module. +The *parameterMetadata* can also be used by the module (or caller) to validate the provided parameters. + +*TODO: if we rename `parameterMetadata` to `parameters`, and make sure we follow the same schema as an ARM template's parameters section, we have an ever stronger correlation between `ARM templates` and `Modules`. This way, we'd also make it a formal part of a module definition rather than a convention* +### Id field + +In many cases, a module logically defines a root (anchor) resource with a set of dependent resources. A module may chose to expose this id to other modules by providing a top level id attribute. + +## Platform libraries + +### Module base implementation + +A module author may take advantage of a core module definition that provides validation of parameters based on parameterMetadata. + +### Resource base implementation +