@kensio/yulin 1.21.17 → 1.21.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/service/athena/engine/sim-athena-engine-answer.d.ts +29 -0
- package/dist/service/athena/engine/sim-athena-engine-answer.js +22 -0
- package/dist/service/athena/engine/sim-athena-engine-run.d.ts +1 -17
- package/dist/service/athena/engine/sim-athena-engine-run.js +1 -8
- package/dist/service/athena/engine/sim-athena-query-engine.d.ts +1 -1
- package/dist/service/athena/engine/sim-athena-query-engine.js +6 -2
- package/dist/service/athena/engine/sim-athena-rejected-statement.d.ts +14 -0
- package/dist/service/athena/engine/sim-athena-rejected-statement.js +103 -0
- package/dist/service/athena/engine/sim-athena-sql-translation.d.ts +18 -7
- package/dist/service/athena/engine/sim-athena-sql-translation.js +18 -7
- package/dist/service/athena/result/sim-athena-query-answer.d.ts +4 -0
- package/dist/service/athena/result/sim-athena-query-answer.js +5 -1
- package/dist/service/cloudformation/cdk/provider/sim-cdk-provider-invoke-arn.d.ts +13 -0
- package/dist/service/cloudformation/cdk/provider/sim-cdk-provider-invoke-arn.js +63 -0
- package/dist/service/cloudformation/cdk/provider/sim-cdk-provider-invoke-auth-z.d.ts +36 -0
- package/dist/service/cloudformation/cdk/provider/sim-cdk-provider-invoke-auth-z.js +47 -0
- package/dist/service/cloudformation/cdk/s3/bucket-notifications/property/sim-cdk-bucket-notification-properties.js +5 -3
- package/dist/service/cloudformation/cdk/s3/bucket-notifications/sim-cdk-bucket-notifications-remover.js +2 -1
- package/dist/service/cloudformation/cdk/s3/bucket-notifications/sim-cdk-bucket-notifications.d.ts +7 -4
- package/dist/service/cloudformation/cdk/s3/bucket-notifications/sim-cdk-bucket-notifications.js +9 -5
- package/dist/service/cloudformation/cdk/ssm/cross-region-parameter/sim-cdk-cross-region-parameter-properties.js +4 -3
- package/dist/service/cloudformation/cdk/ssm/cross-region-parameter/sim-cdk-cross-region-parameter-reader.js +2 -1
- package/dist/service/cloudformation/resource/create/sim-cfn-resource-creator.js +11 -1
- package/dist/service/cloudformation/resource/delete/sim-cfn-resource-deleter.js +11 -1
- package/docs/services/athena/README.md +30 -0
- package/docs/services/cloudformation/README.md +88 -0
- package/package.json +2 -2
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { SimAthenaResolvedResult } from "../result/sim-athena-resolved-result.js";
|
|
2
|
+
/**
|
|
3
|
+
* What the engine came to for one query.
|
|
4
|
+
*
|
|
5
|
+
* A turn-down carries why, because a strict engine fails the query with it and
|
|
6
|
+
* a reader of that failing test has to learn which of them it hit.
|
|
7
|
+
*/
|
|
8
|
+
export interface SimAthenaEngineAnswer {
|
|
9
|
+
/** The rows, where the engine ran the statement. */
|
|
10
|
+
readonly result: SimAthenaResolvedResult | undefined;
|
|
11
|
+
/** What the engine could not do, where it turned the query down. */
|
|
12
|
+
readonly turnedDown: string | undefined;
|
|
13
|
+
/** Why Athena refuses the statement, where the engine found it invalid. */
|
|
14
|
+
readonly rejected: string | undefined;
|
|
15
|
+
}
|
|
16
|
+
/** One answer the engine ran for real. */
|
|
17
|
+
export declare function simAthenaEngineAnswered(result: SimAthenaResolvedResult): SimAthenaEngineAnswer;
|
|
18
|
+
/** One query the engine turned down, and why. */
|
|
19
|
+
export declare function simAthenaEngineTurnedDown(turnedDown: string): SimAthenaEngineAnswer;
|
|
20
|
+
/**
|
|
21
|
+
* One statement Athena refuses, and why.
|
|
22
|
+
*
|
|
23
|
+
* The engine ran nothing. A query that reaches this fails whether or not the
|
|
24
|
+
* engine is strict, since the alternative is a green test on SQL the service
|
|
25
|
+
* rejects.
|
|
26
|
+
*/
|
|
27
|
+
export declare function simAthenaEngineRejected(rejected: string): SimAthenaEngineAnswer;
|
|
28
|
+
/** An engine nobody turned on, which has no opinion about the query. */
|
|
29
|
+
export declare function simAthenaEngineSilent(): SimAthenaEngineAnswer;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** One answer the engine ran for real. */
|
|
2
|
+
export function simAthenaEngineAnswered(result) {
|
|
3
|
+
return { result, turnedDown: undefined, rejected: undefined };
|
|
4
|
+
}
|
|
5
|
+
/** One query the engine turned down, and why. */
|
|
6
|
+
export function simAthenaEngineTurnedDown(turnedDown) {
|
|
7
|
+
return { result: undefined, turnedDown, rejected: undefined };
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* One statement Athena refuses, and why.
|
|
11
|
+
*
|
|
12
|
+
* The engine ran nothing. A query that reaches this fails whether or not the
|
|
13
|
+
* engine is strict, since the alternative is a green test on SQL the service
|
|
14
|
+
* rejects.
|
|
15
|
+
*/
|
|
16
|
+
export function simAthenaEngineRejected(rejected) {
|
|
17
|
+
return { result: undefined, turnedDown: undefined, rejected };
|
|
18
|
+
}
|
|
19
|
+
/** An engine nobody turned on, which has no opinion about the query. */
|
|
20
|
+
export function simAthenaEngineSilent() {
|
|
21
|
+
return { result: undefined, turnedDown: undefined, rejected: undefined };
|
|
22
|
+
}
|
|
@@ -1,25 +1,9 @@
|
|
|
1
1
|
import type { SimAwsCaller } from "../../aws/caller/sim-aws-caller.js";
|
|
2
2
|
import type { SimAthenaPlannedTable } from "../execution/sim-athena-query-refusal.js";
|
|
3
|
-
import type
|
|
3
|
+
import { type SimAthenaEngineAnswer } from "./sim-athena-engine-answer.js";
|
|
4
4
|
import type { SimAthenaSqlParser } from "./sim-athena-parser-module.js";
|
|
5
5
|
import type { SimAthenaSqliteModule } from "./sim-athena-sqlite-module.js";
|
|
6
6
|
import type { SimAthenaTableObjects } from "./sim-athena-table-objects.js";
|
|
7
|
-
/**
|
|
8
|
-
* What the engine came to for one query.
|
|
9
|
-
*
|
|
10
|
-
* A turn-down carries why, because a strict engine fails the query with it and
|
|
11
|
-
* a reader of that failing test has to learn which of them it hit.
|
|
12
|
-
*/
|
|
13
|
-
export interface SimAthenaEngineAnswer {
|
|
14
|
-
/** The rows, where the engine ran the statement. */
|
|
15
|
-
readonly result: SimAthenaResolvedResult | undefined;
|
|
16
|
-
/** What the engine could not do, where it turned the query down. */
|
|
17
|
-
readonly turnedDown: string | undefined;
|
|
18
|
-
}
|
|
19
|
-
/** One answer the engine ran for real. */
|
|
20
|
-
export declare function simAthenaEngineAnswered(result: SimAthenaResolvedResult): SimAthenaEngineAnswer;
|
|
21
|
-
/** One query the engine turned down, and why. */
|
|
22
|
-
export declare function simAthenaEngineTurnedDown(turnedDown: string): SimAthenaEngineAnswer;
|
|
23
7
|
/** What one query is run with, once the engine is on and has somewhere to read. */
|
|
24
8
|
export interface SimAthenaEngineRun {
|
|
25
9
|
readonly parser: SimAthenaSqlParser;
|
|
@@ -1,15 +1,8 @@
|
|
|
1
|
+
import { simAthenaEngineAnswered, simAthenaEngineTurnedDown, } from "./sim-athena-engine-answer.js";
|
|
1
2
|
import { simAthenaRunFailure } from "./sim-athena-turn-down.js";
|
|
2
3
|
import { simAthenaEngineResult } from "./sim-athena-engine-result.js";
|
|
3
4
|
import { simAthenaSqliteDatabase } from "./sim-athena-sqlite-database.js";
|
|
4
5
|
import { simAthenaLoadedTables } from "./sim-athena-loaded-tables.js";
|
|
5
|
-
/** One answer the engine ran for real. */
|
|
6
|
-
export function simAthenaEngineAnswered(result) {
|
|
7
|
-
return { result, turnedDown: undefined };
|
|
8
|
-
}
|
|
9
|
-
/** One query the engine turned down, and why. */
|
|
10
|
-
export function simAthenaEngineTurnedDown(turnedDown) {
|
|
11
|
-
return { result: undefined, turnedDown };
|
|
12
|
-
}
|
|
13
6
|
/**
|
|
14
7
|
* Load the tables, build the database and run the statement.
|
|
15
8
|
*
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { SimAwsCaller } from "../../aws/caller/sim-aws-caller.js";
|
|
2
2
|
import type { SimAthenaPlannedTable } from "../execution/sim-athena-query-refusal.js";
|
|
3
|
-
import { type SimAthenaEngineAnswer } from "./sim-athena-engine-
|
|
3
|
+
import { type SimAthenaEngineAnswer } from "./sim-athena-engine-answer.js";
|
|
4
4
|
import type { SimAthenaTableObjects } from "./sim-athena-table-objects.js";
|
|
5
5
|
/** How one scope's engine is turned on. */
|
|
6
6
|
export interface SimAthenaEngineOptions {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { simAthenaEngineRejected, simAthenaEngineSilent, simAthenaEngineTurnedDown, } from "./sim-athena-engine-answer.js";
|
|
2
|
+
import { simAthenaEngineRun } from "./sim-athena-engine-run.js";
|
|
2
3
|
import { simAthenaSqlParser, } from "./sim-athena-parser-module.js";
|
|
3
4
|
import { simAthenaSqliteModule, } from "./sim-athena-sqlite-module.js";
|
|
4
5
|
import { simAthenaSqliteSql } from "./sim-athena-sql-translation.js";
|
|
@@ -63,7 +64,7 @@ export class SimAthenaQueryEngine {
|
|
|
63
64
|
const sqlite = this.#sqlite;
|
|
64
65
|
const { objects } = request;
|
|
65
66
|
if (parser === undefined || sqlite === undefined) {
|
|
66
|
-
return
|
|
67
|
+
return simAthenaEngineSilent();
|
|
67
68
|
}
|
|
68
69
|
if (objects === undefined) {
|
|
69
70
|
return simAthenaEngineTurnedDown(simAthenaNoObjects);
|
|
@@ -73,6 +74,9 @@ export class SimAthenaQueryEngine {
|
|
|
73
74
|
athenaSql: request.queryString,
|
|
74
75
|
tables: request.tables.map((planned) => planned.table),
|
|
75
76
|
});
|
|
77
|
+
if (translated.rejected !== undefined) {
|
|
78
|
+
return simAthenaEngineRejected(translated.rejected);
|
|
79
|
+
}
|
|
76
80
|
return translated.sql === undefined
|
|
77
81
|
? simAthenaEngineTurnedDown(translated.turnedDown)
|
|
78
82
|
: simAthenaEngineRun({
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Why Athena refuses one parsed statement, where it does.
|
|
3
|
+
*
|
|
4
|
+
* Everything here is a statement SQLite runs happily and the real service
|
|
5
|
+
* rejects. A translation that hands such a statement to SQLite answers a test
|
|
6
|
+
* with rows, and the command then fails against AWS. Each shape is spotted on
|
|
7
|
+
* the tree the Athena grammar produced, before anything is written out for
|
|
8
|
+
* SQLite.
|
|
9
|
+
*
|
|
10
|
+
* Trino and SQLite resolve names by different rules in more places than this.
|
|
11
|
+
* One shape is covered, the one seen reaching production. Add the next when a
|
|
12
|
+
* query hits it.
|
|
13
|
+
*/
|
|
14
|
+
export declare function simAthenaRejectedStatement(ast: unknown): string | undefined;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { isColumnRef, simAthenaAstNodes, } from "./sim-athena-ast-nodes.js";
|
|
2
|
+
/**
|
|
3
|
+
* Why Athena refuses one parsed statement, where it does.
|
|
4
|
+
*
|
|
5
|
+
* Everything here is a statement SQLite runs happily and the real service
|
|
6
|
+
* rejects. A translation that hands such a statement to SQLite answers a test
|
|
7
|
+
* with rows, and the command then fails against AWS. Each shape is spotted on
|
|
8
|
+
* the tree the Athena grammar produced, before anything is written out for
|
|
9
|
+
* SQLite.
|
|
10
|
+
*
|
|
11
|
+
* Trino and SQLite resolve names by different rules in more places than this.
|
|
12
|
+
* One shape is covered, the one seen reaching production. Add the next when a
|
|
13
|
+
* query hits it.
|
|
14
|
+
*/
|
|
15
|
+
export function simAthenaRejectedStatement(ast) {
|
|
16
|
+
for (const node of simAthenaAstNodes(ast)) {
|
|
17
|
+
const sort = node["orderby"];
|
|
18
|
+
const columns = node["columns"];
|
|
19
|
+
if (!Array.isArray(sort) || !Array.isArray(columns)) {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
const alias = aggregatedAlias(sort, columns);
|
|
23
|
+
if (alias !== undefined) {
|
|
24
|
+
return orderByAggregateOverAlias(alias);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
/** What Athena says about an `ORDER BY` aggregate over an aggregate alias. */
|
|
30
|
+
function orderByAggregateOverAlias(alias) {
|
|
31
|
+
return (`Athena refuses this statement. Its ORDER BY aggregates over ${alias}, ` +
|
|
32
|
+
`and the select list names ${alias} as the output alias of an aggregate. ` +
|
|
33
|
+
`Trino resolves a bare name in ORDER BY against the select list before ` +
|
|
34
|
+
`the table. That makes the sort an aggregate over an aggregate, and the ` +
|
|
35
|
+
`service answers "Invalid reference to output projection attribute from ` +
|
|
36
|
+
`ORDER BY aggregation". Sort by ${alias} on its own, or give the alias a ` +
|
|
37
|
+
`name the sort leaves alone.`);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The output alias one `ORDER BY` aggregates over, where the alias aggregates
|
|
41
|
+
* too.
|
|
42
|
+
*
|
|
43
|
+
* A select list of `sum(qty) AS qty` sorted by `ORDER BY sum(qty)` is the
|
|
44
|
+
* shape. SQLite reads that `qty` as the table's column and sorts correctly.
|
|
45
|
+
* Trino reads it as the alias and refuses the query.
|
|
46
|
+
*
|
|
47
|
+
* `ORDER BY qty` over the same select list is the form Athena takes, with no
|
|
48
|
+
* aggregate wrapped around the name. An aggregate over a name the select list
|
|
49
|
+
* leaves alone is legal too, as in `ORDER BY sum(revenue_total)` beside
|
|
50
|
+
* `coalesce(sum(revenue_total), 0) AS revenue`. The aliases are what tell this
|
|
51
|
+
* shape from those two.
|
|
52
|
+
*/
|
|
53
|
+
function aggregatedAlias(sort, columns) {
|
|
54
|
+
const aliases = aggregateAliases(columns);
|
|
55
|
+
if (aliases.size === 0) {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
for (const node of simAthenaAstNodes(sort)) {
|
|
59
|
+
if (!isAggregate(node)) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const arguments_ = simAthenaAstNodes(node["args"]);
|
|
63
|
+
for (const inner of arguments_) {
|
|
64
|
+
const alias = isColumnRef(inner) && inner.table === null
|
|
65
|
+
? aliases.get(inner.column.toLowerCase())
|
|
66
|
+
: undefined;
|
|
67
|
+
if (alias !== undefined) {
|
|
68
|
+
return alias;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
/** Every output alias of one select list whose own expression aggregates. */
|
|
75
|
+
function aggregateAliases(columns) {
|
|
76
|
+
const aliases = new Map();
|
|
77
|
+
for (const column of columns) {
|
|
78
|
+
const alias = column["as"];
|
|
79
|
+
if (typeof alias === "string" && holdsAggregate(column["expr"])) {
|
|
80
|
+
aliases.set(alias.toLowerCase(), alias);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return aliases;
|
|
84
|
+
}
|
|
85
|
+
/** Whether anything under this expression aggregates. */
|
|
86
|
+
function holdsAggregate(expression) {
|
|
87
|
+
for (const node of simAthenaAstNodes(expression)) {
|
|
88
|
+
if (isAggregate(node)) {
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Whether this node is a grouped aggregate call.
|
|
96
|
+
*
|
|
97
|
+
* A window function carries `over` and is computed once the grouped aggregates
|
|
98
|
+
* have been. It takes an output alias in `ORDER BY` happily, and stays out of
|
|
99
|
+
* this.
|
|
100
|
+
*/
|
|
101
|
+
function isAggregate(node) {
|
|
102
|
+
return node["type"] === "aggr_func" && (node["over"] ?? null) === null;
|
|
103
|
+
}
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import type { SimAthenaCatalogTable } from "../table/sim-athena-catalog-table.js";
|
|
2
2
|
import type { SimAthenaSqlParser } from "./sim-athena-parser-module.js";
|
|
3
|
-
/** One statement translated for SQLite, or why it
|
|
3
|
+
/** One statement translated for SQLite, or why it was not. */
|
|
4
4
|
export type SimAthenaTranslatedSql = {
|
|
5
5
|
readonly sql: string;
|
|
6
6
|
readonly turnedDown: undefined;
|
|
7
|
+
readonly rejected: undefined;
|
|
7
8
|
} | {
|
|
8
9
|
readonly sql: undefined;
|
|
9
10
|
readonly turnedDown: string;
|
|
11
|
+
readonly rejected: undefined;
|
|
12
|
+
} | {
|
|
13
|
+
readonly sql: undefined;
|
|
14
|
+
readonly turnedDown: undefined;
|
|
15
|
+
readonly rejected: string;
|
|
10
16
|
};
|
|
11
17
|
interface SimAthenaSqlTranslationRequest {
|
|
12
18
|
readonly parser: SimAthenaSqlParser;
|
|
@@ -15,13 +21,18 @@ interface SimAthenaSqlTranslationRequest {
|
|
|
15
21
|
readonly tables: readonly SimAthenaCatalogTable[];
|
|
16
22
|
}
|
|
17
23
|
/**
|
|
18
|
-
* One Athena statement written back out for SQLite, or why it
|
|
24
|
+
* One Athena statement written back out for SQLite, or why it is not.
|
|
25
|
+
*
|
|
26
|
+
* Three things turn the query down, and each is told apart because a strict
|
|
27
|
+
* engine fails the query with the reason. The Athena grammar can refuse the
|
|
28
|
+
* statement, an `UNNEST` in it can have no `json_each` to become, and the
|
|
29
|
+
* parser can decline to write the statement back out. A lenient engine answers
|
|
30
|
+
* all three the same way, by letting the declared result take the query.
|
|
19
31
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* way, by turning the query down and letting the declared result take it.
|
|
32
|
+
* A rejection is the fourth outcome and a different thing. The statement
|
|
33
|
+
* parsed, and Athena would still refuse to run it. That fails the query
|
|
34
|
+
* whether or not the engine is strict, since falling back would leave a test
|
|
35
|
+
* green on SQL the service rejects.
|
|
25
36
|
*/
|
|
26
37
|
export declare function simAthenaSqliteSql(request: SimAthenaSqlTranslationRequest): SimAthenaTranslatedSql;
|
|
27
38
|
export {};
|
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import { simAthenaSqlForParser, simAthenaSqlForSqlite, } from "./sim-athena-sql-rewrites.js";
|
|
2
2
|
import { simAthenaUnparsedStatement, simAthenaUnrewrittenUnnest, simAthenaUnwrittenStatement, } from "./sim-athena-turn-down.js";
|
|
3
|
+
import { simAthenaRejectedStatement } from "./sim-athena-rejected-statement.js";
|
|
3
4
|
import { simAthenaRewriteUnnest } from "./sim-athena-unnest-rewrite.js";
|
|
4
5
|
/**
|
|
5
|
-
* One Athena statement written back out for SQLite, or why it
|
|
6
|
+
* One Athena statement written back out for SQLite, or why it is not.
|
|
6
7
|
*
|
|
7
|
-
* Three things
|
|
8
|
-
* the query with the reason. The Athena grammar can refuse the
|
|
9
|
-
* `UNNEST` in it can have no `json_each` to become, and the
|
|
10
|
-
* to write the statement back out. A lenient engine answers
|
|
11
|
-
*
|
|
8
|
+
* Three things turn the query down, and each is told apart because a strict
|
|
9
|
+
* engine fails the query with the reason. The Athena grammar can refuse the
|
|
10
|
+
* statement, an `UNNEST` in it can have no `json_each` to become, and the
|
|
11
|
+
* parser can decline to write the statement back out. A lenient engine answers
|
|
12
|
+
* all three the same way, by letting the declared result take the query.
|
|
13
|
+
*
|
|
14
|
+
* A rejection is the fourth outcome and a different thing. The statement
|
|
15
|
+
* parsed, and Athena would still refuse to run it. That fails the query
|
|
16
|
+
* whether or not the engine is strict, since falling back would leave a test
|
|
17
|
+
* green on SQL the service rejects.
|
|
12
18
|
*/
|
|
13
19
|
export function simAthenaSqliteSql(request) {
|
|
14
20
|
const { parser } = request;
|
|
@@ -21,6 +27,10 @@ export function simAthenaSqliteSql(request) {
|
|
|
21
27
|
catch {
|
|
22
28
|
return turnedDown(simAthenaUnparsedStatement);
|
|
23
29
|
}
|
|
30
|
+
const rejected = simAthenaRejectedStatement(ast);
|
|
31
|
+
if (rejected !== undefined) {
|
|
32
|
+
return { sql: undefined, turnedDown: undefined, rejected };
|
|
33
|
+
}
|
|
24
34
|
try {
|
|
25
35
|
if (!simAthenaRewriteUnnest({
|
|
26
36
|
ast,
|
|
@@ -32,6 +42,7 @@ export function simAthenaSqliteSql(request) {
|
|
|
32
42
|
return {
|
|
33
43
|
sql: simAthenaSqlForSqlite(parser.sqlify(ast, { database: "sqlite" })),
|
|
34
44
|
turnedDown: undefined,
|
|
45
|
+
rejected: undefined,
|
|
35
46
|
};
|
|
36
47
|
}
|
|
37
48
|
catch {
|
|
@@ -39,5 +50,5 @@ export function simAthenaSqliteSql(request) {
|
|
|
39
50
|
}
|
|
40
51
|
}
|
|
41
52
|
function turnedDown(reason) {
|
|
42
|
-
return { sql: undefined, turnedDown: reason };
|
|
53
|
+
return { sql: undefined, turnedDown: reason, rejected: undefined };
|
|
43
54
|
}
|
|
@@ -48,6 +48,10 @@ interface SimAthenaQueryAnswerRequest {
|
|
|
48
48
|
* A strict engine is the exception, and it fails the query it turned down.
|
|
49
49
|
* The declaration keeps its place ahead of that, since a test writing one
|
|
50
50
|
* against this exact statement has already said what the engine gets wrong.
|
|
51
|
+
*
|
|
52
|
+
* A statement Athena refuses fails whichever mode the engine is in. That is
|
|
53
|
+
* SQL the service would never have run, and falling back would leave the query
|
|
54
|
+
* green on it.
|
|
51
55
|
*/
|
|
52
56
|
export declare function simAthenaQueryAnswer(request: SimAthenaQueryAnswerRequest): Promise<SimAthenaQueryAnswer>;
|
|
53
57
|
export {};
|
|
@@ -15,6 +15,10 @@ import { simAthenaStrictRefusal } from "../engine/sim-athena-turn-down.js";
|
|
|
15
15
|
* A strict engine is the exception, and it fails the query it turned down.
|
|
16
16
|
* The declaration keeps its place ahead of that, since a test writing one
|
|
17
17
|
* against this exact statement has already said what the engine gets wrong.
|
|
18
|
+
*
|
|
19
|
+
* A statement Athena refuses fails whichever mode the engine is in. That is
|
|
20
|
+
* SQL the service would never have run, and falling back would leave the query
|
|
21
|
+
* green on it.
|
|
18
22
|
*/
|
|
19
23
|
export async function simAthenaQueryAnswer(request) {
|
|
20
24
|
const forQuery = request.results.declaredForQuery(request.queryString);
|
|
@@ -35,7 +39,7 @@ export async function simAthenaQueryAnswer(request) {
|
|
|
35
39
|
return {
|
|
36
40
|
result: request.declared,
|
|
37
41
|
source: "declaration",
|
|
38
|
-
refusal: strictRefusal(request.engine, computed.turnedDown),
|
|
42
|
+
refusal: computed.rejected ?? strictRefusal(request.engine, computed.turnedDown),
|
|
39
43
|
};
|
|
40
44
|
}
|
|
41
45
|
/**
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { SimCfnResource } from "../../resource/sim-cfn-resource.js";
|
|
2
|
+
import type { SimCfnTemplateValueRecord } from "../../template/value/sim-cfn-template-value.js";
|
|
3
|
+
import { type SimLambdaFunctionArn } from "../../../lambda/function/sim-lambda-function-configuration.js";
|
|
4
|
+
/**
|
|
5
|
+
* The provider function a custom Resource's `ServiceToken` names, as an ARN.
|
|
6
|
+
*
|
|
7
|
+
* Nothing comes back where the Resource names no provider this Stack can put
|
|
8
|
+
* an ARN to. A `ServiceToken` built from a Parameter, or one pointing at a
|
|
9
|
+
* Resource type that is not a function, both leave the deployment with nothing
|
|
10
|
+
* to authorize against, and inventing an ARN for it would refuse a template
|
|
11
|
+
* over a permission real CloudFormation never asked for.
|
|
12
|
+
*/
|
|
13
|
+
export declare function simCdkProviderInvokeArn(resolvedProperties: SimCfnTemplateValueRecord, resources: ReadonlyMap<string, SimCfnResource>): SimLambdaFunctionArn | undefined;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { namesSimCfnUnansweredAttribute } from "../../resource/cfn/sim-cfn-unanswered-attribute.js";
|
|
2
|
+
import { parseSimLambdaFunctionArn } from "../../../lambda/function/sim-lambda-function-arn-parts.js";
|
|
3
|
+
import { simLambdaFunctionArn, } from "../../../lambda/function/sim-lambda-function-configuration.js";
|
|
4
|
+
const lambdaFunctionResourceType = "AWS::Lambda::Function";
|
|
5
|
+
/**
|
|
6
|
+
* The provider function a custom Resource's `ServiceToken` names, as an ARN.
|
|
7
|
+
*
|
|
8
|
+
* Nothing comes back where the Resource names no provider this Stack can put
|
|
9
|
+
* an ARN to. A `ServiceToken` built from a Parameter, or one pointing at a
|
|
10
|
+
* Resource type that is not a function, both leave the deployment with nothing
|
|
11
|
+
* to authorize against, and inventing an ARN for it would refuse a template
|
|
12
|
+
* over a permission real CloudFormation never asked for.
|
|
13
|
+
*/
|
|
14
|
+
export function simCdkProviderInvokeArn(resolvedProperties, resources) {
|
|
15
|
+
const serviceToken = resolvedProperties["ServiceToken"];
|
|
16
|
+
if (typeof serviceToken !== "string" || serviceToken === "") {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
return (writtenFunctionArn(serviceToken) ??
|
|
20
|
+
standInFunctionArn(serviceToken, resources));
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The ARN a `ServiceToken` already carries.
|
|
24
|
+
*
|
|
25
|
+
* A provider deployed outside this Stack is named by its ARN outright, and a
|
|
26
|
+
* template written by hand can name one in the same Stack that way too.
|
|
27
|
+
*/
|
|
28
|
+
function writtenFunctionArn(serviceToken) {
|
|
29
|
+
return parseSimLambdaFunctionArn(serviceToken) === undefined
|
|
30
|
+
? undefined
|
|
31
|
+
: serviceToken;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The ARN behind a `ServiceToken` that resolved to an attribute stand-in.
|
|
35
|
+
*
|
|
36
|
+
* CDK writes `ServiceToken` as an `Fn::GetAtt` for the provider function's
|
|
37
|
+
* `Arn`, and this simulator leaves that provider uncreated: the function is
|
|
38
|
+
* declined on its Python runtime and then reported as scaffolding for the
|
|
39
|
+
* custom Resource the simulator carries out itself. An uncreated Resource
|
|
40
|
+
* answers `Fn::GetAtt` with {@link namesSimCfnUnansweredAttribute}'s stand-in
|
|
41
|
+
* rather than an ARN.
|
|
42
|
+
*
|
|
43
|
+
* The name survives that refusal. Sim Lambda works out the name real
|
|
44
|
+
* CloudFormation would have given the function before declining it, so the
|
|
45
|
+
* Resource's `Ref` holds a function name to build the ARN from. A provider
|
|
46
|
+
* that reached neither creation nor a name has only its logical ID behind
|
|
47
|
+
* `Ref`, and an ARN built from that would be denied by a policy naming the
|
|
48
|
+
* function real CloudFormation invokes.
|
|
49
|
+
*/
|
|
50
|
+
function standInFunctionArn(serviceToken, resources) {
|
|
51
|
+
if (!namesSimCfnUnansweredAttribute(serviceToken, resources.keys())) {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
const provider = resources.get(serviceToken.split(".", 1)[0] ?? "");
|
|
55
|
+
if (provider?.type !== lambdaFunctionResourceType ||
|
|
56
|
+
(!provider.deployed && provider.uncreatedPhysicalName === undefined)) {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
const { refValue } = provider;
|
|
60
|
+
return typeof refValue === "string" && refValue !== ""
|
|
61
|
+
? simLambdaFunctionArn(provider.accountRegionScope, refValue)
|
|
62
|
+
: undefined;
|
|
63
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { SimAws } from "../../../aws/sim-aws.js";
|
|
2
|
+
import type { SimAwsCaller } from "../../../aws/caller/sim-aws-caller.js";
|
|
3
|
+
import type { SimCloudFormationParsedResourceType } from "../../resource/factory/sim-cfn-resource-factory.type.js";
|
|
4
|
+
import type { SimCfnResource } from "../../resource/sim-cfn-resource.js";
|
|
5
|
+
import type { SimCfnTemplateValueRecord } from "../../template/value/sim-cfn-template-value.js";
|
|
6
|
+
/**
|
|
7
|
+
* What authorizing one custom Resource's provider invoke needs.
|
|
8
|
+
*/
|
|
9
|
+
export interface SimCdkProviderInvokeAuthZProperties {
|
|
10
|
+
readonly resourceType: SimCloudFormationParsedResourceType;
|
|
11
|
+
readonly resource: SimCfnResource;
|
|
12
|
+
readonly resolvedProperties: SimCfnTemplateValueRecord;
|
|
13
|
+
readonly resources: ReadonlyMap<string, SimCfnResource>;
|
|
14
|
+
readonly simAws: SimAws;
|
|
15
|
+
readonly caller?: SimAwsCaller | undefined;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Authorize the invoke a custom Resource costs the deployment.
|
|
19
|
+
*
|
|
20
|
+
* Real CloudFormation answers a `Custom::` Resource by invoking the function
|
|
21
|
+
* its `ServiceToken` names, as the Stack's execution role. That invoke is the
|
|
22
|
+
* first thing a deployment pays for, and a role without `lambda:InvokeFunction`
|
|
23
|
+
* on the provider fails the Stack there rather than at the work the provider
|
|
24
|
+
* would have done.
|
|
25
|
+
*
|
|
26
|
+
* This simulator carries several custom Resources out itself instead of
|
|
27
|
+
* running the provider, which leaves the invoke unpaid for unless it is
|
|
28
|
+
* authorized here. A deploy Role standing in for a real CloudFormation
|
|
29
|
+
* execution policy is meant to fail the same template the real one refuses.
|
|
30
|
+
*
|
|
31
|
+
* Only the caller's identity policies decide it. The provider function is
|
|
32
|
+
* uncreated in the usual case, so there is no simulated function holding a
|
|
33
|
+
* resource policy to consult, and CDK grants the invoke on the execution role
|
|
34
|
+
* anyway.
|
|
35
|
+
*/
|
|
36
|
+
export declare function authorizeSimCdkProviderInvoke(properties: SimCdkProviderInvokeAuthZProperties): void;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { simScopeIamAuthZ } from "../../../iam/authorize/sim-iam-region-auth-z.js";
|
|
2
|
+
import { SimIamAccessDenied } from "../../../iam/error/sim-iam.error.js";
|
|
3
|
+
import { simCdkProviderInvokeArn } from "./sim-cdk-provider-invoke-arn.js";
|
|
4
|
+
const customResourceProviderName = "Custom";
|
|
5
|
+
const invokeAction = "lambda:InvokeFunction";
|
|
6
|
+
/**
|
|
7
|
+
* Authorize the invoke a custom Resource costs the deployment.
|
|
8
|
+
*
|
|
9
|
+
* Real CloudFormation answers a `Custom::` Resource by invoking the function
|
|
10
|
+
* its `ServiceToken` names, as the Stack's execution role. That invoke is the
|
|
11
|
+
* first thing a deployment pays for, and a role without `lambda:InvokeFunction`
|
|
12
|
+
* on the provider fails the Stack there rather than at the work the provider
|
|
13
|
+
* would have done.
|
|
14
|
+
*
|
|
15
|
+
* This simulator carries several custom Resources out itself instead of
|
|
16
|
+
* running the provider, which leaves the invoke unpaid for unless it is
|
|
17
|
+
* authorized here. A deploy Role standing in for a real CloudFormation
|
|
18
|
+
* execution policy is meant to fail the same template the real one refuses.
|
|
19
|
+
*
|
|
20
|
+
* Only the caller's identity policies decide it. The provider function is
|
|
21
|
+
* uncreated in the usual case, so there is no simulated function holding a
|
|
22
|
+
* resource policy to consult, and CDK grants the invoke on the execution role
|
|
23
|
+
* anyway.
|
|
24
|
+
*/
|
|
25
|
+
export function authorizeSimCdkProviderInvoke(properties) {
|
|
26
|
+
if (properties.resourceType.providerName !== customResourceProviderName) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const functionArn = simCdkProviderInvokeArn(properties.resolvedProperties, properties.resources);
|
|
30
|
+
if (functionArn === undefined) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const { accountId, regionName } = properties.resource.accountRegionScope;
|
|
34
|
+
const decision = simScopeIamAuthZ(properties.simAws.accountRegionScope(accountId, regionName)).authorize({
|
|
35
|
+
action: invokeAction,
|
|
36
|
+
resource: functionArn,
|
|
37
|
+
caller: properties.caller,
|
|
38
|
+
});
|
|
39
|
+
if (decision.isDenied) {
|
|
40
|
+
throw new SimIamAccessDenied({
|
|
41
|
+
principal: decision.caller.principal,
|
|
42
|
+
reason: decision.denialReason,
|
|
43
|
+
action: invokeAction,
|
|
44
|
+
resource: functionArn,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -2,9 +2,11 @@ import { bucketNotificationsError } from "../error/sim-cdk-bucket-notification-e
|
|
|
2
2
|
/**
|
|
3
3
|
* The properties CDK puts on a Custom::S3BucketNotifications Resource.
|
|
4
4
|
*
|
|
5
|
-
* `ServiceToken` is
|
|
6
|
-
* function, whose whole job is the
|
|
7
|
-
* factory makes instead.
|
|
5
|
+
* `ServiceToken` is accepted and read no further here. It points at CDK's own
|
|
6
|
+
* Python provider function, whose whole job is the
|
|
7
|
+
* PutBucketNotificationConfiguration call this factory makes instead. The
|
|
8
|
+
* deployment reads it once more, to authorize the invoke real CloudFormation
|
|
9
|
+
* makes of that function.
|
|
8
10
|
*/
|
|
9
11
|
const knownPropertyNames = new Set([
|
|
10
12
|
"BucketName",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { bucketNotificationsError } from "./error/sim-cdk-bucket-notification-error.js";
|
|
2
2
|
import { SimCdkBucketNotificationProperties } from "./property/sim-cdk-bucket-notification-properties.js";
|
|
3
|
+
import { simCfnResourceCallerOptions } from "../../../resource/caller/sim-cfn-resource-caller-options.js";
|
|
3
4
|
/**
|
|
4
5
|
* Removes the notification configuration a Custom::S3BucketNotifications
|
|
5
6
|
* Resource put on a Bucket.
|
|
@@ -29,6 +30,6 @@ export class SimCdkBucketNotificationsRemover {
|
|
|
29
30
|
Bucket: properties.bucketName,
|
|
30
31
|
NotificationConfiguration: {},
|
|
31
32
|
},
|
|
32
|
-
});
|
|
33
|
+
}, simCfnResourceCallerOptions(context.caller));
|
|
33
34
|
}
|
|
34
35
|
}
|
package/dist/service/cloudformation/cdk/s3/bucket-notifications/sim-cdk-bucket-notifications.d.ts
CHANGED
|
@@ -16,10 +16,13 @@ import type { SimCfnResource, SimCloudFormationResourceCreateContext, SimCloudFo
|
|
|
16
16
|
* bucket alongside the function's `AWS::Lambda::Permission` is the circular
|
|
17
17
|
* dependency this Resource type exists to break.
|
|
18
18
|
*
|
|
19
|
-
* `ServiceToken` is read for the provider function it names
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
19
|
+
* `ServiceToken` is read for the provider function it names. That function is
|
|
20
|
+
* declined on its runtime, as CDK's BucketDeployment provider is, and recorded
|
|
21
|
+
* as inert rather than as a gap, since this factory has already made the call
|
|
22
|
+
* it would have made. The deployment still pays for the invoke real
|
|
23
|
+
* CloudFormation makes, which
|
|
24
|
+
* {@link import("../../provider/sim-cdk-provider-invoke-auth-z.js").authorizeSimCdkProviderInvoke}
|
|
25
|
+
* authorizes for every custom Resource ahead of its factory.
|
|
23
26
|
*/
|
|
24
27
|
export declare class SimCdkBucketNotificationsResourceFactory implements SimCfnServiceResourceFactory {
|
|
25
28
|
/**
|
package/dist/service/cloudformation/cdk/s3/bucket-notifications/sim-cdk-bucket-notifications.js
CHANGED
|
@@ -2,6 +2,7 @@ import { SimCdkBucketNotificationConfiguration } from "./configuration/sim-cdk-b
|
|
|
2
2
|
import { bucketNotificationsError } from "./error/sim-cdk-bucket-notification-error.js";
|
|
3
3
|
import { SimCdkBucketNotificationProperties } from "./property/sim-cdk-bucket-notification-properties.js";
|
|
4
4
|
import { SimCdkBucketNotificationsRemover } from "./sim-cdk-bucket-notifications-remover.js";
|
|
5
|
+
import { simCfnResourceCallerOptions } from "../../../resource/caller/sim-cfn-resource-caller-options.js";
|
|
5
6
|
/**
|
|
6
7
|
* CloudFormation Resource factory for CDK Bucket event notifications.
|
|
7
8
|
*
|
|
@@ -18,10 +19,13 @@ import { SimCdkBucketNotificationsRemover } from "./sim-cdk-bucket-notifications
|
|
|
18
19
|
* bucket alongside the function's `AWS::Lambda::Permission` is the circular
|
|
19
20
|
* dependency this Resource type exists to break.
|
|
20
21
|
*
|
|
21
|
-
* `ServiceToken` is read for the provider function it names
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* `ServiceToken` is read for the provider function it names. That function is
|
|
23
|
+
* declined on its runtime, as CDK's BucketDeployment provider is, and recorded
|
|
24
|
+
* as inert rather than as a gap, since this factory has already made the call
|
|
25
|
+
* it would have made. The deployment still pays for the invoke real
|
|
26
|
+
* CloudFormation makes, which
|
|
27
|
+
* {@link import("../../provider/sim-cdk-provider-invoke-auth-z.js").authorizeSimCdkProviderInvoke}
|
|
28
|
+
* authorizes for every custom Resource ahead of its factory.
|
|
25
29
|
*/
|
|
26
30
|
export class SimCdkBucketNotificationsResourceFactory {
|
|
27
31
|
/**
|
|
@@ -48,7 +52,7 @@ export class SimCdkBucketNotificationsResourceFactory {
|
|
|
48
52
|
NotificationConfiguration: configuration,
|
|
49
53
|
SkipDestinationValidation: properties.skipDestinationValidation,
|
|
50
54
|
},
|
|
51
|
-
});
|
|
55
|
+
}, simCfnResourceCallerOptions(context.caller));
|
|
52
56
|
return undefined;
|
|
53
57
|
}
|
|
54
58
|
/**
|
|
@@ -5,9 +5,10 @@ import { crossRegionParameterError } from "./sim-cdk-cross-region-parameter-erro
|
|
|
5
5
|
* The properties CDK puts on a Custom::CrossRegionStringParameterReader
|
|
6
6
|
* Resource.
|
|
7
7
|
*
|
|
8
|
-
* `ServiceToken` is
|
|
9
|
-
* function, whose whole job is the GetParameter call this factory
|
|
10
|
-
* instead
|
|
8
|
+
* `ServiceToken` is accepted and read no further here. It points at CDK's own
|
|
9
|
+
* provider function, whose whole job is the GetParameter call this factory
|
|
10
|
+
* makes instead, and the deployment reads it once more to authorize the invoke
|
|
11
|
+
* real CloudFormation makes of that function. `RefreshToken` is the logical ID of the function version the
|
|
11
12
|
* parameter was written from, and CDK puts it there so that publishing a new
|
|
12
13
|
* version makes CloudFormation run the reader again. The reader here runs on
|
|
13
14
|
* every deployment either way, so the token has nothing left to say.
|
|
@@ -2,6 +2,7 @@ import { SimSsmParameterNotFound } from "../../../../ssm/error/sim-ssm.error.js"
|
|
|
2
2
|
import { crossRegionParameterError, crossRegionParameterResourceTypeName, } from "./sim-cdk-cross-region-parameter-error.js";
|
|
3
3
|
import { SimCdkCrossRegionParameterProperties } from "./sim-cdk-cross-region-parameter-properties.js";
|
|
4
4
|
import { SimCdkCrossRegionParameterReading } from "./sim-cdk-cross-region-parameter-reading.js";
|
|
5
|
+
import { simCfnResourceCallerOptions } from "../../../resource/caller/sim-cfn-resource-caller-options.js";
|
|
5
6
|
/**
|
|
6
7
|
* CloudFormation Resource factory for CDK's cross-Region parameter reader.
|
|
7
8
|
*
|
|
@@ -64,7 +65,7 @@ export class SimCdkCrossRegionParameterReaderResourceFactory {
|
|
|
64
65
|
const output = await context.simAws
|
|
65
66
|
.accountRegionScope(resource.accountRegionScope.accountId, regionName)
|
|
66
67
|
.ssm()
|
|
67
|
-
.getParameter({ input: { Name: parameterName } });
|
|
68
|
+
.getParameter({ input: { Name: parameterName } }, simCfnResourceCallerOptions(context.caller));
|
|
68
69
|
return output.Parameter?.Value;
|
|
69
70
|
}
|
|
70
71
|
catch (error) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { parseSimCloudFormationResourceType } from "../parser/sim-cfn-resource-parser.js";
|
|
2
2
|
import { resolveSimCloudFormationServiceResourceFactory } from "../resolve/service/sim-cfn-service-resolver.js";
|
|
3
3
|
import { assertDefined } from "../../../../util/type-guard/defined.js";
|
|
4
|
+
import { authorizeSimCdkProviderInvoke } from "../../cdk/provider/sim-cdk-provider-invoke-auth-z.js";
|
|
4
5
|
/**
|
|
5
6
|
* Creates the underlying simulated AWS service Resource for a CloudFormation
|
|
6
7
|
* Resource.
|
|
@@ -35,9 +36,18 @@ export class SimCfnResourceCreator {
|
|
|
35
36
|
const resourceType = parseSimCloudFormationResourceType(type);
|
|
36
37
|
const factory = this.cfnResourceFactory ??
|
|
37
38
|
resolveSimCloudFormationServiceResourceFactory(context.simAws, this.resource.accountRegionScope, resourceType);
|
|
39
|
+
const resolvedProperties = await this.resource.resolvedProperties(context);
|
|
40
|
+
authorizeSimCdkProviderInvoke({
|
|
41
|
+
resourceType,
|
|
42
|
+
resource: this.resource,
|
|
43
|
+
resolvedProperties,
|
|
44
|
+
resources: context.resources,
|
|
45
|
+
simAws: context.simAws,
|
|
46
|
+
caller: context.caller,
|
|
47
|
+
});
|
|
38
48
|
const resolvedContext = {
|
|
39
49
|
...context,
|
|
40
|
-
resolvedProperties
|
|
50
|
+
resolvedProperties,
|
|
41
51
|
};
|
|
42
52
|
return await factory.create(resourceType.resourceTypeName, this.resource, resolvedContext);
|
|
43
53
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { parseSimCloudFormationResourceType } from "../parser/sim-cfn-resource-parser.js";
|
|
2
2
|
import { resolveSimCloudFormationServiceResourceFactory } from "../resolve/service/sim-cfn-service-resolver.js";
|
|
3
3
|
import { assertDefined } from "../../../../util/type-guard/defined.js";
|
|
4
|
+
import { authorizeSimCdkProviderInvoke } from "../../cdk/provider/sim-cdk-provider-invoke-auth-z.js";
|
|
4
5
|
/**
|
|
5
6
|
* Removes the underlying simulated AWS service Resource for a CloudFormation
|
|
6
7
|
* Resource.
|
|
@@ -39,9 +40,18 @@ export class SimCfnResourceDeleter {
|
|
|
39
40
|
const resourceType = parseSimCloudFormationResourceType(type);
|
|
40
41
|
const factory = this.cfnResourceFactory ??
|
|
41
42
|
resolveSimCloudFormationServiceResourceFactory(context.simAws, this.resource.accountRegionScope, resourceType);
|
|
43
|
+
const resolvedProperties = await this.resource.resolvedProperties(context);
|
|
44
|
+
authorizeSimCdkProviderInvoke({
|
|
45
|
+
resourceType,
|
|
46
|
+
resource: this.resource,
|
|
47
|
+
resolvedProperties,
|
|
48
|
+
resources: context.resources,
|
|
49
|
+
simAws: context.simAws,
|
|
50
|
+
caller: context.caller,
|
|
51
|
+
});
|
|
42
52
|
await factory.delete(resourceType.resourceTypeName, this.resource, {
|
|
43
53
|
...context,
|
|
44
|
-
resolvedProperties
|
|
54
|
+
resolvedProperties,
|
|
45
55
|
});
|
|
46
56
|
}
|
|
47
57
|
}
|
|
@@ -381,6 +381,36 @@ Strict mode is off unless a test asks for it, and `disable()` takes it off with
|
|
|
381
381
|
declaration written against one exact query text still answers, with no failure, because that is a
|
|
382
382
|
test's own statement about a query the engine gets wrong.
|
|
383
383
|
|
|
384
|
+
### Failing a statement Athena refuses
|
|
385
|
+
|
|
386
|
+
One failure ignores strict mode. Where the engine can see that Athena would refuse a statement
|
|
387
|
+
outright, the query fails whether strict mode is on or off. A turn-down means the engine reached its
|
|
388
|
+
limits, and a declaration is a fair answer to that. A rejection means the SQL is invalid, and
|
|
389
|
+
answering from a declaration would leave a test green on a query that fails against AWS.
|
|
390
|
+
|
|
391
|
+
The one shape it catches today is an `ORDER BY` that aggregates over an output alias which is itself
|
|
392
|
+
an aggregate.
|
|
393
|
+
|
|
394
|
+
```sql
|
|
395
|
+
SELECT sku, coalesce(sum(qty), 0) AS qty
|
|
396
|
+
FROM rainlytics.lines
|
|
397
|
+
GROUP BY sku
|
|
398
|
+
ORDER BY sum(qty) DESC
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
Trino resolves the bare `qty` in `ORDER BY` against the select list before the table. The sort then
|
|
402
|
+
asks for an aggregate over an aggregate, and Athena answers `COLUMN_NOT_FOUND: Invalid reference to
|
|
403
|
+
output projection attribute from ORDER BY aggregation`. SQLite resolves the same name against the
|
|
404
|
+
table's column, runs the statement and orders it correctly. That is how a green test shipped a
|
|
405
|
+
command that failed against the service.
|
|
406
|
+
|
|
407
|
+
`ORDER BY qty DESC` over the same select list is the form Athena takes, and it is what to write.
|
|
408
|
+
`ORDER BY sum(revenue_total) DESC` beside `coalesce(sum(revenue_total), 0) AS revenue` is fine as
|
|
409
|
+
well, since the two names differ and nothing shadows.
|
|
410
|
+
|
|
411
|
+
Trino and SQLite resolve names by different rules in other places too, and the engine catches this
|
|
412
|
+
one. A statement that behaves differently under the two is worth reporting.
|
|
413
|
+
|
|
384
414
|
### The objects it reads
|
|
385
415
|
|
|
386
416
|
The SerDe class name in the table's storage descriptor says how its objects are decoded.
|
|
@@ -2549,6 +2549,94 @@ working simulation with a hand-written one.
|
|
|
2549
2549
|
The provider is found through the `ServiceToken` its custom resource names it by, not by the logical
|
|
2550
2550
|
ID CDK generated for it. That ID is a hash of the construct path, and no kind of thing to match on.
|
|
2551
2551
|
|
|
2552
|
+
### What a custom resource costs the deploy Role
|
|
2553
|
+
|
|
2554
|
+
Real CloudFormation answers a `Custom::` resource by invoking the provider function its `ServiceToken`
|
|
2555
|
+
names, as the stack's execution role. Yulin carries several of these out itself instead of running the
|
|
2556
|
+
provider, and it authorizes that invoke anyway. A deploy Role holding no `lambda:InvokeFunction` on the
|
|
2557
|
+
provider fails the stack where the real deployment fails.
|
|
2558
|
+
|
|
2559
|
+
The `ServiceToken` decides the ARN. A token written as an ARN is taken as it stands. CDK's `Fn::GetAtt`
|
|
2560
|
+
for the provider's `Arn` is resolved back to the function the stack declares, since the provider is
|
|
2561
|
+
left uncreated and has no ARN of its own to give.
|
|
2562
|
+
|
|
2563
|
+
The work the resource does is authorized too, as the deployment's principal.
|
|
2564
|
+
`Custom::S3BucketNotifications` costs `s3:PutBucketNotification` and
|
|
2565
|
+
`Custom::CrossRegionStringParameterReader` costs `ssm:GetParameter`. `Custom::CDKBucketDeployment`
|
|
2566
|
+
writes its Objects into the simulated Bucket without sending `PutObject`, so nothing there reaches IAM.
|
|
2567
|
+
|
|
2568
|
+
```typescript sim-cloudformation-custom-resource-invoke
|
|
2569
|
+
/**
|
|
2570
|
+
* A deploy Role refused the provider invoke its custom resource needs.
|
|
2571
|
+
*/
|
|
2572
|
+
|
|
2573
|
+
import { SimAws } from "@kensio/yulin";
|
|
2574
|
+
|
|
2575
|
+
const simAws = new SimAws();
|
|
2576
|
+
|
|
2577
|
+
const { Role } = await simAws.iam().createRole({
|
|
2578
|
+
input: {
|
|
2579
|
+
RoleName: "DeployRole",
|
|
2580
|
+
AssumeRolePolicyDocument: JSON.stringify({
|
|
2581
|
+
Version: "2012-10-17",
|
|
2582
|
+
Statement: [
|
|
2583
|
+
{
|
|
2584
|
+
Effect: "Allow",
|
|
2585
|
+
Principal: { Service: "cloudformation.amazonaws.com" },
|
|
2586
|
+
Action: "sts:AssumeRole",
|
|
2587
|
+
},
|
|
2588
|
+
],
|
|
2589
|
+
}),
|
|
2590
|
+
},
|
|
2591
|
+
});
|
|
2592
|
+
|
|
2593
|
+
// Everything the template needs apart from the invoke of the provider.
|
|
2594
|
+
await simAws.iam().putRolePolicy({
|
|
2595
|
+
input: {
|
|
2596
|
+
RoleName: "DeployRole",
|
|
2597
|
+
PolicyName: "DeployPolicy",
|
|
2598
|
+
PolicyDocument: JSON.stringify({
|
|
2599
|
+
Version: "2012-10-17",
|
|
2600
|
+
Statement: [
|
|
2601
|
+
{
|
|
2602
|
+
Effect: "Allow",
|
|
2603
|
+
Action: ["cloudformation:*", "s3:*"],
|
|
2604
|
+
Resource: "*",
|
|
2605
|
+
},
|
|
2606
|
+
],
|
|
2607
|
+
}),
|
|
2608
|
+
},
|
|
2609
|
+
});
|
|
2610
|
+
|
|
2611
|
+
try {
|
|
2612
|
+
await simAws.cloudFormation().deployTemplate({
|
|
2613
|
+
stackName: "uploads-stack",
|
|
2614
|
+
caller: { kind: "arn", arn: Role.Arn },
|
|
2615
|
+
template: {
|
|
2616
|
+
Resources: {
|
|
2617
|
+
Bucket: {
|
|
2618
|
+
Type: "AWS::S3::Bucket",
|
|
2619
|
+
Properties: { BucketName: "uploads" },
|
|
2620
|
+
},
|
|
2621
|
+
BucketNotifications: {
|
|
2622
|
+
Type: "Custom::S3BucketNotifications",
|
|
2623
|
+
Properties: {
|
|
2624
|
+
ServiceToken: "arn:aws:lambda:us-east-1:888888888888:function:cdk",
|
|
2625
|
+
BucketName: { Ref: "Bucket" },
|
|
2626
|
+
NotificationConfiguration: {},
|
|
2627
|
+
Managed: true,
|
|
2628
|
+
},
|
|
2629
|
+
},
|
|
2630
|
+
},
|
|
2631
|
+
},
|
|
2632
|
+
});
|
|
2633
|
+
} catch (error) {
|
|
2634
|
+
// is not authorized to perform: lambda:InvokeFunction on resource:
|
|
2635
|
+
// arn:aws:lambda:us-east-1:888888888888:function:cdk
|
|
2636
|
+
console.log((error as Error).message);
|
|
2637
|
+
}
|
|
2638
|
+
```
|
|
2639
|
+
|
|
2552
2640
|
## S3 Bucket notifications
|
|
2553
2641
|
|
|
2554
2642
|
The `NotificationConfiguration` property of `AWS::S3::Bucket` deploys through the ordinary
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kensio/yulin",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.19",
|
|
4
4
|
"description": "AWS system behaviour simulation for isolated unit testing",
|
|
5
5
|
"repository": "https://github.com/KensioSoftware/yulin",
|
|
6
6
|
"homepage": "https://yulinsim.dev/",
|
|
@@ -299,7 +299,7 @@
|
|
|
299
299
|
"hyparquet": "^1.29.2",
|
|
300
300
|
"hyparquet-writer": "^0.16.9",
|
|
301
301
|
"node-sql-parser": "^5.4.0",
|
|
302
|
-
"oxfmt": "^0.
|
|
302
|
+
"oxfmt": "^0.66.0",
|
|
303
303
|
"oxlint": "^1.77.0",
|
|
304
304
|
"oxlint-tsgolint": "^7.0.2001",
|
|
305
305
|
"semantic-release": "25.0.9",
|