...
 
Commits (3)
......@@ -37,12 +37,28 @@ class Features extends Cli\Controller implements Interfaces\CliControllerInterfa
$manager = Di::_()->get('Features\Manager');
$ttl = $this->getOpt('ttl') ?: 300;
$environmentList = array_filter(explode(',', $this->getOpt('environment') ?: ''));
if (!$environmentList) {
throw new CliException('Specify an environment');
}
while (true /* Forever running task */) {
$this->out([date('c'), "TTL: {$ttl}"], static::OUTPUT_PRE);
foreach ($environmentList as $environment) {
$this->out([
date('c'),
"TTL: {$ttl}",
"Environment: {$environment}"
], static::OUTPUT_PRE);
$sync = $manager
->setEnvironment($environment)
->sync($ttl);
foreach ($manager->sync($ttl) as $key => $output) {
$this->out(sprintf("Sync %s: %s", $key, $output));
foreach ($sync as $key => $output) {
$this->out(sprintf("Sync %s: %s", $key, $output));
}
}
if (!$this->getOpt('forever')) {
......
<?php
/**
* features
*
* @author edgebal
*/
namespace Minds\Controllers\api\v2\admin;
use Exception;
use Minds\Api\Factory;
use Minds\Core\Features\Manager;
use Minds\Core\Session;
use Minds\Entities\User;
use Minds\Interfaces;
use Minds\Core\Di\Di;
class features implements Interfaces\Api, Interfaces\ApiAdminPam
{
/**
* @inheritDoc
*/
public function get($pages)
{
$for = null;
if (isset($_GET['for'])) {
try {
$for = new User(strtolower($_GET['for']));
if (!$for || !$for->guid) {
$for = null;
}
} catch (Exception $e) {
$for = null;
}
}
/** @var Manager $manager */
$manager = Di::_()->get('Features\Manager');
return Factory::response(
$manager->breakdown($for)
);
}
/**
* @inheritDoc
*/
public function post($pages)
{
return Factory::response([]);
}
/**
* @inheritDoc
*/
public function put($pages)
{
return Factory::response([]);
}
/**
* @inheritDoc
*/
public function delete($pages)
{
return Factory::response([]);
}
}
......@@ -12,6 +12,7 @@ use Exception;
use Minds\Core\Di\Di;
use Minds\Core\Features\Exceptions\FeatureNotImplementedException;
use Minds\Core\Sessions\ActiveSession;
use Minds\Entities\User;
/**
* Features Manager
......@@ -28,17 +29,23 @@ class Manager
/** @var string[] */
protected $featureKeys;
/** @var string */
protected $environment;
/**
* Manager constructor.
* @param string $environment
* @param Services\ServiceInterface[] $services
* @param ActiveSession $activeSession
* @param string[] $features
*/
public function __construct(
$environment = null,
$services = null,
$activeSession = null,
array $features = null
) {
$this->environment = $environment;
$this->services = $services ?: [
new Services\Config(),
new Services\Unleash(),
......@@ -48,6 +55,26 @@ class Manager
$this->featureKeys = ($features ?? Di::_()->get('Features\Keys')) ?: [];
}
/**
* Sets the current environment
* @param string $environment
* @return Manager
*/
public function setEnvironment(string $environment): Manager
{
$this->environment = $environment;
return $this;
}
/**
* Gets the current environment based on overrides or environment variables
* @return string
*/
public function getEnvironment()
{
return $this->environment ?: getenv('MINDS_ENV') ?: 'development';
}
/**
* Synchronizes all services using their respective mechanisms
* @param int $ttl
......@@ -57,7 +84,9 @@ class Manager
{
foreach ($this->services as $service) {
try {
$output = $service->sync($ttl) ? 'OK' : 'NOT SYNC\'D';
$output = $service
->setEnvironment($this->getEnvironment())
->sync($ttl);
} catch (Exception $e) {
$output = $e;
}
......@@ -105,6 +134,7 @@ class Manager
$features = array_merge(
$features,
$service
->setEnvironment($this->getEnvironment())
->setUser($this->activeSession->getUser())
->fetch($this->featureKeys)
);
......@@ -114,4 +144,64 @@ class Manager
return $features;
}
/**
* Breakdown for services, features and its individual values for certain user.
* Used by admin interface.
* @param User|null $for
* @return array
*/
public function breakdown(?User $for = null)
{
$env = $this->getEnvironment();
$output = [
'environment' => $env,
'for' => $for ? (string) $for->username : null,
'services' => [
'Default'
],
'features' => [],
];
$cache = [];
foreach ($this->featureKeys as $feature) {
$cache[$feature] = [
'Default' => false,
];
foreach ($this->services as $service) {
$cache[$feature][$service->getReadableName()] = null;
}
}
foreach ($this->services as $service) {
$output['services'][] = $service->getReadableName();
$features = [];
$features = array_merge(
$features,
$service
->setUser($for)
->setEnvironment($env)
->fetch($this->featureKeys)
);
foreach ($features as $feature => $value) {
$cache[$feature][$service->getReadableName()] = $value;
}
}
foreach ($cache as $name => $services) {
$output['features'][] = compact('name', 'services');
}
usort($output['features'], function ($a, $b) {
return $a['name'] <=> $b['name'];
});
return $output;
}
}
......@@ -49,10 +49,10 @@ class Provider extends DiProvider
$this->di->bind('Features\Manager', function ($di) {
return new Manager();
}, [ 'useFactory'=> true ]);
}, [ 'useFactory' => true ]);
$this->di->bind('Features\Canary', function ($di) {
return new Canary();
}, [ 'useFactory'=> true ]);
}, [ 'useFactory' => true ]);
}
}
......@@ -15,9 +15,23 @@ use Minds\Entities\User;
*/
abstract class BaseService implements ServiceInterface
{
/** @var string */
protected $environment;
/** @var User */
protected $user;
/**
* @inheritDoc
* @param string $environment
* @return ServiceInterface
*/
public function setEnvironment(string $environment): ServiceInterface
{
$this->environment = $environment;
return $this;
}
/**
* @inheritDoc
* @param User|null $user
......
......@@ -30,6 +30,14 @@ class Config extends BaseService
$this->config = $config ?: Di::_()->get('Config');
}
/**
* @inheritDoc
*/
public function getReadableName(): string
{
return '$CONFIG';
}
/**
* @inheritDoc
*/
......
......@@ -16,6 +16,14 @@ class Environment extends BaseService
/** @var array|null */
protected $global = null;
/**
* @inheritDoc
*/
public function getReadableName(): string
{
return 'EnvVars';
}
/**
* @param array $global
* @return Environment
......
......@@ -11,6 +11,19 @@ use Minds\Entities\User;
interface ServiceInterface
{
/**
* Readable name. Used for admin interface.
* @return string
*/
public function getReadableName(): string;
/**
* Sets the current interface to sync/fetch
* @param string $environment
* @return ServiceInterface
*/
public function setEnvironment(string $environment): ServiceInterface;
/**
* Sets the current user to calculate context values
* @param User|null $user
......
......@@ -20,7 +20,6 @@ use Minds\UnleashClient\Exceptions\NoContextException;
use Minds\UnleashClient\Factories\FeatureArrayFactory as UnleashFeatureArrayFactory;
use Minds\UnleashClient\Entities\Context;
use Minds\UnleashClient\Unleash as UnleashResolver;
use Minds\UnleashClient\Http\Client as UnleashClient;
/**
* Unleash server (GitLab FF) feature flags service
......@@ -40,8 +39,8 @@ class Unleash extends BaseService
/** @var UnleashFeatureArrayFactory */
protected $unleashFeatureArrayFactory;
/** @var UnleashClient */
protected $unleashClient;
/** @var Unleash\ClientFactory */
protected $unleashClientFactory;
/**
* Unleash constructor.
......@@ -49,20 +48,28 @@ class Unleash extends BaseService
* @param Repository $repository
* @param UnleashResolver $unleashResolver
* @param UnleashFeatureArrayFactory $unleashFeatureArrayFactory
* @param UnleashClient $unleashClient
* @param Unleash\ClientFactory $unleashClientFactory
*/
public function __construct(
$config = null,
$repository = null,
$unleashResolver = null,
$unleashFeatureArrayFactory = null,
$unleashClient = null
$unleashClientFactory = null
) {
$this->config = $config ?: Di::_()->get('Config');
$this->repository = $repository ?: new Repository();
$this->unleashResolver = $unleashResolver ?: new UnleashResolver(Di::_()->get('Logger\Singleton'));
$this->unleashFeatureArrayFactory = $unleashFeatureArrayFactory ?: new UnleashFeatureArrayFactory();
$this->unleashClient = $unleashClient ?: (new Unleash\ClientFactory($this->config, Di::_()->get('Logger\Singleton')))->build();
$this->unleashClientFactory = $unleashClientFactory ?: new Unleash\ClientFactory($this->config, Di::_()->get('Logger\Singleton'));
}
/**
* @inheritDoc
*/
public function getReadableName(): string
{
return 'GitLab';
}
/**
......@@ -71,19 +78,23 @@ class Unleash extends BaseService
*/
public function sync(int $ttl): bool
{
$registered = $this->unleashClient->register();
$client = $this->unleashClientFactory
->build($this->environment);
$registered = $client->register();
if (!$registered) {
throw new Exception('Could not register Unleash client');
}
$now = time();
$features = $this->unleashClient->fetch();
$features = $client->fetch();
foreach ($features as $feature) {
$entity = new Entity();
$entity
->setId($feature['name'])
->setEnvironment($this->environment)
->setFeatureName($feature['name'])
->setData($feature)
->setCreatedAt($now)
->setStaleAt($now + $ttl);
......@@ -104,6 +115,7 @@ class Unleash extends BaseService
* @throws InvalidFeaturesArrayException
* @throws InvalidStrategyImplementationException
* @throws NoContextException
* @throws Exception
*/
public function fetch(array $keys): array
{
......@@ -123,7 +135,9 @@ class Unleash extends BaseService
$features = $this->unleashFeatureArrayFactory
->build(
$this->repository
->getAllData()
->getAllData([
'environment' => $this->environment,
])
->toArray()
);
......
......@@ -36,16 +36,21 @@ class ClientFactory
/**
* Builds an Unleash Client using environment configuration
* @param string $environment
* @return Client
*/
public function build(): Client
public function build(?string $environment): Client
{
$environment = $environment ?: ($configValues['applicationName'] ?? 'development');
$this->logger->info(sprintf("Building Unleash Client for %s", $environment));
$configValues = $this->config->get('unleash');
$config = new Config(
getenv('UNLEASH_API_URL') ?: ($configValues['apiUrl'] ?? null),
getenv('UNLEASH_INSTANCE_ID') ?: ($configValues['instanceId'] ?? null),
getenv('MINDS_ENV') ?: ($configValues['applicationName'] ?? 'development'),
$environment,
$configValues['pollingIntervalSeconds'] ?? null,
$configValues['metricsIntervalSeconds'] ?? null
);
......
......@@ -12,8 +12,10 @@ use Minds\Traits\MagicAttributes;
/**
* Entity for cached feature flags
* @package Minds\Core\Features\Services\Unleash
* @method string getId()
* @method Entity setId(string $id)
* @method string getEnvironment()
* @method Entity setEnvironment(string $environment)
* @method string getFeatureName()
* @method Entity setFeatureName(string $featureName)
* @method array getData()
* @method Entity setData(array $data)
* @method int getCreatedAt()
......@@ -26,7 +28,10 @@ class Entity
use MagicAttributes;
/** @var string */
protected $id;
protected $environment;
/** @var string */
protected $featureName;
/** @var array */
protected $data;
......
......@@ -33,14 +33,27 @@ class Repository
/**
* Returns a list of all feature toggles cached in Cassandra
* @param array $opts
* @return Response
* @throws Exception
*/
public function getList(): Response
public function getList(array $opts = []): Response
{
$cql = "SELECT * FROM feature_toggles_cache";
$opts = array_merge([
'environment' => null,
], $opts);
if (!$opts['environment']) {
throw new Exception('Specify an environment');
}
$cql = "SELECT * FROM feature_toggles_cache_ns WHERE environment = ?";
$values = [
(string) $opts['environment']
];
$prepared = new Custom();
$prepared->query($cql);
$prepared->query($cql, $values);
$response = new Response();
......@@ -50,7 +63,8 @@ class Repository
foreach ($rows ?: [] as $row) {
$entity = new Entity();
$entity
->setId($row['id'])
->setEnvironment($row['environment'])
->setFeatureName($row['feature_name'])
->setData(json_decode($row['data'], true))
->setCreatedAt($row['created_at']->time())
->setStaleAt($row['stale_at']->time());
......@@ -65,13 +79,17 @@ class Repository
/**
* Shortcut method that casts all the data from getList() entities
* @param array $opts getList() opts
* @return Response
* @throws Exception
*/
public function getAllData(): Response
public function getAllData(array $opts = []): Response
{
return $this->getList()->map(function (Entity $entity) {
return $entity->getData();
});
return $this
->getList($opts)
->map(function (Entity $entity) {
return $entity->getData();
});
}
/**
......@@ -82,13 +100,18 @@ class Repository
*/
public function add(Entity $entity): bool
{
if (!$entity->getId()) {
throw new Exception('Invalid Unleash entity name');
if (!$entity->getEnvironment()) {
throw new Exception('Invalid Unleash entity namespace');
}
if (!$entity->getFeatureName()) {
throw new Exception('Invalid Unleash entity feature name');
}
$cql = "INSERT INTO feature_toggles_cache (id, data, created_at, stale_at) VALUES (?, ?, ?, ?)";
$cql = "INSERT INTO feature_toggles_cache_ns (environment, feature_name, data, created_at, stale_at) VALUES (?, ?, ?, ?, ?)";
$values = [
(string) $entity->getId(),
(string) $entity->getEnvironment(),
(string) $entity->getFeatureName(),
(string) json_encode($entity->getData()),
new Timestamp($entity->getCreatedAt()),
new Timestamp($entity->getStaleAt())
......
......@@ -1570,9 +1570,11 @@ CREATE TABLE minds.video_transcodes (
ALTER TABLE minds.video_transcodes ADD failure_reason text;
CREATE TABLE minds.feature_toggles_cache (
id text PRIMARY KEY,
created_at timestamp,
CREATE TABLE minds.feature_toggles_cache_ns (
environment text,
feature_name text,
data text,
stale_at timestamp
created_at timestamp,
stale_at timestamp,
PRIMARY KEY (environment, feature_name)
);
......@@ -31,6 +31,7 @@ class ManagerSpec extends ObjectBehavior
$this->activeSession = $activeSession;
$this->beConstructedWith(
'phpspec',
[ $service1, $service2 ],
$activeSession,
['feature1', 'feature2', 'feature3']
......@@ -44,10 +45,18 @@ class ManagerSpec extends ObjectBehavior
public function it_should_sync()
{
$this->service1->setEnvironment('phpspec')
->shouldBeCalled()
->willReturn($this->service1);
$this->service1->sync(30)
->shouldBeCalled()
->willReturn(true);
$this->service2->setEnvironment('phpspec')
->shouldBeCalled()
->willReturn($this->service2);
$this->service2->sync(30)
->shouldBeCalled()
->willReturn(false);
......@@ -55,8 +64,8 @@ class ManagerSpec extends ObjectBehavior
$this
->sync(30)
->shouldBeAnIterator([
get_class($this->service1->getWrappedObject()) => 'OK',
get_class($this->service2->getWrappedObject()) => 'NOT SYNC\'D',
get_class($this->service1->getWrappedObject()) => true,
get_class($this->service2->getWrappedObject()) => false,
]);
}
......@@ -67,6 +76,10 @@ class ManagerSpec extends ObjectBehavior
->shouldBeCalled()
->willReturn($user);
$this->service1->setEnvironment('phpspec')
->shouldBeCalled()
->willReturn($this->service1);
$this->service1->setUser($user)
->shouldBeCalled()
->willReturn($this->service1);
......@@ -78,6 +91,10 @@ class ManagerSpec extends ObjectBehavior
'feature2' => false,
]);
$this->service2->setEnvironment('phpspec')
->shouldBeCalled()
->willReturn($this->service2);
$this->service2->setUser($user)
->shouldBeCalled()
->willReturn($this->service2);
......@@ -100,6 +117,10 @@ class ManagerSpec extends ObjectBehavior
->shouldBeCalled()
->willReturn($user);
$this->service1->setEnvironment('phpspec')
->shouldBeCalled()
->willReturn($this->service1);
$this->service1->setUser($user)
->shouldBeCalled()
->willReturn($this->service1);
......@@ -111,6 +132,10 @@ class ManagerSpec extends ObjectBehavior
'feature2' => false,
]);
$this->service2->setEnvironment('phpspec')
->shouldBeCalled()
->willReturn($this->service2);
$this->service2->setUser($user)
->shouldBeCalled()
->willReturn($this->service2);
......@@ -134,6 +159,10 @@ class ManagerSpec extends ObjectBehavior
->shouldBeCalled()
->willReturn($user);
$this->service1->setEnvironment('phpspec')
->shouldBeCalled()
->willReturn($this->service1);
$this->service1->setUser($user)
->shouldBeCalled()
->willReturn($this->service1);
......@@ -145,6 +174,10 @@ class ManagerSpec extends ObjectBehavior
'feature2' => false,
]);
$this->service2->setEnvironment('phpspec')
->shouldBeCalled()
->willReturn($this->service2);
$this->service2->setUser($user)
->shouldBeCalled()
->willReturn($this->service2);
......@@ -168,6 +201,10 @@ class ManagerSpec extends ObjectBehavior
->shouldBeCalled()
->willReturn($user);
$this->service1->setEnvironment('phpspec')
->shouldBeCalled()
->willReturn($this->service1);
$this->service1->setUser($user)
->shouldBeCalled()
->willReturn($this->service1);
......@@ -179,6 +216,10 @@ class ManagerSpec extends ObjectBehavior
'feature2' => false,
]);
$this->service2->setEnvironment('phpspec')
->shouldBeCalled()
->willReturn($this->service2);
$this->service2->setUser($user)
->shouldBeCalled()
->willReturn($this->service2);
......
......@@ -28,28 +28,28 @@ class UnleashSpec extends ObjectBehavior
/** @var UnleashFeatureArrayFactory */
protected $unleashFeatureArrayFactory;
/** @var UnleashClient */
protected $unleashClient;
/** @var Unleash\ClientFactory */
protected $unleashClientFactory;
public function let(
Config $config,
Repository $repository,
UnleashResolver $unleashResolver,
UnleashFeatureArrayFactory $unleashFeatureArrayFactory,
UnleashClient $unleashClient
Unleash\ClientFactory $unleashClientFactory
) {
$this->config = $config;
$this->repository = $repository;
$this->unleashResolver = $unleashResolver;
$this->unleashFeatureArrayFactory = $unleashFeatureArrayFactory;
$this->unleashClient = $unleashClient;
$this->unleashClientFactory = $unleashClientFactory;
$this->beConstructedWith(
$config,
$repository,
$unleashResolver,
$unleashFeatureArrayFactory,
$unleashClient
$unleashClientFactory
);
}
......@@ -58,13 +58,18 @@ class UnleashSpec extends ObjectBehavior
$this->shouldHaveType(Unleash::class);
}
public function it_should_sync()
{
$this->unleashClient->register()
public function it_should_sync(
UnleashClient $client
) {
$this->unleashClientFactory->build('phpspec')
->shouldBeCalled()
->willReturn($client);
$client->register()
->shouldBeCalled()
->willReturn(true);
$this->unleashClient->fetch()
$client->fetch()
->shouldBeCalled()
->willReturn([
['name' => 'feature1'],
......@@ -76,6 +81,7 @@ class UnleashSpec extends ObjectBehavior
->willReturn(true);
$this
->setEnvironment('phpspec')
->sync(30)
->shouldReturn(true);
}
......@@ -86,7 +92,9 @@ class UnleashSpec extends ObjectBehavior
$featuresMock = ['featuresMock' . mt_rand()];
$resolvedFeaturesMock = ['resolvedFeaturesMock' . mt_rand()];
$this->repository->getAllData()
$this->repository->getAllData([
'environment' => 'phpspec'
])
->shouldBeCalled()
->willReturn($response);
......@@ -124,6 +132,7 @@ class UnleashSpec extends ObjectBehavior
]);
$this
->setEnvironment('phpspec')
->fetch([
'feature1',
'feature2',
......@@ -149,7 +158,9 @@ class UnleashSpec extends ObjectBehavior
$featuresMock = ['featuresMock' . mt_rand()];
$resolvedFeaturesMock = ['resolvedFeaturesMock' . mt_rand()];
$this->repository->getAllData()
$this->repository->getAllData([
'environment' => 'phpspec'
])
->shouldBeCalled()
->willReturn($response);
......@@ -207,6 +218,7 @@ class UnleashSpec extends ObjectBehavior
]);
$this
->setEnvironment('phpspec')
->setUser($user)
->fetch([
'feature1',
......