foreman_omaha 0.0.3 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +5 -2
  3. data/app/assets/javascripts/foreman_omaha/application.js +1 -0
  4. data/app/assets/stylesheets/foreman_omaha/version_breakdown.scss +13 -0
  5. data/app/controllers/api/v2/omaha_groups_controller.rb +29 -0
  6. data/app/controllers/api/v2/omaha_reports_controller.rb +9 -7
  7. data/app/controllers/omaha_groups_controller.rb +28 -0
  8. data/app/controllers/omaha_hosts_controller.rb +42 -0
  9. data/app/controllers/omaha_reports_controller.rb +0 -11
  10. data/app/helpers/concerns/foreman_omaha/hosts_helper_extensions.rb +15 -6
  11. data/app/helpers/foreman_omaha/application_helper.rb +7 -0
  12. data/app/helpers/foreman_omaha/omaha_groups_helper.rb +52 -0
  13. data/app/helpers/omaha_hosts_helper.rb +43 -0
  14. data/app/models/concerns/foreman_omaha/host_extensions.rb +16 -2
  15. data/app/models/concerns/foreman_omaha/omaha_facet_host_extensions.rb +23 -0
  16. data/app/models/foreman_omaha/omaha_facet.rb +40 -0
  17. data/app/models/foreman_omaha/omaha_group.rb +34 -0
  18. data/app/models/foreman_omaha/omaha_report.rb +13 -21
  19. data/app/models/host_status/omaha_status.rb +6 -16
  20. data/app/services/foreman_omaha/charts/oem_distribution.rb +24 -0
  21. data/app/services/foreman_omaha/charts/status_distribution.rb +57 -0
  22. data/app/services/foreman_omaha/charts/version_distribution.rb +24 -0
  23. data/app/services/foreman_omaha/container_linux_config_transpiler.rb +33 -0
  24. data/app/services/foreman_omaha/fact_parser.rb +19 -3
  25. data/app/services/foreman_omaha/group_version_breakdown.rb +30 -0
  26. data/app/services/foreman_omaha/omaha_report_importer.rb +52 -3
  27. data/app/services/foreman_omaha/renderer_methods.rb +7 -0
  28. data/app/services/foreman_omaha/status_mapper.rb +53 -0
  29. data/app/views/api/v2/omaha_groups/base.json.rabl +3 -0
  30. data/app/views/api/v2/omaha_groups/index.json.rabl +3 -0
  31. data/app/views/api/v2/omaha_groups/main.json.rabl +5 -0
  32. data/app/views/api/v2/omaha_groups/show.json.rabl +3 -0
  33. data/app/views/application/foreman_omaha/_toolbar.html.erb +11 -0
  34. data/app/views/foreman_omaha/api/v2/omaha_facets/base.json.rabl +5 -0
  35. data/app/views/foreman_omaha/api/v2/omaha_facets/base_with_root.json.rabl +3 -0
  36. data/app/views/foreman_omaha/api/v2/omaha_facets/show.json.rabl +3 -0
  37. data/app/views/hosts/_omaha_tab.html.erb +29 -0
  38. data/app/views/omaha_groups/index.html.erb +34 -0
  39. data/app/views/omaha_groups/show.html.erb +196 -0
  40. data/app/views/omaha_hosts/index.html.erb +51 -0
  41. data/app/views/omaha_hosts/welcome.html.erb +12 -0
  42. data/app/views/omaha_reports/_details.html.erb +5 -3
  43. data/app/views/omaha_reports/_list.html.erb +2 -2
  44. data/app/views/omaha_reports/show.html.erb +1 -1
  45. data/app/views/omaha_reports/welcome.html.erb +1 -1
  46. data/config/routes.rb +14 -1
  47. data/db/migrate/20160812083100_add_omaha_fields_to_reports.foreman_omaha.rb +1 -1
  48. data/db/migrate/20171101204100_create_omaha_facets.foreman_omaha.rb +22 -0
  49. data/db/seeds.d/200_omaha_groups.rb +6 -0
  50. data/lib/foreman_omaha/engine.rb +74 -21
  51. data/lib/foreman_omaha/version.rb +1 -1
  52. data/lib/tasks/foreman_omaha_tasks.rake +2 -4
  53. data/test/factories/feature.rb +1 -1
  54. data/test/factories/foreman_omaha_factories.rb +16 -1
  55. data/test/factories/host.rb +21 -0
  56. data/test/factories/smart_proxy.rb +1 -1
  57. data/test/functional/api/v2/omaha_groups_controller_test.rb +32 -0
  58. data/test/functional/api/v2/omaha_reports_controller_test.rb +24 -24
  59. data/test/functional/omaha_groups_controller_test.rb +18 -0
  60. data/test/functional/omaha_hosts_controller_test.rb +11 -0
  61. data/test/functional/omaha_reports_controller_test.rb +13 -13
  62. data/test/test_plugin_helper.rb +3 -3
  63. data/test/unit/charts/oem_distribution_test.rb +26 -0
  64. data/test/unit/charts/status_distribution_test.rb +28 -0
  65. data/test/unit/charts/version_distribution_test.rb +28 -0
  66. data/test/unit/group_version_breakdown_test.rb +65 -0
  67. data/test/unit/host_status/omaha_status_test.rb +19 -0
  68. data/test/unit/host_test.rb +55 -0
  69. data/test/unit/omaha_fact_parser_test.rb +77 -0
  70. data/test/unit/omaha_report_test.rb +63 -17
  71. data/test/unit/renderer_test.rb +21 -0
  72. metadata +80 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: d3ec4d257a6b23da8a778b2c6c7646bda239ed8a
4
- data.tar.gz: 72996f1f0545e2c51e9bda9d0d1ec3c1765ff483
2
+ SHA256:
3
+ metadata.gz: 6ad84de9035d283fda62edcb31c461f88ef3811ce4caa64c2f7b299ec9c20474
4
+ data.tar.gz: 3884e00feb8dff9ca2409006709f344e7085085b11e923058188101b8a196022
5
5
  SHA512:
6
- metadata.gz: 49351ba242b536b61204bd5dde6f559a843e979c7de3e3fddb1a3e7c0e104bd5303fdcce6d7fcb4e3bb3a2d9639a6af872e14167289c5d02164b02a2b0acd314
7
- data.tar.gz: afcc44fb7da39ece8712fd38d18d9dd1e7cff99e938284b90b5d2a4d0909924753f410a189b25f8b67ce8827ab64532917afa3677d73fb0084353f6bd24b3557
6
+ metadata.gz: c0a3e1ffe8c2d3fee38ccc48edfed5ed12e9416fd123378c08f92772386efec252bcf5f2615539d1667292c439e95535f06133700df04f94529a482a6068cdf6
7
+ data.tar.gz: efd82da861cc6e7a6515136df34c4cfc46cd6eb7b6d3a5f480468778bdce57fcab7142c0d0cb9f50587cfda96f26f7bfbddc301681101e22e2424cc35eb667ce
data/README.md CHANGED
@@ -1,14 +1,15 @@
1
1
  # ForemanOmaha
2
2
 
3
3
  This plug-in enables CoreOS updates using [The Foreman](https://theforeman.org/) without using the public update infrastructure.
4
- To follow along with common Foreman architecture, you need this Foreman plug-in installed and a [Smart-Proxy plugin](https://github.com/timogoebel/smart_proxy_omaha). The smart-proxy plug-in does all the heavy lifting. The Foreman plug-in shows facts and reports for your hosts.
4
+ To follow along with common Foreman architecture, you need this Foreman plug-in installed and a [Smart-Proxy plugin](https://github.com/theforeman/smart_proxy_omaha). The smart-proxy plug-in does all the heavy lifting. The Foreman plug-in shows facts and reports for your hosts.
5
5
  Foreman core already supports deploying CoreOS hosts and is great for on-premise setups. This plug-in enables you to better manage your servers.
6
6
 
7
7
  ## Compatibility
8
8
 
9
9
  | Foreman Version | Plugin Version |
10
10
  | --------------- | -------------- |
11
- | >= 1.12 | any |
11
+ | >= 1.12 | ~> 0.0 |
12
+ | >= 1.17 | ~> 1.0 |
12
13
 
13
14
  ## Installation
14
15
 
@@ -22,6 +23,8 @@ To prepare the Foreman database, issue the following command:
22
23
  foreman-rake db:migrate SCOPE=foreman_omaha
23
24
  ```
24
25
 
26
+ This plugins adds a global method `transpile_container_linux_config` to the template renderer that transpiles yaml to ignition. Make sure the binary [ct](https://github.com/coreos/container-linux-config-transpiler) is in your `PATH` when you want to use this feature.
27
+
25
28
  ## Uninstalling
26
29
 
27
30
  You need to revert all database changes made when installing this plug-in:
@@ -0,0 +1 @@
1
+ //= require jquery.matchHeight
@@ -0,0 +1,13 @@
1
+ .version-breakdown {
2
+ .progress {
3
+ overflow: hidden;
4
+ text-overflow: ellipsis;
5
+ }
6
+
7
+ .progress-bar {
8
+ a {
9
+ color: white;
10
+ text-decoration: none;
11
+ }
12
+ }
13
+ }
@@ -0,0 +1,29 @@
1
+ module Api
2
+ module V2
3
+ class OmahaGroupsController < V2::BaseController
4
+ include Api::Version2
5
+
6
+ before_action :find_resource, :only => [:show]
7
+ before_action :setup_search_options, :only => [:index]
8
+
9
+ api :GET, '/omaha_groups/', N_('List all omaha groups')
10
+ param_group :search_and_pagination, ::Api::V2::BaseController
11
+
12
+ def index
13
+ @omaha_groups = resource_scope_for_index
14
+ @total = resource_scope_for_index.count
15
+ end
16
+
17
+ api :GET, '/omaha_groups/:id/', N_('Show a Omaha Group')
18
+ param :id, :identifier, :required => true, :desc => N_('Id of the Omaha Group')
19
+
20
+ def show; end
21
+
22
+ private
23
+
24
+ def resource_class
25
+ ::ForemanOmaha::OmahaGroup
26
+ end
27
+ end
28
+ end
29
+ end
@@ -13,10 +13,13 @@ module Api
13
13
  def_param_group :omaha_report do
14
14
  param :report, Hash, :required => true, :action_aware => true do
15
15
  param :host, String, :required => true, :desc => N_('Hostname or certname')
16
- param :status, String,
16
+ param :status, ::ForemanOmaha::OmahaReport.statuses.keys,
17
17
  :required => true,
18
- :desc => N_('Omaha status, can be one of %s') % ::ForemanOmaha::OmahaReport.statuses.keys.to_sentence
19
- param :omaha_version, String, :required => true, :desc => N_('Omaha OS version')
18
+ :desc => N_('Omaha update status')
19
+ param :omaha_version, String, :required => true, :desc => N_('Omaha OS version using semantic versioning, e.g. 1590.0.0')
20
+ param :machineid, String, :required => true, :desc => N_('Unique machine id of the host')
21
+ param :omaha_group, String, :required => true, :desc => N_('The uuid if the channel that the host is attached to. Use alpha, beta or stable for built-in channels.')
22
+ param :oem, String, :desc => N_('OEM identifier')
20
23
  end
21
24
  end
22
25
 
@@ -37,7 +40,7 @@ module Api
37
40
  param_group :omaha_report, :as => :create
38
41
 
39
42
  def create
40
- @omaha_report = resource_class.import(params[:omaha_report], detected_proxy.try(:id))
43
+ @omaha_report = resource_class.import(params.to_unsafe_h[:omaha_report], detected_proxy.try(:id))
41
44
  process_response @omaha_report.errors.empty?
42
45
  rescue ::Foreman::Exception => e
43
46
  render_message(e.to_s, :status => :unprocessable_entity)
@@ -51,12 +54,11 @@ module Api
51
54
  end
52
55
 
53
56
  api :GET, '/hosts/:host_id/omaha_reports/last', N_('Show the last report for a host')
57
+ param :host_id, String, :required => true, :desc => N_('ID of host')
54
58
  param :id, :identifier, :required => true
55
59
 
56
60
  def last
57
- if params[:host_id].present?
58
- conditions = { :host_id => resource_finder(Host.authorized(:view_hosts), params[:host_id]).try(:id) }
59
- end
61
+ conditions = params[:host_id].present? ? { :host_id => resource_finder(Host.authorized(:view_hosts), params[:host_id]).try(:id) } : nil
60
62
  max_id = resource_scope.where(conditions).maximum(:id)
61
63
  @omaha_report = resource_scope.find(max_id)
62
64
  render :show
@@ -0,0 +1,28 @@
1
+ class OmahaGroupsController < ApplicationController
2
+ include Foreman::Controller::AutoCompleteSearch
3
+
4
+ before_action :setup_search_options, :only => :index
5
+ before_action :find_resource, :only => :show
6
+
7
+ def index
8
+ @omaha_groups = resource_base_with_search
9
+ end
10
+
11
+ def show
12
+ @out_of_sync = ForemanOmaha::OmahaFacet.where(:omaha_group => @omaha_group).out_of_sync.includes(:host)
13
+ @latest_operatingsystem = @omaha_group.latest_operatingsystem
14
+ @status_distribution = ForemanOmaha::Charts::StatusDistribution.new(@omaha_group.hosts)
15
+ @version_distribution = ForemanOmaha::Charts::VersionDistribution.new(@omaha_group.hosts)
16
+ @oem_distribution = ForemanOmaha::Charts::OemDistribution.new(@omaha_group.hosts)
17
+ end
18
+
19
+ private
20
+
21
+ def model_of_controller
22
+ ::ForemanOmaha::OmahaGroup
23
+ end
24
+
25
+ def resource_class
26
+ ::ForemanOmaha::OmahaGroup
27
+ end
28
+ end
@@ -0,0 +1,42 @@
1
+ class OmahaHostsController < ApplicationController
2
+ include Foreman::Controller::AutoCompleteSearch
3
+ include ScopesPerAction
4
+
5
+ before_action :setup_search_options, :only => :index
6
+
7
+ add_scope_for(:index) do |base_scope|
8
+ base_scope.includes(:operatingsystem).includes(:omaha_facet).includes(:omaha_facet => :omaha_group)
9
+ end
10
+
11
+ def index
12
+ @hosts = action_scope_for(:index, resource_base_with_search)
13
+ @last_report_ids = ForemanOmaha::OmahaReport.where(:host_id => @hosts.map(&:id)).group(:host_id).maximum(:id)
14
+ end
15
+
16
+ def welcome
17
+ if begin
18
+ resource_base.first.nil?
19
+ rescue StandardError
20
+ false
21
+ end
22
+ @welcome = true
23
+ render :welcome
24
+ end
25
+ rescue StandardError
26
+ not_found
27
+ end
28
+
29
+ protected
30
+
31
+ def model_of_controller
32
+ ::Host::Managed
33
+ end
34
+
35
+ def resource_base
36
+ super.joins(:omaha_facet)
37
+ end
38
+
39
+ def controller_permission
40
+ 'hosts'
41
+ end
42
+ end
@@ -42,17 +42,6 @@ class OmahaReportsController < ApplicationController
42
42
  end
43
43
  end
44
44
 
45
- # TODO: Remove for Foreman 1.14+
46
- def resource_base_with_search
47
- resource_base.search_for(params[:search], :order => params[:order])
48
- end
49
-
50
- # TODO: Remove for Foreman 1.14+
51
- def resource_base_search_and_page(tables = [])
52
- base = tables.empty? ? resource_base_with_search : resource_base_with_search.eager_load(*tables)
53
- base.paginate(:page => params[:page], :per_page => params[:per_page])
54
- end
55
-
56
45
  private
57
46
 
58
47
  def resource_base
@@ -2,14 +2,23 @@ module ForemanOmaha
2
2
  module HostsHelperExtensions
3
3
  extend ActiveSupport::Concern
4
4
 
5
- included do
6
- alias_method_chain :show_appropriate_host_buttons, :foreman_omaha
5
+ module Overrides
6
+ def show_appropriate_host_buttons(host)
7
+ buttons = super
8
+ if host.omaha_reports.any?
9
+ buttons << link_to_if_authorized(
10
+ _('Omaha'),
11
+ hash_for_host_omaha_reports_path(:host_id => host),
12
+ :title => _('Browse host omaha reports'),
13
+ :class => 'btn btn-default'
14
+ )
15
+ end
16
+ buttons.compact
17
+ end
7
18
  end
8
19
 
9
- def show_appropriate_host_buttons_with_foreman_omaha(host)
10
- buttons = show_appropriate_host_buttons_without_foreman_omaha(host)
11
- buttons << (link_to_if_authorized(_('Omaha'), hash_for_host_omaha_reports_path(:host_id => host), :title => _('Browse host omaha reports'), :class => 'btn btn-default') if host.omaha_reports.any?)
12
- buttons.compact
20
+ included do
21
+ prepend Overrides
13
22
  end
14
23
  end
15
24
  end
@@ -0,0 +1,7 @@
1
+ module ForemanOmaha
2
+ module ApplicationHelper
3
+ def facets_count(association, resource_name = controller.resource_name)
4
+ @facets_count ||= Host::Managed.reorder('').authorized.joins(association).group("#{resource_name}_id").count
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,52 @@
1
+ module ForemanOmaha
2
+ module OmahaGroupsHelper
3
+ def omaha_version_breakdown_bar(omaha_group)
4
+ stylesheet 'foreman_omaha/version_breakdown'
5
+ colors = {
6
+ :success => ['#3f9c35', '#6ec664', '#2d7623'],
7
+ :warning => ['#ec7a08', '#f7bd7f', '#f39d3c'],
8
+ :danger => ['#cc0000', '#a30000', '#8b0000', '#470000']
9
+ }
10
+ versions = ForemanOmaha::GroupVersionBreakdown.new(:omaha_group => omaha_group).version_breakdown
11
+ version_list = omaha_group.operatingsystems.pluck(:major, :minor).map { |v| Gem::Version.new(v.join('.')) }.sort.reverse
12
+ current = version_list.max
13
+ content_tag :div, :class => 'progress version-breakdown' do
14
+ safe_join(versions.map do |version|
15
+ semver = Gem::Version.new(version[:version])
16
+ position = version_list.index(semver)
17
+ css = {
18
+ :width => "#{version[:percentage]}%"
19
+ }
20
+ label = if semver > current
21
+ :info
22
+ elsif semver == current
23
+ :success
24
+ elsif position == 1
25
+ :warning
26
+ else
27
+ :danger
28
+ end
29
+ if colors.key?(label)
30
+ color = colors[label].first
31
+ colors[label].rotate!
32
+ css[:'background-color'] = color
33
+ end
34
+ content_tag :div,
35
+ :class => "progress-bar progress-bar-#{label}",
36
+ :role => 'progressbar',
37
+ :title => version[:version],
38
+ :style => css_style(css) do
39
+ link_to(
40
+ version[:version],
41
+ hosts_path(:search => "omaha_group = #{omaha_group} and omaha_version = #{version[:version]}")
42
+ )
43
+ end
44
+ end)
45
+ end
46
+ end
47
+
48
+ def css_style(hash)
49
+ hash.map { |k, v| "#{k}: #{v};" }.join(' ')
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,43 @@
1
+ module OmahaHostsHelper
2
+ def last_omaha_report_column(record)
3
+ time = record.omaha_facet.last_report? ? _('%s ago') % time_ago_in_words(record.omaha_facet.last_report) : ''
4
+ link_to_if_authorized(time,
5
+ hash_for_host_omaha_report_path(:host_id => record.to_param, :id => 'last'),
6
+ last_omaha_report_tooltip(record))
7
+ end
8
+
9
+ def last_omaha_report_tooltip(record)
10
+ opts = { :rel => 'twipsy' }
11
+ if @last_report_ids[record.id]
12
+ opts['data-original-title'] = _('View last report details')
13
+ else
14
+ opts.merge!(:disabled => true, :class => 'disabled', :onclick => 'return false')
15
+ opts['data-original-title'] = _('Report Already Deleted') unless record.omaha_facet.last_report.nil?
16
+ end
17
+ opts
18
+ end
19
+
20
+ def list_view_class_for_omaha_status(status)
21
+ klasses = [
22
+ iconclass_for_omaha_status(status),
23
+ 'list-view-pf-icon-md'
24
+ ]
25
+ klasses << case status.to_sym
26
+ when :complete
27
+ 'list-view-pf-icon-success'
28
+ when :downloading
29
+ 'list-view-pf-icon-info'
30
+ when :downloaded
31
+ 'list-view-pf-icon-success'
32
+ when :installed
33
+ 'fa fa-sign-in text-success'
34
+ when :instance_hold
35
+ 'list-view-pf-icon-warning'
36
+ when :error
37
+ 'list-view-pf-icon-danger'
38
+ else
39
+ 'list-view-pf-icon-info'
40
+ end
41
+ klasses.join(' ')
42
+ end
43
+ end
@@ -1,11 +1,25 @@
1
1
  module ForemanOmaha
2
2
  module HostExtensions
3
3
  extend ActiveSupport::Concern
4
+
4
5
  included do
5
6
  has_many :omaha_reports, :class_name => '::ForemanOmaha::OmahaReport',
6
- :foreign_key => :host_id, :dependent => :destroy
7
+ :foreign_key => :host_id, :dependent => :destroy,
8
+ :inverse_of => :host
7
9
  has_one :last_omaha_report_object, -> { order("#{Report.table_name}.id DESC") },
8
- :foreign_key => :host_id, :class_name => '::ForemanOmaha::OmahaReport'
10
+ :foreign_key => :host_id, :class_name => '::ForemanOmaha::OmahaReport',
11
+ :inverse_of => :host
12
+
13
+ before_save :clear_omaha_facet_on_build
14
+ end
15
+
16
+ def clear_omaha_facet
17
+ omaha_facet.destroy if omaha_facet
18
+ end
19
+
20
+ def clear_omaha_facet_on_build
21
+ return unless respond_to?(:old) && old && build? && !old.build?
22
+ clear_omaha_facet
9
23
  end
10
24
  end
11
25
  end
@@ -0,0 +1,23 @@
1
+ module ForemanOmaha
2
+ module OmahaFacetHostExtensions
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ has_one :omaha_facet, :class_name => '::ForemanOmaha::OmahaFacet', :foreign_key => :host_id, :inverse_of => :host, :dependent => :destroy
7
+ has_one :omaha_group, :through => :omaha_facet, :inverse_of => :hosts
8
+
9
+ accepts_nested_attributes_for :omaha_facet, :update_only => true, :reject_if => ->(attrs) { attrs.values.compact.empty? }
10
+
11
+ scoped_search :on => :last_report, :relation => :omaha_facet, :rename => :last_omaha_report, :complete_value => true, :only_explicit => true
12
+ scoped_search :on => :machineid, :relation => :omaha_facet, :rename => :omaha_machineid, :complete_value => true, :only_explicit => true
13
+ scoped_search :on => :version, :relation => :omaha_facet, :rename => :omaha_version, :complete_value => true, :only_explicit => true
14
+ scoped_search :on => :oem, :relation => :omaha_facet, :rename => :omaha_oem, :complete_value => true, :only_explicit => true
15
+ scoped_search :on => :status, :relation => :omaha_facet, :rename => :omaha_status, :only_explicit => true, :complete_value => {
16
+ :unknown => 0, :complete => 1, :downloading => 2, :downloaded => 3,
17
+ :installed => 4, :instance_hold => 5, :error => 6
18
+ }
19
+
20
+ scoped_search :on => :name, :relation => :omaha_group, :complete_value => true, :rename => :omaha_group
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,40 @@
1
+ module ForemanOmaha
2
+ class OmahaFacet < ApplicationRecord
3
+ include Facets::Base
4
+
5
+ VALID_OMAHA_STATUSES = [:unknown, :complete, :downloading, :downloaded,
6
+ :installed, :instance_hold, :error].freeze
7
+
8
+ enum :status => VALID_OMAHA_STATUSES
9
+
10
+ belongs_to :omaha_group, :inverse_of => :omaha_facets, :class_name => 'ForemanOmaha::OmahaGroup'
11
+
12
+ scope :out_of_sync, ->(*args) { where(['last_report < ?', (args.first || (30 + Setting[:outofsync_interval]).minutes.ago)]) }
13
+
14
+ validates_lengths_from_database
15
+
16
+ validates :omaha_group, :presence => true, :allow_blank => false
17
+ validates :host, :presence => true, :allow_blank => false
18
+ validates :version, format: { with: /\A[0-9]+\.[0-9]+\.[0-9]+\z/, message: _('must use semantic versioning') }
19
+
20
+ def to_status_label
21
+ status_mapper.to_label
22
+ end
23
+
24
+ def major
25
+ return unless version
26
+ version.split('.').first
27
+ end
28
+
29
+ def minor
30
+ return unless version
31
+ version.split('.').last(2).join('.')
32
+ end
33
+
34
+ private
35
+
36
+ def status_mapper
37
+ StatusMapper.new(status)
38
+ end
39
+ end
40
+ end