diff --git a/cmd/infrakit/main.go b/cmd/infrakit/main.go index 71697d89b..bc5f64796 100644 --- a/cmd/infrakit/main.go +++ b/cmd/infrakit/main.go @@ -35,6 +35,7 @@ import ( _ "github.com/docker/infrakit/pkg/cli/v0" // Callable backends via playbook or via lib + _ "github.com/docker/infrakit/pkg/callable/backend/controller" _ "github.com/docker/infrakit/pkg/callable/backend/http" _ "github.com/docker/infrakit/pkg/callable/backend/instance" _ "github.com/docker/infrakit/pkg/callable/backend/print" diff --git a/docs/controller/pool/workers.yml b/docs/controller/pool/workers.yml index ddd3a2435..52622f289 100644 --- a/docs/controller/pool/workers.yml +++ b/docs/controller/pool/workers.yml @@ -29,11 +29,15 @@ properties: select: az: az1 type: compute + tags: + node_label: join(`-`, `node`,index()) init: | sudo apt-get install -y docker properties: instanceType: xlarge vcpu: 8 mem: 512G + private_ip: allocate_ip() simulator_cap: 1000 # simulator max capacity - simulator_delay: 1s # simulator provision latency \ No newline at end of file + simulator_delay: 5s # simulator provision latency + node: join(`-`, `node`, index()) diff --git a/examples/playbooks/aws/README.md b/examples/playbooks/aws/README.md index 35820a5d7..bcb039f0c 100644 --- a/examples/playbooks/aws/README.md +++ b/examples/playbooks/aws/README.md @@ -40,7 +40,7 @@ $ infrakit local resource tail / --view 'str://{{.Type}} - {{.ID}} - {{.Message} 3. In another terminal, commit the spec to monitor resources as they are created: ``` -$infrakit use aws inventory.yml | infrakit local mystack/inventory commit -y - +$infrakit use aws inventory --plugin mystack/inventory ``` Before any resources are created, we expect to see no metadata: @@ -50,7 +50,48 @@ $ infrakit local inventory/myproject keys -al total 0: ``` -4. Commit the `mystack.yml` playbook to the resource controller. This file +4. Set up project specific variables: this example uses the `metadata` plugin +to set some global variables. Run this to set them + +``` +$ infrakit use aws vars +Project? [myproject]: +CIDR block? [10.0.0.0/16]: +CIDR block? [10.0.100.0/24]: +CIDR block? [10.0.200.0/24]: +Availability Zone? [eu-central-1a]: +Availability Zone? [eu-central-1b]: +Proposing 0 changes, hash=b3e009daf23a4c248eb7a7003e778c78 +{ + "cidr": "10.0.0.0/16", + "project": "myproject", + "subnet1": { + "az": "eu-central-1a", + "cidr": "10.0.100.0/24" + }, + "subnet2": { + "az": "eu-central-1b", + "cidr": "10.0.200.0/24" + } +} +Project is myproject +Proposing 0 changes, hash=b3e009daf23a4c248eb7a7003e778c78 +{ + "cidr": "10.0.0.0/16", + "project": "myproject", + "subnet1": { + "az": "eu-central-1a", + "cidr": "10.0.100.0/24" + }, + "subnet2": { + "az": "eu-central-1b", + "cidr": "10.0.200.0/24" + } +} +``` + + +5. Commit the `mystack.yml` playbook to the resource controller. This file has specs of all the resources and their dependencies in one place. The playbook also contains other commands to provision the resources individually (eg. `infrakit use aws vpc` will provision just a vpc). @@ -68,7 +109,7 @@ the resources in the VPC. In this case it will provision these resources: Commit the file: ``` -$ $ infrakit use aws mystack | infrakit local mystack/resource commit -y - +$ infrakit use aws vpc --plugin mystack/resource Please enter your user name: [davidchung]: Project? [myproject]: CIDR block? [10.0.0.0/16]: @@ -522,11 +563,27 @@ networking/aws/ec2-vpc/myproject-vpc/Tags/infrakit_link_context networking/aws/ec2-vpc/myproject-vpc/Tags/infrakit_link_created ``` -5. Provision a spot instance in your new VPC +6. Provision a spot instance in your new VPC There's an playbook command called `spot` which will guide you through -provisioning a single spot instance in one of the subnets. You can pick -`subnet1` or `subnet2` in the prompt. +provisioning a single spot instance in one of the subnets. +When answering the questions you will be asked to provide vpc and subnet +ids. To get those values, do + +``` +$ infrakit local mystack/resource describe -o +COLLECTION KEY STATE DATA +myproject igw READY igw-08e6cb5872f1b83cb +myproject rtb READY rtb-0c3ff9daa89a5e06c +myproject sg1 READY sg-0ea554fba9c28b3bf +myproject subnet1 READY subnet-01b5e450f49749676 +myproject subnet2 READY subnet-09c5c220a71268f7a +myproject vpc READY vpc-07003eb7f376414b1 +``` + +The resource's infrastructure resource ids will be listed as `DATA` along side the logical +ids (e.g `sg1`) you have given them in the `mystack.yml`. Use these values in the steps that follow +to provision a single spot instance or a pool of spot instances. ``` $ infrakit use aws spot @@ -543,7 +600,9 @@ Security group ID? [sg-2e3f8143]: ``` This command can sometimes timeout because it takes a while to provision a spot -instance. In this case, you can see if it's created: +instance. You can set `INFRAKIT_CLIENT_TIMEOUT=10s` as environment variable prior +to running `infrakit use aws spot`. In case the client times out, you can see if +the instance has been created: ``` $ infrakit local aws/ec2-spot-instance describe @@ -557,38 +616,127 @@ or via the inventory controller. We query for entries under the `compute` cate ``` $ infrakit local inventory/myproject keys compute/aws/ec2-spot-instance -myproject-Zm6UfgDt +myproject-YjM9d5Vs + +infrakit local inventory/myproject keys -al compute/aws/ec2-spot-instance/myproject-YjM9d5Vs +total 132: +ID +LogicalID +Properties/Instance/AmiLaunchIndex +Properties/Instance/Architecture +Properties/Instance/BlockDeviceMappings/[0]/DeviceName +Properties/Instance/BlockDeviceMappings/[0]/Ebs/AttachTime +Properties/Instance/BlockDeviceMappings/[0]/Ebs/DeleteOnTermination +Properties/Instance/BlockDeviceMappings/[0]/Ebs/Status +Properties/Instance/BlockDeviceMappings/[0]/Ebs/VolumeId +Properties/Instance/ClientToken +Properties/Instance/EbsOptimized +Properties/Instance/EnaSupport +Properties/Instance/Hypervisor +Properties/Instance/IamInstanceProfile +Properties/Instance/ImageId +Properties/Instance/InstanceId +Properties/Instance/InstanceLifecycle +Properties/Instance/InstanceType +Properties/Instance/KernelId +Properties/Instance/KeyName +Properties/Instance/LaunchTime +Properties/Instance/Monitoring/State +Properties/Instance/NetworkInterfaces/[0]/Association/IpOwnerId +# more fields... + +$ infrakit local inventory/myproject cat compute/aws/ec2-spot-instance/myproject-YjM9d5Vs/Properties/Instance/PublicIpAddress +18.184.52.135 +``` + +Let's ssh in: + +``` +~$ eval `ssh-agent -s` +Agent pid 35295 +~$ ssh-add ~/.ssh/infrakit +Identity added: /Users/davidchung/.ssh/infrakit (/Users/davidchung/.ssh/infrakit) +~$ ssh ubuntu@$(infrakit local inventory/myproject cat compute/aws/ec2-spot-instance/myproject-YjM9d5Vs/Properties/Instance/PublicIpAddress) +The authenticity of host '18.184.52.135 (18.184.52.135)' can't be established. +ECDSA key fingerprint is SHA256:EnKhV+8cgUjQzL1Wvh2nwS+T5Meoxn6K/diAJtM+o9Y. +Are you sure you want to continue connecting (yes/no)? yes +Warning: Permanently added '18.184.52.135' (ECDSA) to the list of known hosts. +Welcome to Ubuntu 16.04.4 LTS (GNU/Linux 4.4.0-1052-aws x86_64) -$infrakit local inventory/myproject keys compute/aws/ec2-spot-instance/myproject-Zm6UfgDt/Properties/Instance + * Documentation: https://help.ubuntu.com + * Management: https://landscape.canonical.com + * Support: https://ubuntu.com/advantage -# A bunch of fields... + Get cloud support with Ubuntu Advantage Cloud Guest: + http://www.ubuntu.com/business/services/cloud -$ infrakit local inventory/myproject cat compute/aws/ec2-spot-instance/myproject-Zm6UfgDt/Properties/Instance/PublicIpAddress -18.196.88.253 +58 packages can be updated. +17 updates are security updates. + + + +The programs included with the Ubuntu system are free software; +the exact distribution terms for each program are described in the +individual files in /usr/share/doc/*/copyright. + +Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by +applicable law. + +To run a command as administrator (user "root"), use "sudo ". +See "man sudo_root" for details. ``` -Let's try to ssh in: +7. Provision a pool of nodes: You can use the `pool` controller to provision +a pool of spot instances: ``` -$ ssh ubuntu@$(infrakit local inventory/myproject cat compute/aws/ec2-spot-instance/myproject-Zm6UfgDt/Properties/Instance/PublicIpAddress) -Welcome to Ubuntu 16.04.3 LTS (GNU/Linux 4.4.0-1041-aws x86_64) +$ infrakit use aws nodes --plugin mystack/pool --az eu-central-1a --subnet-id subnet-01b5e450f49749676 --security-group-id sg-0ea554fba9c28b3bf --accept-defaults +``` +This will provision 5 spot instances. You can watch the progress via - * Documentation: https://help.ubuntu.com - * Management: https://landscape.canonical.com - * Support: https://ubuntu.com/advantage +``` +$ watch -d infrakit local mystack/pool describe -o +Every 2.0s: infrakit local mystack/pool describe -o - Get cloud support with Ubuntu Advantage Cloud Guest: - http://www.ubuntu.com/business/services/cloud +COLLECTION KEY STATE DATA +myproject-nodes myproject-nodes_0000 READY sir-46yrgvnn +myproject-nodes myproject-nodes_0001 READY sir-dxt8hk4m +myproject-nodes myproject-nodes_0002 READY sir-4z6rh6sq +myproject-nodes myproject-nodes_0003 READY sir-46tihzkq +myproject-nodes myproject-nodes_0004 READY sir-15frhx2p + +``` +You can scale down this pool of nodes by adding a `--count` flag: + +``` +infrakit use aws nodes --plugin mystack/pool --az eu-central-1a --subnet-id subnet-01b5e450f49749676 --security-group-id sg-0ea554fba9c28b3bf --count 1 --accept-defaults +``` + +You will see that some nodes become `UNMATCHED`: + +``` +Every 2.0s: infrakit local mystack/pool describe -o -107 packages can be updated. -48 updates are security updates. +COLLECTION KEY STATE DATA +myproject-nodes myproject-nodes_0000 READY sir-46yrgvnn +myproject-nodes myproject-nodes_0001 UNMATCHED sir-dxt8hk4m +myproject-nodes myproject-nodes_0002 UNMATCHED sir-4z6rh6sq +myproject-nodes myproject-nodes_0003 UNMATCHED sir-46tihzkq +myproject-nodes myproject-nodes_0004 UNMATCHED sir-15frhx2p +``` +Slowly you will see the unmatched nodes be terminated and removed: + +``` +Every 2.0s: infrakit local mystack/pool describe -o -*** System restart required *** -Last login: Mon Mar 19 00:20:36 2018 from 97.105.231.235 -ubuntu@ip-10-0-200-100:~$ +COLLECTION KEY STATE DATA +myproject-nodes myproject-nodes_0000 READY sir-46yrgvnn +myproject-nodes myproject-nodes_0003 TERMINATING sir-46tihzkq +myproject-nodes myproject-nodes_0004 TERMINATING sir-15frhx2p ``` + ## Clean up *Currently we do not support termination of resources. So you must do this manually.* diff --git a/examples/playbooks/aws/index.yml b/examples/playbooks/aws/index.yml index 661237a4a..0d83d5d97 100644 --- a/examples/playbooks/aws/index.yml +++ b/examples/playbooks/aws/index.yml @@ -9,10 +9,10 @@ vars : vars.sh inventory : inventory.yml # The resource config for entire mystack -phase1 : mystack.yml +vpc : mystack.yml # The resource config for nodes after phase1 -phase2 : nodes.yml +nodes : nodes.yml # The specific resources we can provision resources : resources/index.yml diff --git a/examples/playbooks/aws/inventory.yml b/examples/playbooks/aws/inventory.yml index d67132c9b..b4b6777a2 100644 --- a/examples/playbooks/aws/inventory.yml +++ b/examples/playbooks/aws/inventory.yml @@ -1,4 +1,4 @@ -{{/* =% text %= */}} +{{/* =% controllerCommit %= */}} {{ $project := param "project" "string" "project" | prompt "Project?" "string" "myproject" }} diff --git a/examples/playbooks/aws/mystack.yml b/examples/playbooks/aws/mystack.yml index 1198f41a0..7d1a0d72c 100644 --- a/examples/playbooks/aws/mystack.yml +++ b/examples/playbooks/aws/mystack.yml @@ -1,4 +1,4 @@ -{{/* =% text %= */}} +{{/* =% controllerCommit %= */}} {{ $project := metadata `mystack/vars/project` }} {{ $cidr := metadata `mystack/vars/cidr` }} diff --git a/examples/playbooks/aws/nodes.yml b/examples/playbooks/aws/nodes.yml index 5a2833fb3..da48de0ae 100644 --- a/examples/playbooks/aws/nodes.yml +++ b/examples/playbooks/aws/nodes.yml @@ -1,65 +1,67 @@ -{{/* =% text %= */}} +{{/* =% controllerCommit %= */}} -{{ $subnet := param "subnet" "string" "subnet" | prompt "Subnet?" "string" "subnet2" }} +{{ $project := metadata `mystack/vars/project` }} {{ $instanceType := param "instance-type" "string" "instance type" | prompt "Instance type?" "string" "t2.micro" }} - -{{ $imageId := param "image-id" "string" "Image ID" | prompt "AMI?" "string" "ami-df8406b0" }} - +{{ $imageId := param "image-id" "string" "Image ID" | prompt "AMI?" "string" "ami-7c412f13" }} {{ $keyName := param "key" "string" "ssh key name" | prompt "SSH key?" "string" "infrakit"}} - {{ $spotPrice := param "spot-price" "string" "Spot price" | prompt "Spot price?" "string" "0.03" }} -{{/* subnet id from subnet name */}} -{{ $project := metadata `mystack/vars/project` }} - -{{ $subnetKey := list `resource` $project $subnet `Properties` | join `/` }} +{{ $subnetId := param "subnet-id" "string" "subnet ID" | prompt "Subnet?" "string" "" }} +{{ $az := param "az" "string" "availability zone" | prompt "AZ?" "string" "" }} +{{ $securityGroupID := param "security-group-id" "string" "security group" | prompt "Security group ID?" "string" "" }} -{{ $subnetId := list $subnetKey `SubnetId` | join `/` | metadata }} -{{ $az := list $subnetKey `AvailabilityZone` | join `/` | metadata }} -{{ $cidr := list $subnetKey `CidrBlock` | join `/` | metadata }} +{{ $count := param "count" "int" "Count" | prompt "How many?" "int" 6 }} +{{ $parallelism := param "parallelism" "int" "Parallelism" | prompt "How many at a time?" "int" 2 }} -{{ $securityGroupKey := list `resource` $project `sg1` `Properties` | join `/` }} -{{ $securityGroupID := list $securityGroupKey `GroupId` | join `/` | metadata }} - -{{ $privateIp := param "private-ip" "string" "IP" | prompt "Private IP address?" "string" $cidr }} - -kind: resource +kind: pool metadata: name: {{ $project }}-nodes options: ObserveInterval: 10s -properties: + # This is the size of the buffered channel for the finite state machine work queue -- needs to be at least the count. + BufferSize: {{ mul $count 10 | add 1024 }} # controls the fsm engine's queue capacity + # observation's inbound channel capacity -- needs to be at least the same as the number of resource objects. + ChannelBufferSize: {{ mul $count 10 | add 1024 }} + # How long to wait before we start the provision process, in unit of time (ticks) + WaitBeforeProvision: 3 + # How long to wait before we start the termination / destroy, in unit of time (ticks) + WaitBeforeDestroy: 3 - - host1: - plugin: aws/ec2-spot-instance - select: - Name: {{ $project }}-host1 - infrakit_scope: {{ $project }} - init: | +properties: + plugin: aws/ec2-spot-instance + count: {{ $count }} # how many in the pool + parallelism: {{ $parallelism }} # how many to provision / destroy at a time + select: + az: az1 + type: compute + tags: + node_label: test + init: | #!/bin/bash sudo add-apt-repository ppa:gophers/archive sudo apt-get update -y - sudo apt-get install -y wget curl git golang-1.9-go - wget -qO- https://get.docker.com | sh - ln -s /usr/lib/go-1.9/bin/go /usr/local/bin/go - properties: - RequestSpotInstancesInput: - SpotPrice: "{{ $spotPrice }}" - Type: one-time - LaunchSpecification: - ImageId: {{ $imageId }} - InstanceType: {{ $instanceType }} - KeyName: {{ $keyName }} - NetworkInterfaces: - - AssociatePublicIpAddress: true - DeleteOnTermination: true - DeviceIndex: 0 - Groups: - - {{ $securityGroupID }} - NetworkInterfaceId: null - PrivateIpAddress: {{ $privateIp }} - SubnetId: {{ $subnetId }} - Placement: - AvailabilityZone: {{ $az }} + # install golang + sudo apt-get install -y wget curl git golang-1.10-go + ln -s /usr/lib/go-1.10/bin/go /usr/local/bin/go + # install docker CE + curl -sSL https://get.docker.com | sh + # install infrakit + curl -sSL https://docker.github.io/infrakit/install | sh + properties: + RequestSpotInstancesInput: + SpotPrice: "{{ $spotPrice }}" + Type: one-time + LaunchSpecification: + ImageId: {{ $imageId }} + InstanceType: {{ $instanceType }} + KeyName: {{ $keyName }} + NetworkInterfaces: + - AssociatePublicIpAddress: true + DeleteOnTermination: true + DeviceIndex: 0 + Groups: + - {{ $securityGroupID }} + SubnetId: {{ $subnetId }} + Placement: + AvailabilityZone: {{ $az }} diff --git a/examples/playbooks/aws/resources/provision-spot-instance.yml b/examples/playbooks/aws/resources/provision-spot-instance.yml index 2cb142c2b..0a21c1b62 100644 --- a/examples/playbooks/aws/resources/provision-spot-instance.yml +++ b/examples/playbooks/aws/resources/provision-spot-instance.yml @@ -2,14 +2,13 @@ {{/* =% instanceProvision `aws/ec2-spot-instance` %= */}} {{ $project := param "project" "string" "project" | prompt "Project?" "string" "myproject" }} -{{ $imageId := param "image-id" "string" "Image ID" | prompt "AMI?" "string" "ami-df8406b0" }} +{{ $imageId := param "image-id" "string" "Image ID" | prompt "AMI?" "string" "ami-7c412f13" }} {{ $instanceType := param "instance-type" "string" "instance type" | prompt "Instance type?" "string" "t2.micro" }} {{ $name := param "name" "string" "Host name" | prompt "Host name?" "string" (cat $project `-` (randAlphaNum 8) | nospace ) }} {{ $spotPrice := param "spot-price" "string" "Spot price" | prompt "Spot price?" "string" "0.03" }} {{ $keyName := param "key" "string" "ssh key name" | prompt "SSH key?" "string" "infrakit"}} {{ $subnetId := param "subnet-id" "string" "subnet ID" | prompt "Subnet?" "string" "" }} {{ $az := param "az" "string" "availability zone" | prompt "AZ?" "string" "" }} -{{ $privateIp := param "private-ip" "string" "IP" | prompt "Private IP address?" "string" "" }} {{ $securityGroupID := param "security-group-id" "string" "security group" | prompt "Security group ID?" "string" "" }} @@ -39,7 +38,7 @@ Properties: Groups: - {{ $securityGroupID }} NetworkInterfaceId: null - PrivateIpAddress: {{ $privateIp }} + PrivateIpAddress: null # let their IPAM pick an ip address PrivateIpAddresses: null SecondaryPrivateIpAddressCount: null SubnetId: {{ $subnetId }} diff --git a/examples/playbooks/aws/start.sh b/examples/playbooks/aws/start.sh index a70fbbc70..f70aef65e 100644 --- a/examples/playbooks/aws/start.sh +++ b/examples/playbooks/aws/start.sh @@ -1,13 +1,15 @@ {{/* =% sh %= */}} -{{ $clearState := flag "clear-state" "bool" "Clear stored states" | prompt "Clear state?" "bool" true }} +{{ $clearState := flag "clear-state" "bool" "Clear stored states" | prompt "Clear state?" "bool" false }} {{ $profile := flag "aws-cred-profile" "string" "Profile name" | prompt "Profile for your .aws/credentials?" "string" "default" }} {{ $region := flag "region" "string" "aws region" | prompt "Region?" "string" "eu-central-1"}} +echo "Clear stale pids" +rm -rf $HOME/.infrakit/plugins/* # remove sockets, pid files, etc. + {{ if $clearState }} echo "Clear local state from previous runs" -rm -rf $HOME/.infrakit/plugins/* # remove sockets, pid files, etc. rm -rf $HOME/.infrakit/configs/* # for file based manager # Since we are using file based leader detection, write the default name (manager1) to the leader file. echo manager1 > $HOME/.infrakit/leader @@ -32,8 +34,8 @@ AWS_ACCESS_KEY_ID={{ $creds.aws_access_key_id }} \ AWS_SECRET_ACCESS_KEY={{ $creds.aws_secret_access_key }} \ INFRAKIT_AWS_REGION={{ $region }} \ INFRAKIT_AWS_NAMESPACE_TAGS="infrakit_namespace={{ $namespace }}" \ -INFRAKIT_MANAGER_CONTROLLERS=resource,inventory \ -infrakit plugin start manager:mystack vars group resource inventory aws \ +INFRAKIT_MANAGER_CONTROLLERS=resource,inventory,pool \ +infrakit plugin start manager:mystack vars group resource inventory pool aws \ --log 5 --log-stack --log-debug-V 1000 \ --log-debug-match module=controller/resource \ --log-debug-match module=provider/aws \ diff --git a/pkg/callable/backend/controller/commit.go b/pkg/callable/backend/controller/commit.go new file mode 100644 index 000000000..358259316 --- /dev/null +++ b/pkg/callable/backend/controller/commit.go @@ -0,0 +1,63 @@ +package controller + +import ( + "context" + "fmt" + + "github.com/docker/infrakit/pkg/callable/backend" + "github.com/docker/infrakit/pkg/run/scope" + "github.com/docker/infrakit/pkg/spi/controller" + "github.com/docker/infrakit/pkg/types" +) + +func init() { + backend.Register("controllerCommit", Commit, + func(params backend.Parameters) { + params.String("plugin", "", "plugin") + }) +} + +// Commit returns an executable function based on that specification to call the named controller plugin's +// Commit method. +// The optional parameter in the playbook script can be overridden by the value of the `--plugin` flag +// in the command line. +func Commit(scope scope.Scope, test bool, opt ...interface{}) (backend.ExecFunc, error) { + + return func(ctx context.Context, script string, parameters backend.Parameters, args []string) error { + + var name string + + // Optional parameter for plugin name can be overridden by the value of the flag (--plugin): + if len(opt) > 0 { + s, is := opt[0].(string) + if !is { + return fmt.Errorf("first param (pluginName) must be string") + } + name = s + } + if n, err := parameters.GetString("plugin"); err != nil { + return err + } else if n != "" { + name = n + } + + c, err := scope.Controller(name) + if err != nil { + return err + } + + spec := types.Spec{} + if err := types.Decode([]byte(script), &spec); err != nil { + return err + } + + object, err := c.Commit(controller.Enforce, spec) + if err != nil { + return err + } + + out := backend.GetWriter(ctx) + fmt.Fprintln(out, object) + return nil + }, nil +} diff --git a/pkg/callable/callable.go b/pkg/callable/callable.go index ef53fa42a..87077a011 100644 --- a/pkg/callable/callable.go +++ b/pkg/callable/callable.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "io/ioutil" + "math" "net" "os" "strconv" @@ -19,12 +20,6 @@ import ( "github.com/docker/infrakit/pkg/types" ) -const ( - // none is used to determine if the user has set the bool flag value. this allows the - // use of pipe to a prompt like {{ $foo = flag "flag" "bool" "message" | prompt "foo?" "bool" }} - none = -1 -) - // Options has optional settings for a call. It can be in a CLI (where Parameters are implemented by flags) or // programmatic, where Parameters are implemented as maps that can be set in a golang program. type Options struct { @@ -89,6 +84,12 @@ func NewCallable(scope scope.Scope, src string, parameters backend.Parameters, o } } +const ( + missingInt = int(math.MinInt64) + missingFloat = float64(math.SmallestNonzeroFloat64) + missingBool = "" +) + // name, type, description of the flag, and a default value, which can be nil // the returned value if the nil value func (c *Callable) defineParameter(name, ftype, desc string, def interface{}) (interface{}, error) { @@ -113,7 +114,8 @@ func (c *Callable) defineParameter(name, ftype, desc string, def interface{}) (i return defaultValue, nil case "int": - defaultValue := 0 // TODO - encode a nil with a special value? + defaultValue := missingInt + // TODO - encode a nil with a special value? if def != nil { if v, ok := def.(int); ok { defaultValue = v @@ -125,7 +127,7 @@ func (c *Callable) defineParameter(name, ftype, desc string, def interface{}) (i return defaultValue, nil case "float": - defaultValue := float64(0.) + defaultValue := missingFloat if def != nil { if v, ok := def.(float64); ok { defaultValue = v @@ -183,9 +185,8 @@ func (c *Callable) defineParameter(name, ftype, desc string, def interface{}) (i return defaultValue, nil } // At definition time, there is no default value, so we use string - // to model three states: true, false, none - parameters.Int(name, none, desc) - return none, nil + parameters.String(name, missingBool, desc+" (use string bool value [true|false])") + return missingBool, nil } return nil, fmt.Errorf("unknown type %v", ftype) } @@ -219,14 +220,14 @@ func (c *Callable) getParameter(name, ftype, desc string, def interface{}) (inte if def == nil { // If default v is not specified, then we assume the flag was defined // using a string to handle the none case - v, err := parameters.GetInt(name) + v, err := parameters.GetString(name) if err != nil { - return none, err + return missingBool, err } - if v == none { - return none, nil // + if v == missingBool { + return missingBool, nil // } - return v > 0, nil + return strconv.ParseBool(v) } return parameters.GetBool(name) } @@ -243,11 +244,11 @@ func Missing(t string, v interface{}) bool { case "string": return v.(string) == "" case "int": - return v.(int) == 0 + return v.(int) == missingInt case "float": - return v.(float64) == 0. + return v.(float64) == missingFloat case "bool": - return v == none + return v == missingBool } return true } diff --git a/pkg/callable/callable_test.go b/pkg/callable/callable_test.go index e63679226..c243231c7 100644 --- a/pkg/callable/callable_test.go +++ b/pkg/callable/callable_test.go @@ -21,9 +21,9 @@ import ( func TestMissing(t *testing.T) { require.True(t, Missing("string", "")) - require.True(t, Missing("int", 0)) - require.True(t, Missing("float", 0.)) - require.True(t, Missing("bool", none)) + require.True(t, Missing("int", missingInt)) + require.True(t, Missing("float", missingFloat)) + require.True(t, Missing("bool", missingBool)) require.False(t, Missing("bool", false)) require.False(t, Missing("bool", true)) } diff --git a/pkg/controller/internal/collection.go b/pkg/controller/internal/collection.go index dbf0b12e6..4e666f4dd 100644 --- a/pkg/controller/internal/collection.go +++ b/pkg/controller/internal/collection.go @@ -14,6 +14,7 @@ import ( "github.com/docker/infrakit/pkg/spi/event" "github.com/docker/infrakit/pkg/spi/instance" "github.com/docker/infrakit/pkg/spi/metadata" + "github.com/docker/infrakit/pkg/template" "github.com/docker/infrakit/pkg/types" ) @@ -50,10 +51,17 @@ type Item struct { // Error associates an error func (i *Item) Error(err error) { + + const errorKey = "error" + if i.Data == nil { i.Data = map[string]interface{}{} } - i.Data["error"] = err.Error() + if err != nil { + i.Data[errorKey] = err.Error() + } else { + delete(i.Data, errorKey) + } } // Collection is a Managed that tracks a set of finite state machines. @@ -157,6 +165,22 @@ func NewCollection(scope scope.Scope, topics ...types.Path) (*Collection, error) return c, nil } +// Render renders the template in any using the receiver as context +func (c *Collection) Render(any *types.Any, item Item) (*types.Any, error) { + // Run the spec as a template and evaluate it. So for any escaped {{}} sequences + // we can process things + unescaped := string(template.Unescape(any.Bytes())) + templateEngine, err := c.scope.TemplateEngine("str://"+unescaped, template.Options{}) + if err != nil { + return nil, err + } + rendered, err := templateEngine.Render(item) + if err != nil { + return nil, err + } + return types.AnyString(rendered), nil +} + // Metadata returns a metadata plugin implementation. Optional; ok to be nil func (c *Collection) Metadata() metadata.Plugin { return c.metadata diff --git a/pkg/controller/pool/collection.go b/pkg/controller/pool/collection.go index 2376fbb16..22f806a82 100644 --- a/pkg/controller/pool/collection.go +++ b/pkg/controller/pool/collection.go @@ -366,7 +366,10 @@ func (c *collection) run(ctx context.Context) { } accessor := c.accessor - spec, err := c.buildSpec(item, accessor.Spec) + accessorSpec := accessor.Spec + accessorSpec.Properties = types.AnyBytes(accessor.Spec.Properties.Bytes()) + + spec, err := c.buildSpec(item, accessorSpec) if err != nil { log.Error("Error building spec", @@ -505,6 +508,7 @@ func (c *collection) run(ctx context.Context) { log.Debug("found", "instance", n, "key", k, "V", debugV2) item.State.Signal(resourceFound) item.Data["instance"] = n + item.Error(nil) // clear any previous error if this is from a retry } c.MetadataExport(c.accessor.KeyOf, export) @@ -570,13 +574,20 @@ func (c *collection) configureAccessor(spec types.Spec, access *internal.Instanc } func (c *collection) buildSpec(item *internal.Item, spec instance.Spec) (instance.Spec, error) { - // Turn the spec into a blob and use that to parse the dependencies - specAny, err := types.AnyValue(spec) + any, err := types.AnyValue(spec) + if err != nil { + return spec, err + } + + specAny, err := c.Render(any, *item) // as template if err != nil { return spec, err } + // Evaluate any dependencies + // TODO - there's no actual 'cross-controller' dependencies implemented yet. + // This is a placeholder. evaled := types.EvalDepends(specAny, func(p types.Path) (interface{}, error) { v := types.Get(p, c.resources) diff --git a/pkg/controller/pool/fsm.go b/pkg/controller/pool/fsm.go index 4ef78cc90..e64753db2 100644 --- a/pkg/controller/pool/fsm.go +++ b/pkg/controller/pool/fsm.go @@ -288,6 +288,9 @@ func BuildModel(properties pool.Properties, options pool.Options) (*Model, error }, fsm.State{ Index: cannotProvision, + Transitions: map[fsm.Signal]fsm.Index{ + resourceFound: ready, // in case a client timeout puts us in fail state... + }, }, fsm.State{ Index: cannotTerminate, diff --git a/pkg/controller/resource/collection.go b/pkg/controller/resource/collection.go index 28e495736..d1f097863 100644 --- a/pkg/controller/resource/collection.go +++ b/pkg/controller/resource/collection.go @@ -474,7 +474,7 @@ func (c *collection) run(ctx context.Context) { if item != nil { accessor := c.accessors[item.Key] - spec, err := c.populateDependencies(item.Key, accessor.Spec) + spec, err := c.populateDependencies(item, accessor.Spec) if err != nil { log.Error("Dependency missing", @@ -824,10 +824,17 @@ func processDestroyWatches(properties resource.Properties) (watch *Watch, watchi // Assumption: the spec.Properties is fully rendered. We can take the spec.Properties and // generate a list of dependencies via depends(). Now we are rendering this spec.Properties // into the final form with all the dependencies substituted. -func (c *collection) populateDependencies(resourceName string, spec instance.Spec) (instance.Spec, error) { +func (c *collection) populateDependencies(item *internal.Item, spec instance.Spec) (instance.Spec, error) { + + resourceName := item.Key // Turn the spec into a blob and use that to parse the dependencies - specAny, err := types.AnyValue(spec) + any, err := types.AnyValue(spec) + if err != nil { + return spec, err + } + + specAny, err := c.Render(any, *item) if err != nil { return spec, err } diff --git a/pkg/controller/resource/fsm.go b/pkg/controller/resource/fsm.go index 4678773b1..87b22486c 100644 --- a/pkg/controller/resource/fsm.go +++ b/pkg/controller/resource/fsm.go @@ -255,6 +255,11 @@ func BuildModel(properties resource.Properties, options resource.Options) (*Mode }, fsm.State{ Index: cannotProvision, + Transitions: map[fsm.Signal]fsm.Index{ + // we may have timeout on the synchronous call to provision, + // but later on we observe the instance actually being created. + resourceFound: ready, + }, }, fsm.State{ Index: cannotTerminate,