Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
57 changes: 48 additions & 9 deletions docs/api/classes/_testing_.testrunner.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

# Class: TestRunner

TestRunner makes it easier to write table-driven tests for KPT functions.
TestRunner makes it easy to write unit tests for KPT functions.

## Hierarchy

Expand All @@ -16,7 +16,8 @@ TestRunner makes it easier to write table-driven tests for KPT functions.

### Methods

* [run](_testing_.testrunner.md#run)
* [assert](_testing_.testrunner.md#assert)
* [assertCallback](_testing_.testrunner.md#assertcallback)

## Constructors

Expand All @@ -34,20 +35,58 @@ Name | Type |

## Methods

### run
### assert

▸ **run**(`input`: [Configs](_types_.configs.md), `expectedOutput?`: [Configs](_types_.configs.md) | [ConfigError](_errors_.configerror.md), `expectException?`: undefined | false | true): *function*
▸ **assert**(`input`: [Configs](_types_.configs.md), `expectedOutput?`: [Configs](_types_.configs.md), `expectedException?`: undefined | object, `expectedExceptionMessage?`: string | RegExp): *Promise‹void›*

Generates a callback for a test framework to execute.
Runs the KptFunc and asserts the expected output or exception.

Example usage:

```
const RUNNER = new TestRunner(myFunc);

it('function is a NO OP', async () => {
await RUNNER.assert());
};
```

**Parameters:**

Name | Type | Default | Description |
------ | ------ | ------ | ------ |
`input` | [Configs](_types_.configs.md) | new Configs() | input Configs passed to the function. It is deep-copied before running the function. If undefined, assumes an empty Configs. |
`expectedOutput?` | [Configs](_types_.configs.md) | - | expected resultant Configs after KptFunc has successfully completed. If undefined, assumes the output should remain unchanged (NO OP). |
`expectedException?` | undefined | object | - | expected exception to be thrown. If given, expectedOutput is ignored. |
`expectedExceptionMessage?` | string | RegExp | - | expected message of expection to be thrown. If given, expectedOutput is ignored. |

**Returns:** *Promise‹void›*

___

### assertCallback

▸ **assertCallback**(`input`: [Configs](_types_.configs.md), `expectedOutput?`: [Configs](_types_.configs.md), `expectedException?`: undefined | object, `expectedExceptionMessage?`: string | RegExp): *function*

Similar to [assert](_testing_.testrunner.md#assert) method, but instead returns an assertion function that can be passed directly to 'it'.

Example usage:

```
const RUNNER = new TestRunner(myFunc);

it('function is a NO OP', RUNNER.assertCallback());
```

**Parameters:**

Name | Type | Default | Description |
------ | ------ | ------ | ------ |
`input` | [Configs](_types_.configs.md) | new Configs() | is the initial set of Configs to test. By default assumes an empty set of Configs. |
`expectedOutput?` | [Configs](_types_.configs.md) | [ConfigError](_errors_.configerror.md) | - | is the expected resulting Configs or ConfigError produced by the KptFunc. If undefined, assumes the output should remain unchanged. |
`expectException?` | undefined | false | true | - | indicates that KptFunc is expected to throw an exception. |
`input` | [Configs](_types_.configs.md) | new Configs() | input Configs passed to the function. It is deep-copied before running the function. If undefined, assumes an empty Configs. |
`expectedOutput?` | [Configs](_types_.configs.md) | - | expected resultant Configs after KptFunc has successfully completed. If undefined, assumes the output should remain unchanged (NO OP). |
`expectedException?` | undefined | object | - | expected exception to be thrown. If given, expectedOutput is ignored. |
`expectedExceptionMessage?` | string | RegExp | - | expected message of expection to be thrown. If given, expectedOutput is ignored. |

**Returns:** *function*

▸ (): *void*
▸ (): *Promise‹void*
6 changes: 4 additions & 2 deletions docs/api/classes/_types_.configs.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ ___

Returns the value for the given key if functionConfig is of kind ConfigMap.

Throws an exception if functionConfig kind is not a ConfigMap.
Throws a TypeError exception if functionConfig kind is not a ConfigMap.

Returns undefined if functionConfig is undefined OR
if the ConfigMap has no such key in the 'data' section.
Expand All @@ -160,7 +160,7 @@ ___

▸ **getFunctionConfigValueOrThrow**(`key`: string): *string*

Similar to [getFunctionConfigValue](_types_.configs.md#getfunctionconfigvalue) except it throws an exception if the given key is undefined.
Similar to [getFunctionConfigValue](_types_.configs.md#getfunctionconfigvalue) except it throws a TypeError exception if the given key is undefined.

**Parameters:**

Expand All @@ -182,7 +182,9 @@ The ordering of objects with the same key is deterministic.

Example: Partition configs by Namespace:

```
const configsByNamespace = configs.groupBy((o) => o.metadata.namespace)
```

**Parameters:**

Expand Down
8 changes: 4 additions & 4 deletions docs/api/interfaces/_types_.kptfunc.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ Interface describing KPT functions.

## Callable

▸ (`configs`: [Configs](../classes/_types_.configs.md)): *void | [ConfigError](../classes/_errors_.configerror.md)*
▸ (`configs`: [Configs](../classes/_types_.configs.md)): *Promise‹void*

A function consumes and optionally mutates Kubernetes configurations using the given [Configs](../classes/_types_.configs.md) object.

The function should:
- Return a [ConfigError](../classes/_errors_.configerror.md) when encountering one or more configuration-related issues.
- Throw an error when encountering operational issues such as IO exceptions.
- Throw a [ConfigError](../classes/_errors_.configerror.md) when encountering one or more configuration-related issues.
- Throw other error types when encountering operational issues such as IO exceptions.
- Avoid writing to stdout (e.g. using process.stdout) as it is used for chaining functions.
Use stderr instead.

Expand All @@ -26,7 +26,7 @@ Name | Type |
------ | ------ |
`configs` | [Configs](../classes/_types_.configs.md) |

**Returns:** *void | [ConfigError](../classes/_errors_.configerror.md)*
**Returns:** *Promise‹void*

## Index

Expand Down
9 changes: 6 additions & 3 deletions docs/api/modules/_run_.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,17 @@

### run

▸ **run**(`fn`: [KptFunc](../interfaces/_types_.kptfunc.md)): *void*
▸ **run**(`fn`: [KptFunc](../interfaces/_types_.kptfunc.md)): *Promise‹void*

Executes the KptFunc. This is the main entrypoint for all kpt functions.
This is the main entrypoint for running a KPT function.

This method does not throw any errors and can be invoked at the top-level without getting
an unhandled promise rejection error.

**Parameters:**

Name | Type |
------ | ------ |
`fn` | [KptFunc](../interfaces/_types_.kptfunc.md) |

**Returns:** *void*
**Returns:** *Promise‹void*
25 changes: 21 additions & 4 deletions tests/e2e.bats
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,35 @@ function runp() {

@test "no-op print stdout" {
runp "docker run -i gcr.io/kpt-functions/no-op:${TAG} -i /dev/null"
[ "$status" -eq 0 ]
[ "$output" = "${EMPTY_OUTPUT}" ]
}

@test "no-op pipe" {
runp "docker run -i gcr.io/kpt-functions/no-op:${TAG} -i /dev/null |
docker run -i gcr.io/kpt-functions/no-op:${TAG}"
[ "$status" -eq 0 ]
[ "$output" = "${EMPTY_OUTPUT}" ]
}

@test "no-op output to /dev/null" {
runp "docker run -i gcr.io/kpt-functions/no-op:${TAG} -i /dev/null |
docker run -i gcr.io/kpt-functions/no-op:${TAG} |
docker run -i gcr.io/kpt-functions/no-op:${TAG} -o /dev/null"
[ "$output" = "" ]
runp "docker run -i gcr.io/kpt-functions/no-op:${TAG} -i /dev/null |
docker run -i gcr.io/kpt-functions/no-op:${TAG} |
docker run -i gcr.io/kpt-functions/no-op:${TAG} -o /dev/null"
[ "$status" -eq 0 ]
[ "$output" = "" ]
}

@test "all demo functions" {
runp "docker run -i -u $(id -u) -v $(pwd):/source gcr.io/kpt-functions/read-yaml:${TAG} -i /dev/null -d source_dir=/source |
docker run -i gcr.io/kpt-functions/mutate-psp:${TAG} |
docker run -i gcr.io/kpt-functions/expand-team-cr:${TAG} |
docker run -i gcr.io/kpt-functions/validate-rolebinding:${TAG} -d subject_name=alice@foo-corp.com |
docker run -i -u $(id -u) -v $(pwd):/sink gcr.io/kpt-functions/write-yaml:${TAG} -o /dev/null -d sink_dir=/sink -d overwrite=true"
[ "$status" -eq 0 ]
[ "$output" = "" ]

# Check expected diff
ls payments-dev payments-prod
grep allowPrivilegeEscalation podsecuritypolicy_psp.yaml
}
4 changes: 2 additions & 2 deletions ts/create-kpt-functions/templates/func.mustache
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { KptFunc } from '@googlecontainertools/kpt-functions';
import { Configs } from '@googlecontainertools/kpt-functions';

export const {{func_name}}: KptFunc = (configs) => {
export async function {{func_name}}(configs: Configs) {
// TODO: implement.
};

Expand Down
2 changes: 1 addition & 1 deletion ts/create-kpt-functions/templates/package.json.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"kpt:type-create": "kpt type-create"
},
"dependencies": {
"@googlecontainertools/kpt-functions": "^0.10.0"
"@googlecontainertools/kpt-functions": "^0.11.0-rc.1"
},
"devDependencies": {
"@googlecontainertools/create-kpt-functions": "^0.14.4",
Expand Down
20 changes: 9 additions & 11 deletions ts/create-kpt-functions/templates/test.mustache
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
import { Configs } from '@googlecontainertools/kpt-functions';
import { Configs, TestRunner } from '@googlecontainertools/kpt-functions';
import { {{{func_name}}} } from './{{{file_name}}}';

describe('{{{func_name}}}', () => {
it('does something', () => {
// 1. TODO: Create test fixture for Configs consumed by the function.
const actualConfigs = new Configs();
const RUNNER = new TestRunner({{{func_name}}});

// 2. Invoke the function.
{{{func_name}}}(actualConfigs);
describe('{{{func_name}}}', () => {
it('does something', async () => {
// TODO: Populate the input to the function.
const input = new Configs();

// 3. TODO: Create test fixture for Configs expected to be returned by the function.
const expectedConfigs = new Configs();
// TODO: Populate the expected output of the function.
const expectedOutput = new Configs();

// 4. TODO: Assert function behavior including any side-effects.
expect(actualConfigs.getAll()).toEqual(expectedConfigs.getAll());
await RUNNER.assert(input, expectedOutput);
});
});
6 changes: 3 additions & 3 deletions ts/demo-functions/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion ts/demo-functions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"kpt:type-create": "kpt type-create"
},
"dependencies": {
"@googlecontainertools/kpt-functions": "^0.10.0",
"@googlecontainertools/kpt-functions": "^0.11.0-rc.1",
"glob": "^7.1.3",
"js-yaml": "^3.13.1"
},
Expand Down
6 changes: 3 additions & 3 deletions ts/demo-functions/src/expand_team_cr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@
* limitations under the License.
*/

import { KptFunc } from '@googlecontainertools/kpt-functions';
import { Configs } from '@googlecontainertools/kpt-functions';
import { isTeam, Team } from './gen/dev.cft.anthos.v1alpha1';
import { Namespace } from './gen/io.k8s.api.core.v1';
import { RoleBinding, Subject } from './gen/io.k8s.api.rbac.v1';

const ENVIRONMENTS = ['dev', 'prod'];

export const expandTeamCr: KptFunc = (configs) => {
export async function expandTeamCr(configs: Configs) {
// For each 'Team' custom resource in the input:
// 1. Generate a per-enviroment Namespace.
// 2. Generate RoleBindings in each Namespace.
Expand All @@ -34,7 +34,7 @@ export const expandTeamCr: KptFunc = (configs) => {
configs.insert(...createRoleBindings(team, ns));
});
});
};
}

function createRoleBindings(team: Team, namespace: string): RoleBinding[] {
return (team.spec.roles || []).map((item) => {
Expand Down
15 changes: 9 additions & 6 deletions ts/demo-functions/src/expand_team_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,16 @@ function team(name: string, ...roles: Team.Spec.Item[]): Team {
const RUNNER = new TestRunner(expandTeamCr);

describe(expandTeamCr.name, () => {
it('does nothing to empty repos', RUNNER.run());
it('does nothing to empty repos', RUNNER.assertCallback());

it('does nothing to non-Team objects', RUNNER.run(new Configs([Namespace.named('backend')])));
it(
'does nothing to non-Team objects',
RUNNER.assertCallback(new Configs([Namespace.named('backend')])),
);

it(
'expands an empty team to its environments',
RUNNER.run(
RUNNER.assertCallback(
new Configs([team('backend')]),
new Configs([
team('backend'),
Expand All @@ -52,7 +55,7 @@ describe(expandTeamCr.name, () => {

it(
'expands a Team with group roles',
RUNNER.run(
RUNNER.assertCallback(
new Configs([
team('backend', {
role: 'admin',
Expand Down Expand Up @@ -106,7 +109,7 @@ describe(expandTeamCr.name, () => {

it(
'expands a Team with user roles',
RUNNER.run(
RUNNER.assertCallback(
new Configs([
team('backend', {
role: 'admin',
Expand Down Expand Up @@ -160,7 +163,7 @@ describe(expandTeamCr.name, () => {

it(
'expands a Team with both user and group roles',
RUNNER.run(
RUNNER.assertCallback(
new Configs([
team('backend', {
role: 'admin',
Expand Down
6 changes: 3 additions & 3 deletions ts/demo-functions/src/mutate_psp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,17 @@
* limitations under the License.
*/

import { KptFunc } from '@googlecontainertools/kpt-functions';
import { Configs } from '@googlecontainertools/kpt-functions';
import { isPodSecurityPolicy } from './gen/io.k8s.api.policy.v1beta1';

export const mutatePsp: KptFunc = (configs) => {
export async function mutatePsp(configs: Configs) {
// Iterate over all PodSecurityPolicy objects in the input and if
// the 'allowPrivilegeEscalation' field is not to 'false', set the field to false.
configs
.get(isPodSecurityPolicy)
.filter((psp) => psp.spec && psp.spec.allowPrivilegeEscalation !== false)
.forEach((psp) => (psp!.spec!.allowPrivilegeEscalation = false));
};
}

mutatePsp.usage = `
Mutates all PodSecurityPolicy by setting 'spec.allowPrivilegeEscalation' field to 'false'.
Expand Down
6 changes: 3 additions & 3 deletions ts/demo-functions/src/mutate_psp_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,15 @@ function psp(allowPrivilegeEscalation: boolean): PodSecurityPolicy {
const RUNNER = new TestRunner(mutatePsp);

describe('mutatePsp', () => {
it('passes empty repos', RUNNER.run());
it('passes empty repos', RUNNER.assertCallback());

it(
'modifies PSP with allowPrivilegeEscalation = true to false',
RUNNER.run(new Configs([psp(true)]), new Configs([psp(false)])),
RUNNER.assertCallback(new Configs([psp(true)]), new Configs([psp(false)])),
);

it(
'leaves PSP with allowPrivilegeEscalation = false alone',
RUNNER.run(new Configs([psp(false)])),
RUNNER.assertCallback(new Configs([psp(false)])),
);
});
4 changes: 2 additions & 2 deletions ts/demo-functions/src/no_op.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
* limitations under the License.
*/

import { KptFunc } from '@googlecontainertools/kpt-functions';
import { Configs } from '@googlecontainertools/kpt-functions';

export const noOp: KptFunc = (configs) => {};
export async function noOp(configs: Configs) {}

noOp.usage = `
A NO OP kpt function used for testing and demo purposes.
Expand Down
Loading