]> gitweb.fluxo.info Git - leap/leap_cli.git/commitdiff
more robust env pinning, fixes several edge case bugs.
authorelijah <elijah@riseup.net>
Mon, 6 Oct 2014 06:19:24 +0000 (23:19 -0700)
committerelijah <elijah@riseup.net>
Mon, 6 Oct 2014 06:19:24 +0000 (23:19 -0700)
lib/leap_cli.rb
lib/leap_cli/config/filter.rb [new file with mode: 0644]
lib/leap_cli/config/manager.rb
lib/leap_cli/config/object_list.rb

index fbfde593529b05a6682da831e0bd7370bea867cf..be16cf4ec06748582b58d03f3f01080f45b872ef 100644 (file)
@@ -34,6 +34,7 @@ require 'leap_cli/config/tag'
 require 'leap_cli/config/provider'
 require 'leap_cli/config/secrets'
 require 'leap_cli/config/object_list'
+require 'leap_cli/config/filter'
 require 'leap_cli/config/manager'
 
 require 'leap_cli/markdown_document_listener'
diff --git a/lib/leap_cli/config/filter.rb b/lib/leap_cli/config/filter.rb
new file mode 100644 (file)
index 0000000..ce218da
--- /dev/null
@@ -0,0 +1,146 @@
+#
+# Many leap_cli commands accept a list of filters to select a subset of nodes for the command to
+# be applied to. This class is a helper for manager to run these filters.
+#
+# Classes other than Manager should not use this class.
+#
+
+module LeapCli
+  module Config
+    class Filter
+
+      #
+      # filter -- array of strings, each one a filter
+      # options -- hash, possible keys include
+      #   :nopin -- disregard environment pinning
+      #   :local -- if false, disallow local nodes
+      #
+      def initialize(filters, options, manager)
+        @filters = filters.nil? ? [] : filters.dup
+        @environments = []
+        @options = options
+        @manager = manager
+
+        # split filters by pulling out items that happen
+        # to be environment names.
+        if LeapCli.leapfile.environment.nil? || @options[:nopin]
+          @environments = []
+        else
+          @environments = [LeapCli.leapfile.environment]
+        end
+        @filters.select! do |filter|
+          filter_text = filter.sub(/^\+/,'')
+          if is_environment?(filter_text)
+            if filter_text == LeapCli.leapfile.environment
+              # silently ignore already pinned environments
+            elsif (filter =~ /^\+/ || @filters.first == filter) && !@environments.empty?
+              LeapCli::Util.bail! do
+                LeapCli::Util.log "Environments are exclusive: no node is in two environments." do
+                  LeapCli::Util.log "Tried to filter on '#{@environments.join('\' AND \'')}' AND '#{filter_text}'"
+                end
+              end
+            else
+              @environments << filter_text
+            end
+            false
+          else
+            true
+          end
+        end
+
+        # don't let the first filter have a + prefix
+        if @filters[0] =~ /^\+/
+          @filters[0] = @filters[0][1..-1]
+        end
+      end
+
+      # actually run the filter, returns a filtered list of nodes
+      def nodes()
+        if @filters.empty?
+          return nodes_for_empty_filter
+        else
+          return nodes_for_filter
+        end
+      end
+
+      private
+
+      def nodes_for_empty_filter
+        node_list = @manager.nodes
+        if @environments.any?
+          node_list = node_list[ @environments.collect{|e|[:environment, env_to_filter(e)]} ]
+        end
+        if @options[:local] === false
+          node_list = node_list[:environment => '!local']
+        end
+        node_list
+      end
+
+      def nodes_for_filter
+        node_list = Config::ObjectList.new
+        @filters.each do |filter|
+          if filter =~ /^\+/
+            keep_list = nodes_for_name(filter[1..-1])
+            node_list.delete_if do |name, node|
+              if keep_list[name]
+                false
+              else
+                true
+              end
+            end
+          else
+            node_list.merge!(nodes_for_name(filter))
+          end
+        end
+        node_list
+      end
+
+      private
+
+      #
+      # returns a set of nodes corresponding to a single name,
+      # where name could be a node name, service name, or tag name.
+      #
+      # For services and tags, we only include nodes for the
+      # environments that are active
+      #
+      def nodes_for_name(name)
+        if node = @manager.nodes[name]
+          return Config::ObjectList.new(node)
+        elsif @environments.empty?
+          if @manager.services[name]
+            @manager.env('_all_').services[name].node_list
+          elsif @manager.tags[name]
+            @manager.env('_all_').tags[name].node_list
+          end
+        else
+          node_list = Config::ObjectList.new
+          if @manager.services[name]
+            @environments.each do |env|
+              node_list.merge!(@manager.env(env).services[name].node_list)
+            end
+          elsif @manager.tags[name]
+            @environments.each do |env|
+              node_list.merge!(@manager.env(env).tags[name].node_list)
+            end
+          end
+          return node_list
+        end
+      end
+
+      #
+      # when pinning, we use the name 'default' to specify nodes
+      # without an environment set, but when filtering, we need to filter
+      # on :environment => nil.
+      #
+      def env_to_filter(environment)
+        environment == 'default' ? nil : environment
+      end
+
+      def is_environment?(text)
+        text == 'default' || @manager.environment_names.include?(text)
+      end
+
+    end
+  end
+end
index 26d45c39fccfcdfc8ddfa150168b03306d9f9d50..be958316a24863e0917503c3b5600ebc1eba783e 100644 (file)
@@ -229,57 +229,19 @@ module LeapCli
       # returns a node list consisting only of nodes that satisfy the filter criteria.
       #
       # filter: condition [condition] [condition] [+condition]
-      # condition: [node_name | service_name | tag_name]
+      # condition: [node_name | service_name | tag_name | environment_name]
       #
       # if conditions is prefixed with +, then it works like an AND. Otherwise, it works like an OR.
       #
-      # The environment is pinned, then all filters get an automatic +environment_name
-      # applied (whatever the name happens to be).
+      # args:
+      # filter -- array of filter terms, one per item
       #
       # options:
       # :local -- if :local is false and the filter is empty, then local nodes are excluded.
       # :nopin -- if true, ignore environment pinning
       #
       def filter(filters=nil, options={})
-        # handle empty filter
-        if filters.nil? || filters.empty?
-          node_list = self.nodes
-          if LeapCli.leapfile.environment
-            node_list = node_list[:environment => LeapCli.leapfile.environment_filter]
-          end
-          if options[:local] === false
-            node_list = node_list[:environment => '!local']
-          end
-          return node_list
-        end
-
-        # handle non-empty filters
-        if filters[0] =~ /^\+/
-          # don't let the first filter have a + prefix
-          filters[0] = filters[0][1..-1]
-        end
-        node_list = Config::ObjectList.new
-        filters.each do |filter|
-          if filter =~ /^\+/
-            keep_list = nodes_for_name(filter[1..-1])
-            node_list.delete_if do |name, node|
-              if keep_list[name]
-                false
-              else
-                true
-              end
-            end
-          else
-            node_list.merge!(nodes_for_name(filter))
-          end
-        end
-
-        # optionally apply environment pin
-        if !options[:nopin] && LeapCli.leapfile.environment
-          node_list = node_list[:environment => LeapCli.leapfile.environment_filter]
-        end
-
-        return node_list
+        Filter.new(filters, options, self).nodes()
       end
 
       #
@@ -512,21 +474,6 @@ module LeapCli
         end
       end
 
-      #
-      # returns a set of nodes corresponding to a single name, where name could be a node name, service name, or tag name.
-      #
-      def nodes_for_name(name)
-        if node = self.nodes[name]
-          Config::ObjectList.new(node)
-        elsif service = self.services[name]
-          service.node_list
-        elsif tag = self.tags[name]
-          tag.node_list
-        else
-          {}
-        end
-      end
-
       def validate_provider(provider)
         # nothing yet.
       end
index cd69d9bd7992a9890a1f893053e96735d6dabcc2..33ca4dde3f6d26108f44db12d1cd5487a4d0bf19 100644 (file)
@@ -20,8 +20,6 @@ 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']
@@ -30,47 +28,22 @@ module LeapCli
       # nodes[:public_dns => true]
       #   all nodes with public dns
       #
-      # nodes[:services => 'openvpn', :services => 'tor']
+      # nodes[:services => 'openvpn', 'location.country_code' => 'US']
+      #   all nodes with services containing 'openvpn' OR country code of US
+      #
+      # Sometimes, you want to do an OR condition with multiple conditions
+      # for the same field. Since hash keys must be unique, you can use
+      # an array representation instead:
+      #
+      # nodes[[:services, 'openvpn'], [:services, 'tor']]
       #   nodes with openvpn OR tor service
       #
       # nodes[:services => 'openvpn'][:tags => 'production']
       #   nodes with openvpn AND are production
       #
       def [](key)
-        if key.is_a? Hash
-          results = Config::ObjectList.new
-          key.each do |field, match_value|
-            field = field.is_a?(Symbol) ? field.to_s : field
-            match_value = match_value.is_a?(Symbol) ? match_value.to_s : match_value
-            if match_value.is_a?(String) && match_value =~ /^!/
-              operator = :not_equal
-              match_value = match_value.sub(/^!/, '')
-            else
-              operator = :equal
-            end
-            each do |name, config|
-              value = config[field]
-              if value.is_a? Array
-                if operator == :equal && value.include?(match_value)
-                  results[name] = config
-                elsif operator == :not_equal && !value.include?(match_value)
-                  results[name] = config
-                end
-              else
-                if operator == :equal && value == match_value
-                  results[name] = config
-                elsif operator == :not_equal && value != match_value
-                  results[name] = config
-                end
-              end
-            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
+        if key.is_a?(Hash) || key.is_a?(Array)
+          filter(key)
         else
           super key.to_s
         end
@@ -88,15 +61,40 @@ module LeapCli
         end
       end
 
-      # def <<(object)
-      #   if object.is_a? Config::ObjectList
-      #     self.merge!(object)
-      #   elsif object['name']
-      #     self[object['name']] = object
-      #   else
-      #     raise ArgumentError.new('argument must be a Config::Object or a Config::ObjectList')
-      #   end
-      # end
+      #
+      # filters this object list, producing a new list.
+      # filter is an array or a hash. see []
+      #
+      def filter(filter)
+        results = Config::ObjectList.new
+        filter.each do |field, match_value|
+          field = field.is_a?(Symbol) ? field.to_s : field
+          match_value = match_value.is_a?(Symbol) ? match_value.to_s : match_value
+          if match_value.is_a?(String) && match_value =~ /^!/
+            operator = :not_equal
+            match_value = match_value.sub(/^!/, '')
+          else
+            operator = :equal
+          end
+          each do |name, config|
+            value = config[field]
+            if value.is_a? Array
+              if operator == :equal && value.include?(match_value)
+                results[name] = config
+              elsif operator == :not_equal && !value.include?(match_value)
+                results[name] = config
+              end
+            else
+              if operator == :equal && value == match_value
+                results[name] = config
+              elsif operator == :not_equal && value != match_value
+                results[name] = config
+              end
+            end
+          end
+        end
+        results
+      end
 
       def add(name, object)
         self[name] = object