Skip to content
This repository has been archived by the owner on Apr 17, 2023. It is now read-only.

Commit

Permalink
api: added a validate endpoint
Browse files Browse the repository at this point in the history
For now I've implemented the endpoint for namespaces and registries.
Moreover, this commit also adds an index endpoint for registries.

Signed-off-by: Miquel Sabaté Solà <msabate@suse.com>
  • Loading branch information
mssola committed Oct 9, 2017
1 parent 1372853 commit a9bdab5
Show file tree
Hide file tree
Showing 11 changed files with 222 additions and 65 deletions.
37 changes: 12 additions & 25 deletions app/assets/javascripts/modules/namespaces/components/new-form.js
Expand Up @@ -28,9 +28,12 @@ export default {
team: this.teamName || '',
},
timeout: {
name: null,
validate: null,
team: null,
},
errors: {
name: [],
},
};
},

Expand Down Expand Up @@ -64,41 +67,25 @@ export default {
namespace: {
name: {
required,
format(value) {
// extracted from models/namespace.rb
const regexp = /^[a-z0-9]+(?:[._-][a-z0-9]+)*$/;

// required already taking care of this
if (value === '') {
return true;
}

return regexp.test(value);
},
available(value) {
clearTimeout(this.timeout.name);
validate(value) {
clearTimeout(this.timeout.validate);

// required already taking care of this
if (value === '') {
return true;
}

return new Promise((resolve) => {
const searchName = () => {
const promise = NamespacesService.existsByName(value);

promise.then((exists) => {
// leave it for the back-end
if (exists === null) {
resolve(true);
}
const validate = () => {
const promise = NamespacesService.validate(value);

// if exists, invalid
resolve(!exists);
promise.then((data) => {
set(this.errors, 'name', data.messages.name);
resolve(data.valid);
});
};

this.timeout.name = setTimeout(searchName, 1000);
this.timeout.validate = setTimeout(validate, 1000);
});
},
},
Expand Down
22 changes: 8 additions & 14 deletions app/assets/javascripts/modules/namespaces/services/namespaces.js
Expand Up @@ -8,9 +8,9 @@ const customActions = {
method: 'GET',
url: '/namespaces/typeahead/{teamName}',
},
existsByName: {
method: 'HEAD',
url: '/namespaces',
validate: {
method: 'GET',
url: 'api/v1/namespaces/validate',
},
};

Expand All @@ -36,16 +36,10 @@ function searchTeam(teamName) {
return resource.teamTypeahead({ teamName });
}

function existsByName(name) {
return resource.existsByName({ name })
.then(() => true)
.catch((response) => {
if (response.status === 404) {
return false;
}

return null;
});
function validate(name) {
return resource.validate({ name })
.then(response => response.data)
.catch(() => null);
}

function get(id) {
Expand Down Expand Up @@ -84,5 +78,5 @@ export default {
changeVisibility,
searchTeam,
teamExists,
existsByName,
validate,
};
23 changes: 2 additions & 21 deletions app/controllers/namespaces_controller.rb
Expand Up @@ -7,15 +7,9 @@ class NamespacesController < ApplicationController
after_action :verify_policy_scoped, only: :index

# GET /namespaces
# GET /namespaces.json
def index
# TODO: remove this!
if request.head?
check_namespace_by_name if params[:name]
else
respond_to do |format|
format.html { skip_policy_scope }
end
respond_to do |format|
format.html { skip_policy_scope }
end
end

Expand Down Expand Up @@ -113,19 +107,6 @@ def visibility_param
value
end

# Checks if namespaces exists based on the name parameter.
# Renders an empty response with 200 if exists or 404 otherwise.
def check_namespace_by_name
skip_policy_scope
namespace = Namespace.find_by(name: params[:name])

if namespace
head :ok
else
head :not_found
end
end

def set_namespace
@namespace = Namespace.find(params[:id])
end
Expand Down
7 changes: 3 additions & 4 deletions app/views/namespaces/components/_form.html.slim
Expand Up @@ -9,10 +9,9 @@ div ref="form" class="collapse"
span.help-block
span v-if="!$v.namespace.name.required"
| Name can't be blank
span v-if="!$v.namespace.name.format"
| Name can only contain lower case alphanumeric characters, with optional underscores and dashes in the middle
span v-if="!$v.namespace.name.available"
| Name has already been taken
span.error-messages v-if="errors.name && errors.name.length"
span.error-message v-for="(error, index) in errors.name" :key="index"
| Name {{ error }}

.form-group.has-feedback :class=="{ 'has-error': $v.namespace.team.$error }" v-if="!teamName"
= f.label :team, {class: "control-label col-md-2"}
Expand Down
2 changes: 1 addition & 1 deletion app/views/teams/components/_form.html.slim
Expand Up @@ -3,7 +3,7 @@ div ref="form" class="collapse"
.form-group :class=="{ 'has-error': $v.team.name.$error }"
= f.label :name, {class: 'control-label col-md-2'}
.col-md-7
= f.text_field(:name, class: 'form-control', required: true, placeholder: "New team's name", "@input" => "$v.team.name.$touch()", "v-model.trim" => "team.name")
= f.text_field(:name, class: 'form-control', required: true, placeholder: "New team's name".html_safe, "@input" => "$v.team.name.$touch()", "v-model.trim" => "team.name")
span.help-block
span v-if="!$v.team.name.required"
| Name can't be blank
Expand Down
36 changes: 36 additions & 0 deletions lib/api/entities.rb
Expand Up @@ -12,6 +12,19 @@ class Messages < Grape::Entity
expose :message
end

# Messages for /validate calls.
class Status < Grape::Entity
expose :messages, documentation: {
type: Hash,
desc: "Detailed hash with the fields"
}

expose :valid, documentation: {
type: "Boolean",
desc: "Whether the given resource is valid or not"
}
end

# Users and application tokens

class Users < Grape::Entity
Expand All @@ -33,6 +46,29 @@ class ApplicationTokens < Grape::Entity
expose :plain_token, if: { type: :create }
end

# Registry

class Registries < Grape::Entity
expose :id, unless: { type: :create }, documentation: { type: Integer }
expose :name, documentation: {
type: String,
desc: "The name of the registry"
}
expose :hostname, documentation: {
type: String,
desc: "The hostname of the registry"
}
expose :external_hostname, documentation: {
type: String,
desc: "An external hostname of the registry, useful if behind a proxy with a different FQDN"
}
expose :use_ssl, documentation: {
type: ::Grape::API::Boolean,
desc: "Whether the registry uses SSL or not"
}
expose :created_at, :updated_at, documentation: { type: DateTime }
end

# Repositories and tags

class Tags < Grape::Entity
Expand Down
2 changes: 2 additions & 0 deletions lib/api/root_api.rb
Expand Up @@ -3,6 +3,7 @@
require "api/entities"
require "api/helpers"
require "api/v1/namespaces"
require "api/v1/registries"
require "api/v1/repositories"
require "api/v1/tags"
require "api/v1/teams"
Expand Down Expand Up @@ -44,6 +45,7 @@ class RootAPI < Grape::API
helpers ::API::Helpers

mount ::API::V1::Namespaces
mount ::API::V1::Registries
mount ::API::V1::Repositories
mount ::API::V1::Tags
mount ::API::V1::Teams
Expand Down
20 changes: 20 additions & 0 deletions lib/api/v1/namespaces.rb
Expand Up @@ -27,6 +27,26 @@ class Namespaces < Grape::API
type: current_type
end

desc "Validates the given namespace",
tags: ["namespaces"],
detail: "Validates the given namespace.",
entity: API::Entities::Status,
failure: [
[401, "Authentication fails."],
[403, "Authorization fails."]
]

params do
requires :name, type: String, documentation: { desc: "Name to be checked." }
end

get "/validate" do
namespace = Namespace.new(name: params[:name], registry: Registry.get)
valid = namespace.valid?
obj = { valid: valid, messages: namespace.errors.messages }
present obj, with: API::Entities::Status
end

desc "Create a namespace",
entity: API::Entities::Teams,
failure: [
Expand Down
58 changes: 58 additions & 0 deletions lib/api/v1/registries.rb
@@ -0,0 +1,58 @@
module API
module V1
class Registries < Grape::API
version "v1", using: :path

resource :registries do
before do
authorization!(force_admin: true)
end

desc "Returns a list of registries",
tags: ["registries"],
detail: "This will expose all accessible registries.",
is_array: true,
entity: API::Entities::Registries,
failure: [
[401, "Authentication fails."],
[403, "Authorization fails."]
]

get do
present Registry.all, with: API::Entities::Registries
end

desc "Validates the given registry",
tags: ["registries"],
detail: "Besides containing the usual Status object, it adds a `reachable` " \
"field in the `messages` hash. This field is a string containing the error " \
"as given by the registry. If empty then everything went well",
entity: API::Entities::Status,
failure: [
[401, "Authentication fails."],
[403, "Authorization fails."]
]

params do
requires :name,
using: API::Entities::Registries.documentation.slice(:name)
requires :hostname,
using: API::Entities::Registries.documentation.slice(:hostname)
optional :external_hostname,
using: API::Entities::Registries.documentation.slice(:external_hostname)
requires :use_ssl,
using: API::Entities::Registries.documentation.slice(:use_ssl)
end

get "/validate" do
r = Registry.new(permitted_params)
valid = r.valid?
reachable = r.reachable?
fields = r.errors.messages.merge(reachable: reachable)
obj = { valid: valid && reachable, messages: fields }
present obj, with: API::Entities::Status
end
end
end
end
end
21 changes: 21 additions & 0 deletions spec/api/grape_api/v1/namespaces_spec.rb
Expand Up @@ -222,4 +222,25 @@
expect(namespace_creation_activity.trackable).to eq(namespace)
end
end

context "GET /api/v1/namespaces/validate" do
it "returns the proper response when the namespace exists" do
ns = create(:namespace, visibility: public_visibility, team: team)

get "/api/v1/namespaces/validate", { name: ns.name }, @admin_header
expect(response).to have_http_status(:success)

data = JSON.parse(response.body)
expect(data["valid"]).to be_falsey
expect(data["messages"]["name"]).to eq(["has already been taken"])
end

it "returns the proper response when the namespace does not exist" do
get "/api/v1/namespaces/validate", { name: "somename" }, @admin_header
expect(response).to have_http_status(:success)

data = JSON.parse(response.body)
expect(data["valid"]).to be_truthy
end
end
end

0 comments on commit a9bdab5

Please sign in to comment.