Project

General

Profile

Revision ada932ab

Added by Dmitri Dolguikh over 7 years ago

Fixes #7647: Added support for external registries

View differences:

app/assets/javascripts/foreman_docker/image_step.js
10 10
  });
11 11

  
12 12
  var target = $('#search');
13
  autoCompleteImage(target);
13
  //autoCompleteImage(target);
14 14
  target.autocomplete({
15 15
    source: function( request, response ) { autoCompleteImage(target); },
16 16
    delay: 500,
17 17
    minLength: 1
18 18
  });
19

  
20
  $('#hub_tab').click( function() {
21
      $('#image_registry_id').val('');
22
  });
19 23
});
20 24

  
21 25
function autoCompleteImage(item) {
22 26
  $.ajax({
23 27
    type:'get',
24 28
    url: $(item).attr('data-url'),
25
    data:'search=' + item.val(),
29
    data: { search: item.val(), registry_id: $('#image_registry_id').val() },
30
    //data:'search=' + item.val(),
26 31
    success:function (result) {
27 32
      if(result == 'true'){
28 33
        $('#search-addon').attr('title', 'Image found in the compute resource');
......
47 52
  tag.addClass('tags-autocomplete-loading');
48 53
  tag.val('');
49 54
  var source = [];
50
  $.getJSON( tag.data("url"), { search: $('#search').val() },
55
  $.getJSON( tag.data("url"), { search: $('#search').val(), registry_id: $('#image_registry_id').val() },
51 56
      function(data) {
52 57
        $('#searching_spinner').hide();
53 58
        tag.removeClass('tags-autocomplete-loading');
......
67 72
    type:'get',
68 73
    dataType:'text',
69 74
    url: $(item).attr('data-url'),
70
    data:'search=' + $('#search').val(),
75
    data: { search: $('#search').val(), registry_id: $('#image_registry_id').val() },
71 76
    success: function (result) {
72 77
      $('#image_search_results').html(result);
73 78
    },
app/controllers/containers/steps_controller.rb
16 16
      render_wizard
17 17
    end
18 18

  
19
    # rubocop:disable Metrics/MethodLength
19 20
    def update
20 21
      case step
21 22
      when :preliminary
22 23
        @container.update_attribute(:compute_resource_id, params[:container][:compute_resource_id])
23 24
      when :image
25
        image = update_or_create_image(params[:image][:image_id], params[:image][:registry_id])
24 26
        @container.update_attributes!(
25
            :image => (image = DockerImage.find_or_create_by_image_id!(params[:image])),
27
            :image => image,
26 28
            :tag => DockerTag.find_or_create_by_tag_and_docker_image_id!(params[:container][:tag],
27 29
                                                                         image.id))
28 30
      when :configuration
......
41 43

  
42 44
    private
43 45

  
46
    def update_or_create_image(id, registry_id)
47
      image = DockerImage.find_or_create_by_image_id!(id)
48
      begin
49
        image.registries << DockerRegistry.find(registry_id) \
50
          unless params[:image][:registry_id].empty?
51
      # rubocop:disable Lint/HandleExceptions
52
      rescue ActiveRecord::StatementInvalid
53
        # ignore, someone else already added the image to the registry
54
      end
55
      image
56
    end
57

  
44 58
    def finish_wizard_path
45 59
      container_path(:id => params[:container_id])
46 60
    end
app/controllers/containers_controller.rb
1
# rubocop:disable Metrics/ClassLength
1 2
class ContainersController < ::ApplicationController
2 3
  before_filter :find_container, :only => [:show, :auto_complete_image, :auto_complete_image_tags,
3 4
                                           :search_image, :commit]
5
  before_filter :find_registry, :only => [:auto_complete_image, :auto_complete_image_tags,
6
                                          :search_image]
4 7

  
5 8
  def index
6 9
    @container_resources = allowed_resources.select { |cr| cr.provider == 'Docker' }
......
34 37
  end
35 38

  
36 39
  def auto_complete_image
37
    render :text => @container.compute_resource.exist?(params[:search]).to_s
40
    exist = if @registry.nil?
41
              @container.compute_resource.exist?(params[:search])
42
            else
43
              registry_auto_complete_image(params[:search])
44
            end
45
    render :text => exist.to_s
46
  end
47

  
48
  def registry_auto_complete_image(term)
49
    result = ::Service::RegistryApi.new(:url => @registry.url).search(term)
50
    registry_name = term.split('/').size > 1 ? term :
51
        'library/' + term
52
    result['results'].any? { |r| r['name'] == registry_name }
38 53
  end
39 54

  
40 55
  def auto_complete_image_tags
41 56
    # This is the format jQuery UI autocomplete expects
42
    tags = @container.compute_resource.tags(params[:search])
57
    tags = if @registry.nil?
58
             @container.compute_resource.tags(params[:search])
59
           else
60
             registry_auto_complete_image_tags(params[:search])
61
           end
43 62
    respond_to do |format|
44 63
      format.js do
45 64
        tags.map! { |tag| { :label => CGI.escapeHTML(tag), :value => CGI.escapeHTML(tag) } }
......
48 67
    end
49 68
  end
50 69

  
70
  def registry_auto_complete_image_tags(term)
71
    ::Service::RegistryApi.new(:url => @registry.url).list_repository_tags(term).keys
72
  end
73

  
51 74
  def commit
52 75
    Docker::Container.get(@container.uuid).commit(:author  => params[:commit][:author],
53 76
                                                  :repo    => params[:commit][:repo],
......
63 86
  end
64 87

  
65 88
  def search_image
66
    images = @container.compute_resource.search(params[:search])
89
    images = if @registry.nil?
90
               @container.compute_resource.search(params[:search])
91
             else
92
               r = ::Service::RegistryApi.new(:url => @registry.url).search(params[:search])
93
               r['results']
94
             end
67 95
    respond_to do |format|
68 96
      format.js { render :partial => 'image_search_results', :locals => { :images => images } }
69 97
    end
......
118 146
    @container = Container.authorized("#{action_permission}_#{controller_name}".to_sym)
119 147
                          .find(params[:id])
120 148
  end
149

  
150
  def find_registry
151
    return if params[:registry_id].empty?
152
    @registry = DockerRegistry.authorized("#{action_permission}_#{controller_name}".to_sym)
153
    .find(params[:registry_id])
154
  end
121 155
end
app/controllers/registries_controller.rb
1
class RegistriesController < ::ApplicationController
2
  include Foreman::Controller::AutoCompleteSearch
3
  before_filter :find_registry, :only => [:edit, :update, :destroy]
4

  
5
  def index
6
    @registries = DockerRegistry.search_for(params[:search], :order => params[:order])
7
      .paginate :page => params[:page]
8
  end
9

  
10
  def new
11
    @registry = DockerRegistry.new
12
  end
13

  
14
  def create
15
    @registry = DockerRegistry.new(params[:docker_registry])
16
    if @registry.save
17
      process_success
18
    else
19
      process_error
20
    end
21
  end
22

  
23
  def edit
24
  end
25

  
26
  def update
27
    if @registry.update_attributes(params[:docker_registry])
28
      process_success
29
    else
30
      process_error
31
    end
32
  end
33

  
34
  def destroy
35
    if @registry.destroy
36
      process_success
37
    else
38
      process_error
39
    end
40
  end
41

  
42
  def find_registry
43
    @registry = DockerRegistry.find(params[:id])
44
  rescue ActiveRecord::RecordNotFound
45
    not_found
46
  end
47
end
app/helpers/container_steps_helper.rb
8 8
      _('Environment')
9 9
    )
10 10
  end
11

  
12
  def select_registry(f)
13
    field(f, 'image[registry_id]', :label => _("Registry")) do
14
      collection_select :image, :registry_id,
15
                        DockerRegistry.with_taxonomy_scope_override(@location, @organization)
16
                          .authorized(:view_registries),
17
                        :id, :name,
18
                        { :prompt => _("Select a registry") },
19
                        :class => "form-control", :disabled => f.object.image.present?
20
    end
21
  end
11 22
end
app/models/docker_image.rb
2 2
  has_many :tags, :class_name => 'DockerTag', :foreign_key => 'docker_image_id',
3 3
                  :dependent  => :destroy
4 4
  has_many :containers
5
  has_many :docker_image_docker_registries
6
  has_many :registries, :class_name => 'DockerRegistry', :uniq => true,
7
           :through => :docker_image_docker_registries
5 8

  
6 9
  attr_accessible :image_id, :size
7 10

  
app/models/docker_image_docker_registry.rb
1
class DockerImageDockerRegistry < ActiveRecord::Base
2
  belongs_to :image, :class_name => DockerImage
3
  belongs_to :registry, :class_name => DockerRegistry
4
end
app/models/docker_registry.rb
1
class DockerRegistry < ActiveRecord::Base
2
  include Authorizable
3
  include Taxonomix
4

  
5
  has_many :docker_image_docker_registries
6
  has_many :images, :class_name => 'DockerImage',
7
           :through => :docker_image_docker_registries, :uniq => true
8

  
9
  scoped_search :on => :name, :complete_value => true
10
  scoped_search :on => :url
11

  
12
  def used_location_ids
13
    Location.joins(:taxable_taxonomies).where(
14
        'taxable_taxonomies.taxable_type' => 'DockerRegistry',
15
        'taxable_taxonomies.taxable_id' => id).pluck(:id)
16
  end
17

  
18
  def used_organization_ids
19
    Organization.joins(:taxable_taxonomies).where(
20
        'taxable_taxonomies.taxable_type' => 'DockerRegistry',
21
        'taxable_taxonomies.taxable_id' => id).pluck(:id)
22
  end
23
end
app/models/service/registry_api.rb
1
module Service
2
  class RegistryApi
3
    DEFAULTS = { :url => 'http://localhost:5000' }
4
    attr_reader :config
5

  
6
    def initialize(params = {})
7
      @config = DEFAULTS.merge(params)
8
    end
9

  
10
    def search(aquery)
11
      response = RestClient.get(config[:url] + '/v1/search',
12
                                :params => { :q => aquery }, :accept => :json)
13
      JSON.parse(response.body)
14
    end
15

  
16
    def list_repository_tags(arepository)
17
      response = RestClient.get(config[:url] + "/v1/repositories/#{arepository}/tags",
18
                                :accept => :json)
19
      JSON.parse(response.body)
20
    end
21
  end
22
end
app/views/containers/steps/image.html.erb
1 1
<%= javascript 'foreman_docker/image_step' %>
2 2
<%= stylesheet 'foreman_docker/autocomplete' %>
3
<%= render :layout => 'title', :locals => { :step => 2 } do %>
4
  <ul class="nav nav-tabs" data-tabs="tabs">
5
    <li class="active"><a href="#primary" data-toggle="tab">
6
      <span class="glyphicon glyphicon-cloud-download"></span>
7
      <%= _("Docker hub") %>
8
    </a></li>
9
  </ul>
10 3

  
11
  <%= form_for @container, :class => 'form-horizontal', :url => wizard_path, :method => :put do |f| %>
4
<%= render :layout => 'title', :locals => { :step => 2 } do %>
5
    <ul class="nav nav-tabs" data-tabs="tabs">
6
      <li class="active"><a href="#hub" data-toggle="tab" id="hub_tab">
7
        <span class="glyphicon glyphicon-cloud-download"></span>
8
        <%= _("Docker hub") %>
9
      </a></li>
10
      <li><a href="#registry" data-toggle="tab" id="registry_tab">
11
        <span class="glyphicon glyphicon-cloud-download"></span>
12
        <%= _("External registry") %>
13
      </a></li>
14
    </ul>
12 15

  
13
  <div class="tab-content">
14
    <div class="tab-pane active" id="hub">
15
      <div class="form-group col-md-6">
16
        <%= label_tag "image[search]", _('Search'), :class=>"col-sm-2 control-label" %>
17
        <div class="input-group">
18
          <%= auto_complete_search(:image, '',
19
                                   :'data-url'  => auto_complete_image_container_path(@container),
20
                                   :value       => f.object.image.present? ? f.object.image.image_id : '',
21
                                   :id          => :search,
22
                                   :focus_on_load => true,
23
                                   :placeholder => _('Find your favorite container, e.g: centos')) %>
24
          <span class="input-group-addon glyphicon" id="search-addon"></span>
25
          <span class="input-group-btn">
26
            <%= button_tag(:class      => 'btn btn-default',
27
                           :type       => 'button',
28
                           :id         => 'search_image',
29
                           :'data-url' => search_image_container_path(@container),
30
                           :onclick    => 'searchImage(this)') do %>
31
            <span class="glyphicon glyphicon-search"></span>
32
            <% end %>
33
          </span>
34
        </div>
35
      </div>
36
      <%= text_f f, :tag,
37
                    :value      => f.object.tag.present? ? f.object.tag.tag : '',
38
                    :id         => 'tag',
39
                    :'data-url' => auto_complete_image_tags_container_path(@container) %>
40
      <div class="col-md-12">
41
        <div id='searching_spinner' class='col-md-offset-3 hide'>
42
          <span id='waiting_text'>
43
          </span>
44
          <%= image_tag('/assets/spinner.gif', :id => 'loading_images_indicator') %>
16
    <%= form_for @container, :class => 'form-horizontal', :url => wizard_path, :method => :put do |f| %>
17
        <div class="tab-content">
18
          <div class="tab-pane active" id="hub">
19
          </div>
20
          <div class="tab-pane" id="registry">
21
            <div class="input-group col-md-6">
22
              <%= select_registry f %>
23
            </div>
24
          </div>
25
          <div>
26
            <div class="form-group col-md-6">
27
              <%= label_tag "image[search]", _('Search'), :class=>"col-sm-2 control-label" %>
28
              <div class="input-group">
29
                <%= auto_complete_search('image[image_id]', '',
30
                                         :'data-url'  => auto_complete_image_container_path(@container),
31
                                         :value       => f.object.image.present? ? f.object.image.image_id : '',
32
                                         :id          => :search,
33
                                         :focus_on_load => true,
34
                                         :placeholder => _('Find your favorite container, e.g: centos')) %>
35
                <span class="input-group-addon glyphicon" id="search-addon"></span>
36
                <span class="input-group-btn">
37
                  <%= button_tag(:class      => 'btn btn-default',
38
                                 :type       => 'button',
39
                                 :id         => 'search_image',
40
                                 :'data-url' => search_image_container_path(@container),
41
                                 :onclick    => 'searchImage(this)') do %>
42
                    <span class="glyphicon glyphicon-search"></span>
43
                  <% end %>
44
                </span>
45
              </div>
46
            </div>
47
            <%= text_f f, :tag,
48
                          :value      => f.object.tag.present? ? f.object.tag.tag : '',
49
                          :id         => 'tag',
50
                          :'data-url' => auto_complete_image_tags_container_path(@container) %>
51
            <div class="col-md-12">
52
              <div id='searching_spinner' class='col-md-offset-3 hide'>
53
                <span id='waiting_text'>
54
                </span>
55
                <%= image_tag('/assets/spinner.gif', :id => 'loading_images_indicator') %>
56
              </div>
57
              <div id='image_search_results'>
58
              </div>
59
            </div>
60
          </div>
61
          <%= render :partial => 'form_buttons' %>
45 62
        </div>
46
        <div id='image_search_results'>
47
        </div>
48
      </div>
49
      <hr/>
50
      <%= render :partial => 'form_buttons' %>
51
    </div>
52
  </div>
53

  
54
  <% end %>
63
    <% end %>
55 64
<% end %>
app/views/containers/steps/preliminary.html.erb
12 12
        </div>
13 13
      <% end %>
14 14
    </div>
15

  
16 15
    <%= render :partial => 'form_buttons' %>
17 16
  <% end %>
18 17
<% end %>
app/views/registries/_form.html.erb
1
<%= form_for @registry, :url => (@registry.new_record? ? registries_path : registry_path(:id => @registry.id)) do |f| %>
2
    <%= base_errors_for @registry %>
3
    <ul class="nav nav-tabs" data-tabs="tabs">
4
      <li class="active"><a href="#primary" data-toggle="tab"><%= _("Registry") %></a></li>
5
      <% if show_location_tab? %>
6
          <li><a href="#locations" data-toggle="tab"><%= _("Locations") %></a></li>
7
      <% end %>
8
      <% if show_organization_tab? %>
9
          <li><a href="#organizations" data-toggle="tab"><%= _("Organizations") %></a></li>
10
      <% end %>
11
    </ul>
12

  
13
    <div class="tab-content">
14
      <div class="tab-pane active" id="primary">
15
        <%= text_f   f, :name, :help_inline => _("Registry name") %>
16
        <%= text_f   f, :url, :help_inline => _("Registry url") %>
17
        <%= text_f   f, :description, :help_inline => _("Describing of the registry") %>
18
      </div>
19

  
20
      <%= render 'taxonomies/loc_org_tabs', :f => f, :obj => @registry %>
21
    </div>
22

  
23
    <%= submit_or_cancel f %>
24
<% end %>
app/views/registries/edit.html.erb
1
<% title _("Edit Registry") %>
2

  
3
<%= render :partial => 'form' %>
app/views/registries/index.html.erb
1
<% title _("Registries") %>
2

  
3
<% if authorized? %>
4
    <% title_actions button_group(link_to_if_authorized _("New Registry"), hash_for_new_registry_path,
5
                                                        :class => 'btn-success' ) %>
6
<% end %>
7

  
8
<table class="table table-bordered table-striped table-condensed" data-table="inline">
9
  </thead>
10
  <tr>
11
    <th class="text-center"><%= sort :name, :as => _("Name") %></th>
12
    <th class="hidden-tablet hidden-xs text-center"><%= sort :url, :as => _("Url") %></th>
13
    <th class="hidden-tablet hidden-xs text-center"><%= _("Description") %></th>
14
    <th></th>
15
  </tr>
16
  </thead>
17

  
18
  <% @registries.each do |r| %>
19
      <tr>
20
        <td><%= link_to_if_authorized trunc(r.name), hash_for_edit_registry_path(:id => r).merge(:auth_object => r, :authorizer => authorizer) %></td>
21
        <td class="hidden-tablet hidden-xs text-center"><%= trunc(r.url) %></td>
22
        <td class="hidden-tablet hidden-xs text-center"><%= trunc(r.description) %></td>
23
        <td><%= display_delete_if_authorized hash_for_registry_path(:id => r).merge(:auth_object => r, :authorizer => authorizer), :confirm => _("Delete %s?") % r.name %></td>
24
      </tr>
25
  <% end %>
26
</table>
27

  
28
<%= will_paginate_with_info @registries %>
app/views/registries/new.html.erb
1
<% title _("New Registry") %>
2

  
3
<%= render :partial => 'form' %>
config/routes.rb
8 8
    get :auto_complete_image_tags, :on => :member
9 9
    get :search_image,             :on => :member
10 10
  end
11
  resources :registries, :only => [:index, :new, :create, :update, :destroy, :edit]
11 12
end
db/migrate/20141024163003_create_docker_registries.rb
1
class CreateDockerRegistries < ActiveRecord::Migration
2
  def change
3
    create_table :docker_registries do |t|
4
      t.string :url
5
      t.string :name
6
      t.integer :id
7
      t.string :description
8
      t.timestamps
9
    end
10

  
11
    create_table :docker_image_docker_registries do |t|
12
      t.integer :id
13
      t.integer :docker_registry_id
14
      t.integer :docker_image_id
15
    end
16

  
17
    add_index :docker_image_docker_registries,
18
              [:docker_registry_id, :docker_image_id],
19
              :name => 'by_docker_image_and_registry',
20
              :unique => true
21
  end
22
end
lib/foreman_docker/engine.rb
45 45
          menu :top_menu, :new_container, :caption => N_('New container'),
46 46
                                          :url_hash => { :controller => :containers,
47 47
                                                         :action => :new }
48
          menu :top_menu, :registries, :caption => N_('Registries'),
49
                                       :url_hash => { :controller => :registries,
50
                                                      :action => :index }
48 51
        end
49 52

  
50 53
        security_block :containers do
......
57 60
                                          :containers         => [:new]
58 61
          permission :destroy_containers, :containers         => [:destroy]
59 62
        end
63

  
64
        security_block :registries do
65
          permission :view_registries,    :registries         => [:index, :show]
66
          permission :create_registries,  :registries         => [:new, :create, :update, :edit]
67
          permission :destroy_registries, :registries         => [:destroy]
68
        end
60 69
      end
61 70

  
62 71
    end
test/factories/docker_registry.rb
1
FactoryGirl.define do
2
  factory :docker_registry do
3
    sequence(:name) { |n| "image#{n}" }
4
    sequence(:url) { |n| "http://localhost/#{n}" }
5
  end
6

  
7
  trait :with_location do
8
    locations { [FactoryGirl.build(:location)] }
9
  end
10

  
11
  trait :with_organization do
12
    organizations { [FactoryGirl.build(:organization)] }
13
  end
14
end
test/functionals/containers_steps_controller_test.rb
8 8

  
9 9
    test 'sets a docker image and tag for a new container' do
10 10
      put :update, { :id => :image,
11
                     :image => { :docker_registry_id => '',
12
                                 :image_id => 'centos' },
11 13
                     :container_id => @container.id,
12
                     :image => 'centos',
13 14
                     :container => { :tag => 'latest' } }, set_session_user
14 15
      assert_response :found
15 16
      assert_redirected_to container_step_path(:container_id => @container.id,
test/units/docker_registry_test.rb
1
require 'test_plugin_helper'
2

  
3
class DockerRegistryTest < ActiveSupport::TestCase
4
  test 'used_location_ids should return coorect location ids' do
5
    location = FactoryGirl.build(:location)
6
    r = as_admin do
7
      FactoryGirl.create(:docker_registry, :locations => ([location]))
8
    end
9
    assert r.used_location_ids.include?(location.id)
10
  end
11

  
12
  test 'used_organization_ids should return coorect organization ids' do
13
    organization = FactoryGirl.build(:organization)
14
    r = as_admin do
15
      FactoryGirl.create(:docker_registry, :organizations => ([organization]))
16
    end
17
    assert r.used_organization_ids.include?(organization.id)
18
  end
19
end

Also available in: Unified diff