Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Application strategies part1 #1186

Draft
wants to merge 40 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
bf5172c
Start of the application strategies. Add, edit and delete strategy.
IrinaSouth Oct 2, 2024
57be98f
Fix creating vs. updating strategy
IrinaSouth Oct 3, 2024
96019c8
Merge branch 'main' into feature/application-strategies-part1
IrinaSouth Oct 3, 2024
35737ba
fix async create
IrinaSouth Oct 3, 2024
8051e1d
return error if strategy already exists
IrinaSouth Oct 3, 2024
dca0e4f
tidy up old code
IrinaSouth Oct 4, 2024
0690f3e
ability to add application strategy from the feature value editing sc…
IrinaSouth Oct 22, 2024
dd6eef1
refactor feature value display on the main dashboard and add shared s…
IrinaSouth Oct 25, 2024
17e0659
make strategy editable, fix page reload, ability to remove strategy, …
IrinaSouth Oct 31, 2024
bb7f428
add backend saving support
rvowles Nov 4, 2024
6ea6161
update strategy functionality
IrinaSouth Nov 5, 2024
72e1cfe
changes to ensure we can test properly
rvowles Nov 5, 2024
f1b2122
updates
IrinaSouth Nov 5, 2024
1bedf9e
server related problems
rvowles Nov 6, 2024
fe342da
update issues
rvowles Nov 7, 2024
f0c6de0
ordering of shared strategies
rvowles Nov 7, 2024
923a484
prevent duplicate strategies being passed and sort out deleting
rvowles Nov 8, 2024
d56dc60
delete check and lock check
rvowles Nov 8, 2024
ce594db
fix behaviour when no apps present
IrinaSouth Nov 9, 2024
45c1eb4
support usage API feature
rvowles Nov 9, 2024
5f93906
updated messaging api to deliver feature diffs
rvowles Nov 9, 2024
4cb4537
add application prefix
rvowles Nov 9, 2024
60d979b
display usage in envs and features
IrinaSouth Nov 16, 2024
2a5bee6
more tests but still borked
rvowles Nov 18, 2024
606f943
add slack handlebars for application strategy
IrinaSouth Nov 19, 2024
c01f7ec
unbust build
rvowles Nov 21, 2024
01ed3d3
deal with app strategies being the _only_ thing that changed
rvowles Nov 24, 2024
7811b9f
enable reordering of app strategies
IrinaSouth Nov 27, 2024
bdda85b
add app strategy permissions
IrinaSouth Nov 29, 2024
568b88c
make table non-paginated
IrinaSouth Dec 3, 2024
87825f0
add new properties to app strategy
IrinaSouth Dec 3, 2024
ce25eaa
dawg, updated list application strategies
rvowles Dec 3, 2024
f5d4b30
update
IrinaSouth Dec 4, 2024
e5fa5f8
added pagination for application strategies
rvowles Dec 6, 2024
a8ebd68
include new permission
IrinaSouth Dec 7, 2024
9696113
lots of changes around publishing when changes happen for a app strategy
rvowles Dec 7, 2024
ca27eac
include new permission part 2
IrinaSouth Dec 7, 2024
5c8afbd
fixes around publishing behaviour + testing
rvowles Dec 9, 2024
c8efc0d
update app strategy role permissions for API
rvowles Dec 9, 2024
8062be8
set max on listing app strategies
IrinaSouth Dec 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 114 additions & 2 deletions adks/e2e-sdk/app/steps/strategies.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { Then } from '@cucumber/cucumber';
import {Given, Then, When} from '@cucumber/cucumber';
import { SdkWorld } from '../support/world';
import DataTable from '@cucumber/cucumber/lib/models/data_table';
import { RolloutStrategy } from '../apis/mr-service';
import {
ApplicationRolloutStrategy,
CreateApplicationRolloutStrategy,
RolloutStrategy,
RolloutStrategyInstance
} from '../apis/mr-service';
import waitForExpect from 'wait-for-expect';
import { expect } from 'chai';
import {makeid} from "../support/random";

Then(/^I (cannot|can) create custom flag rollout strategies$/, async function(can: string, table: DataTable) {
const world = this as SdkWorld;
Expand All @@ -28,6 +34,112 @@ Then(/^I (cannot|can) create custom flag rollout strategies$/, async function(ca
}
});

Given('I create an application strategy tagged {string}', async function(strategyKey: string) {
const world = this as SdkWorld;

expect(world.application, 'You must have an application to create an application strategy').to.not.be.undefined;

const data = await world.applicationStrategyApi.createApplicationStrategy(world.application.id, new CreateApplicationRolloutStrategy({
name: makeid(10), disabled: false, attributes: []
}));
expect(data.status).to.eq(201);
world.applicationStrategies[strategyKey] = data.data;
});

function validateWorldForApplicationStrategies(world: SdkWorld, strategy: ApplicationRolloutStrategy, strategyKey: string) {
expect(strategy, `The strategy referenced by key ${strategyKey} does not exist`).to.not.be.undefined;
expect(world.environment).to.not.be.undefined;
expect(world.feature).to.not.be.undefined;
}

When('the application strategy {string} should be used in {int} environment with {int} feature', async function(key: string, envCount: number, featureCount: number) {
const world = this as SdkWorld;

const strategy = world.applicationStrategies[key];
validateWorldForApplicationStrategies(world, strategy, key);
const appStrategy = await world.applicationStrategyApi.getApplicationStrategy(world.application.id, strategy.id, undefined, true);
expect(appStrategy.status).to.eq(200);
expect(appStrategy.data.usage).to.not.be.undefined;
expect(appStrategy.data.usage.length).to.eq(envCount);
if (envCount > 0) {
expect(appStrategy.data.usage[0].featuresCount).to.eq(featureCount);
}

const listStrat = await world.applicationStrategyApi.listApplicationStrategies(world.application.id, undefined, true);
expect(listStrat.status).to.eq(200);
const s = listStrat.data.items.find(str => str.id == strategy.id);
expect(s.usage.length).to.eq(envCount);
if (envCount > 0) {
expect(s.usage[0].featuresCount).to.eq(featureCount);
}
});



When('I delete the application strategy called {string} from the current environment feature value', async function (strategyKey: string) {
const world = this as SdkWorld;

const strategy = world.applicationStrategies[strategyKey];
validateWorldForApplicationStrategies(world, strategy, strategyKey);

const featureValue = await world.getFeatureValue();
featureValue.rolloutStrategyInstances = featureValue.rolloutStrategyInstances.filter(rsi => rsi.strategyId != strategy.id);
const updatedValue = await world.updateFeature(featureValue);
expect(updatedValue.rolloutStrategyInstances.find(rsi =>
rsi.strategyId === strategy.id)).to.be.undefined;
});

When('I attach application strategy {string} to the current environment feature value', async function (strategyKey: string) {
const world = this as SdkWorld;

const strategy = world.applicationStrategies[strategyKey];
validateWorldForApplicationStrategies(world, strategy, strategyKey);

const featureValue = await world.getFeatureValue();

featureValue.rolloutStrategyInstances.push(new RolloutStrategyInstance({ strategyId: strategy.id,
value: true }));

const updatedValue = await world.updateFeature(featureValue);
expect(updatedValue.rolloutStrategyInstances.find(rsi =>
rsi.strategyId === strategy.id && rsi.value)).to.not.be.undefined;
});

When('I swap the order of {string} and {string} they remain swapped', async function (key1: string, key2: string) {
const world = this as SdkWorld;

const strategy1 = world.applicationStrategies[key1];
validateWorldForApplicationStrategies(world, strategy1, key1);
const strategy2 = world.applicationStrategies[key2];
validateWorldForApplicationStrategies(world, strategy2, key2);

const featureValue = await world.getFeatureValue();
const key1Index = featureValue.rolloutStrategyInstances.findIndex(s => s.strategyId == strategy1.id);
expect(key1Index, `cannot find strategy ${key1} in strategies`).to.not.eq(-1);
const key2Index= featureValue.rolloutStrategyInstances.findIndex(s => s.strategyId == strategy2.id);
expect(key2Index, `cannot find strategy ${key2} in strategies`).to.not.eq(-1);

const rsi = featureValue.rolloutStrategyInstances[key1Index];
featureValue.rolloutStrategyInstances[key1Index] = featureValue.rolloutStrategyInstances[key2Index];
featureValue.rolloutStrategyInstances[key2Index] = rsi;

const updatedValue = await world.updateFeature(featureValue);
expect(updatedValue.rolloutStrategyInstances[key1Index].strategyId, `Strategy 2 did not swap`).to.eq(strategy2.id);
expect(updatedValue.rolloutStrategyInstances[key2Index].strategyId).to.eq(strategy1.id);
});

Then('there is an application strategy called {string} in the current environment feature value', async function (strategyKey: string) {
const world = this as SdkWorld;

const strategy = world.applicationStrategies[strategyKey];
validateWorldForApplicationStrategies(world, strategy, strategyKey);

const featureValue = await world.getFeatureValue();

expect(featureValue.rolloutStrategyInstances.find(rsi =>
rsi.strategyId === strategy.id && rsi.value)).to.not.be.undefined;
});

Then("the edge repository has a feature {string} with a strategy", async function(key: string, table: DataTable) {
const world = this as SdkWorld;

Expand Down
5 changes: 4 additions & 1 deletion adks/e2e-sdk/app/support/world.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
ServiceAccountPermission,
ServiceAccountServiceApi,
TokenizedPerson,
WebhookServiceApi
WebhookServiceApi, ApplicationRolloutStrategyServiceApi, ApplicationRolloutStrategy
} from '../apis/mr-service';
import { axiosLoggingAttachment, logger } from './logging';
import globalAxios, { AxiosInstance, InternalAxiosRequestConfig } from 'axios';
Expand Down Expand Up @@ -57,6 +57,7 @@ export class SdkWorld extends World {
public readonly edgeApi: EdgeService;
public readonly historyApi: FeatureHistoryServiceApi;
public readonly systemConfigApi: SystemConfigServiceApi;
public readonly applicationStrategyApi: ApplicationRolloutStrategyServiceApi;

public readonly webhookApi: WebhookServiceApi;
private _clientContext: ClientContext;
Expand All @@ -66,6 +67,7 @@ export class SdkWorld extends World {
public person: Person
public featureGroup: FeatureGroup;
public serviceAccount?: ServiceAccount;
public applicationStrategies: Record<string,ApplicationRolloutStrategy> = {};

constructor(props: any) {
super(props);
Expand All @@ -92,6 +94,7 @@ export class SdkWorld extends World {
this.webhookApi = new WebhookServiceApi(this.adminApiConfig);
this.historyApi = new FeatureHistoryServiceApi(this.adminApiConfig);
this.systemConfigApi = new SystemConfigServiceApi(this.adminApiConfig);
this.applicationStrategyApi = new ApplicationRolloutStrategyServiceApi(this.adminApiConfig);

const edgeConfig = new EdgeConfig({ basePath: this.featureUrl, axiosInstance: this.adminApiConfig.axiosInstance});
this.edgeApi = new EdgeService(edgeConfig);
Expand Down
21 changes: 21 additions & 0 deletions adks/e2e-sdk/features/app_strategies.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
Feature: We can save, change and retrieve application strategies on feature values

Background:
Given I create a new portfolio
And I create an application
And I create a service account and full permissions based on the application environments

@appstrat
Scenario: I can create a feature value with an application strategy attached
Given I create an application strategy tagged "first"
And There is a new feature flag
And I set the feature flag to on and unlocked
When I attach application strategy "first" to the current environment feature value
And the application strategy "first" should be used in 1 environment with 1 feature
Then I create an application strategy tagged "second"
When I attach application strategy "second" to the current environment feature value
And there is an application strategy called "first" in the current environment feature value
And I swap the order of "first" and "second" they remain swapped
And I delete the application strategy called "second" from the current environment feature value
# And I attach application strategy "first" to the current environment feature value
# And I set the feature flag to off and locked
30 changes: 20 additions & 10 deletions admin-frontend/open_admin_app/lib/api/client_api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -135,14 +135,17 @@ class ManagementRepositoryClientBloc implements Bloc {

_landingActions = [];

for (var action in la) { action(this); }
for (var action in la) {
action(this);
}
}

void swapRoutes(RouteChange route) {
// this is for gross route changes, and causes the widget to redraw
// for multi-tabbed routes, we don't want this to happen, so we separate the two
if (!_routerRedrawRouteSource.hasValue ||
(_routerRedrawRouteSource.hasValue && _routerRedrawRouteSource.value?.route != route.route)) {
(_routerRedrawRouteSource.hasValue &&
_routerRedrawRouteSource.value?.route != route.route)) {
_routerRedrawRouteSource.add(route);
}

Expand Down Expand Up @@ -205,10 +208,18 @@ class ManagementRepositoryClientBloc implements Bloc {
return personState.personCanEditFeaturesForApplication(getCurrentAid());
}

bool get userHasAppStrategyEditRoleInCurrentApplication {
return personState.personCanEditStrategiesForApplication(getCurrentAid());
}

bool get userHasFeatureCreationRoleInCurrentApplication {
return personState.personCanCreateFeaturesForApplication(getCurrentAid());
}

bool get userHasAppStrategyCreationRoleInCurrentApplication {
return personState.personCanCreateStrategiesForApplication(getCurrentAid());
}

bool get userHasFeaturePermissionsInCurrentApplication {
return personState.personCanAnythingFeaturesForApplication(getCurrentAid());
}
Expand Down Expand Up @@ -247,12 +258,13 @@ class ManagementRepositoryClientBloc implements Bloc {
webInterface.setOrigin();

// attach a request id from this client to every outgoing request
(_client.apiClientDelegate as DioClientDelegate).client.interceptors.add(InterceptorsWrapper(
onRequest: (RequestOptions options, RequestInterceptorHandler handler) {
options.headers.putIfAbsent("baggage", () => "x-fh-reqid=${requestIdCounter++}" );
return handler.next(options);
}
));
(_client.apiClientDelegate as DioClientDelegate).client.interceptors.add(
InterceptorsWrapper(onRequest:
(RequestOptions options, RequestInterceptorHandler handler) {
options.headers
.putIfAbsent("baggage", () => "x-fh-reqid=${requestIdCounter++}");
return handler.next(options);
}));

_client.passErrorsAsApiResponses = true;

Expand Down Expand Up @@ -450,7 +462,6 @@ class ManagementRepositoryClientBloc implements Bloc {
// currently empty
void personUpdated(Person person) {}


bool isPortfolioOrSuperAdminForCurrentPid() {
return currentPid == null ? false : isPortfolioOrSuperAdmin(currentPid!);
}
Expand Down Expand Up @@ -559,7 +570,6 @@ class ManagementRepositoryClientBloc implements Bloc {
personState.dispose();
}


String registrationUrl(String token) {
var tokenizedPart = 'register-url?token=$token';
final url =
Expand Down
54 changes: 44 additions & 10 deletions admin-frontend/open_admin_app/lib/common/person_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -40,32 +40,65 @@ class PersonState {
_personSource.add(p);
}

final _featureCreateRoles = [ApplicationRoleType.EDIT, ApplicationRoleType.EDIT_AND_DELETE, ApplicationRoleType.CREATE];
final _featureEditDeleteRoles = [ApplicationRoleType.EDIT, ApplicationRoleType.EDIT_AND_DELETE];
final _featureCreateRoles = [
ApplicationRoleType.FEATURE_EDIT,
ApplicationRoleType.FEATURE_EDIT_AND_DELETE,
ApplicationRoleType.FEATURE_CREATE
];
final _featureEditDeleteRoles = [
ApplicationRoleType.FEATURE_EDIT,
ApplicationRoleType.FEATURE_EDIT_AND_DELETE
];

final _appStrategyCreateRoles = [
ApplicationRoleType.FEATURE_EDIT,
ApplicationRoleType.FEATURE_EDIT_AND_DELETE,
ApplicationRoleType.FEATURE_CREATE
];
final _appStrategyEditDeleteRoles = [
ApplicationRoleType.FEATURE_EDIT,
ApplicationRoleType.FEATURE_EDIT_AND_DELETE
];

bool personCanEditFeaturesForApplication(String? appId) {
return _personHasApplicationRoleInApp(appId, _featureEditDeleteRoles );
return _personHasApplicationRoleInApp(appId, _featureEditDeleteRoles);
}

bool personCanEditStrategiesForApplication(String? appId) {
return _personHasApplicationRoleInApp(appId, _appStrategyEditDeleteRoles);
}

bool personCanCreateFeaturesForApplication(String? appId) {
return _personHasApplicationRoleInApp(appId, _featureCreateRoles );
return _personHasApplicationRoleInApp(appId, _featureCreateRoles);
}

bool personCanCreateStrategiesForApplication(String? appId) {
return _personHasApplicationRoleInApp(appId, _appStrategyCreateRoles);
}

// if we add roles that are NOT feature related, this will need to change to exclude them
bool personCanAnythingFeaturesForApplication(String? appId) {
return _isUserIsSuperAdmin ||
person.groups.any((gp) => gp.applicationRoles.any((ar) => ar.applicationId == appId && ar.roles.isNotEmpty) == true);
person.groups.any((gp) =>
gp.applicationRoles.any(
(ar) => ar.applicationId == appId && ar.roles.isNotEmpty) ==
true);
}

bool _personHasApplicationRoleInApp(String? appId, List<ApplicationRoleType> roles) {
bool _personHasApplicationRoleInApp(
String? appId, List<ApplicationRoleType> roles) {
if (appId == null) {
return _isUserIsSuperAdmin;
}

return _isUserIsSuperAdmin ||
person.groups.any((gp) => gp.applicationRoles.any((ar) => ar.applicationId == appId &&
( gp.admin == true || ar.roles.any((roleForAppInGroup) => roles.contains(roleForAppInGroup)) )
) == true);
person.groups.any((gp) =>
gp.applicationRoles.any((ar) =>
ar.applicationId == appId &&
(gp.admin == true ||
ar.roles.any((roleForAppInGroup) =>
roles.contains(roleForAppInGroup)))) ==
true);
}

bool userHasPortfolioPermission(String? pid) {
Expand All @@ -80,7 +113,8 @@ class PersonState {
if (appId == null) return false;

return person.groups.any((g) =>
g.applicationRoles.any((appRole) => appRole.applicationId == appId) == true);
g.applicationRoles.any((appRole) => appRole.applicationId == appId) ==
true);
}

// if they are admin in a group where there is no portfolio id, they are super-admin
Expand Down
27 changes: 27 additions & 0 deletions admin-frontend/open_admin_app/lib/config/route_handlers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ import 'package:open_admin_app/api/client_api.dart';
import 'package:open_admin_app/api/router.dart';
import 'package:open_admin_app/routes/admin_service_accounts_route.dart';
import 'package:open_admin_app/routes/api_keys_route.dart';
import 'package:open_admin_app/routes/application_strategies_route.dart';
import 'package:open_admin_app/routes/apps_route.dart';
import 'package:open_admin_app/routes/create_admin_service_accounts_route.dart';
import 'package:open_admin_app/routes/create_application_strategy_route.dart';
import 'package:open_admin_app/routes/create_user_route.dart';
import 'package:open_admin_app/routes/edit_admin_service_account_route.dart';
import 'package:open_admin_app/routes/edit_application_strategy_route.dart';
import 'package:open_admin_app/routes/edit_user_route.dart';
import 'package:open_admin_app/routes/feature_groups_route.dart';
import 'package:open_admin_app/routes/features_overview_route.dart';
Expand All @@ -23,6 +26,8 @@ import 'package:open_admin_app/routes/oauth2_fail_route.dart';
import 'package:open_admin_app/routes/register_url_route.dart';
import 'package:open_admin_app/routes/setup_route.dart';
import 'package:open_admin_app/routes/signin_route.dart';
import 'package:open_admin_app/widgets/application-strategies/application_strategy_bloc.dart';
import 'package:open_admin_app/widgets/application-strategies/edit_application_strategy_bloc.dart';
import 'package:open_admin_app/widgets/apps/apps_bloc.dart';
import 'package:open_admin_app/widgets/apps/manage_app_bloc.dart';
import 'package:open_admin_app/widgets/apps/manage_service_accounts_bloc.dart';
Expand Down Expand Up @@ -224,6 +229,28 @@ class RouteCreator {
child: FeatureGroupsRoute(createApp: _actionCreate(params)));
}

Widget applicationStrategies(mrBloc,
{Map<String, List<String?>> params = const {}}) {
return BlocProvider<ApplicationStrategyBloc>(
creator: (context, bag) => ApplicationStrategyBloc(mrBloc),
child: ApplicationStrategyRoute(createApp: _actionCreate(params)));
}

Widget createApplicationStrategy(mrBloc,
{Map<String, List<String?>> params = const {}}) {
return BlocProvider<EditApplicationStrategyBloc>(
creator: (context, bag) => EditApplicationStrategyBloc(mrBloc),
child: const CreateApplicationStrategyRoute());
}

Widget editApplicationStrategy(mrBloc,
{Map<String, List<String?>> params = const {}}) {
return BlocProvider<EditApplicationStrategyBloc>(
creator: (context, bag) => EditApplicationStrategyBloc(mrBloc,
strategyId: params['id']!.elementAt(0)),
child: const EditApplicationStrategyRoute());
}

Widget serviceEnvsHandler(ManagementRepositoryClientBloc mrBloc,
{Map<String, List<String>>? params}) {
return BlocProvider<ServiceAccountEnvBloc>(
Expand Down
Loading
Loading