Project

General

Profile

Revision 329cfcba

Added by Sebastian Gräßl about 5 years ago

Fixes #18902 - Enable autocomplete for container image search

This enables the autocomplete for the image name search and
improves the usability of searching for container images on
Docker Hub or an external Registry.

View differences:

app/assets/javascripts/foreman_docker/container_image_search.js
1
function ContainerImageSearch() {
2
  this.initialize = function (registryType) {
3
    this.registryType = registryType;
4
    this.form = $('form[data-registry="' + this.registryType + '"]');
5
    this.inputs = {
6
      image: this.getInput('image'),
7
      tag: this.getInput('tag')
8
    };
9
    this.results = this.form.find('.registry-search-results')
10
    this.resultsList = this.results.find('.registry-search-results-list');
11
    this.resultsList.on('click', this.selectImage.bind(this));
12

  
13
    this.searchButton = this.form.find('.image-search-button');
14
    this.searchButton.on('click', function (event) {
15
      event.preventDefault();
16
      this.fullResultList();
17
    }.bind(this))
18

  
19
    this.setupInputs();
20
  };
21

  
22
  this.registryId = function () {
23
    return $('#docker_container_wizard_states_image_registry_id').val();
24
  };
25

  
26
  this.getInput = function (input) {
27
    return this.form.find('input[data-' + input + ']:first');
28
  };
29

  
30
  this.getFormGroup = function (input) {
31
    return input.closest('.form-group');
32
  }
33

  
34
  this.getSpinner = function (input) {
35
    return this.getFormGroup(input).find('.autocomplete-status');
36
  };
37

  
38
  this.getInlineHelp = function (input) {
39
    return this.getFormGroup(input).find('.help-inline');
40
  }
41

  
42
  this.validRequest = function () {
43
    return (this.registryType == 'registry' && this.registryId() != '' ||
44
           this.registryType == 'hub' && this.registryId() == '') &&
45
           this.inputs.image.val() != '';
46
  }
47

  
48
  this.getAutocompleteResults = function (tag, input, callback, params) {
49
    if(!this.validRequest())
50
      return;
51

  
52
    var spinner = this.getSpinner(tag),
53
        imageName = this.inputs.image.val(),
54
        tagsOnly = tag.data('tag'),
55
        params = $.extend({
56
          registry: this.registryType,
57
          search: tagsOnly ? imageName + ':' + input.term : input.term,
58
          registry_id: this.registryId(),
59
          tags: tagsOnly
60
        }, params)
61

  
62
    spinner.removeClass('pficon pficon-error-circle-o pficon-ok')
63
           .addClass('spinner spinner-xs').show();
64

  
65
    $.getJSON(tag.data("url"), params, function (data) {
66
      this.setAutocompleteConfirmationStatus(tag, data)
67
      callback(data);
68
    }.bind(this))
69
    .error(function(result) {
70
      notify('<p>' + result.responseText + '</p>', 'danger');
71
    })
72
  };
73

  
74
  this.fullResultList = function (event) {
75
    if(!this.validRequest())
76
      return;
77

  
78
    var list = this.resultsList,
79
        input = this.inputs.image;
80

  
81
    input.autocomplete('disable')
82
    list.empty();
83

  
84
    $.ajax({
85
      type:'get',
86
      dataType:'html',
87
      url: this.searchButton.data('url'),
88
      data: {
89
        registry: this.registryType,
90
        search: input.val(),
91
        registry_id: this.registryId()
92
      },
93
      success: function (result) {
94
        list.html(result).show();
95
      },
96
      error: function(result) {
97
        notify('<p>' + result.responseText + '</p>', 'danger');
98
    }});
99
  };
100

  
101
  this.selectImage = function (event) {
102
    var link = $(event.target);
103
    if (link.hasClass('repository-name')) {
104
      event.preventDefault();
105
      this.inputs.image
106
        .val(link.text())
107
      this.inputs.tag.val('').focus();
108
    }
109
  };
110

  
111
  this.setAutocompleteConfirmationStatus = function (field, results) {
112
    var spinner = this.getSpinner(field),
113
        inlineHelp = this.getInlineHelp(field),
114
        resultType = field.data('tag') ? 'Tag' : 'Image',
115
        result = results.filter(function (item) {
116
          return item.value == field.val();
117
        }),
118
        available = result.length > 0;
119

  
120
    inlineHelp.find('.autocomplete-confirmation').remove()
121
    spinner.removeClass('spinner spinner-xs pficon-error-circle-o pficon-ok');
122

  
123
    if (field.val() == '')
124
      return;
125

  
126
    if (available) {
127
      spinner.addClass('pficon pficon-ok');
128
    } else {
129
      spinner.addClass('pficon pficon-error-circle-o');
130
    };
131

  
132
    inlineHelp.append(this.confirmationWrapper(resultType, field.val(), available));
133
  };
134

  
135
  this.confirmationWrapper = function(resultType, value, available) {
136
    var wrapper = '<span class="autocomplete-confirmation">&nbsp;' +
137
                  resultType + ' <strong>' + value + '</strong> is '
138

  
139
    if (!available)
140
      wrapper += '<strong>not</strong>';
141

  
142
    return wrapper + ' available.</span>';
143
  };
144

  
145
  this.confirmAutocomplete = function (field, autocomplete) {
146
    this.getAutocompleteResults(field, { term: field.val() }, function (results) {
147
      this.setAutocompleteConfirmationStatus(field, results)
148
    }.bind(this));
149
  };
150

  
151
  this.setupAutoCompleteInput = function (field) {
152
    var options = $.extend({
153
      source: function (input, callback) {
154
        this.getAutocompleteResults(field, input, callback)
155
      }.bind(this),
156
      delay: 500,
157
      minLength: field.data('min-length')
158
    }, options);
159

  
160
    field.autocomplete(options);
161

  
162
    field.on('blur', function () {
163
      this.confirmAutocomplete(field)
164
    }.bind(this))
165
  };
166

  
167
  this.setupInputs = function () {
168
    var image = this.inputs.image,
169
        tag = this.inputs.tag;
170

  
171
    this.setupAutoCompleteInput(tag)
172
    this.setupAutoCompleteInput(image)
173

  
174
    // Trigger search on pressing enter in image search
175
    image.on("keypress", function(e) {
176
      if (e.keyCode == 13) {
177
        e.preventDefault();
178
        this.fullResultList()
179
      }
180
    }.bind(this))
181

  
182
    image.on('focus', function () {
183
      image.autocomplete('enable')
184
    });
185

  
186
    tag.on('focus', function () {
187
      if (tag.val() == '')
188
        tag.autocomplete('search', '');
189
    });
190
  };
191

  
192
  this.initialize.apply(this, arguments);
193
  return this;
194
}
195

  
196
$(document).ready(function() {
197
  var hubSearch = new ContainerImageSearch('hub'),
198
      registrySearch = new ContainerImageSearch('registry');
199

  
200
  $('#hub_tab').click( function() {
201
      $('#docker_container_wizard_states_image_registry_id').val('');
202
  });
203
});
app/assets/javascripts/foreman_docker/image_step.js
1
$(document).ready(function() {
2
  setupAutoComplete("hub");
3
  setupAutoComplete("registry");
4
  $('#hub_tab').click( function() {
5
      $('#docker_container_wizard_states_image_registry_id').val('');
6
  });
7
});
8

  
9
function setupAutoComplete(registryType) {
10
  var tag = getTag(registryType),
11
      repo = getRepo(registryType);
12

  
13
  tag.autocomplete({
14
    source: [],
15
    autoFocus: true,
16
    delay: 500,
17
    minLength: 0
18
  }).focus( function() {
19
    $(this).data("uiAutocomplete").search($(this).val());
20
  });
21

  
22
  repo.autocomplete({
23
    source: function( request, response ) { autoCompleteRepo(repo); },
24
    delay: 500,
25
    minLength: 1
26
  });
27
}
28

  
29
function paramsForSearch(registryType) {
30
  var image = getRepo(registryType),
31
      tag = getTag(registryType),
32
      registryId = $('#docker_container_wizard_states_image_registry_id').val(),
33
      params = {
34
        registry: registryType,
35
        search: image.val()
36
      }
37

  
38
  if (tag.val() != '') {
39
    params.search = image.val() + ':' + tag.val();
40
  };
41

  
42
  if (registryType == 'registry' && registryId != '') {
43
    params.registry_id = registryId;
44
  };
45

  
46
  return params;
47
}
48

  
49
function autoCompleteRepo(item) {
50
  var registryType = $(item).data('registry'),
51
      search_add_on = getImageConfirmation(registryType),
52
      tag = getTag(registryType),
53
      params = paramsForSearch(registryType);
54

  
55
  if (params.search == '' ||
56
      (registryType == 'registry' && typeof params.registry_id == 'undefined')) {
57
    return;
58
  }
59

  
60
  // Patternfly spinner uses 'float: left' and moves it to the left of the
61
  // search button. Instead, we use FontAwesome's spinner to keep it at
62
  // the right.
63
  search_add_on.attr('class', 'fa fa-spin fa-circle-o-notch');
64

  
65
  $.ajax({
66
    type:'get',
67
    url: $(item).attr('data-url'),
68
    data: params,
69
    success:function (result) {
70
       if(result == 'true'){
71
        search_add_on.attr('title', 'Image found in the compute resource');
72
        search_add_on.attr('class', 'pficon pficon-ok');
73
        setWaitingText('Image found: <strong>' + item.val() + '</strong>. Retrieving available tags, please wait...', registryType);
74
        setAutocompleteTags(registryType);
75
      } else {
76
        search_add_on.attr('title', 'Image NOT found in the compute resource');
77
        search_add_on.attr('class', 'pficon pficon-error-circle-o');
78
        tag.autocomplete('option', 'source', []);
79
      }
80
    },
81
    error: function(result) {
82
      $.jnotify(result.responseText, "error", true);
83
    }
84
  });
85
}
86

  
87
function setAutocompleteTags(registryType) {
88
  var registryType = registryType,
89
      tag = getTag(registryType),
90
      source = [];
91

  
92
  tag.addClass('spinner-label');
93
  tag.val('');
94

  
95
  $.getJSON( tag.data("url"),
96
      paramsForSearch(registryType),
97
      function(data) {
98
        getSearchSpinner(registryType).hide();
99
        tag.removeClass('spinner-label');
100
        $.each( data, function(index, value) {
101
          source.push({label: value.label, value: value.value});
102
        });
103
        tag.focus();
104
      })
105
      .error(function(result) {
106
        $.jnotify(result.responseText, "error", true);
107
      });
108
  tag.autocomplete('option', 'source', source);
109
}
110

  
111
function searchRepo(item) {
112
  var registryType = $(item).data('registry'),
113
      results = getRepositorySearchResults(registryType),
114
      search = getRepo(registryType),
115
      searching_spinner = getSearchSpinner(registryType);
116
  setWaitingText('<strong>Searching</strong> in the hub, this can be slow, please wait...', registryType);
117
  results.html('');
118
  results.show();
119
  $.ajax({
120
    type:'get',
121
    dataType:'text',
122
    url: $(item).attr('data-url'),
123
    data: paramsForSearch(registryType),
124
    success: function (result) {
125
      results.html(result);
126
    },
127
    error: function(result) {
128
      $.jnotify(result.responseText, "error", true);
129
    },
130
    complete: function (result) {
131
      searching_spinner.hide();
132
    }
133
  });
134
}
135

  
136
function repoSelected(item) {
137
  var registryType = "hub";
138
  if ($(item).data("hub") !== true) {
139
    registryType = "registry";
140
  }
141

  
142
  getRepositorySearchResults(registryType).hide();
143
  setWaitingText('Image selected: <strong>' + item.text + '</strong>. Retrieving available tags, please wait...', registryType);
144
  getRepo(registryType).val(item.text);
145
  setAutocompleteTags(registryType);
146
}
147

  
148
function setWaitingText(string, registryType) {
149
  getWaitText(registryType).html(string);
150
  getSearchSpinner(registryType).show();
151
}
152

  
153
function getTag(registryType) {
154
  return  $('form[data-registry="' + registryType + '"] input[data-tag]:first');
155
}
156

  
157
function getRepo(registryType) {
158
  return  $('form[data-registry="' + registryType + '"] input[data-search]:first');
159
}
160

  
161
function getSearchSpinner(registryType) {
162
  return  $('form[data-registry="' + registryType + '"] [data-search-spinner]:first');
163
}
164

  
165
function getRepositorySearchResults(registryType) {
166
  return  $('form[data-registry="' + registryType + '"] [data-repository-search-results]:first');
167
}
168

  
169
function getImageConfirmation(registryType) {
170
  return  $('form[data-registry="' + registryType + '"] #image-confirmation');
171
}
172

  
173
function getWaitText(registryType) {
174
  return  $('form[data-registry="' + registryType + '"] [data-wait-text]:first');
175
}
app/assets/stylesheets/foreman_docker/autocomplete.css.scss
1 1
#image-confirmation {
2 2
  margin-left: 10px;
3 3
}
4
.small-gutter {
5
  padding-right: 10px;
6
  padding-left: 10px;
7
}
app/controllers/image_search_controller.rb
1 1
class ImageSearchController < ::ApplicationController
2
  def auto_complete_repository_name
3
    catch_network_errors do
4
      available = image_search_service.available?(params[:search])
5
      render :text => available.to_s
6
    end
7
  end
8

  
9
  def auto_complete_image_tag
10
    catch_network_errors do
11
      tags = image_search_service.search({
12
        term: params[:search],
13
        tags: 'true'
14
      })
15

  
16
      respond_to do |format|
17
        format.js do
18
          render :json => prepare_for_autocomplete(tags)
19
        end
20
      end
21
    end
22
  end
23

  
24 2
  def search_repository
25 3
    catch_network_errors do
26
      repositories = image_search_service.search({
27
        term: params[:search].split(':').first,
28
        tags: 'false'
29
      })
4
      tags_enabled = params[:tags] || 'false'
5
      result = image_search_service.search(term: params[:search], tags: tags_enabled)
30 6

  
31 7
      respond_to do |format|
32
        format.js do
33
          render :partial => 'repository_search_results',
34
                 :locals  => { :repositories => repositories,
35
                               :use_hub => use_hub? }
8
        format.js { render json: prepare_for_autocomplete(result) }
9
        format.html do
10
          render partial: 'repository_search_results', locals: { repositories: result }
36 11
        end
37 12
      end
38 13
    end
......
51 26
           :status => 500
52 27
  end
53 28

  
54
  def use_hub?
55
    @registry.nil?
56
  end
57

  
58 29
  def action_permission
59 30
    case params[:action]
60
    when 'auto_complete_repository_name', 'auto_complete_image_tag', 'search_repository'
31
    when 'search_repository'
61 32
      :search_repository
62 33
    else
63 34
      super
app/models/foreman_docker/docker.rb
42 42
      ::Docker::Image.all({ 'filter' => filter }, docker_connection)
43 43
    end
44 44

  
45
    def tags_for_local_image(image)
46
      image.info['RepoTags'].map do |image_tag|
45
    def tags_for_local_image(image, tag = nil)
46
      result = image.info['RepoTags'].map do |image_tag|
47 47
        _, tag = image_tag.split(':')
48 48
        tag
49 49
      end
50
      result = filter_tags(result, tag) if tag
51
      result
50 52
    end
51 53

  
52 54
    def exist?(name)
......
127 129

  
128 130
    protected
129 131

  
132
    def filter_tags(result, query)
133
      result.select do |tag_name|
134
        tag_name['name'] =~ /^#{query}/
135
      end
136
    end
137

  
130 138
    def docker_command
131 139
      yield
132 140
    rescue Excon::Errors::Error, ::Docker::Error::DockerError => e
app/views/containers/steps/_form_buttons.html.erb
1 1
<div class="clearfix">
2
  <div class="form-actions">
2
  <div class="form-actions col-md-6 col-md-offset-2 small-gutter">
3 3
    <% if step == :preliminary %>
4 4
      <%= link_to _("Cancel"), containers_path, :class => "btn btn-danger" %>
5 5
    <% else %>
6 6
      <a class="btn btn-default" href="<%= previous_wizard_path %>">
7
        <span class="glyphicon glyphicon-chevron-left"></span>
8
        <%= _("Back") %>
7
        <%= _("Previous Step") %>
9 8
      </a>
10 9
    <% end %>
11
    <% next_id = defined?(registry) ? "next_#{registry}" : "next" %>
12
    <%= button_tag(:id => next_id, :type => 'submit', :class => "btn btn-primary pull-right") do %>
10

  
11
    <%= button_tag(:id => 'next', :type => 'submit', :class => "btn btn-primary") do %>
13 12
      <% if last_step? %>
14 13
        <%= _("Submit") %>
15 14
      <% else %>
16
        <%= _("Next") %>
17
        <span class="glyphicon glyphicon-chevron-right"></span>
15
        <%= _("Next Step") %>
18 16
      <% end %>
19 17
    <% end %>
20 18
  </div>
app/views/containers/steps/_image_hub_tab.html.erb
17 17
  <% end %>
18 18

  
19 19
  <% if registry == "registry" -%>
20
    <div class="input-group col-md-6">
21 20
      <%= select_registry f %>
22
    </div>
23 21
  <% end -%>
24 22

  
25 23
  <% help_type = f.object.errors[:repository_name].present? ? :help_block : :help_inline %>
26 24
  <%= text_f(f, :repository_name,
27 25
    :label => _('Search'),
28
    :size => 'col-md-6',
29 26
    :wrapper_class => (image_search_wrapper_class(model) if tab_active?(registry)),
30
    :'data-url'  => auto_complete_repository_name_image_search_path(model.compute_resource_id),
27
    :'data-url'  => search_repository_image_search_path(model.compute_resource_id),
31 28
    :value       => f.object.repository_name.present? ? f.object.repository_name : '',
32 29
    :'data-registry' => registry,
33 30
    :'data-image' => true,
34
    :'data-search' => true,
35 31
    :'data-min-length' => 1,
36 32
    :focus_on_load => true,
37 33
    :placeholder => _('Find your favorite container, e.g. centos'),
38 34
    :control_group_id => "#{registry}_image_search",
39
    help_type => link_to_function(
40
        icon_text('search', ''),
41
        'searchRepo(this)',
42
        :class => 'btn btn-default',
43
        :id => "search_repository_#{registry}",
44
        :'data-registry' => registry,
45
        :'data-url' => search_repository_image_search_path(model.compute_resource_id)
46
      ) + content_tag(:span, '', :id => 'image-confirmation').html_safe) %>
35
    :help_inline => spinner('', class: 'hide autocomplete-status')) %>
47 36

  
48 37
  <%= text_f f, :tag,
49 38
    :control_group_id => "#{registry}_tag_search",
50 39
    :'data-registry' => registry,
51 40
    :'data-tag' => true,
52
    :'data-url' => auto_complete_image_tag_image_search_path(model.compute_resource_id) %>
41
    :'data-min-length' => 0,
42
    :'data-url' => search_repository_image_search_path(model.compute_resource_id),
43
    :help_inline => spinner('', class: 'hide autocomplete-status') %>
44

  
45
  <div class="col-md-6 col-md-offset-2 small-gutter">
46
    <%= link_to(icon_text('search', _('Search for images')), "#",
47
          :id => "search_repository_button_#{registry}",
48
          :class => 'image-search-button btn btn-default',
49
          :'data-registry' => registry,
50
          :'data-url' => search_repository_image_search_path(model.compute_resource_id)) %>
51
  </div>
53 52

  
54
  <div class="col-md-12">
53
  <div class="registry-search-results col-md-12">
55 54
    <div data-search-spinner=true class='col-md-offset-3 hide'>
56 55
      <span data-wait-text=true>
57 56
      </span>
58
      <%= spinner('', :id => "loading_repositories_indicator_#{registry}") %>
57
      <%= spinner('') %>
59 58
    </div>
60
    <div data-repository-search-results=true >
59
    <div class="registry-search-results-list">
61 60
    </div>
62 61
  </div>
63 62
  <%= render :partial => 'form_buttons', locals: { :registry => registry} %>
app/views/containers/steps/image.html.erb
1
<%= javascript 'foreman_docker/image_step' %>
1
<%= javascript 'foreman_docker/container_image_search' %>
2 2
<%= stylesheet 'foreman_docker/autocomplete' %>
3 3

  
4 4
<%= render :layout => 'title', :locals => { :step => 2 } do %>
......
11 11
      <% end -%>
12 12

  
13 13
      <li class='<%= tab_class(:hub) %>'><a href="#hub" data-toggle="tab" id="hub_tab">
14
        <span class="glyphicon glyphicon-cloud-download"></span>
15 14
        <%= _("Docker hub") %>
16 15
      </a></li>
17 16
      <li class='<%= tab_class(:registry) %>'><a href="#registry" data-toggle="tab" id="registry_tab">
18
        <span class="glyphicon glyphicon-cloud-download"></span>
19 17
        <%= _("External registry") %>
20 18
      </a></li>
21 19
    </ul>
app/views/image_search/_repository_search_results.html.erb
1 1
<% repositories.each do |repository| %>
2
    <h3><%= link_to cleanup_image_name(repository['name']), '#', :onclick => 'repoSelected(this)', "data-hub" => use_hub %></h3>
2
    <h3><%= link_to cleanup_image_name(repository['name']), '#', class: 'repository-name' %></h3>
3 3
    <p>
4 4
      <%= '<span class="glyphicon glyphicon-certificate" title="Official"></span>'.html_safe if repository['is_official'] %>
5 5
      <%= '<span class="glyphicon glyphicon-thumbs-up" title="Trusted"></span>'.html_safe    if repository['is_trusted'] %>
lib/foreman_docker/engine.rb
22 22

  
23 23
    foreman_docker_assets = %w[foreman_docker/autocomplete.css
24 24
                               foreman_docker/terminal.css
25
                               foreman_docker/image_step.js
25
                               foreman_docker/container_image_search.js
26 26
                               foreman_docker/create_registry.js]
27 27

  
28 28
    initializer "foreman_docker.assets.precompile" do |app|
test/functionals/image_search_controller_test.rb
18 18
      .stubs(:find).returns(registry)
19 19
  end
20 20

  
21
  describe '#auto_complete_repository_name' do
22
    test 'returns if an image is available' do
23
      exists = ['true', 'false'].sample
24
      search_type = ['hub', 'registry'].sample
25
      subject.instance_variable_set(:@image_search_service, image_search_service)
26
      image_search_service.expects(:available?).returns(exists)
27

  
28
      xhr :get, :auto_complete_repository_name,
29
        { registry: search_type, search: term,
30
          id: compute_resource }, set_session_user
31
      assert_equal exists, response.body
32
    end
33

  
34
    context 'it is a Docker Hub tab request' do
35
      let(:search_type) { 'hub' }
21
  describe '#search_repository' do
22
    let(:search_types) { ['hub', 'registry'] }
36 23

  
37
      test 'it queries the compute_resource and Docker Hub' do
38
        compute_resource.expects(:image).with(term)
39
          .returns(term)
40
        compute_resource.expects(:tags_for_local_image)
41
          .returns(tags)
42
        docker_hub.expects(:tags).returns([])
24
    describe 'calls #search on image_search_service' do
25
      setup do
26
        subject.instance_variable_set(:@image_search_service, image_search_service)
27
      end
43 28

  
44
        xhr :get, :auto_complete_repository_name,
45
          { registry: search_type, search: term,
29
      test 'passes params search and tags' do
30
        tags_enabled = ['true', 'false'].sample
31
        image_search_service.expects(:search).with({ term: term, tags: tags_enabled })
32
          .returns([])
33
        xhr :get, :search_repository,
34
          { registry: search_types.sample, search: term, tags: tags_enabled,
46 35
            id: compute_resource }, set_session_user
47 36
      end
48
    end
49

  
50
    context 'it is a External Registry tab request' do
51
      let(:search_type) { 'registry' }
52 37

  
53
      test 'it only queries the registry api' do
54
        compute_resource.expects(:image).with(term).never
55
        docker_hub.expects(:tags).never
56
        registry.api.expects(:tags).with(term, nil)
57
          .returns(['latest'])
38
      test 'returns an array of { label:, value: } hashes' do
39
        image_search_service.expects(:search).with({ term: term, tags: 'true' })
40
          .returns(tags)
41
        xhr :get, :search_repository,
42
          { registry: search_types.sample, search: term, tags: 'true',
43
            id: compute_resource }, set_session_user
44
        assert_equal tags.first, JSON.parse(response.body).first['value']
45
      end
58 46

  
59
        xhr :get, :auto_complete_repository_name,
60
          { registry: search_type, registry_id: registry,
61
            search: term, id: compute_resource }, set_session_user
47
      test 'returns html with the found images' do
48
        image_search_service.expects(:search)
49
          .with({ term: term, tags: 'false' })
50
          .returns([{ 'name' => term }])
51
        xhr :get, :search_repository,
52
          { registry: search_types.sample, search: term,
53
            id: compute_resource, format: :html}, set_session_user
54
        assert response.body.include?(term)
62 55
      end
63
    end
64
  end
65 56

  
66
  describe '#auto_complete_image_tag' do
67
    let(:tag_fragment) { 'lat' }
68
    let(:term) { "#{docker_image}:#{tag_fragment}"}
69

  
70
    test 'returns an array of { label:, value: } hashes' do
71
      search_type = ['hub', 'registry'].sample
72
      subject.instance_variable_set(:@image_search_service, image_search_service)
73
      image_search_service.expects(:search)
74
        .with({ term: term, tags: 'true' })
75
        .returns(tags)
76
      xhr :get, :auto_complete_image_tag,
77
        { registry: search_type, search: term,
78
          id: compute_resource }, set_session_user
79
      assert_equal tags.first, JSON.parse(response.body).first['value']
80
    end
57
      [Docker::Error::DockerError, Excon::Errors::Error, Errno::ECONNREFUSED].each do |error|
58
        test "search_repository catch exceptions on network errors like #{error}" do
59
          image_search_service.expects(:search)
60
            .raises(error)
61
          xhr :get, :search_repository,
62
            { registry: search_types.sample, search: term, id: compute_resource }, set_session_user
81 63

  
82
    context 'a Docker Hub tab request' do
83
      let(:search_type) { 'hub' }
64
          assert_response :error
65
          assert response.body.include?('An error occured during repository search:')
66
        end
67
      end
84 68

  
85
      test 'it searches Docker Hub and the ComputeResource' do
86
        compute_resource.expects(:image).with(docker_image)
87
          .returns(term)
88
        compute_resource.expects(:tags_for_local_image)
89
          .returns(tags)
90
        docker_hub.expects(:tags).returns([])
69
      test "centos 7 search responses are handled correctly" do
70
        repository = "registry-fancycorp.rhcloud.com/fancydb-rhel7/fancydb"
71
        repo_full_name = "redhat.com: #{repository}"
72
        expected = [{  "description" => "Really fancy database app...",
73
                       "is_official" => true,
74
                       "is_trusted" => true,
75
                       "name" =>  repo_full_name,
76
                       "star_count" => 0
77
                    }]
78
        image_search_service.expects(:search).returns(expected)
79
        xhr :get, :search_repository,
80
          { registry: search_types.sample, search: 'centos', id: compute_resource, format: :html }, set_session_user
81
        assert_response :success
82
        refute response.body.include?(repo_full_name)
83
        assert response.body.include?(repository)
84
      end
91 85

  
92
        xhr :get, :auto_complete_image_tag,
93
          { registry: search_type, search: term,
94
            id: compute_resource }, set_session_user
86
      test "fedora search responses are handled correctly" do
87
        repository = "registry-fancycorp.rhcloud.com/fancydb-rhel7/fancydb"
88
        repo_full_name = repository
89
        request.env["HTTP_ACCEPT"] = "application/javascript"
90
        expected = [{ "description" => "Really fancy database app...",
91
                      "is_official" => true,
92
                      "is_trusted" => true,
93
                      "name" =>  repo_full_name,
94
                      "star_count" => 0
95
                    }]
96
        image_search_service.expects(:search).returns(expected)
97
        xhr :get, :search_repository,
98
          { registry: search_types.sample, search: term, id: compute_resource, format: :html }, set_session_user
99
        assert_response :success
100
        assert response.body.include?(repo_full_name)
101
        assert response.body.include?(repository)
95 102
      end
96 103
    end
97 104

  
98
    context 'it is a External Registry tab request' do
99
      let(:search_type) { 'registry' }
105
    describe 'for image names' do
106
      context 'with a Docker Hub tab request' do
107
        let(:search_type) { 'hub' }
100 108

  
101
      test 'it only queries the registry api' do
102
        compute_resource.expects(:image).with(docker_image).never
103
        docker_hub.expects(:tags).never
104
        registry.api.expects(:tags).with(docker_image, tag_fragment)
105
          .returns([])
109
        test 'it searches Docker Hub and the ComputeResource' do
110
          compute_resource.expects(:local_images)
111
            .returns([OpenStruct.new(info: { 'RepoTags' => [term] })])
112
          docker_hub.expects(:search).returns({})
106 113

  
107
        xhr :get, :auto_complete_image_tag,
108
          { registry: search_type, registry_id: registry,
109
            search: term, id: compute_resource }, set_session_user
114
          xhr :get, :search_repository,
115
            { registry: search_type, search: term,
116
              id: compute_resource }, set_session_user
117
        end
110 118
      end
111
    end
112
  end
113 119

  
114
  describe '#search_repository' do
115
    test 'returns html with the found images' do
116
      search_type = ['hub', 'registry'].sample
117
      subject.instance_variable_set(:@image_search_service, image_search_service)
118
      image_search_service.expects(:search)
119
        .with({ term: term, tags: 'false' })
120
        .returns([{ 'name' => term}])
121
      xhr :get, :search_repository,
122
        { registry: search_type, search: term,
123
          id: compute_resource }, set_session_user
124
      assert response.body.include?(term)
125
    end
120
      context 'with a External Registry tab request' do
121
        let(:search_type) { 'registry' }
126 122

  
127
    context 'a Docker Hub tab request' do
128
      let(:search_type) { 'hub' }
123
        test 'it only queries the registry api' do
124
          compute_resource.expects(:local_images).with(docker_image).never
125
          docker_hub.expects(:search).never
126
          registry.api.expects(:search).with(docker_image)
127
            .returns({})
129 128

  
130
      test 'it searches Docker Hub and the ComputeResource' do
131
        compute_resource.expects(:local_images)
132
          .returns([OpenStruct.new(info: { 'RepoTags' => [term] })])
133
        docker_hub.expects(:search).returns({})
134

  
135
        xhr :get, :search_repository,
136
          { registry: search_type, search: term,
137
            id: compute_resource }, set_session_user
129
          xhr :get, :search_repository,
130
            { registry: search_type, registry_id: registry,
131
              search: term, id: compute_resource }, set_session_user
132
        end
138 133
      end
139 134
    end
140 135

  
141
    context 'it is a External Registry tab request' do
142
      let(:search_type) { 'registry' }
136
    describe 'for tags' do
137
      let(:tag_fragment) { 'lat' }
138
      let(:term) { "#{docker_image}:#{tag_fragment}"}
143 139

  
144
      test 'it only queries the registry api' do
145
        compute_resource.expects(:local_images).with(docker_image).never
146
        docker_hub.expects(:search).never
147
        registry.api.expects(:search).with(docker_image)
148
          .returns({})
140
      context 'with a Docker Hub tab request' do
141
        let(:search_type) { 'hub' }
149 142

  
150
        xhr :get, :search_repository,
151
          { registry: search_type, registry_id: registry,
152
            search: term, id: compute_resource }, set_session_user
143
        test 'it searches Docker Hub and the ComputeResource' do
144
          compute_resource.expects(:image).with(docker_image)
145
            .returns(term)
146
          compute_resource.expects(:tags_for_local_image)
147
            .returns(tags)
148
          docker_hub.expects(:tags).returns([])
149

  
150
          xhr :get, :search_repository,
151
            { registry: search_type, search: term, tags: 'true',
152
              id: compute_resource }, set_session_user
153
        end
153 154
      end
154
    end
155
  end
156 155

  
157
  [Docker::Error::DockerError, Excon::Errors::Error, Errno::ECONNREFUSED].each do |error|
158
    test 'auto_complete_repository_name catches exceptions on network errors' do
159
      ForemanDocker::ImageSearch.any_instance.expects(:available?)
160
        .raises(error)
161
      xhr :get, :auto_complete_repository_name,
162
        { registry: 'hub', search: term, id: compute_resource }, set_session_user
163
      assert_response_is_expected
164
    end
156
      context 'with a External Registry tab request' do
157
        let(:search_type) { 'registry' }
165 158

  
166
    test 'auto_complete_image_tag catch exceptions on network errors' do
167
      ForemanDocker::ImageSearch.any_instance.expects(:search).raises(error)
168
      xhr :get, :auto_complete_image_tag,
169
        { registry: 'hub', search:  term, id: compute_resource }, set_session_user
170
      assert_response_is_expected
171
    end
159
        test 'it only queries the registry api' do
160
          compute_resource.expects(:image).with(docker_image).never
161
          docker_hub.expects(:tags).never
162
          registry.api.expects(:tags).with(docker_image, tag_fragment)
163
            .returns([])
172 164

  
173
    test 'search_repository catch exceptions on network errors' do
174
      ForemanDocker::ImageSearch.any_instance.expects(:search).raises(error)
175
      xhr :get, :search_repository,
176
        { registry: 'hub', search: term, id: compute_resource }, set_session_user
177
      assert_response_is_expected
165
          xhr :get, :search_repository,
166
            { registry: search_type, registry_id: registry, tags: 'true',
167
              search: term, id: compute_resource }, set_session_user
168
        end
169
      end
178 170
    end
179 171
  end
180

  
181
  test "centos 7 search responses are handled correctly" do
182
    repository = "registry-fancycorp.rhcloud.com/fancydb-rhel7/fancydb"
183
    repo_full_name = "redhat.com: #{repository}"
184
    request.env["HTTP_ACCEPT"] = "application/javascript"
185
    expected = [{  "description" => "Really fancy database app...",
186
                   "is_official" => true,
187
                   "is_trusted" => true,
188
                   "name" =>  repo_full_name,
189
                   "star_count" => 0
190
                }]
191
    ForemanDocker::ImageSearch.any_instance.expects(:search).returns(expected).at_least_once
192
    xhr :get, :search_repository,
193
      { registry: 'hub', search: 'centos', id: compute_resource }, set_session_user
194
    assert_response :success
195
    refute response.body.include?(repo_full_name)
196
    assert response.body.include?(repository)
197
  end
198

  
199
  test "fedora search responses are handled correctly" do
200
    repository = "registry-fancycorp.rhcloud.com/fancydb-rhel7/fancydb"
201
    repo_full_name = repository
202
    request.env["HTTP_ACCEPT"] = "application/javascript"
203
    expected = [{ "description" => "Really fancy database app...",
204
                  "is_official" => true,
205
                  "is_trusted" => true,
206
                  "name" =>  repo_full_name,
207
                  "star_count" => 0
208
                }]
209
    ForemanDocker::ImageSearch.any_instance.expects(:search).returns(expected).at_least_once
210
    xhr :get, :search_repository,
211
      { registry: 'hub', search: 'centos', id: compute_resource  }, set_session_user
212
    assert_response :success
213
    assert response.body.include?(repo_full_name)
214
    assert response.body.include?(repository)
215
  end
216

  
217
  def assert_response_is_expected
218
    assert_response :error
219
    assert response.body.include?('An error occured during repository search:')
220
  end
221 172
end
test/integration/container_steps_test.rb
1 1
require 'integration_test_helper'
2 2

  
3
class ContainerStepsTest < ActionDispatch::IntegrationTest
4
  test 'shows a link to a new compute resource if none is available' do
5
    visit new_container_path
6
    assert has_selector?("div.alert", :text => 'Please add a new one')
3
class ContainerStepsTest < IntegrationTestWithJavascript
4
  let(:wizard_state) { DockerContainerWizardState.create! }
5
  let(:compute_resource) { FactoryGirl.create(:docker_cr) }
6
  let(:registry) { FactoryGirl.create(:docker_registry) }
7
  let(:image_search_service) { ForemanDocker::ImageSearch.new }
8
  let(:image_search_results) do
9
    [{ 'name' => 'my_fake_repository_result',
10
       'star_count' => 300,
11
       'description' => 'fake repository' }]
7 12
  end
8 13

  
9
  test 'shows taxonomies tabs' do
10
    visit new_container_path
11
    assert has_selector?("a", :text => 'Locations') if SETTINGS[:locations_enabled]
12
    assert has_selector?("a", :text => 'Organizations') if SETTINGS[:organizations_enabled]
14
  let(:preliminary) do
15
    DockerContainerWizardStates::Preliminary.create!(
16
      :wizard_state => wizard_state,
17
      :compute_resource => compute_resource
18
    )
19
  end
20

  
21
  setup do
22
    stub_image_existance
23
    stub_registry_api
24
    ImageSearchController.any_instance.stubs(:image_search_service)
25
                         .returns(image_search_service)
26
  end
27

  
28
  describe 'on preliminary step' do
29
    test 'it shows docker compute resources' do
30
      compute_resource.save
31
      visit wizard_state_step_path(:wizard_state_id => wizard_state, :id => :preliminary)
32
      assert_text compute_resource.name
33
    end
34

  
35
    context 'when no compute resources are available' do
36
      setup do
37
        ForemanDocker::Docker.destroy_all
38
      end
39

  
40
      test 'shows a link to a new compute resource if none is available' do
41
        visit wizard_state_step_path(:wizard_state_id => wizard_state, :id => :preliminary)
42
        assert has_selector?("div.alert", :text => 'Please add a new one')
43
      end
44
    end
45

  
46
    test 'shows taxonomies tabs' do
47
      visit wizard_state_step_path(:wizard_state_id => wizard_state, :id => :preliminary)
48
      assert has_selector?("a", :text => 'Locations') if SETTINGS[:locations_enabled]
49
      assert has_selector?("a", :text => 'Organizations') if SETTINGS[:organizations_enabled]
50
    end
51
  end
52

  
53
  describe 'on image step' do
54
    ['hub', 'registry'].each do |tab|
55
      describe "on #{tab} tab" do
56
        let(:image_field_id) { "#{tab}_docker_container_wizard_states_image_repository_name" }
57
        let(:tag_field_id) { "#{tab}_docker_container_wizard_states_image_tag" }
58

  
59
        setup do
60
          registry.save
61
          wizard_state.preliminary = preliminary
62
          visit wizard_state_step_path(:wizard_state_id => wizard_state, :id => :image)
63
          find("##{tab}_tab").click
64

  
65
          if tab == 'registry'
66
            find("#docker_container_wizard_states_image_registry_id option:last-child", :visible => false)
67
              .select_option
68
          end
69
        end
70

  
71
        test 'clicking on search loads repositories' do
72
          image_search_service.expects(:search).at_least(2)
73
                              .returns(image_search_results)
74
          fill_in image_field_id, :with => "fake"
75
          wait_for_ajax
76
          find("#search_repository_button_#{tab}").click
77
          wait_for_ajax
78
          assert_text 'my_fake_repository_result'
79
        end
80

  
81
        describe 'autcomplete' do
82
          describe 'for image name' do
83
            test 'entering text triggers autocomplete' do
84
              image_search_service.expects(:search)
85
                                  .returns(image_search_results)
86
              fill_in image_field_id, :with => 'fa'
87
              wait_for_ajax
88
              has_selector?('.ui-autocomplete a', :text => 'my_fake_repository_result')
89
            end
90

  
91
            if tab == 'registry'
92
              context 'when no registry is selected' do
93
                test 'it does not trigger a autocomplete' do
94
                  image_search_service.expects(:search).never
95
                  find("#docker_container_wizard_states_image_registry_id option:first-child", :visible => false).select_option
96
                  fill_in image_field_id, :with => 'fa'
97
                  wait_for_ajax
98
                  has_no_selector?('.ui-autocomplete a', :text => 'my_fake_repository_result')
99
                end
100
              end
101
            end
102
          end
103

  
104
          describe 'for tag' do
105
            let(:image_name) { 'fake' }
106
            let(:tag_snippet) { 'lat' }
107

  
108
            setup do
109
              fill_in image_field_id, :with => 'fake'
110
            end
111

  
112
            test 'it searches for tags' do
113
              image_search_service.expects(:search).at_least(2)
114
                                  .returns(['latest'])
115
              fill_in tag_field_id, :with => tag_snippet
116
              wait_for_ajax
117
              has_selector?('.ui-autocomplete a', :text => 'latest')
118
            end
119
          end
120
        end
121
      end
122
    end
13 123
  end
14
  # test 'clicking on search loads repositories' do
15
  #   Capybara.javascript_driver = :webkit
16
  #   container = FactoryGirl.create(:container)
17
  #   visit container_step_path(:container_id => container.id, :id => :repository)
18
  #   ComputeResource.any_instance.expects(:search).returns([{'name' => 'my_fake_repository_result',
19
  #                                                           'star_count' => 300,
20
  #                                                           'description' => 'fake repository'}])
21
  #   click_button 'search_repository'
22
  #   assert has_link? 'my_fake_repository_result'
23
  # end
24 124
end

Also available in: Unified diff