Skip to content

Commit

Permalink
tec: Add flag system for experimental features
Browse files Browse the repository at this point in the history
  • Loading branch information
marienfressinaud committed Apr 20, 2021
1 parent f2078d8 commit 599b8da
Show file tree
Hide file tree
Showing 11 changed files with 487 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ This guide is intended to people who want to install flusio on their own server.

1. [Deploy in production](/docs/production.md)
1. [How to update flusio](/docs/update.md)
1. [Enable experimental features](/docs/feature_flags.md)
1. [CHANGELOG](/CHANGELOG.md)

You also might be interested by the following:
Expand Down
55 changes: 55 additions & 0 deletions docs/feature_flags.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Enable experimental features

Some features might be available behind a feature flag. This allows to ship
new code and tests it in production without showing unfinished work to users.
You can enable them for specific users.

If you’re unfamiliar with the CLI, you’re encouraged to take a look at [its
documentation](/docs/cli.md).

First of all, you can list the features with the following command:

```console
$ ./cli --request /features
feeds
```

Here, there is only one feature: feeds.

You should now list the users to get there ids:

```console
$ ./cli --request /users
44ff4da402379f91ab0b1cf2a12bf6d4 2021-04-07 [email protected]
a97f04ac01bce558a06fca5023cd3b54 2021-04-07 [email protected]
8dff621fbf93ee18b39ee48fe6ec44d4 2021-04-14 [email protected]
```

Each line corresponds to a user, it shows: id, creation date and email.

If you want to enable the feature `feeds` for the user `[email protected]`, you
must run the following command:

```console
$ ./cli --request /features/enable -ptype=feeds -puser_id=8dff621fbf93ee18b39ee48fe6ec44d4
feeds is enabled for user 8dff621fbf93ee18b39ee48fe6ec44d4 ([email protected])
```

Then, the user should be able to access the `feeds` feature.

You can list the enabled flags:

```console
$ ./cli --request /features/flags
feeds 8dff621fbf93ee18b39ee48fe6ec44d4 [email protected]
```

Each line corresponds to an enabled flag, it shows: flag type, user id, user
email.

You can disable the flags at any moment:

```console
$ ./cli --request /features/disable -ptype=feeds -puser_id=8dff621fbf93ee18b39ee48fe6ec44d4
feeds is disabled for user 8dff621fbf93ee18b39ee48fe6ec44d4 ([email protected])
```
5 changes: 5 additions & 0 deletions src/Routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,11 @@ public static function loadCli($router)
$router->addRoute('cli', '/users/create', 'Users#create');
$router->addRoute('cli', '/users/clean', 'Users#clean');

$router->addRoute('cli', '/features', 'FeatureFlags#index');
$router->addRoute('cli', '/features/flags', 'FeatureFlags#flags');
$router->addRoute('cli', '/features/enable', 'FeatureFlags#enable');
$router->addRoute('cli', '/features/disable', 'FeatureFlags#disable');

$router->addRoute('cli', '/feeds', 'Feeds#index');
$router->addRoute('cli', '/feeds/add', 'Feeds#add');
$router->addRoute('cli', '/feeds/sync', 'Feeds#sync');
Expand Down
116 changes: 116 additions & 0 deletions src/cli/FeatureFlags.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<?php

namespace flusio\cli;

use Minz\Response;
use flusio\models;

/**
* @author Marien Fressinaud <[email protected]>
* @license http://www.gnu.org/licenses/agpl-3.0.en.html AGPL
*/
class FeatureFlags
{
/**
* List all the available feature flags types
*
* @response 200
*/
public function index($request)
{
$types = models\FeatureFlag::VALID_TYPES;

if ($types) {
return Response::text(200, implode("\n", $types));
} else {
return Response::text(200, 'No types are available');
}
}

/**
* List all the users for which feature flags are enabled
*
* @response 200
*/
public function flags($request)
{
$feature_flags = models\FeatureFlag::listAll();
$output = [];
foreach ($feature_flags as $feature_flag) {
$user = $feature_flag->user();
$output[] = "{$feature_flag->type} {$user->id} {$user->email}";
}
sort($output);

if (!$output) {
$output[] = 'No feature flags';
}

return Response::text(200, implode("\n", $output));
}

/**
* Enable a feature flag for a given user
*
* @request_param string type
* @request_param string user_id
*
* @response 400 if type is invalid
* @response 404 if user doesn’t exist
* @response 200
*/
public function enable($request)
{
$type = $request->param('type');
$user_id = $request->param('user_id');

if (!models\FeatureFlag::validateType($type)) {
return Response::text(400, "{$type} is not a valid feature flag type");
}

$user = models\User::find($user_id);
if (!$user) {
return Response::text(404, "User {$user_id} doesn’t exist");
}

models\FeatureFlag::findOrCreateBy([
'type' => $type,
'user_id' => $user->id,
]);

return Response::text(200, "{$type} is enabled for user {$user->id} ({$user->email})");
}

/**
* Disable a feature flag for a given user
*
* @request_param string type
* @request_param string user_id
*
* @response 400 if the flag isn't enabled
* @response 404 if user doesn’t exist
* @response 200
*/
public function disable($request)
{
$type = $request->param('type');
$user_id = $request->param('user_id');

$user = models\User::find($user_id);
if (!$user) {
return Response::text(404, "User {$user_id} doesn’t exist");
}

$feature_flag = models\FeatureFlag::findBy([
'type' => $type,
'user_id' => $user->id,
]);
if (!$feature_flag) {
return Response::text(400, "Feature flag {$type} isn’t enabled for user {$user_id}");
}

models\FeatureFlag::delete($feature_flag->id);

return Response::text(200, "{$type} is disabled for user {$user->id} ({$user->email})");
}
}
9 changes: 9 additions & 0 deletions src/cli/System.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@ public function usage()
$usage .= "\n";
$usage .= " /database/status Return the status of the DB connection\n";
$usage .= "\n";
$usage .= " /features List the available features types\n";
$usage .= " /features/flags List the enabled feature flags\n";
$usage .= " /features/enable Enable a feature flag for a user\n";
$usage .= " -ptype=TEXT where TEXT is the feature flag type\n";
$usage .= " -puser_id=ID where ID is the user’s id\n";
$usage .= " /features/disable Disable a feature flag for a user\n";
$usage .= " -ptype=TEXT where TEXT is the feature flag type\n";
$usage .= " -puser_id=ID where ID is the user’s id\n";
$usage .= "\n";
$usage .= " /feeds List the feeds\n";
$usage .= " /feeds/add Add a feed\n";
$usage .= " -purl=URL where URL is the link to the feed\n";
Expand Down
36 changes: 36 additions & 0 deletions src/migrations/Migration202104190001CreateFeatureFlags.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

namespace flusio\migrations;

class Migration202104190001CreateFeatureFlags
{
public function migrate()
{
$database = \Minz\Database::get();

$database->exec(<<<'SQL'
CREATE TABLE feature_flags (
id SERIAL PRIMARY KEY,
created_at TIMESTAMPTZ NOT NULL,
type TEXT NOT NULL,
user_id TEXT REFERENCES users ON DELETE CASCADE ON UPDATE CASCADE
);
CREATE UNIQUE INDEX idx_feature_flags_type_user_id ON feature_flags(type, user_id);
SQL);

return true;
}

public function rollback()
{
$database = \Minz\Database::get();

$database->exec(<<<'SQL'
DROP INDEX idx_feature_flags_type_user_id;
DROP TABLE feature_flags;
SQL);

return true;
}
}
55 changes: 55 additions & 0 deletions src/models/FeatureFlag.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

namespace flusio\models;

/**
* @author Marien Fressinaud <[email protected]>
* @license http://www.gnu.org/licenses/agpl-3.0.en.html AGPL
*/
class FeatureFlag extends \Minz\Model
{
use DaoConnector;

public const VALID_TYPES = ['feeds'];

public const PROPERTIES = [
'id' => [
'type' => 'integer',
],

'created_at' => [
'type' => 'datetime',
],

'type' => [
'type' => 'string',
'required' => true,
'validator' => '\flusio\models\FeatureFlag::validateType',
],

'user_id' => [
'type' => 'string',
'required' => true,
],
];

/**
* Return the user associated to the feature flag.
*
* @return \flusio\models\User
*/
public function user()
{
return User::find($this->user_id);
}

/**
* @param string $type
*
* @return boolean
*/
public static function validateType($type)
{
return in_array($type, self::VALID_TYPES);
}
}
19 changes: 19 additions & 0 deletions src/models/dao/FeatureFlag.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace flusio\models\dao;

/**
* @author Marien Fressinaud <[email protected]>
* @license http://www.gnu.org/licenses/agpl-3.0.en.html AGPL
*/
class FeatureFlag extends \Minz\DatabaseModel
{
/**
* @throws \Minz\Errors\DatabaseError
*/
public function __construct()
{
$properties = array_keys(\flusio\models\FeatureFlag::PROPERTIES);
parent::__construct('feature_flags', 'id', $properties);
}
}
9 changes: 9 additions & 0 deletions src/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@ CREATE TABLE users (

CREATE INDEX idx_users_email ON users(email);

CREATE TABLE feature_flags (
id SERIAL PRIMARY KEY,
created_at TIMESTAMPTZ NOT NULL,
type TEXT NOT NULL,
user_id TEXT REFERENCES users ON DELETE CASCADE ON UPDATE CASCADE
);

CREATE UNIQUE INDEX idx_feature_flags_type_user_id ON feature_flags(type, user_id);

CREATE TABLE sessions (
id TEXT PRIMARY KEY,
created_at TIMESTAMPTZ NOT NULL,
Expand Down
17 changes: 17 additions & 0 deletions tests/bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -328,3 +328,20 @@
},
]
);

\Minz\Tests\DatabaseFactory::addFactory(
'feature_flag',
'\flusio\models\dao\FeatureFlag',
[
'created_at' => function () use ($faker) {
return $faker->iso8601;
},
'type' => function () use ($faker) {
return $faker->randomElement(\flusio\models\FeatureFlag::VALID_TYPES);
},
'user_id' => function () {
$user_factory = new \Minz\Tests\DatabaseFactory('user');
return $user_factory->create();
},
]
);
Loading

0 comments on commit 599b8da

Please sign in to comment.