-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.pkl
More file actions
429 lines (421 loc) · 16.9 KB
/
Copy pathapp.pkl
File metadata and controls
429 lines (421 loc) · 16.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
amends "@app-contract-toolkit/App.pkl"
import "@app-contract-toolkit/Connectors.pkl"
name = "openapi"
displayName = "OpenAPI Spec Loader"
connector = Connectors.API
icon = "http://assets.atlan.com/assets/apispec.png"
workflowType = name
hasCredentialConfig = true
// app_id maps the contract to its Global Marketplace app record. It is emitted
// as a top-level `app_id:` key in atlan.yaml and read by the publish step
// (application-sdk build-and-publish-app.yaml → parse_atlan_yaml.py). Without
// it the publish POST sends an empty app_id and the GM returns 404, silently
// breaking every semver release (regression introduced when atlan.yaml became
// fully pkl-generated; see CONNECT release-pipeline fix).
metadata {
["release_model"] = "semver"
["app_id"] = "019d1f6b-6fea-7db3-96d8-e61e159d0351"
}
// The top-level `execution_mode: native` above is NOT enough on its own: it is
// synced to the app catalog (/marketplace/apps) but is not part of the version
// config. atlan-cli publishes only the `deploy:` block as the version config
// (readDeployConfig in pkg/atlan/app_workflow.go serialises jsonData["deploy"]
// and sends it as `config`), and canary reads execution mode from that config.
// With no `deploy:` block at all, canary saw cfgExecutionMode: null and fell
// back to argo — silently running this app on the legacy orchestrator.
//
// Setting `deploy` (DeployConfig? — null by default, "null = Heracles defaults")
// is what makes the toolkit emit a `deploy:` block. An empty DeployConfig emits
// exactly two keys, both ungated in renderDeployYaml: `execution_mode` (native)
// and `splitDeploymentEnabled` (true). The latter is the toolkit default and
// matches every other native app in the fleet. Note it was previously left to
// the Heracles default rather than pinned here, so it is now explicit where it
// was implicit — worth an eyeball on the first deploy.
deploy = new DeployConfig {}
// CONNECT-55: replace the auto-generated publish node to add assertion-only
// mode. Defining a "publish" key here REPLACES the default node (it does not
// merge with the auto-generation layer), so the four cache / current-state
// args the default node enables must be restated explicitly to preserve them —
// only `assertion_only_enabled` is genuinely new.
//
// The connector emits `assertion_only_enabled` in its output (True when
// connection_usage=REUSE); AE resolves the JSONPath at dispatch. When True,
// publish-app skips the diff entirely and forwards the transformed rows as
// pure upserts (no archival) — see atlan-publish-app assertion-only contract.
extraNodes {
["publish"] = new PublishNode {
args {
["connection_cache_enabled"] = true
["connection_cache_via_app_enabled"] = true
["current_state_enabled"] = true
["current_state_via_app_enabled"] = true
["assertion_only_enabled"] = "$.extract.outputs.assertion_only_enabled"
}
}
}
// ---- Credential config (object store) ----
//
// The object-store credential the CLOUD import path uses. Generated by this
// contract as app/generated/atlan-connectors-openapi.json (the app's own
// credential configmap) + _e2e_credential.py, loaded via the standard
// credential route (the generated `openapi_credential: CredentialRef`). This
// replaces the former standalone contract/csa-connectors-objectstore.pkl
// (legacy Credential.pkl route) — same fields, now generated from app.pkl.
//
// Only SECRETS / connectivity live here. The non-secret spec locators
// (import_type, spec_url, spec_prefix, spec_key) remain plain workflow config
// in uiConfig below — they are not credential material. The auth-type radio is
// the object-store provider selector (S3 / GCS / ADLS); the field names +
// nested extra.* match exactly what the SDK's CloudStore.from_credentials()
// consumes (authType / username / password / extra.*).
credentialConnectorType = "rest"
credentialAuthTitle = "Cloud object store"
credentialAuthDefault = "s3"
credentialAuthOptions {
["s3"] = new AuthOption {
label = "S3"
fields {
new FieldSpec {
name = "username"
displayName = "AWS access key"
helpText = "Enter your AWS access key."
required = false
width = 4
}
new FieldSpec {
name = "password"
displayName = "AWS secret key"
helpText = "Enter your AWS secret key."
sensitive = true
fieldType = "password"
required = false
width = 4
}
}
nestedExtraLabel = "S3 details"
nestedExtraHeader = "S3 details"
extraFields {
new FieldSpec {
name = "aws_role_arn"
displayName = "AWS Role ARN"
helpText = "ARN of the AWS role to assume"
placeholder = "arn:aws:iam::123456789012:role/roleName"
required = false
width = 8
validationRules {
new Dynamic {
pattern = #"^arn:aws:iam::\d{12}:role/.+"#
message = "Please enter a valid role ARN"
trigger = "blur"
}
}
}
new FieldSpec {
name = "region"
displayName = "Region"
helpText = "Enter your AWS region."
placeholder = "us-west-1"
required = false
width = 4
}
new FieldSpec {
name = "s3_bucket"
displayName = "Bucket"
helpText = "Enter the bucket from which to retrieve the object store object(s)."
placeholder = "bucket-name"
required = false
width = 4
}
}
}
["gcs"] = new AuthOption {
label = "GCS"
fields {
new FieldSpec {
name = "username"
displayName = "Project ID"
helpText = "Enter the ID of the GCP project."
required = false
width = 4
}
new FieldSpec {
name = "password"
displayName = "Service account JSON"
helpText = "Enter the JSON for your service account credentials."
sensitive = true
fieldType = "password"
required = false
width = 8
}
}
nestedExtraLabel = "GCS details"
nestedExtraHeader = "GCS details"
extraFields {
new FieldSpec {
name = "gcs_bucket"
displayName = "Bucket"
helpText = "Enter the bucket from which to retrieve the object store object(s)."
placeholder = "bucket-name"
required = false
width = 4
}
}
}
["adls"] = new AuthOption {
label = "ADLS"
fields {
new FieldSpec {
name = "username"
displayName = "Azure client ID"
helpText = "Enter the unique application (client) ID assigned to your app by Azure AD when the app was registered."
required = false
width = 4
}
new FieldSpec {
name = "password"
displayName = "Azure client secret"
helpText = "Enter your client secret."
sensitive = true
fieldType = "password"
required = false
width = 4
}
}
nestedExtraLabel = "ADLS details"
nestedExtraHeader = "ADLS details"
extraFields {
new FieldSpec {
name = "azure_tenant_id"
displayName = "Azure tenant ID"
helpText = "Enter the unique identifier of the Azure Active Directory instance."
sensitive = true
fieldType = "password"
required = false
width = 4
}
new FieldSpec {
name = "storage_account_name"
displayName = "Storage account name"
helpText = "Enter the name of your storage account."
required = false
width = 4
}
new FieldSpec {
name = "adls_container"
displayName = "Container"
helpText = "Enter the container from which to retrieve the object store object(s)."
placeholder = "container-name"
required = false
width = 4
}
}
}
}
// UI element keys, enum values, titles, help text, and rules are kept aligned
// with the Kotlin/CSA reference package (`@csa/openapi-spec-loader`) to keep
// the contract backwards-compatible for existing workflow configs. import_type
// is the top-level source discriminator (URL vs object store); spec_url /
// spec_prefix / spec_key are non-secret config attributes (NOT credential
// material). Object-store auth for the CLOUD path comes from the generated
// credential above.
uiConfig = new UIConfig {
tasks {
["Configuration"] {
description = "OpenAPI spec configuration"
inputs {
// Hidden manifest-only field (P029 / DISTR-752): Heracles/AE derives
// agent-vs-direct credential routing from `extraction_method` at
// dispatch. This app has one fixed extraction path (task-based, no
// agent-mode toggle), so the value is a constant, not a user choice.
//
// Keyed `extraction-method` (kebab), the fleet convention every other
// connector uses (mysql, metabase, …). The toolkit derives the manifest
// arg `extraction_method` from it via toPyName, and its default-emit
// guard recognises the field by this spelling (FND-1437).
["extraction-method"] = new TextInput {
title = "Extraction method"
hide = true
default = "direct"
}
["import_type"] = new Radio {
title = "Import spec from"
helpText = "Select how you want to provide the OpenAPI spec file to be imported."
possibleValues {
["URL"] = "URL"
["CLOUD"] = "Object storage"
}
default = "URL"
}
["spec_url"] = new TextInput {
title = "Specification URL"
required = true
helpText = "Full URL to the JSON form of the OpenAPI specification."
placeholderText = "https://petstore3.swagger.io/api/v3/openapi.json"
}
["spec_prefix"] = new TextInput {
title = "Prefix (path)"
helpText = "Enter the directory (path) within the object store from which to retrieve the OpenAPI spec file."
placeholderText = "path/to/file"
width = 4
}
["spec_key"] = new TextInput {
title = "Object key (filename)"
helpText = "Enter the object key (filename), including its extension, within the object store and prefix."
placeholderText = "openapi.json"
width = 4
}
// Object-store credential for the CLOUD import path — openapi's own
// inline credential widget (kept in this Configuration step, gated by
// import_type=CLOUD below), now pointing at the app-generated
// `atlan-connectors-openapi` configmap (from credentialAuthOptions
// above) instead of the platform-shared `csa-connectors-objectstore`.
// The provider radio (S3 / GCS / ADLS) + fields are rendered from that
// configmap. Pre-migration configs stored a bare credential GUID here
// and resolve by GUID at runtime, but that is NOT backward-compatible
// (CONNECT-800): GUID resolution reads a per-app blob the platform
// normally writes, so a GUID predating this credential route has
// nothing to read and the run fails. See app/connector.py's
// cloud_source fallback for the backfill route.
["cloud_source"] = new CredentialInput {
title = "Object store"
credType = "atlan-connectors-openapi"
helpText = "Object store from which to retrieve the OpenAPI spec file."
allowTestAuthentication = false
}
}
}
["Connection"] {
description = "Connection details"
inputs {
// CONNECT-55: CREATE runs the normal publish diff (archives assets no
// longer present). REUSE targets an existing connection that may be
// shared with other sources, so the connector derives assertion-only
// publish (upsert-only: no diff, no deletes) — see the `publish` node
// override above. Default REUSE matches the CSA reference package.
["connection_usage"] = new Radio {
title = "Connection"
required = true
possibleValues {
["CREATE"] = "Create"
["REUSE"] = "Reuse"
}
default = "REUSE"
helpText = "Whether to create a new connection to hold these API assets, or reuse an existing connection."
}
["connection"] = new ConnectionCreator {
title = "Connection"
required = true
helpText = "Enter details for a new connection to be created."
}
["connection_qualified_name"] = new ConnectionSelector {
title = "Connection"
required = true
helpText = "Select an existing connection to load assets into."
width = 4
connectorFilter = Connectors.API.value
}
}
}
}
rules {
new UIRule {
whenInputs { ["import_type"] = "URL" }
required { "spec_url" }
}
new UIRule {
whenInputs { ["import_type"] = "CLOUD" }
required { "cloud_source"; "spec_prefix"; "spec_key" }
}
new UIRule {
whenInputs { ["connection_usage"] = "REUSE" }
required { "connection_qualified_name" }
}
new UIRule {
whenInputs { ["connection_usage"] = "CREATE" }
required { "connection" }
}
}
}
// K016: `output_file` is the `run` entry point's public hand-off — publish-app
// reads it via the DAG, so the file's shape is a cross-app boundary, not an
// internal detail. Declared here (single entry point ⇒ top level), keyed by the
// contract field name.
//
// The artifact is NDJSON despite its `.json` suffix: _transform_blocking writes
// one `to_nested_bytes()` record per line (app/connector.py). Each record is a
// pyatlan_v9 Atlas nested-format entity — a msgspec Struct with `rename="camel"`
// and `omit_defaults=True` (pyatlan_v9/model/assets/referenceable.py), so only
// the envelope below is guaranteed on every line.
//
// The declaration is the STANDARD ATLAS MODEL, not an app-specific shape — so
// it is quoted from the model, not inferred from this connector. The required
// four below are exactly pyatlan_v9's own mandatory set (`Asset.validate()`:
// "Checks that required fields (type_name, name, qualified_name) are set") plus
// the `attributes` container those two leaves live in. Any Atlas consumer can
// rely on them; a producer that drops one has broken the model, not a local
// convention.
//
// The remaining five are the sync-metadata block `apply_sync_metadata` stamps
// (app/asset_mapper.py). They are `required = false` because `map_connection`
// does NOT call it — a Connection line legitimately carries none of them, while
// every APISpec/APIPath line carries all five. `required = false` still asserts
// the TYPE whenever the field appears, which is the regression worth catching;
// `required = true` here would fail on a valid Connection record.
artifactSchemas {
["output_file"] = new ArtifactSchema {
format = "ndjson"
description = "Atlas entities emitted by transform: one Connection (unless connection_usage=REUSE), then APISpec, then APIPath records."
fields {
new ArtifactField {
name = "typeName"
type = "string"
description = "Atlas type of the entity on this line — Connection, APISpec or APIPath. Mandatory on every Atlas asset."
}
new ArtifactField {
name = "attributes"
type = "struct"
description = "Atlas nested-format attribute bag; every asset field other than the Atlas envelope lives in here."
}
new ArtifactField {
name = "attributes.qualifiedName"
type = "string"
description = "Atlan qualified name; the identity publish-app upserts on. Formats are documented in app/asset_mapper.py."
}
new ArtifactField {
name = "attributes.name"
type = "string"
description = "Human-readable asset name shown in the Atlan UI. Mandatory on every Atlas asset."
}
new ArtifactField {
name = "status"
type = "string"
required = false
description = "Atlas entity status, stamped ACTIVE by apply_sync_metadata. Absent on Connection lines, which do not go through it."
}
new ArtifactField {
name = "attributes.tenantId"
type = "string"
required = false
description = "Owning tenant, stamped by apply_sync_metadata. Absent on Connection lines."
}
new ArtifactField {
name = "attributes.lastSyncRun"
type = "string"
required = false
description = "Temporal workflow ID of the run that produced this asset; how downstream diffing scopes a run. Absent on Connection lines."
}
new ArtifactField {
name = "attributes.lastSyncRunAt"
type = "int"
required = false
description = "Run start time as a millisecond UNIX timestamp. Integer, not a string date. Absent on Connection lines."
}
new ArtifactField {
name = "attributes.lastSyncWorkflowName"
type = "string"
required = false
description = "Workflow type that produced this asset. Absent on Connection lines."
}
}
}
}