qb 0.3.8 → 0.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +4 -0
  3. data/exe/.qb_interop_receive +39 -0
  4. data/exe/qb +8 -5
  5. data/lib/qb.rb +64 -19
  6. data/lib/qb/cli/run.rb +1 -1
  7. data/lib/qb/github.rb +4 -0
  8. data/lib/qb/github/api.rb +43 -0
  9. data/lib/qb/github/issue.rb +83 -0
  10. data/lib/qb/github/repo_id.rb +127 -0
  11. data/lib/qb/github/resource.rb +59 -0
  12. data/lib/qb/github/types.rb +45 -0
  13. data/lib/qb/package.rb +131 -0
  14. data/lib/qb/package/gem.rb +2 -4
  15. data/lib/qb/package/version.rb +175 -2
  16. data/lib/qb/path.rb +2 -2
  17. data/lib/qb/repo.rb +137 -1
  18. data/lib/qb/repo/git.rb +55 -23
  19. data/lib/qb/repo/git/github.rb +137 -0
  20. data/lib/qb/role.rb +74 -30
  21. data/lib/qb/util.rb +1 -4
  22. data/lib/qb/util/docker_mixin.rb +30 -2
  23. data/lib/qb/util/interop.rb +68 -7
  24. data/lib/qb/util/logging.rb +215 -0
  25. data/lib/qb/util/stdio.rb +25 -9
  26. data/lib/qb/version.rb +16 -28
  27. data/library/stream +2 -2
  28. data/plugins/filter_plugins/ruby_interop_plugins.py +49 -31
  29. data/qb.gemspec +16 -4
  30. data/roles/qb.role/defaults/main.yml +6 -2
  31. data/roles/{qb.bump → qb/pkg/bump}/.qb-options.yml +0 -0
  32. data/roles/{qb.bump → qb/pkg/bump}/README.md +0 -0
  33. data/roles/{qb.bump → qb/pkg/bump}/defaults/main.yml +0 -0
  34. data/roles/{qb.bump → qb/pkg/bump}/library/bump +0 -0
  35. data/roles/{qb.bump → qb/pkg/bump}/meta/main.yml +0 -0
  36. data/roles/{qb.bump → qb/pkg/bump}/meta/qb.yml +0 -0
  37. data/roles/{qb.bump → qb/pkg/bump}/tasks/frontend/level/dev.yml +0 -0
  38. data/roles/{qb.bump → qb/pkg/bump}/tasks/frontend/level/rc.yml +0 -0
  39. data/roles/{qb.bump → qb/pkg/bump}/tasks/frontend/level/release.yml +0 -0
  40. data/roles/{qb.bump → qb/pkg/bump}/tasks/frontend/main.yml +0 -0
  41. data/roles/{qb.bump → qb/pkg/bump}/tasks/main.yml +1 -1
  42. data/roles/{qb.qb_role → qb/role/qb}/.qb-options.yml +0 -0
  43. data/roles/{qb.qb_role → qb/role/qb}/defaults/main.yml +0 -0
  44. data/roles/{qb.qb_role → qb/role/qb}/meta/main.yml +0 -0
  45. data/roles/{qb.qb_role → qb/role/qb}/meta/qb.yml +0 -0
  46. data/roles/{qb.qb_role → qb/role/qb}/tasks/main.yml +0 -0
  47. data/roles/{qb.qb_role → qb/role/qb}/templates/.gitkeep +0 -0
  48. data/roles/{qb.qb_role → qb/role/qb}/templates/qb.yml.j2 +0 -0
  49. data/roles/qb/test/rspec/spec/issue/defaults/main.yml +2 -0
  50. data/roles/qb/test/rspec/spec/issue/meta/main.yml +8 -0
  51. data/roles/qb/test/rspec/spec/issue/meta/qb.yml +67 -0
  52. data/roles/qb/test/rspec/spec/issue/tasks/main.yml +21 -0
  53. metadata +95 -26
@@ -403,28 +403,40 @@ class QB::Role
403
403
  end
404
404
 
405
405
 
406
- # @todo This was gonna be something at some point, but didn't get done.
406
+ # Do our best to figure out a role name from a path (that might not exist).
407
407
  #
408
- def self.guess_role_name dest
409
- raise "TODO: Not implemented!!!"
410
-
411
- path = QB::Util.resolve dest
408
+ # We needs this for when we're creating a role.
409
+ #
410
+ # @param [String | Pathname] path
411
+ #
412
+ #
413
+ # @return [String]
414
+ #
415
+ def self.default_role_name path
416
+ resolved_path = QB::Util.resolve path
412
417
 
413
- search_dirs = search_path.find_all { |pathname|
414
- path.fnmatch?( pathname / '**' )
418
+ # Find the first directory in the search path that contains the path,
419
+ # if any do.
420
+ #
421
+ # It *could* be in more than one in funky situations like overlapping
422
+ # search paths or link silliness, but that doesn't matter - we consider
423
+ # the first place we find it to be the relevant once, since the search
424
+ # path is most-important-first.
425
+ #
426
+ search_dir = search_path.find { |pathname|
427
+ resolved_path.fnmatch? ( pathname / '**' ).to_s
415
428
  }
416
429
 
417
- case search_dirs.length
418
- when 0
430
+ if search_dir.nil?
419
431
  # It's not in any of the search directories
420
432
  #
421
433
  # If it has 'roles' as a segment than use what's after the last occurrence
422
434
  # of that (unless there isn't anything).
423
435
  #
424
- segments = path.to_s.split File::SEPARATOR
436
+ segments = resolved_path.to_s.split File::SEPARATOR
425
437
 
426
438
  if index = segments.rindex( 'roles' )
427
- name_segs = segments[index..-1]
439
+ name_segs = segments[( index + 1 )..( -1 )]
428
440
 
429
441
  unless name_segs.empty?
430
442
  return File.join name_segs
@@ -432,16 +444,15 @@ class QB::Role
432
444
  end
433
445
 
434
446
  # Ok, that didn't work... just return the basename I guess...
435
- File.basename path
436
-
437
- when 1
438
-
439
- else
440
- # Multiple matches?!?!?
441
-
447
+ return File.basename resolved_path
442
448
 
443
449
  end
444
- end # #guess_role_name
450
+
451
+ # it's in the search path, return the relative path from the containing
452
+ # search dir to the resolved path (string version of it).
453
+ resolved_path.relative_path_from( search_dir ).to_s
454
+
455
+ end # #default_role_name
445
456
 
446
457
 
447
458
  # Instance Attributes
@@ -871,20 +882,53 @@ class QB::Role
871
882
  end
872
883
 
873
884
 
874
- # Get the {Gem::Requirement} parse of the `qb_requirement` key in
875
- # {#meta} (if it is defined), which specifies the required version of
876
- # `qb` for the role.
885
+ # Parsed tree structure of version requirements of the role from the
886
+ # `requirements` value in the QB meta data.
877
887
  #
878
- # @return [Gem::Requirement, nil]
879
- # The requirement if `required_qb_version` key is in {#meta}, else `nil`.
888
+ # @return [Hash]
889
+ # Tree where the leaves are {Gem::Requirement}.
880
890
  #
881
- def qb_requirement
882
- if meta['requirements'] &&
883
- meta['requirements']['gems'] &&
884
- meta['requirements']['gems']['qb']
885
- Gem::Requirement.new meta['requirements']['gems']['qb']
891
+ def requirements
892
+ @requirements ||= NRSER.map_leaves(
893
+ meta_or 'requirements', {'gems' => {}}
894
+ ) { |key_path, req_str|
895
+ Gem::Requirement.new req_str
896
+ }
897
+ end # #requirements
898
+
899
+
900
+ # Check the role's requirements.
901
+ #
902
+ # @return [nil]
903
+ #
904
+ # @raise [QB::AnsibleVersionError]
905
+ # If the version of Ansible found does not satisfy the role's requirements.
906
+ #
907
+ # @raise [QB::QBVersionError]
908
+ # If the the version of QB we're running does not satisfy the role's
909
+ # requirements.
910
+ #
911
+ def check_requirements
912
+ if ansible_req = requirements['ansible']
913
+ unless ansible_req.satisfied_by? QB.ansible_version
914
+ raise QB::AnsibleVersionError.squished <<-END
915
+ QB #{ QB::VERSION } requires Ansible #{ ansible_req },
916
+ found version #{ QB.ansible_version } at #{ `which ansible` }
917
+ END
918
+ end
886
919
  end
887
- end
920
+
921
+ if qb_req = requirements.dig( 'gems', 'qb' )
922
+ unless qb_req.satisfied_by? QB.gem_version
923
+ raise QB::QBVersionError.squished <<-END
924
+ Role #{ self } requires QB #{ qb_req },
925
+ using QB #{ QB.gem_version } from #{ QB::ROOT }.
926
+ END
927
+ end
928
+ end
929
+
930
+ nil
931
+ end # #check_requirements
888
932
 
889
933
 
890
934
  # Language Inter-Op
@@ -1,3 +1,4 @@
1
+ require_relative './util/logging'
1
2
  require_relative './util/stdio'
2
3
  require_relative './util/interop'
3
4
  require_relative './util/bundler'
@@ -23,10 +24,6 @@ module QB
23
24
 
24
25
  full_string_words = words full_string
25
26
 
26
- QB.debug "HERE",
27
- input_words: input_words,
28
- full_string_words: full_string_words
29
-
30
27
  full_string_words.each_with_index {|word, start_index|
31
28
  # compute the end index in full_string_words
32
29
  end_index = start_index + input_words.length - 1
@@ -4,14 +4,42 @@ module QB
4
4
  module Util
5
5
 
6
6
  # Mixin to help working with Docker.
7
- module DockerMixin
7
+ module DockerMixin
8
+
9
+ # Character limit for Docker image tags.
10
+ #
11
+ # @return [Fixnum]
12
+ #
8
13
  DOCKER_TAG_MAX_CHARACTERS = 128
14
+
9
15
 
10
- # Regexp to validate strings as Docker tags.
16
+ # Regexp to validate strings as Docker tags:
17
+ #
18
+ # 1. Must start with an ASCII alpha-numeric - `A-Z`, `a-z`, `0-9`.
19
+ #
20
+ # 2. The rest of the characters can be:
21
+ #
22
+ # 1. `A-Z`
23
+ # 2. `a-z`
24
+ # 3. `_`
25
+ # 4. `.`
26
+ # 5. `-`
27
+ #
28
+ # Note that it *can not* include `+`, so [Semver][] strings with
29
+ # build info after the `+` are not legal.
30
+ #
31
+ # 3. Must be {QB::Util::DockerMixin::DOCKER_TAG_MAX_CHARACTERS} in length
32
+ # or less.
33
+ #
34
+ # [Semver]: https://semver.org/
35
+ #
36
+ # @return [Regexp]
37
+ #
11
38
  DOCKER_TAG_VALID_RE = \
12
39
  /\A[A-Za-z0-9_][A-Za-z0-9_\.\-]{0,#{ DOCKER_TAG_MAX_CHARACTERS - 1}}\z/.
13
40
  freeze
14
41
 
42
+
15
43
  # Class methods to extend the receiver with when {QB::Util::DockerMixin}
16
44
  # is included.
17
45
  module ClassMethods
@@ -6,8 +6,24 @@ module QB
6
6
  module Util
7
7
 
8
8
  module Interop
9
+ include SemanticLogger::Loggable
10
+
9
11
  class << self
10
12
 
13
+ def send_to_instance data, method_name, *args
14
+ logger.debug "Starting #send_to_instance..."
15
+
16
+ obj = if data.is_a?( Hash ) &&
17
+ data.key?( NRSER::Meta::Props::DEFAULT_CLASS_KEY )
18
+ NRSER::Meta::Props.UNSAFE_load_instance_from_data data
19
+ else
20
+ data
21
+ end
22
+
23
+ obj.send method_name, *args
24
+ end # #send_to_instance
25
+
26
+
11
27
  # @todo Document receive method.
12
28
  #
13
29
  # @param [type] arg_name
@@ -16,28 +32,73 @@ module Interop
16
32
  # @return [return_type]
17
33
  # @todo Document return value.
18
34
  #
19
- def receive
35
+ def send_to_const name, method_name, *args
36
+ logger.debug "Starting #send_to_const..."
37
+
38
+ const = name.to_const
39
+
40
+ logger.debug "Found constant", const: const
41
+
42
+ const.public_send method_name, *args
43
+
44
+ end # #receive
45
+
46
+
47
+ # @todo Document receive method.
48
+ #
49
+ # @param [type] arg_name
50
+ # @todo Add name param description.
51
+ #
52
+ # @return [return_type]
53
+ # @todo Document return value.
54
+ #
55
+ def receive
56
+ logger.debug "Starting #receive..."
57
+
20
58
  # method body
21
59
  yaml = $stdin.read
22
60
 
23
61
  payload = YAML.load yaml
24
62
 
25
- data = payload.fetch 'data'
63
+ logger.debug "Parsed",
64
+ payload: payload
65
+
66
+ # data = payload.fetch 'data'
26
67
  method = payload.fetch 'method'
27
68
  args = payload['args'] || []
28
69
  kwds = payload['kwds'] || {}
29
70
  args << kwds.symbolize_keys unless kwds.empty?
30
71
 
31
- obj = if data.is_a?(Hash) && data.key?(NRSER::Meta::Props::CLASS_KEY)
32
- NRSER::Meta::Props.from_data data
72
+ result = if payload['data']
73
+ send_to_instance payload['data'], method, *args
74
+
75
+ elsif payload['const']
76
+ send_to_const payload['const'], method, *args
77
+
33
78
  else
34
- data
79
+ raise ArgumentError.new binding.erb <<-ERB
80
+ Expected payload to have 'data' or 'const' keys, neither found:
81
+
82
+ Payload:
83
+
84
+ <%= payload.pretty_inspect %>
85
+
86
+ Input YAML:
87
+
88
+ <%= yaml %>
89
+
90
+ ERB
35
91
  end
36
92
 
37
- result = obj.send method, *args
93
+ logger.debug "send succeeded", result: result
94
+
95
+ yaml = result.to_yaml # don't work: sort_keys: true, use_header: true
96
+
97
+ logger.debug "writing YAML:\n\n#{ yaml }"
38
98
 
39
- $stdout.write result.to_yaml
99
+ $stdout.write yaml
40
100
 
101
+ logger.debug "done."
41
102
  end # #receive
42
103
 
43
104
  end # class << self
@@ -0,0 +1,215 @@
1
+ # Requirements
2
+ # =======================================================================
3
+
4
+ # Stdlib
5
+ # -----------------------------------------------------------------------
6
+
7
+ # Deps
8
+ # -----------------------------------------------------------------------
9
+ require 'awesome_print'
10
+ require 'semantic_logger'
11
+
12
+ # Project / Package
13
+ # -----------------------------------------------------------------------
14
+
15
+
16
+ # Refinements
17
+ # =======================================================================
18
+
19
+
20
+ # Declarations
21
+ # =======================================================================
22
+
23
+ module QB; end
24
+ module QB::Util; end
25
+
26
+
27
+ # Definitions
28
+ # =======================================================================
29
+
30
+ # Utility methods to setup logging with [semantic_logger][].
31
+ #
32
+ # [semantic_logger]: http://rocketjob.github.io/semantic_logger/
33
+ #
34
+ module QB::Util::Logging
35
+ include SemanticLogger::Loggable
36
+
37
+
38
+ # @todo document Formatters module.
39
+ module Formatters
40
+
41
+ # Custom tweaked color formatter (for CLI output).
42
+ #
43
+ # - Turns on multiline output in Awesome Print by default.
44
+ #
45
+ class Color < SemanticLogger::Formatters::Color
46
+
47
+ # Constants
48
+ # ======================================================================
49
+
50
+
51
+ # Class Methods
52
+ # ======================================================================
53
+
54
+
55
+ # Attributes
56
+ # ======================================================================
57
+
58
+
59
+ # Constructor
60
+ # ======================================================================
61
+
62
+ # Instantiate a new `ColorFormatter`.
63
+ def initialize **options
64
+ super ap: { multiline: true },
65
+ color_map: SemanticLogger::Formatters::Color::ColorMap.new(
66
+ debug: SemanticLogger::AnsiColors::MAGENTA,
67
+ ),
68
+ **options
69
+ end # #initialize
70
+
71
+
72
+ # Instance Methods
73
+ # ======================================================================
74
+
75
+
76
+ # Upcase the log level.
77
+ #
78
+ # @return [String]
79
+ #
80
+ def level
81
+ "#{ color }#{ log.level.upcase }#{ color_map.clear }"
82
+ end
83
+
84
+
85
+ # Create the log entry text. Overridden to customize appearance -
86
+ # generally reduce amount of info and put payload on it's own line.
87
+ #
88
+ # We need to replace *two* super functions, the first being
89
+ # [SemanticLogger::Formatters::Color#call][]:
90
+ #
91
+ # def call(log, logger)
92
+ # self.color = color_map[log.level]
93
+ # super(log, logger)
94
+ # end
95
+ #
96
+ # [SemanticLogger::Formatters::Color#call]: https://github.com/rocketjob/semantic_logger/blob/v4.2.0/lib/semantic_logger/formatters/color.rb#L98
97
+ #
98
+ # which doesn't do all too much, and the next being it's super-method,
99
+ # [SemanticLogger::Formatters::Default#call][]:
100
+ #
101
+ # # Default text log format
102
+ # # Generates logs of the form:
103
+ # # 2011-07-19 14:36:15.660235 D [1149:ScriptThreadProcess] Rails -- Hello World
104
+ # def call(log, logger)
105
+ # self.log = log
106
+ # self.logger = logger
107
+ #
108
+ # [time, level, process_info, tags, named_tags, duration, name, message, payload, exception].compact.join(' ')
109
+ # end
110
+ #
111
+ # [SemanticLogger::Formatters::Default#call]: https://github.com/rocketjob/semantic_logger/blob/v4.2.0/lib/semantic_logger/formatters/default.rb#L64
112
+ #
113
+ # which does most the real assembly.
114
+ #
115
+ # @param [SemanticLogger::Log] log
116
+ # The log entry to format.
117
+ #
118
+ # See [SemanticLogger::Log](https://github.com/rocketjob/semantic_logger/blob/v4.2.0/lib/semantic_logger/log.rb)
119
+ #
120
+ # @param [SemanticLogger::Logger] logger
121
+ # The logger doing the logging (pretty sure, haven't checked).
122
+ #
123
+ # See [SemanticLogger::Logger](https://github.com/rocketjob/semantic_logger/blob/v4.2.0/lib/semantic_logger/logger.rb)
124
+ #
125
+ # @return [return_type]
126
+ # @todo Document return value.
127
+ #
128
+ def call log, logger
129
+ # SemanticLogger::Formatters::Color code
130
+ self.color = color_map[log.level]
131
+
132
+ # SemanticLogger::Formatters::Default code
133
+ self.log = log
134
+ self.logger = logger
135
+
136
+ [
137
+ # time, annoyingly noisy and don't really need for local CLI app
138
+ level,
139
+ process_info,
140
+ tags,
141
+ named_tags,
142
+ duration,
143
+ name,
144
+ ].compact.join( ' ' ) +
145
+ "\n" +
146
+ [
147
+ message,
148
+ payload,
149
+ exception,
150
+ ].compact.join(' ') +
151
+ "\n" # I like extra newline to space shit out
152
+
153
+ end # #call
154
+
155
+
156
+ end # class Color
157
+
158
+ end # module Formatters
159
+
160
+
161
+
162
+
163
+ # Module (Class) Methods
164
+ # =====================================================================
165
+
166
+ # Setup logging.
167
+ #
168
+ # @param [type] arg_name
169
+ # @todo Add name param description.
170
+ #
171
+ # @return [return_type]
172
+ # @todo Document return value.
173
+ #
174
+ def self.setup level: nil
175
+ if level.nil?
176
+ if ENV['QB_LOG_LEVEL']
177
+ level = ENV['QB_LOG_LEVEL'].to_sym
178
+ else
179
+ level = :info
180
+ end
181
+ end
182
+
183
+ SemanticLogger.default_level = level
184
+
185
+ @appender ||= SemanticLogger.add_appender(
186
+ io: $stderr,
187
+ formatter: Formatters::Color.new,
188
+ )
189
+
190
+ # Set ENV vars (that Ansible modules will have access to!)
191
+
192
+ ENV['QB_LOG_LEVEL'] = level.to_s
193
+
194
+ if level == :debug
195
+ ENV['QB_DEBUG'] = 'true'
196
+ logger.debug "debug logging is ON"
197
+ else
198
+ ENV.delete 'QB_DEBUG'
199
+ end
200
+
201
+ nil
202
+ end # .setup
203
+
204
+
205
+ def self.appender
206
+ @appender
207
+ end
208
+
209
+
210
+ end # module QB::Util::Logging
211
+
212
+
213
+
214
+ # Post-Processing
215
+ # =======================================================================