Skip to content

Commit

Permalink
fix: remove last few handle methods, update docs
Browse files Browse the repository at this point in the history
  • Loading branch information
ssube committed Jan 1, 2019
1 parent 362e8ab commit 915df8a
Show file tree
Hide file tree
Showing 6 changed files with 120 additions and 167 deletions.
73 changes: 25 additions & 48 deletions src/controller/KubernetesController.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import * as k8s from '@kubernetes/client-node';
import { Inject } from 'noicejs';

import { CheckRBAC, HandleNoun, HandleVerb } from 'src/controller';
import { BaseController } from 'src/controller/BaseController';
import { Controller, ControllerData, ControllerOptions } from 'src/controller/Controller';
import { Command, CommandVerb } from 'src/entity/Command';
import { InvalidArgumentError } from 'src/error/InvalidArgumentError';

export const NOUN_POD = 'pod';
export const NOUN_SERVICE = 'service';
export const NOUN_POD = 'k8s-pod';
export const NOUN_SERVICE = 'k8s-service';

export interface KubernetesControllerData extends ControllerData {
context: {
Expand Down Expand Up @@ -37,50 +37,10 @@ export class KubernetesController extends BaseController<KubernetesControllerDat
this.client = config.makeApiClient(k8s.Core_v1Api);
}

public async handle(cmd: Command): Promise<void> {
switch (cmd.noun) {
case NOUN_POD:
return this.handlePods(cmd);
case NOUN_SERVICE:
return this.handleServices(cmd);
default:
throw new InvalidArgumentError(`unknown kind: ${cmd.noun}`);
}
}

protected async loadConfig() {
const config = new k8s.KubeConfig();
if (this.data.context.default) {
config.loadFromDefault();
}
if (this.data.context.cluster) {
config.loadFromCluster();
}
if (this.data.context.path) {
config.loadFromFile(this.data.context.path);
}
return config;
}

protected async handlePods(cmd: Command): Promise<void> {
switch (cmd.verb) {
case CommandVerb.List:
return this.listPods(cmd);
default:
return this.reply(cmd.context, 'invalid verb');
}
}

protected async handleServices(cmd: Command): Promise<void> {
switch (cmd.verb) {
case CommandVerb.List:
return this.listServices(cmd);
default:
return this.reply(cmd.context, 'invalid verb');
}
}

protected async listPods(cmd: Command): Promise<void> {
@HandleNoun(NOUN_POD)
@HandleVerb(CommandVerb.List)
@CheckRBAC()
public async listPods(cmd: Command): Promise<void> {
const ns = cmd.getHeadOrDefault('ns', this.data.default.namespace);
this.logger.debug({ cmd, ns }, 'listing k8s pods');

Expand All @@ -89,12 +49,29 @@ export class KubernetesController extends BaseController<KubernetesControllerDat
return this.transformJSON(cmd, response.body.items);
}

protected async listServices(cmd: Command): Promise<void> {
@HandleNoun(NOUN_SERVICE)
@HandleVerb(CommandVerb.List)
@CheckRBAC()
public async listServices(cmd: Command): Promise<void> {
const ns = cmd.getHeadOrDefault('ns', this.data.default.namespace);
this.logger.debug({ cmd, ns }, 'listing k8s svcs');

const response = await this.client.listNamespacedService(ns);
this.logger.debug({ pods: response.body }, 'found pods');
return this.transformJSON(cmd, response.body.items);
}

protected async loadConfig() {
const config = new k8s.KubeConfig();
if (this.data.context.default) {
config.loadFromDefault();
}
if (this.data.context.cluster) {
config.loadFromCluster();
}
if (this.data.context.path) {
config.loadFromFile(this.data.context.path);
}
return config;
}
}
43 changes: 16 additions & 27 deletions src/controller/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,44 +16,33 @@ See [docs/controller](../../docs/controller) for example config for each control

Most controllers should extend the `BaseController`, which implements noun and filter checks, as well as transforms.

Controllers typically handle a few nouns. To keep the noun/verb logic simple, many controllers use a pair of
`switch`es:
Controllers typically handle a few nouns. To keep the noun/verb logic simple, controllers use a set of decorators:

```typescript
public handle(cmd: Command) {
switch (cmd.noun) {
case NOUN_FOO:
return this.handleFoo(cmd);
default:
return this.reply('unknown noun');
}
}

public handleFoo(cmd: Command) {
switch (cmd.verb) {
case CommandVerb.Get:
return this.getFoo(cmd);
default:
return this.reply('unknown verb');
}
@HandleNoun(NOUN_FOO)
@HandleVerb(CommandVerb.Create)
public async createFoo(cmd: Command) {
const foo = await this.fooRepository.create({ /* ... */ });
return this.transformJSON(foo);
}
```

Be careful of the 20 method limit on classes. A class that handles three nouns would have one noun switch method, three
verb switch methods, and up to fifteen handlers (nineteen methods total).

Permissions should be implemented early in the handler methods and failures must exit early:
Grants can be checked with the RBAC decorator:

```typescript
@CheckRBAC({
defaultGrant: true, /* check the noun:verb grant along with any others */
grants: ['special:grant'], /* other required grants */
user: true, /* require a logged in user */
})
public async getFoo(cmd: Command) {
if (!cmd.context.checkGrants([
'foo:get',
])) {
return this.reply('permission denied');
}
/* ... */
}
```

Errors thrown during a handler method, sync or async, will be caught by the base controller and the message used as a
reply.

## Nouns

| Noun | Controller |
Expand Down
8 changes: 6 additions & 2 deletions src/controller/RandomController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import { isNil, isNumber } from 'lodash';
import { max, min, random, randomInt } from 'mathjs';
import { Inject } from 'noicejs';

import { CheckRBAC, HandleNoun, HandleVerb } from 'src/controller';
import { BaseController } from 'src/controller/BaseController';
import { Controller, ControllerData, ControllerOptions } from 'src/controller/Controller';
import { Command } from 'src/entity/Command';
import { Command, CommandVerb } from 'src/entity/Command';
import { countList } from 'src/utils';

export type RandomControllerData = ControllerData;
Expand All @@ -18,7 +19,10 @@ export class RandomController extends BaseController<RandomControllerData> imple
super(options, 'isolex#/definitions/service-controller-random', [NOUN_RANDOM]);
}

public async handle(cmd: Command): Promise<void> {
@HandleNoun(NOUN_RANDOM)
@HandleVerb(CommandVerb.Get)
@CheckRBAC()
public async getRandom(cmd: Command): Promise<void> {
const args = cmd.data.get('args');
if (!Array.isArray(args)) {
return this.reply(cmd.context, 'no arguments were provided!');
Expand Down
8 changes: 6 additions & 2 deletions src/controller/ReactionController.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Inject } from 'noicejs';

import { CheckRBAC, HandleNoun, HandleVerb } from 'src/controller';
import { BaseController } from 'src/controller/BaseController';
import { Controller, ControllerData, ControllerOptions } from 'src/controller/Controller';
import { Command } from 'src/entity/Command';
import { Command, CommandVerb } from 'src/entity/Command';
import { Message } from 'src/entity/Message';
import { TYPE_TEXT } from 'src/utils/Mime';

Expand Down Expand Up @@ -30,7 +31,10 @@ export class ReactionController extends BaseController<ReactionControllerData> i
this.reactions = new Map(Object.entries(options.data.reactions));
}

public async handle(cmd: Command): Promise<void> {
@HandleNoun(NOUN_REACTION)
@HandleVerb(CommandVerb.Create)
@CheckRBAC()
public async createReaction(cmd: Command): Promise<void> {
const reactions = [];
const body = cmd.get(this.data.field);
for (const [key, next] of this.reactions) {
Expand Down
32 changes: 13 additions & 19 deletions src/controller/github/PRController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { defaults } from '@octokit/graphql';
import { isNil } from 'lodash';
import { Inject } from 'noicejs';

import { CheckRBAC, HandleNoun, HandleVerb } from 'src/controller';
import { BaseController } from 'src/controller/BaseController';
import { Controller, ControllerData, ControllerOptions } from 'src/controller/Controller';
import { Command, CommandVerb } from 'src/entity/Command';
Expand Down Expand Up @@ -100,25 +101,9 @@ export class GithubPRController extends BaseController<GithubPRControllerData> i
});
}

public async handle(cmd: Command): Promise<void> {
if (cmd.noun !== NOUN_PULL_REQUEST) {
return this.reply(cmd.context, 'invalid noun');
}

switch (cmd.verb) {
case CommandVerb.Delete:
return this.deleteRequest(cmd);
case CommandVerb.Get:
return this.getRequest(cmd);
case CommandVerb.List:
return this.listRequests(cmd);
case CommandVerb.Update:
return this.updateRequest(cmd);
default:
return this.reply(cmd.context, 'invalid verb');
}
}

@HandleNoun(NOUN_PULL_REQUEST)
@HandleVerb(CommandVerb.Delete)
@CheckRBAC()
public async deleteRequest(cmd: Command): Promise<void> {
const owner = cmd.getHeadOrDefault('owner', cmd.context.name);
const project = cmd.getHead('project');
Expand All @@ -139,6 +124,9 @@ export class GithubPRController extends BaseController<GithubPRControllerData> i
return this.reply(cmd.context, `closed pull request ${requestNumber}`);
}

@HandleNoun(NOUN_PULL_REQUEST)
@HandleVerb(CommandVerb.Get)
@CheckRBAC()
public async getRequest(cmd: Command): Promise<void> {
const owner = cmd.getHeadOrDefault('owner', cmd.context.name);
const project = cmd.getHead('project');
Expand All @@ -148,6 +136,9 @@ export class GithubPRController extends BaseController<GithubPRControllerData> i
return this.transformJSON(cmd, [response.data.repository.pullRequest]);
}

@HandleNoun(NOUN_PULL_REQUEST)
@HandleVerb(CommandVerb.List)
@CheckRBAC()
public async listRequests(cmd: Command): Promise<void> {
const owner = cmd.getHeadOrDefault('owner', cmd.context.name);
const project = cmd.getHead('project');
Expand All @@ -159,6 +150,9 @@ export class GithubPRController extends BaseController<GithubPRControllerData> i
return this.transformJSON(cmd, response.data.repository.pullRequests.nodes);
}

@HandleNoun(NOUN_PULL_REQUEST)
@HandleVerb(CommandVerb.Update)
@CheckRBAC()
public async updateRequest(cmd: Command): Promise<void> {
const message = cmd.getHead('message');
const owner = cmd.getHeadOrDefault('owner', cmd.context.name);
Expand Down
Loading

0 comments on commit 915df8a

Please sign in to comment.