summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDouwe Maan <douwe@gitlab.com>2016-01-14 12:05:53 +0000
committerDouwe Maan <douwe@gitlab.com>2016-01-14 12:05:53 +0000
commitf981da44ab88012db984e1457170067b345660c1 (patch)
tree8b1663915dee7a4e7ac12ae571ed220d92984816
parent642699b059f51d604f18ee57f01d8b24eb05bfa8 (diff)
parent4435a3a8c380002356bfb3d76acd8609e4c3fb25 (diff)
downloadgitlab-ce-f981da44ab88012db984e1457170067b345660c1.tar.gz
Merge branch 'ci/api-variables' into 'master'
Add variables API References #4264 See merge request !2264
-rw-r--r--CHANGELOG1
-rw-r--r--app/models/ci/variable.rb6
-rw-r--r--doc/api/README.md1
-rw-r--r--doc/api/build_variables.md128
-rw-r--r--lib/api/api.rb1
-rw-r--r--lib/api/entities.rb4
-rw-r--r--lib/api/variables.rb95
-rw-r--r--spec/factories/ci/variables.rb22
-rw-r--r--spec/requests/api/variables_spec.rb182
9 files changed, 439 insertions, 1 deletions
diff --git a/CHANGELOG b/CHANGELOG
index c2e10f62d9b..a0c56837f89 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -45,6 +45,7 @@ v 8.4.0 (unreleased)
- Allow subsequent validations in CI Linter
- Show referenced MRs & Issues only when the current viewer can access them
- Fix Encoding::CompatibilityError bug when markdown content has some complex URL (Jason Lee)
+ - Add API support for managing build variables of project
- Allow broadcast messages to be edited
v 8.3.4
diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb
index 7f6f497f325..e786bd7dd93 100644
--- a/app/models/ci/variable.rb
+++ b/app/models/ci/variable.rb
@@ -18,8 +18,12 @@ module Ci
belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id
- validates_presence_of :key
validates_uniqueness_of :key, scope: :gl_project_id
+ validates :key,
+ presence: true,
+ length: { within: 0..255 },
+ format: { with: /\A[a-zA-Z0-9_]+\z/,
+ message: "can contain only letters, digits and '_'." }
attr_encrypted :value, mode: :per_attribute_iv_and_salt, key: Gitlab::Application.secrets.db_key_base
end
diff --git a/doc/api/README.md b/doc/api/README.md
index 25a31b235cc..c3401bcbc9d 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -23,6 +23,7 @@
- [Namespaces](namespaces.md)
- [Settings](settings.md)
- [Keys](keys.md)
+- [Build Variables](build_variables.md)
## Clients
diff --git a/doc/api/build_variables.md b/doc/api/build_variables.md
new file mode 100644
index 00000000000..b96f1bdac8a
--- /dev/null
+++ b/doc/api/build_variables.md
@@ -0,0 +1,128 @@
+# Build Variables
+
+## List project variables
+
+Get list of a project's build variables.
+
+```
+GET /projects/:id/variables
+```
+
+| Attribute | Type | required | Description |
+|-----------|---------|----------|---------------------|
+| `id` | integer | yes | The ID of a project |
+
+```
+curl -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables"
+```
+
+```json
+[
+ {
+ "key": "TEST_VARIABLE_1",
+ "value": "TEST_1"
+ },
+ {
+ "key": "TEST_VARIABLE_2",
+ "value": "TEST_2"
+ }
+]
+```
+
+## Show variable details
+
+Get the details of a project's specific build variable.
+
+```
+GET /projects/:id/variables/:key
+```
+
+| Attribute | Type | required | Description |
+|-----------|---------|----------|-----------------------|
+| `id` | integer | yes | The ID of a project |
+| `key` | string | yes | The `key` of a variable |
+
+```
+curl -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/TEST_VARIABLE_1"
+```
+
+```json
+{
+ "key": "TEST_VARIABLE_1",
+ "value": "TEST_1"
+}
+```
+
+## Create variable
+
+Create a new build variable.
+
+```
+POST /projects/:id/variables
+```
+
+| Attribute | Type | required | Description |
+|-----------|---------|----------|-----------------------|
+| `id` | integer | yes | The ID of a project |
+| `key` | string | yes | The `key` of a variable; must have no more than 255 characters; only `A-Z`, `a-z`, `0-9`, and `_` are allowed |
+| `value` | string | yes | The `value` of a variable |
+
+```
+curl -X POST -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables" -F "key=NEW_VARIABLE" -F "value=new value"
+```
+
+```json
+{
+ "key": "NEW_VARIABLE",
+ "value": "new value"
+}
+```
+
+## Update variable
+
+Update a project's build variable.
+
+```
+PUT /projects/:id/variables/:key
+```
+
+| Attribute | Type | required | Description |
+|-----------|---------|----------|-------------------------|
+| `id` | integer | yes | The ID of a project |
+| `key` | string | yes | The `key` of a variable |
+| `value` | string | yes | The `value` of a variable |
+
+```
+curl -X PUT -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/NEW_VARIABLE" -F "value=updated value"
+```
+
+```json
+{
+ "key": "NEW_VARIABLE",
+ "value": "updated value"
+}
+```
+
+## Remove variable
+
+Remove a project's build variable.
+
+```
+DELETE /projects/:id/variables/:key
+```
+
+| Attribute | Type | required | Description |
+|-----------|---------|----------|-------------------------|
+| `id` | integer | yes | The ID of a project |
+| `key` | string | yes | The `key` of a variable |
+
+```
+curl -X DELETE -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/VARIABLE_1"
+```
+
+```json
+{
+ "key": "VARIABLE_1",
+ "value": "VALUE_1"
+}
+```
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 7834262d612..098dd975840 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -54,5 +54,6 @@ module API
mount Keys
mount Tags
mount Triggers
+ mount Variables
end
end
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index e3bc3316ce5..e2feb7bb6d9 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -366,5 +366,9 @@ module API
class TriggerRequest < Grape::Entity
expose :id, :variables
end
+
+ class Variable < Grape::Entity
+ expose :key, :value
+ end
end
end
diff --git a/lib/api/variables.rb b/lib/api/variables.rb
new file mode 100644
index 00000000000..d9a055f6c92
--- /dev/null
+++ b/lib/api/variables.rb
@@ -0,0 +1,95 @@
+module API
+ # Projects variables API
+ class Variables < Grape::API
+ before { authenticate! }
+ before { authorize_admin_project }
+
+ resource :projects do
+ # Get project variables
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # page (optional) - The page number for pagination
+ # per_page (optional) - The value of items per page to show
+ # Example Request:
+ # GET /projects/:id/variables
+ get ':id/variables' do
+ variables = user_project.variables
+ present paginate(variables), with: Entities::Variable
+ end
+
+ # Get specific variable of a project
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # key (required) - The `key` of variable
+ # Example Request:
+ # GET /projects/:id/variables/:key
+ get ':id/variables/:key' do
+ key = params[:key]
+ variable = user_project.variables.find_by(key: key.to_s)
+
+ return not_found!('Variable') unless variable
+
+ present variable, with: Entities::Variable
+ end
+
+ # Create a new variable in project
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # key (required) - The key of variable
+ # value (required) - The value of variable
+ # Example Request:
+ # POST /projects/:id/variables
+ post ':id/variables' do
+ required_attributes! [:key, :value]
+
+ variable = user_project.variables.create(key: params[:key], value: params[:value])
+
+ if variable.valid?
+ present variable, with: Entities::Variable
+ else
+ render_validation_error!(variable)
+ end
+ end
+
+ # Update existing variable of a project
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # key (optional) - The `key` of variable
+ # value (optional) - New value for `value` field of variable
+ # Example Request:
+ # PUT /projects/:id/variables/:key
+ put ':id/variables/:key' do
+ variable = user_project.variables.find_by(key: params[:key].to_s)
+
+ return not_found!('Variable') unless variable
+
+ attrs = attributes_for_keys [:value]
+ if variable.update(attrs)
+ present variable, with: Entities::Variable
+ else
+ render_validation_error!(variable)
+ end
+ end
+
+ # Delete existing variable of a project
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # key (required) - The ID of a variable
+ # Example Request:
+ # DELETE /projects/:id/variables/:key
+ delete ':id/variables/:key' do
+ variable = user_project.variables.find_by(key: params[:key].to_s)
+
+ return not_found!('Variable') unless variable
+ variable.destroy
+
+ present variable, with: Entities::Variable
+ end
+ end
+ end
+end
diff --git a/spec/factories/ci/variables.rb b/spec/factories/ci/variables.rb
new file mode 100644
index 00000000000..8f62d64411b
--- /dev/null
+++ b/spec/factories/ci/variables.rb
@@ -0,0 +1,22 @@
+# == Schema Information
+#
+# Table name: ci_variables
+#
+# id :integer not null, primary key
+# project_id :integer not null
+# key :string(255)
+# value :text
+# encrypted_value :text
+# encrypted_value_salt :string(255)
+# encrypted_value_iv :string(255)
+# gl_project_id :integer
+#
+
+# Read about factories at https://github.com/thoughtbot/factory_girl
+
+FactoryGirl.define do
+ factory :ci_variable, class: Ci::Variable do
+ sequence(:key) { |n| "VARIABLE_#{n}" }
+ value 'VARIABLE_VALUE'
+ end
+end
diff --git a/spec/requests/api/variables_spec.rb b/spec/requests/api/variables_spec.rb
new file mode 100644
index 00000000000..9744729ba0c
--- /dev/null
+++ b/spec/requests/api/variables_spec.rb
@@ -0,0 +1,182 @@
+require 'spec_helper'
+
+describe API::API, api: true do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ let(:user2) { create(:user) }
+ let!(:project) { create(:project, creator_id: user.id) }
+ let!(:master) { create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) }
+ let!(:developer) { create(:project_member, user: user2, project: project, access_level: ProjectMember::DEVELOPER) }
+ let!(:variable) { create(:ci_variable, project: project) }
+
+ describe 'GET /projects/:id/variables' do
+ context 'authorized user with proper permissions' do
+ it 'should return project variables' do
+ get api("/projects/#{project.id}/variables", user)
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_a(Array)
+ end
+ end
+
+ context 'authorized user with invalid permissions' do
+ it 'should not return project variables' do
+ get api("/projects/#{project.id}/variables", user2)
+
+ expect(response.status).to eq(403)
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'should not return project variables' do
+ get api("/projects/#{project.id}/variables")
+
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/variables/:key' do
+ context 'authorized user with proper permissions' do
+ it 'should return project variable details' do
+ get api("/projects/#{project.id}/variables/#{variable.key}", user)
+
+ expect(response.status).to eq(200)
+ expect(json_response['value']).to eq(variable.value)
+ end
+
+ it 'should respond with 404 Not Found if requesting non-existing variable' do
+ get api("/projects/#{project.id}/variables/non_existing_variable", user)
+
+ expect(response.status).to eq(404)
+ end
+ end
+
+ context 'authorized user with invalid permissions' do
+ it 'should not return project variable details' do
+ get api("/projects/#{project.id}/variables/#{variable.key}", user2)
+
+ expect(response.status).to eq(403)
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'should not return project variable details' do
+ get api("/projects/#{project.id}/variables/#{variable.key}")
+
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/variables' do
+ context 'authorized user with proper permissions' do
+ it 'should create variable' do
+ expect do
+ post api("/projects/#{project.id}/variables", user), key: 'TEST_VARIABLE_2', value: 'VALUE_2'
+ end.to change{project.variables.count}.by(1)
+
+ expect(response.status).to eq(201)
+ expect(json_response['key']).to eq('TEST_VARIABLE_2')
+ expect(json_response['value']).to eq('VALUE_2')
+ end
+
+ it 'should not allow to duplicate variable key' do
+ expect do
+ post api("/projects/#{project.id}/variables", user), key: variable.key, value: 'VALUE_2'
+ end.to change{project.variables.count}.by(0)
+
+ expect(response.status).to eq(400)
+ end
+ end
+
+ context 'authorized user with invalid permissions' do
+ it 'should not create variable' do
+ post api("/projects/#{project.id}/variables", user2)
+
+ expect(response.status).to eq(403)
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'should not create variable' do
+ post api("/projects/#{project.id}/variables")
+
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+
+ describe 'PUT /projects/:id/variables/:key' do
+ context 'authorized user with proper permissions' do
+ it 'should update variable data' do
+ initial_variable = project.variables.first
+ value_before = initial_variable.value
+
+ put api("/projects/#{project.id}/variables/#{variable.key}", user), value: 'VALUE_1_UP'
+
+ updated_variable = project.variables.first
+
+ expect(response.status).to eq(200)
+ expect(value_before).to eq(variable.value)
+ expect(updated_variable.value).to eq('VALUE_1_UP')
+ end
+
+ it 'should responde with 404 Not Found if requesting non-existing variable' do
+ put api("/projects/#{project.id}/variables/non_existing_variable", user)
+
+ expect(response.status).to eq(404)
+ end
+ end
+
+ context 'authorized user with invalid permissions' do
+ it 'should not update variable' do
+ put api("/projects/#{project.id}/variables/#{variable.key}", user2)
+
+ expect(response.status).to eq(403)
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'should not update variable' do
+ put api("/projects/#{project.id}/variables/#{variable.key}")
+
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+
+ describe 'DELETE /projects/:id/variables/:key' do
+ context 'authorized user with proper permissions' do
+ it 'should delete variable' do
+ expect do
+ delete api("/projects/#{project.id}/variables/#{variable.key}", user)
+ end.to change{project.variables.count}.by(-1)
+ expect(response.status).to eq(200)
+ end
+
+ it 'should responde with 404 Not Found if requesting non-existing variable' do
+ delete api("/projects/#{project.id}/variables/non_existing_variable", user)
+
+ expect(response.status).to eq(404)
+ end
+ end
+
+ context 'authorized user with invalid permissions' do
+ it 'should not delete variable' do
+ delete api("/projects/#{project.id}/variables/#{variable.key}", user2)
+
+ expect(response.status).to eq(403)
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'should not delete variable' do
+ delete api("/projects/#{project.id}/variables/#{variable.key}")
+
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+end