Commit 23e06634 authored by Kamil Trzciński's avatar Kamil Trzciński

Add feature flags backend controls

parent 9aba3a90
class Projects::FeatureFlagsController < Projects::ApplicationController
respond_to :html
before_action :authorize_read_feature_flags!
before_action :authorize_update_feature_flags!, only: [:edit, :update]
before_action :authorize_admin_feature_flags!, only: [:destroy]
before_action :feature_flag, only: [:edit, :update, :destroy]
def index
@feature_flags = project.project_feature_flags
@unleash_instanceid = project.project_feature_flags_access_tokens.first&.token || project.project_feature_flags_access_tokens.create!.token
end
def new
@feature_flag = project.project_feature_flags.new
end
def create
@feature_flag = project.project_feature_flags.create(create_params)
if @feature_flag.persisted?
flash[:notice] = 'Feature flag was successfully created.'
redirect_to project_feature_flags_path(@project)
else
render :new
end
end
def edit
end
def update
if feature_flag.update(update_params)
flash[:notice] = 'Feature flag was successfully updated.'
redirect_to project_feature_flags_path(@project)
else
render :edit
end
end
protected
def feature_flag
@feature_flag ||= project.project_feature_flags.find(params[:id])
end
def create_params
params.require(:project_feature_flag)
.permit(:name, :description, :active)
end
def update_params
params.require(:project_feature_flag)
.permit(:description, :active)
end
end
......@@ -284,7 +284,7 @@ module ProjectsHelper
nav_tabs << :pipelines
end
if can?(current_user, :read_environment, project) || can?(current_user, :read_cluster, project)
if can?(current_user, :read_environment, project) || can?(current_user, :read_cluster, project) || can?(current_user, :read_feature_flags, project)
nav_tabs << :operations
end
......@@ -304,6 +304,7 @@ module ProjectsHelper
def tab_ability_map
{
environments: :read_environment,
feature_flags: :read_feature_flags,
milestones: :read_milestone,
snippets: :read_project_snippet,
settings: :admin_project,
......
class ProjectFeatureFlagAccessToken < ActiveRecord::Base
class ProjectFeatureFlagsAccessToken < ActiveRecord::Base
include TokenAuthenticatable
belongs_to :project
......@@ -7,4 +7,6 @@ class ProjectFeatureFlagAccessToken < ActiveRecord::Base
validates :token, presence: true
add_authentication_token_field :token
before_validation :ensure_token!
end
......@@ -15,6 +15,7 @@ class ProjectPolicy < BasePolicy
note
pipeline
pipeline_schedule
feature_flags
build
trigger
environment
......@@ -234,6 +235,7 @@ class ProjectPolicy < BasePolicy
enable :update_container_image
enable :create_environment
enable :create_deployment
enable :read_feature_flags
end
rule { can?(:maintainer_access) }.policy do
......@@ -258,6 +260,9 @@ class ProjectPolicy < BasePolicy
enable :read_cluster
enable :create_cluster
enable :create_environment_terminal
enable :create_feature_flags
enable :update_feature_flags
enable :admin_feature_flags
end
rule { (mirror_available & can?(:admin_project)) | admin }.enable :admin_remote_mirror
......@@ -306,6 +311,7 @@ class ProjectPolicy < BasePolicy
prevent(*create_read_update_admin_destroy(:build))
prevent(*create_read_update_admin_destroy(:pipeline_schedule))
prevent(*create_read_update_admin_destroy(:environment))
prevent(*create_read_update_admin_destroy(:feature_flags))
prevent(*create_read_update_admin_destroy(:cluster))
prevent(*create_read_update_admin_destroy(:deployment))
end
......
......@@ -195,16 +195,16 @@
= _('Charts')
- if project_nav_tab? :operations
= nav_link(controller: [:environments, :clusters, :user, :gcp]) do
= link_to metrics_project_environments_path(@project), class: 'shortcuts-operations' do
= nav_link(controller: [:environments, :clusters, :user, :gcp, :feature_flags]) do
= link_to project_environments_path(@project), class: 'shortcuts-operations' do
.nav-icon-container
= sprite_icon('cloud-gear')
%span.nav-item-name
= _('Operations')
%ul.sidebar-sub-level-items
= nav_link(controller: [:environments, :clusters, :user, :gcp], html_options: { class: "fly-out-top-item" } ) do
= link_to metrics_project_environments_path(@project) do
= nav_link(controller: [:environments, :clusters, :user, :gcp, :feature_flags], html_options: { class: "fly-out-top-item" } ) do
= link_to project_environments_path(@project) do
%strong.fly-out-top-item-name
= _('Operations')
%li.divider.fly-out-top-item
......@@ -249,6 +249,12 @@
%span= _("Got it!")
= sprite_icon('thumb-up')
- if project_nav_tab? :feature_flags
= nav_link(controller: :feature_flags) do
= link_to project_feature_flags_path(@project), title: 'Feature Flags', class: 'shortcuts-feature-flags' do
%span
= _('Feature Flags')
- if project_nav_tab? :container_registry
= nav_link(controller: %w[projects/registry/repositories]) do
= link_to project_container_registry_index_path(@project), class: 'shortcuts-container-registry' do
......
- if @feature_flag.errors.any?
#error_explanation
.alert.alert-danger
- @feature_flag.errors.full_messages.each do |msg|
%p= msg
- unless @feature_flag.persisted?
.form-group.row
= f.label :name, class: 'col-form-label col-sm-2' do
Name
.col-sm-10
= f.text_field :name, required: true, autocomplete: 'off', class: 'form-control', disabled: @feature_flag.persisted?
.form-group.row
= f.label :active, class: 'col-form-label col-sm-2' do
Active
.col-sm-10
= f.check_box :active
.form-group.row
= f.label :description, class: 'col-form-label col-sm-2' do
Description
.col-sm-10
= f.text_area :description, rows: 5, class: 'form-control'
%span.help-inline Write a description of your feature flag
.card
.card-header
Feature flags (#{@feature_flags.count})
%ul.content-list.pages-domain-list
- @feature_flags.each do |feature_flag|
%li.pages-domain-list-item.unstyled
= feature_flag.name
%div.controls.d-none.d-md-block
= link_to 'Edit', edit_project_feature_flag_path(@project, feature_flag), class: "btn btn-sm btn-grouped"
= link_to 'Remove', project_feature_flag_path(@project, feature_flag), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove btn-sm btn-grouped"
%p
- if feature_flag.active?
%span.badge.badge-success Enabled
- else
%span.badge.badge-danger Disabled
\ No newline at end of file
.card.bg-info
.card-header
Configure feature flags
.card-body
%p
Learn how to enable feature flags for your application.
%ol
%li
= _("Install a compatible with client library")
= (_("(checkout the %{link} for information on how to install it).") % { link: "here" }).html_safe
%li
= _("Specify the following URL during for the library configuration setup:")
%code#coordinator_address= "#{root_url(only_path: false)}api/v4/unleash"
%li
= _("Use the following application name:")
%code#registration_token= @project.id
%li
= _("Use the following application name:")
%code#registration_token= @unleash_instanceid
%li
= _("You can also see all features online:")
%code#registration_token= "#{root_url(only_path: false)}api/v4/unleash/features?appname=#{@project.id}&instanceid=#{@unleash_instanceid}"
\ No newline at end of file
- add_to_breadcrumbs "Feature Flags", project_feature_flags_path(@project)
- breadcrumb_title @feature_flag.name
- page_title @feature_flag.name
%h3.page-title
= @feature_flag.name
%hr.clearfix
%div
= form_for [@project, @feature_flag], url: project_feature_flag_path(@project, @feature_flag), html: { class: 'fieldset-form' } do |f|
= render 'form', { f: f }
.form-actions
= f.submit 'Save Changes', class: "btn btn-save"
- page_title 'Feature Flags'
%h3.page-title.with-button
Feature Flags
- if can?(current_user, :create_feature_flags, @project)
= link_to new_project_feature_flag_path(@project), class: 'btn btn-new float-right', title: 'New Feature Flag' do
New Feature Flag
%p.light
With GitLab Feature Flags
%hr.clearfix
= render 'use'
= render 'list'
- add_to_breadcrumbs "Feature Flags", project_feature_flags_path(@project)
- page_title 'New Feature Flags'
%h3.page-title
New Feature Flags
%hr.clearfix
%div
= form_for [@project, @feature_flag], url: project_feature_flags_path(@project), html: { class: 'fieldset-form' } do |f|
= render 'form', { f: f }
.form-actions
= f.submit 'Create New Feature Flag', class: "btn btn-save"
.float-right
= link_to _('Cancel'), project_feature_flags_path(@project), class: 'btn btn-cancel'
......@@ -351,6 +351,8 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
namespace :ci do
resource :lint, only: [:show, :create]
end
resources :feature_flags
end
draw :legacy_builds
......
......@@ -19,7 +19,7 @@ class AddFeatureFlagsToProjects < ActiveRecord::Migration
t.index [:project_id, :name], unique: true
end
create_table :project_feature_flag_access_tokens do |t|
create_table :project_feature_flags_access_tokens do |t|
t.integer :project_id, null: false
t.string :token, null: false
......
......@@ -2090,6 +2090,24 @@ ActiveRecord::Schema.define(version: 20180926140319) do
add_index "project_deploy_tokens", ["project_id", "deploy_token_id"], name: "index_project_deploy_tokens_on_project_id_and_deploy_token_id", unique: true, using: :btree
create_table "project_feature_flags", force: :cascade do |t|
t.integer "project_id", null: false
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
t.string "name", null: false
t.text "description"
t.boolean "active", null: false
end
add_index "project_feature_flags", ["project_id", "name"], name: "index_project_feature_flags_on_project_id_and_name", unique: true, using: :btree
create_table "project_feature_flags_access_tokens", force: :cascade do |t|
t.integer "project_id", null: false
t.string "token", null: false
end
add_index "project_feature_flags_access_tokens", ["project_id", "token"], name: "project_feature_flag_access_token", unique: true, using: :btree
create_table "project_features", force: :cascade do |t|
t.integer "project_id", null: false
t.integer "merge_requests_access_level"
......@@ -3247,6 +3265,8 @@ ActiveRecord::Schema.define(version: 20180926140319) do
add_foreign_key "project_custom_attributes", "projects", on_delete: :cascade
add_foreign_key "project_deploy_tokens", "deploy_tokens", on_delete: :cascade
add_foreign_key "project_deploy_tokens", "projects", on_delete: :cascade
add_foreign_key "project_feature_flags", "projects", on_delete: :cascade
add_foreign_key "project_feature_flags_access_tokens", "projects", on_delete: :cascade
add_foreign_key "project_features", "projects", name: "fk_18513d9b92", on_delete: :cascade
add_foreign_key "project_group_links", "projects", name: "fk_daa8cee94c", on_delete: :cascade
add_foreign_key "project_import_data", "projects", name: "fk_ffb9ee3a10", on_delete: :cascade
......
......@@ -1515,31 +1515,17 @@ module API
end
expose :label, using: Entities::LabelBasic
expose :action
end
class UnleashIssue < Grape::Entity
class UnleashFeature < Grape::Entity
expose :name
expose :title, as: :description
expose :enabled
expose :strategies
private
def name
"\##{object.iid}"
end
def enabled
true
end
def strategies
[{ name: 'default' }]
end
expose :description, unless: ->(feature) { feature.description.nil? }
expose :active, as: :enabled
end
class UnleashFeatureResponse < Grape::Entity
class UnleashFeatures < Grape::Entity
expose :version
expose :features, with: UnleashIssue
expose :project_feature_flags, as: :features, with: UnleashFeature
private
......
......@@ -2,23 +2,39 @@ module API
class Unleash < Grape::API
include PaginationParams
#before { authenticate! }
before do
unauthorized! unless access_token
end
get ':unleash/features' do
present @project, with: Entities::UnleashFeatures
end
post 'unleash/client/register' do
status :ok
end
params do
requires :id, type: String, desc: 'The ID of a project'
post 'unleash/client/metrics' do
status :ok
end
resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
get ':id/unleash/features' do
issues = IssuesFinder.new(current_user, project_id: user_project.id, label_name: 'rollout')
present issues, with: Entities::UnleashFeatureResponse
private
helpers do
def project
@project ||= find_project(unleash_appname)
end
def access_token
@access_token ||= ProjectFeatureFlagsAccessToken.find_by(token: unleash_instanceid, project: project)
end
post ':id/unleash/client/register' do
status :ok
def unleash_appname
params[:appname] || env[:HTTP_UNLEASH_APPNAME]
end
post ':id/unleash/client/metrics' do
status :ok
def unleash_instanceid
params[:instanceid] || env[:HTTP_UNLEASH_INSTANCEID]
end
end
end
......
......@@ -72,12 +72,12 @@ module Gitlab
end
def feature_flag_regex
/\A[a-z]([-a-z0-9]*[a-z0-9])?\z/
/\A[a-z]([-_a-z0-9]*[a-z0-9])?\z/
end
def feature_flag_regex_message
"can contain only lowercase letters, digits, '_' and '-'. " \
"Must start with a letter, and cannot end with '-'"
"Must start with a letter, and cannot end with '-' or '_'"
end
def build_trace_section_regex
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment