From 3ddd8e93a161d748e5703b0856cb2eded0dd19c5 Mon Sep 17 00:00:00 2001
From: elijah <elijah@riseup.net>
Date: Wed, 18 Jun 2014 23:59:05 -0700
Subject: added support for 'control files', files like the .json configuration
 files but contain arbitrary ruby code evaluated in the context of the node.

---
 lib/leap_cli/config/macros.rb      | 90 +++++++++++++++++++++++++++++++++++---
 lib/leap_cli/config/manager.rb     | 53 ++++++++++++++++++++++
 lib/leap_cli/config/node.rb        | 23 ++++++++++
 lib/leap_cli/config/object_list.rb |  7 +++
 lib/leap_cli/config/secrets.rb     |  5 +++
 lib/leap_cli/version.rb            |  2 +-
 6 files changed, 174 insertions(+), 6 deletions(-)

diff --git a/lib/leap_cli/config/macros.rb b/lib/leap_cli/config/macros.rb
index 2c1f1bd..59453b0 100644
--- a/lib/leap_cli/config/macros.rb
+++ b/lib/leap_cli/config/macros.rb
@@ -38,6 +38,59 @@ module LeapCli; module Config
       nodes[:environment => @node.environment]
     end
 
+    #
+    # returns a list of nodes that match the location name
+    # and environment of @node.
+    #
+    def nodes_near_me
+      if @node['location'] && @node['location']['name']
+        nodes_like_me['location.name' => @node.location.name]
+      else
+        nodes_like_me['location' => nil]
+      end
+    end
+
+    #
+    #
+    # picks a node out from the node list in such a way that:
+    #
+    # (1) which nodes picked which nodes is saved in secrets.json
+    # (2) when other nodes call this macro with the same node list, they are guaranteed to get a different node
+    # (3) if all the nodes in the pick_node list have been picked, remaining nodes are distributed randomly.
+    #
+    # if the node_list is empty, an exception is raised.
+    # if node_list size is 1, then that node is returned and nothing is
+    # memorized via the secrets.json file.
+    #
+    # `label` is needed to distinguish between pools of nodes for different purposes.
+    #
+    # TODO: more evenly balance after all the nodes have been picked.
+    #
+    def pick_node(label, node_list)
+      if node_list.any?
+        if node_list.size == 1
+          return node_list.values.first
+        else
+          secrets_key = "pick_node(:#{label},#{node_list.keys.sort.join(',')})"
+          secrets_value = @manager.secrets.retrieve(secrets_key, @node.environment) || {}
+          secrets_value[@node.name] ||= begin
+            node_to_pick = nil
+            node_list.each_node do |node|
+              next if secrets_value.values.include?(node.name)
+              node_to_pick = node.name
+            end
+            node_to_pick ||= secrets_value.values.shuffle.first # all picked already, so pick a random one.
+            node_to_pick
+          end
+          picked_node_name = secrets_value[@node.name]
+          @manager.secrets.set(secrets_key, secrets_value, @node.environment)
+          return node_list[picked_node_name]
+        end
+      else
+        raise ArgumentError.new('pick_node(node_list): node_list cannot be empty')
+      end
+    end
+
     ##
     ## FILES
     ##
@@ -167,9 +220,7 @@ module LeapCli; module Config
     #
     def hostnames(nodes)
       @referenced_nodes ||= ObjectList.new
-      if nodes.is_a? Config::Object
-        nodes = ObjectList.new nodes
-      end
+      nodes = listify(nodes)
       nodes.each_node do |node|
         @referenced_nodes[node.name] ||= node
       end
@@ -248,6 +299,7 @@ module LeapCli; module Config
     #
     def stunnel_client(node_list, port, options={})
       @next_stunnel_port ||= 4000
+      node_list = listify(node_list)
       hostnames(node_list) # record the hosts
       result = Config::ObjectList.new
       node_list.each_node do |node|
@@ -420,9 +472,22 @@ module LeapCli; module Config
     end
 
     #
-    # wrap something that might fail in `try`. e.g.
+    # applies a JSON partial to this node
+    #
+    def apply_partial(partial_path)
+      manager.partials(partial_path).each do |partial_data|
+        self.deep_merge!(partial_data)
+      end
+    end
+
+    #
+    # If at first you don't succeed, then it is time to give up.
     #
-    # "= try{ nodes[:services => 'tor'].first.ip_address } "
+    # try{} returns nil if anything in the block throws an exception.
+    #
+    # You can wrap something that might fail in `try`, like so.
+    #
+    #   "= try{ nodes[:services => 'tor'].first.ip_address } "
     #
     def try(&block)
       yield
@@ -430,5 +495,20 @@ module LeapCli; module Config
       nil
     end
 
+    private
+
+    #
+    # returns a node list, if argument is not already one
+    #
+    def listify(node_list)
+      if node_list.is_a? Config::ObjectList
+        node_list
+      elsif node_list.is_a? Config::Object
+        Config::ObjectList.new(node_list)
+      else
+        raise ArgumentError, 'argument must be a node or node list, not a `%s`' % node_list.class, caller
+      end
+    end
+
   end
 end; end
diff --git a/lib/leap_cli/config/manager.rb b/lib/leap_cli/config/manager.rb
index 00d2f97..7b3fb27 100644
--- a/lib/leap_cli/config/manager.rb
+++ b/lib/leap_cli/config/manager.rb
@@ -107,14 +107,23 @@ module LeapCli
           end
         end
 
+        # apply inheritance
         @nodes.each do |name, node|
           Util::assert! name =~ /^[0-9a-z-]+$/, "Illegal character(s) used in node name '#{name}'"
           @nodes[name] = apply_inheritance(node)
         end
 
+        # remove disabled nodes
         unless options[:include_disabled]
           remove_disabled_nodes
         end
+
+        # apply control files
+        @nodes.each do |name, node|
+          control_files(node).each do |file|
+            node.instance_eval File.read(file), file, 1
+          end
+        end
       end
 
       #
@@ -255,6 +264,28 @@ module LeapCli
         @nodes[node.name] = apply_inheritance!(node)
       end
 
+      #
+      # returns all the partial data for the specified partial path.
+      # partial path is always relative to provider root, but there must be multiple files
+      # that match because provider root might be the base provider or the local provider.
+      #
+      def partials(partial_path)
+        @partials ||= {}
+        if @partials[partial_path].nil?
+          [Path.provider_base, Path.provider].each do |provider_dir|
+            path = File.join(provider_dir, partial_path)
+            if File.exists?(path)
+              @partials[partial_path] ||= []
+              @partials[partial_path] << load_json(path, Config::Object)
+            end
+          end
+          if @partials[partial_path].nil?
+            raise RuntimeError, 'no such partial path `%s`' % partial_path, caller
+          end
+        end
+        @partials[partial_path]
+      end
+
       private
 
       def load_all_json(pattern, object_class, options={})
@@ -438,6 +469,28 @@ module LeapCli
         # nothing yet.
       end
 
+      #
+      # returns a list of 'control' files for this node.
+      # a control file is like a service or a tag JSON file, but it contains
+      # raw ruby code that gets evaluated in the context of the node.
+      # Yes, this entirely breaks our functional programming model
+      # for JSON generation.
+      #
+      def control_files(node)
+        files = []
+        [Path.provider_base, @provider_dir].each do |provider_dir|
+          [['services', :service_config], ['tags', :tag_config]].each do |attribute, path_sym|
+            node[attribute].each do |attr_value|
+              path = Path.named_path([path_sym, "#{attr_value}.rb"], provider_dir).sub(/\.json$/,'')
+              if File.exists?(path)
+                files << path
+              end
+            end
+          end
+        end
+        return files
+      end
+
     end
   end
 end
diff --git a/lib/leap_cli/config/node.rb b/lib/leap_cli/config/node.rb
index 740f9bb..30af5d1 100644
--- a/lib/leap_cli/config/node.rb
+++ b/lib/leap_cli/config/node.rb
@@ -33,6 +33,29 @@ module LeapCli; module Config
       return vagrant_range.include?(ip_address)
     end
 
+    #
+    # Return a hash table representation of ourselves, with the key equal to the @node.name,
+    # and the value equal to the fields specified in *keys.
+    #
+    # Also, the result is flattened to a single hash, so a key of 'a.b' becomes 'a_b'
+    #
+    # compare to Object#pick(*keys). This method is the sames as Config::ObjectList#pick_fields,
+    # but works on a single node.
+    #
+    # Example:
+    #
+    #  node.pick('domain.internal') =>
+    #
+    #    {
+    #      'node1': {
+    #        'domain_internal': 'node1.example.i'
+    #      }
+    #    }
+    #
+    def pick_fields(*keys)
+      {@node.name => self.pick(*keys)}
+    end
+
     #
     # can be overridden by the platform.
     # returns a list of node names that should be tested before this node
diff --git a/lib/leap_cli/config/object_list.rb b/lib/leap_cli/config/object_list.rb
index e975a5f..cd69d9b 100644
--- a/lib/leap_cli/config/object_list.rb
+++ b/lib/leap_cli/config/object_list.rb
@@ -20,6 +20,8 @@ module LeapCli
       # If the key is a hash, we treat it as a condition and filter all the Config::Objects using the condition.
       # A new ObjectList is returned.
       #
+      # If the key is an array, it is treated as an array of node names
+      #
       # Examples:
       #
       # nodes['vpn1']
@@ -64,6 +66,11 @@ module LeapCli
             end
           end
           results
+        elsif key.is_a? Array
+          key.inject(Config::ObjectList.new) do |list, node_name|
+            list[node_name] = super(node_name.to_s)
+            list
+          end
         else
           super key.to_s
         end
diff --git a/lib/leap_cli/config/secrets.rb b/lib/leap_cli/config/secrets.rb
index 963fd6b..4450b9c 100644
--- a/lib/leap_cli/config/secrets.rb
+++ b/lib/leap_cli/config/secrets.rb
@@ -13,6 +13,11 @@ module LeapCli; module Config
       @discovered_keys = {}
     end
 
+     # we can't use fetch() or get(), since those already have special meanings
+     def retrieve(key, environment=nil)
+       self.fetch(environment||'default', {})[key.to_s]
+     end
+
     def set(key, value, environment=nil)
       environment ||= 'default'
       key = key.to_s
diff --git a/lib/leap_cli/version.rb b/lib/leap_cli/version.rb
index cf7e35c..df0f87a 100644
--- a/lib/leap_cli/version.rb
+++ b/lib/leap_cli/version.rb
@@ -1,6 +1,6 @@
 module LeapCli
   unless defined?(LeapCli::VERSION)
-    VERSION = '1.5.6'
+    VERSION = '1.5.7'
     COMPATIBLE_PLATFORM_VERSION = '0.5.2'..'1.99'
     SUMMARY = 'Command line interface to the LEAP platform'
     DESCRIPTION = 'The command "leap" can be used to manage a bevy of servers running the LEAP platform from the comfort of your own home.'
-- 
cgit v1.2.3