From fc8b3c70779ada71d2eb32be4f6d1652331fb8ef Mon Sep 17 00:00:00 2001 From: Mateusz Szostok Date: Mon, 7 Feb 2022 20:04:13 +0100 Subject: [PATCH 01/15] First version adjusted after rebase with main --- cmd/k8s-engine/main.go | 3 +- hub-js/graphql/local/schema.graphql | 665 +++++++------- internal/k8s-engine/controller/suite_test.go | 4 +- .../graphql/domain/policy/converter.go | 59 +- .../graphql/domain/policy/fixtures_test.go | 103 ++- internal/k8s-engine/policy/fixtures_test.go | 4 +- internal/logger/noop.go | 13 + pkg/engine/api/graphql/models_gen.go | 34 +- pkg/engine/api/graphql/schema.graphql | 34 + pkg/engine/api/graphql/schema_gen.go | 574 +++++++++++- pkg/engine/client/action.go | 137 +++ pkg/engine/client/client.go | 172 +--- pkg/engine/client/fields.go | 12 + pkg/engine/client/policy.go | 55 ++ pkg/engine/k8s/policy/fixtures_test.go | 22 +- pkg/engine/k8s/policy/from_yaml_test.go | 4 +- .../k8s/policy/metadata/fixtures_test.go | 179 +++- pkg/engine/k8s/policy/metadata/metadata.go | 14 + .../k8s/policy/metadata/metadata_resolver.go | 90 +- .../policy/metadata/metadata_resolver_test.go | 19 +- pkg/engine/k8s/policy/predefined.go | 2 +- pkg/engine/k8s/policy/type_instance.go | 136 +++ pkg/engine/k8s/policy/type_instance_test.go | 131 +++ pkg/engine/k8s/policy/types.go | 36 +- pkg/engine/k8s/policy/types_test.go | 20 +- pkg/engine/k8s/policy/workflow_test.go | 2 +- .../k8s/policy/zz_generated.deepcopy.go | 85 +- pkg/hub/api/graphql/local/models_gen.go | 13 +- pkg/hub/api/graphql/local/schema_gen.go | 814 +++++++++++------- pkg/hub/client/policy_enforced_client.go | 75 +- pkg/hub/client/policy_enforced_client_test.go | 183 +++- pkg/hub/client/policy_merger.go | 42 +- pkg/hub/client/policy_merger_test.go | 496 +++++++++-- .../public/facade/types/additional_refs.go | 72 ++ pkg/sdk/apis/0.0.1/types/types.extend.go | 24 +- pkg/sdk/apis/0.0.1/types/types.extend_test.go | 33 + pkg/sdk/renderer/argo/dedicated_renderer.go | 135 ++- .../renderer/argo/dedicated_renderer_test.go | 4 +- pkg/sdk/renderer/argo/fixtures_test.go | 22 +- pkg/sdk/renderer/argo/helpers.go | 12 +- pkg/sdk/renderer/argo/renderer.go | 25 +- pkg/sdk/renderer/argo/renderer_test.go | 16 +- pkg/sdk/renderer/argo/typeinstance_handler.go | 11 +- pkg/sdk/renderer/argo/types.go | 16 +- .../manifest/json_remote_implementation.go | 55 +- pkg/sdk/validation/policy/fixtures_test.go | 123 ++- pkg/sdk/validation/policy/policy.go | 27 +- pkg/sdk/validation/policy/policy_test.go | 30 +- 48 files changed, 3684 insertions(+), 1153 deletions(-) create mode 100644 internal/logger/noop.go create mode 100644 pkg/engine/client/action.go create mode 100644 pkg/engine/client/policy.go create mode 100644 pkg/engine/k8s/policy/type_instance.go create mode 100644 pkg/engine/k8s/policy/type_instance_test.go create mode 100644 pkg/hub/client/public/facade/types/additional_refs.go create mode 100644 pkg/sdk/apis/0.0.1/types/types.extend_test.go diff --git a/cmd/k8s-engine/main.go b/cmd/k8s-engine/main.go index a4fc02591..e0adc07c1 100644 --- a/cmd/k8s-engine/main.go +++ b/cmd/k8s-engine/main.go @@ -42,6 +42,7 @@ var ( const ( graphQLServerName = "engine-graphql" policyServiceName = "policy-svc" + argoRendererName = "argo-renderer" ) // Config holds application related configuration @@ -111,7 +112,7 @@ func main() { interfaceIOValidator := actionvalidation.NewValidator(hubClient) policyIOValidator := policyvalidation.NewValidator(hubClient) wfValidator := renderer.NewWorkflowInputValidator(interfaceIOValidator, policyIOValidator) - argoRenderer := argo.NewRenderer(cfg.Renderer, hubClient, typeInstanceHandler, wfValidator) + argoRenderer := argo.NewRenderer(logger.Named(argoRendererName), cfg.Renderer, hubClient, typeInstanceHandler, wfValidator) wfCli, err := wfclientset.NewForConfig(k8sCfg) exitOnError(err, "while creating Argo client") diff --git a/hub-js/graphql/local/schema.graphql b/hub-js/graphql/local/schema.graphql index 307b68f5d..c033fe731 100644 --- a/hub-js/graphql/local/schema.graphql +++ b/hub-js/graphql/local/schema.graphql @@ -2,10 +2,10 @@ # To make it work for other graphql client we need to add them to the schema manually, based on: # https://github.com/neo4j-graphql/neo4j-graphql-js/blob/master/src/augment/directives.js directive @relation( - name: String - direction: String - from: String - to: String + name: String + direction: String + from: String + to: String ) on FIELD_DEFINITION | OBJECT directive @cypher(statement: String) on FIELD_DEFINITION @@ -33,440 +33,455 @@ LockOwner defines owner name who locked a given TypeInstance scalar LockOwnerID type TypeInstance { - id: ID! @id - - lockedBy: LockOwnerID - - """ - Common properties for all TypeInstances which cannot be changed - """ - typeRef: TypeInstanceTypeReference! - @relation(name: "OF_TYPE", direction: "OUT") - uses: [TypeInstance!]! @relation(name: "USES", direction: "OUT") - usedBy: [TypeInstance!]! @relation(name: "USES", direction: "IN") - - latestResourceVersion: TypeInstanceResourceVersion - @cypher( - statement: "MATCH (this)-[:CONTAINS]->(tir:TypeInstanceResourceVersion) RETURN tir ORDER BY tir.resourceVersion DESC LIMIT 1" - ) - firstResourceVersion: TypeInstanceResourceVersion - @cypher( - statement: "MATCH (this)-[:CONTAINS]->(tir:TypeInstanceResourceVersion) RETURN tir ORDER BY tir.resourceVersion ASC LIMIT 1" - ) - previousResourceVersion: TypeInstanceResourceVersion - @cypher( - statement: "MATCH (this)-[:CONTAINS]->(tir:TypeInstanceResourceVersion) RETURN tir ORDER BY tir.resourceVersion DESC SKIP 1 LIMIT 1" - ) - resourceVersion(resourceVersion: Int!): TypeInstanceResourceVersion - @cypher( - statement: "MATCH (this)-[:CONTAINS]->(tir:TypeInstanceResourceVersion {resourceVersion: $resourceVersion}) RETURN tir" - ) - resourceVersions: [TypeInstanceResourceVersion!]! - @relation(name: "CONTAINS", direction: "OUT") + id: ID! @id + + lockedBy: LockOwnerID + + """ + Common properties for all TypeInstances which cannot be changed + """ + typeRef: TypeInstanceTypeReference! + @relation(name: "OF_TYPE", direction: "OUT") + uses: [TypeInstance!]! @relation(name: "USES", direction: "OUT") + usedBy: [TypeInstance!]! @relation(name: "USES", direction: "IN") + + latestResourceVersion: TypeInstanceResourceVersion + @cypher( + statement: "MATCH (this)-[:CONTAINS]->(tir:TypeInstanceResourceVersion) RETURN tir ORDER BY tir.resourceVersion DESC LIMIT 1" + ) + firstResourceVersion: TypeInstanceResourceVersion + @cypher( + statement: "MATCH (this)-[:CONTAINS]->(tir:TypeInstanceResourceVersion) RETURN tir ORDER BY tir.resourceVersion ASC LIMIT 1" + ) + previousResourceVersion: TypeInstanceResourceVersion + @cypher( + statement: "MATCH (this)-[:CONTAINS]->(tir:TypeInstanceResourceVersion) RETURN tir ORDER BY tir.resourceVersion DESC SKIP 1 LIMIT 1" + ) + resourceVersion(resourceVersion: Int!): TypeInstanceResourceVersion + @cypher( + statement: "MATCH (this)-[:CONTAINS]->(tir:TypeInstanceResourceVersion {resourceVersion: $resourceVersion}) RETURN tir" + ) + resourceVersions: [TypeInstanceResourceVersion!]! + @relation(name: "CONTAINS", direction: "OUT") } type TypeInstanceResourceVersion { - resourceVersion: Int! @index - createdBy: String + resourceVersion: Int! @index + createdBy: String + + metadata: TypeInstanceResourceVersionMetadata! + @relation(name: "DESCRIBED_BY", direction: "OUT") + spec: TypeInstanceResourceVersionSpec! + @relation(name: "SPECIFIED_BY", direction: "OUT") - metadata: TypeInstanceResourceVersionMetadata! - @relation(name: "DESCRIBED_BY", direction: "OUT") - spec: TypeInstanceResourceVersionSpec! - @relation(name: "SPECIFIED_BY", direction: "OUT") + # TODO: can be changed to TypeInstance, and a proper relation set + backend: TypeInstanceBackend! } type TypeInstanceResourceVersionMetadata { - attributes: [AttributeReference!] - @relation(name: "CHARACTERIZED_BY", direction: "OUT") + attributes: [AttributeReference!] + @relation(name: "CHARACTERIZED_BY", direction: "OUT") } type TypeInstanceResourceVersionSpec { - value: Any! - @cypher( - statement: """ - RETURN apoc.convert.fromJsonMap(this.value) - """ - ) - - """ - CURRENTLY NOT IMPLEMENTED - """ - instrumentation: TypeInstanceInstrumentation - @relation(name: "INSTRUMENTED_WITH", direction: "OUT") + value: Any! + @cypher( + statement: """ + RETURN apoc.convert.fromJsonMap(this.value) + """ + ) + + """ + CURRENTLY NOT IMPLEMENTED + """ + instrumentation: TypeInstanceInstrumentation + @relation(name: "INSTRUMENTED_WITH", direction: "OUT") +} + +type TypeInstanceBackend { + id: String! } type TypeInstanceTypeReference { - path: NodePath! - revision: Version! + path: NodePath! + revision: Version! } input AttributeReferenceInput { - path: NodePath! - revision: Version! + path: NodePath! + revision: Version! } type AttributeReference { - path: NodePath! - revision: Version! + path: NodePath! + revision: Version! } """ CURRENTLY NOT IMPLEMENTED """ type TypeInstanceInstrumentation { - metrics: TypeInstanceInstrumentationMetrics - @relation(name: "MEASURED_BY", direction: "OUT") - health: TypeInstanceInstrumentationHealth - @relation(name: "INDICATED_BY", direction: "OUT") + metrics: TypeInstanceInstrumentationMetrics + @relation(name: "MEASURED_BY", direction: "OUT") + health: TypeInstanceInstrumentationHealth + @relation(name: "INDICATED_BY", direction: "OUT") } """ CURRENTLY NOT IMPLEMENTED """ type TypeInstanceInstrumentationMetrics { - endpoint: String - regex: String # optional regex for scraping metrics - dashboards: [TypeInstanceInstrumentationMetricsDashboard!]! - @relation(name: "ON", direction: "OUT") + endpoint: String + regex: String # optional regex for scraping metrics + dashboards: [TypeInstanceInstrumentationMetricsDashboard!]! + @relation(name: "ON", direction: "OUT") } """ CURRENTLY NOT IMPLEMENTED """ type TypeInstanceInstrumentationMetricsDashboard { - url: String! + url: String! } """ CURRENTLY NOT IMPLEMENTED """ type TypeInstanceInstrumentationHealth { - url: String - method: HTTPRequestMethod - - # resolver, which does a HTTP call on a given URL - # and expects status code greater than or equal to 200 - # and less than 400 - # TODO implement TypeInstance health check, for resolution of this field - status: TypeInstanceInstrumentationHealthStatus + url: String + method: HTTPRequestMethod + + # resolver, which does a HTTP call on a given URL + # and expects status code greater than or equal to 200 + # and less than 400 + # TODO implement TypeInstance health check, for resolution of this field + status: TypeInstanceInstrumentationHealthStatus } """ CURRENTLY NOT IMPLEMENTED """ enum TypeInstanceInstrumentationHealthStatus { - UNKNOWN - READY - FAILING + UNKNOWN + READY + FAILING } enum HTTPRequestMethod { - GET - POST + GET + POST } input AttributeFilterInput { - path: NodePath! - rule: FilterRule = INCLUDE + path: NodePath! + rule: FilterRule = INCLUDE - """ - If not provided, any revision of the Attribute applies to this filter - """ - revision: Version + """ + If not provided, any revision of the Attribute applies to this filter + """ + revision: Version } enum FilterRule { - INCLUDE - EXCLUDE + INCLUDE + EXCLUDE } input TypeInstanceFilter { - attributes: [AttributeFilterInput] - typeRef: TypeRefFilterInput - createdBy: String + attributes: [AttributeFilterInput] + typeRef: TypeRefFilterInput + createdBy: String } input TypeRefFilterInput { - path: NodePath! + path: NodePath! - """ - If not provided, it returns TypeInstances for all revisions of given Type - """ - revision: Version + """ + If not provided, it returns TypeInstances for all revisions of given Type + """ + revision: Version } input TypeInstanceTypeReferenceInput { - path: NodePath! - revision: Version! + path: NodePath! + revision: Version! +} + +input TypeInstanceBackendInput { + id: String! } input CreateTypeInstanceInput { - """ - Used to define the relationships, between the created TypeInstances - """ - alias: String - - createdBy: String - typeRef: TypeInstanceTypeReferenceInput! - attributes: [AttributeReferenceInput!] - value: Any + """ + Used to define the relationships, between the created TypeInstances + """ + alias: String + + createdBy: String + typeRef: TypeInstanceTypeReferenceInput! + attributes: [AttributeReferenceInput!] + """ + If not provided, TypeInstance value is stored as static value in Local Hub core storage. + """ + backend: TypeInstanceBackendInput + value: Any } input TypeInstanceUsesRelationInput { - """ - Can be existing TypeInstance ID or alias of a TypeInstance from typeInstances list - """ - from: String! - - """ - Can be existing TypeInstance ID or alias of a TypeInstance from typeInstances list - """ - to: String! + """ + Can be existing TypeInstance ID or alias of a TypeInstance from typeInstances list + """ + from: String! + + """ + Can be existing TypeInstance ID or alias of a TypeInstance from typeInstances list + """ + to: String! } input CreateTypeInstancesInput { - typeInstances: [CreateTypeInstanceInput!]! - usesRelations: [TypeInstanceUsesRelationInput!]! + typeInstances: [CreateTypeInstanceInput!]! + usesRelations: [TypeInstanceUsesRelationInput!]! } type CreateTypeInstanceOutput { - id: ID! - alias: String! + id: ID! + alias: String! } """ At least one property needs to be specified. """ input UpdateTypeInstanceInput { - """ - The attributes property is optional. If not provided, previous value is used. - """ - attributes: [AttributeReferenceInput!] - - """ - The value property is optional. If not provided, previous value is used. - """ - value: Any + """ + The attributes property is optional. If not provided, previous value is used. + """ + attributes: [AttributeReferenceInput!] + + """ + The value property is optional. If not provided, previous value is used. + """ + value: Any } input UpdateTypeInstancesInput { - """ - Allows you to update TypeInstances which are locked by a given ownerID. If not provided, - you can update only those TypeInstances which are not locked. - """ - ownerID: LockOwnerID - createdBy: String - - id: ID! - typeInstance: UpdateTypeInstanceInput! + """ + Allows you to update TypeInstances which are locked by a given ownerID. If not provided, + you can update only those TypeInstances which are not locked. + """ + ownerID: LockOwnerID + createdBy: String + + id: ID! + typeInstance: UpdateTypeInstanceInput! } input LockTypeInstancesInput { - ids: [ID!]! - ownerID: LockOwnerID! + ids: [ID!]! + ownerID: LockOwnerID! } input UnlockTypeInstancesInput { - ids: [ID!]! - ownerID: LockOwnerID! + ids: [ID!]! + ownerID: LockOwnerID! } type Query { - typeInstances(filter: TypeInstanceFilter = {}): [TypeInstance!]! - @cypher( - statement: """ - WITH [x IN $filter.attributes WHERE x.rule = "EXCLUDE" | x ] AS excluded, - [x IN $filter.attributes WHERE x.rule = "INCLUDE" | x ] AS included - - CALL { - WITH excluded - UNWIND excluded AS f - MATCH (ex:AttributeReference {path: f.path}) - WHERE (f.revision IS NULL) OR (ex.revision = f.revision) - RETURN collect(ex) as excludedAttributes - } - - MATCH (tir:TypeInstanceResourceVersion)-[:DESCRIBED_BY]->(meta:TypeInstanceResourceVersionMetadata) - OPTIONAL MATCH (meta)-[:CHARACTERIZED_BY]->(attr:AttributeReference) - MATCH (ti:TypeInstance)-[:OF_TYPE]->(typeRef:TypeInstanceTypeReference) - MATCH (ti:TypeInstance)-[:CONTAINS]->(tir) - WHERE - $filter = {} OR - ( - ( - $filter.typeRef IS NULL - OR - ( + typeInstances(filter: TypeInstanceFilter = {}): [TypeInstance!]! + @cypher( + statement: """ + WITH [x IN $filter.attributes WHERE x.rule = "EXCLUDE" | x ] AS excluded, + [x IN $filter.attributes WHERE x.rule = "INCLUDE" | x ] AS included + + CALL { + WITH excluded + UNWIND excluded AS f + MATCH (ex:AttributeReference {path: f.path}) + WHERE (f.revision IS NULL) OR (ex.revision = f.revision) + RETURN collect(ex) as excludedAttributes + } + + MATCH (tir:TypeInstanceResourceVersion)-[:DESCRIBED_BY]->(meta:TypeInstanceResourceVersionMetadata) + OPTIONAL MATCH (meta)-[:CHARACTERIZED_BY]->(attr:AttributeReference) + MATCH (ti:TypeInstance)-[:OF_TYPE]->(typeRef:TypeInstanceTypeReference) + MATCH (ti:TypeInstance)-[:CONTAINS]->(tir) + WHERE + $filter = {} OR + ( + ( + $filter.typeRef IS NULL + OR + ( ($filter.typeRef.revision IS NULL AND typeRef.path = $filter.typeRef.path) OR (typeRef.path = $filter.typeRef.path AND typeRef.revision = $filter.typeRef.revision) - ) - ) - AND - ($filter.createdBy IS NULL OR tir.createdBy = $filter.createdBy) - AND - ( - $filter.attributes IS NULL - OR - ( + ) + ) + AND + ($filter.createdBy IS NULL OR tir.createdBy = $filter.createdBy) + AND + ( + $filter.attributes IS NULL + OR + ( all(inc IN included WHERE - (tir)-[:DESCRIBED_BY]->(meta:TypeInstanceResourceVersionMetadata)-[:CHARACTERIZED_BY]->(attr:AttributeReference {path: inc.path}) - AND - (inc.revision IS NULL OR attr.revision = inc.revision) + (tir)-[:DESCRIBED_BY]->(meta:TypeInstanceResourceVersionMetadata)-[:CHARACTERIZED_BY]->(attr:AttributeReference {path: inc.path}) + AND + (inc.revision IS NULL OR attr.revision = inc.revision) ) AND none(exc IN excludedAttributes WHERE (tir)-[:DESCRIBED_BY]->(meta:TypeInstanceResourceVersionMetadata)-[:CHARACTERIZED_BY]->(exc)) - ) + ) + ) + ) + + RETURN DISTINCT ti + """ + ) + + typeInstance(id: ID!): TypeInstance + @cypher( + statement: """ + MATCH (this:TypeInstance {id: $id}) + RETURN this + """ ) - ) - - RETURN DISTINCT ti - """ - ) - - typeInstance(id: ID!): TypeInstance - @cypher( - statement: """ - MATCH (this:TypeInstance {id: $id}) - RETURN this - """ - ) } type Mutation { - createTypeInstances( - in: CreateTypeInstancesInput! - ): [CreateTypeInstanceOutput!]! - - # TODO extend input with TypeInstanceInstrumentation - createTypeInstance(in: CreateTypeInstanceInput!): TypeInstance! - @cypher( - statement: """ - WITH apoc.convert.toJson($in.value) as value - MERGE (typeRef:TypeInstanceTypeReference {path: $in.typeRef.path, revision: $in.typeRef.revision}) - - CREATE (ti:TypeInstance {id: apoc.create.uuid()}) - CREATE (ti)-[:OF_TYPE]->(typeRef) - - CREATE (tir: TypeInstanceResourceVersion {resourceVersion: 1, createdBy: $in.createdBy}) - CREATE (ti)-[:CONTAINS]->(tir) - - CREATE (tir)-[:DESCRIBED_BY]->(metadata: TypeInstanceResourceVersionMetadata) - CREATE (tir)-[:SPECIFIED_BY]->(spec: TypeInstanceResourceVersionSpec {value: value}) - - FOREACH (attr in $in.attributes | - MERGE (attrRef: AttributeReference {path: attr.path, revision: attr.revision}) - CREATE (metadata)-[:CHARACTERIZED_BY]->(attrRef) - ) - - RETURN ti - """ - ) - - updateTypeInstances(in: [UpdateTypeInstancesInput]!): [TypeInstance!]! - @cypher( - statement: """ - CALL { - UNWIND $in AS item - RETURN collect(item.id) as allInputIDs - } - - // Check if all TypeInstances were found - WITH * - CALL { - WITH allInputIDs - MATCH (ti:TypeInstance) - WHERE ti.id IN allInputIDs - WITH collect(ti.id) as foundIDs - RETURN foundIDs - } - CALL apoc.util.validate(size(foundIDs) < size(allInputIDs), apoc.convert.toJson({code: 404, ids: foundIDs}), null) - - // Check if given TypeInstances are not already locked by others - WITH * - CALL { - WITH * - UNWIND $in AS item - MATCH (tic:TypeInstance {id: item.id}) - WHERE tic.lockedBy IS NOT NULL AND (item.ownerID IS NULL OR tic.lockedBy <> item.ownerID) - WITH collect(tic.id) as lockedIDs - RETURN lockedIDs - } - CALL apoc.util.validate(size(lockedIDs) > 0, apoc.convert.toJson({code: 409, ids: lockedIDs}), null) - - UNWIND $in as item - MATCH (ti: TypeInstance {id: item.id}) - CALL { - WITH ti - MATCH (ti)-[:CONTAINS]->(latestRevision:TypeInstanceResourceVersion) - RETURN latestRevision - ORDER BY latestRevision.resourceVersion DESC LIMIT 1 - } - - CREATE (tir: TypeInstanceResourceVersion {resourceVersion: latestRevision.resourceVersion + 1, createdBy: item.createdBy}) - CREATE (ti)-[:CONTAINS]->(tir) - - // Handle the `spec.value` property - CREATE (spec: TypeInstanceResourceVersionSpec) - CREATE (tir)-[:SPECIFIED_BY]->(spec) - - WITH ti, tir, spec, latestRevision, item - CALL apoc.do.when( - item.typeInstance.value IS NOT NULL, - ' - SET spec.value = apoc.convert.toJson(item.typeInstance.value) RETURN spec - ', - ' - MATCH (latestRevision)-[:SPECIFIED_BY]->(latestSpec: TypeInstanceResourceVersionSpec) - SET spec.value = latestSpec.value RETURN spec - ', - {spec:spec, latestRevision: latestRevision, item: item}) YIELD value - - // Handle the `metadata.attributes` property - CREATE (metadata: TypeInstanceResourceVersionMetadata) - CREATE (tir)-[:DESCRIBED_BY]->(metadata) - - WITH ti, tir, latestRevision, metadata, item - CALL apoc.do.when( - item.typeInstance.attributes IS NOT NULL, - ' - FOREACH (attr in item.typeInstance.attributes | + createTypeInstances( + in: CreateTypeInstancesInput! + ): [CreateTypeInstanceOutput!]! + + # TODO extend input with TypeInstanceInstrumentation + createTypeInstance(in: CreateTypeInstanceInput!): TypeInstance! + @cypher( + statement: """ + WITH apoc.convert.toJson($in.value) as value + MERGE (typeRef:TypeInstanceTypeReference {path: $in.typeRef.path, revision: $in.typeRef.revision}) + + CREATE (ti:TypeInstance {id: apoc.create.uuid()}) + CREATE (ti)-[:OF_TYPE]->(typeRef) + + CREATE (tir: TypeInstanceResourceVersion {resourceVersion: 1, createdBy: $in.createdBy}) + CREATE (ti)-[:CONTAINS]->(tir) + + CREATE (tir)-[:DESCRIBED_BY]->(metadata: TypeInstanceResourceVersionMetadata) + CREATE (tir)-[:SPECIFIED_BY]->(spec: TypeInstanceResourceVersionSpec {value: value}) + + FOREACH (attr in $in.attributes | + MERGE (attrRef: AttributeReference {path: attr.path, revision: attr.revision}) + CREATE (metadata)-[:CHARACTERIZED_BY]->(attrRef) + ) + + RETURN ti + """ + ) + + updateTypeInstances(in: [UpdateTypeInstancesInput]!): [TypeInstance!]! + @cypher( + statement: """ + CALL { + UNWIND $in AS item + RETURN collect(item.id) as allInputIDs + } + + // Check if all TypeInstances were found + WITH * + CALL { + WITH allInputIDs + MATCH (ti:TypeInstance) + WHERE ti.id IN allInputIDs + WITH collect(ti.id) as foundIDs + RETURN foundIDs + } + CALL apoc.util.validate(size(foundIDs) < size(allInputIDs), apoc.convert.toJson({code: 404, ids: foundIDs}), null) + + // Check if given TypeInstances are not already locked by others + WITH * + CALL { + WITH * + UNWIND $in AS item + MATCH (tic:TypeInstance {id: item.id}) + WHERE tic.lockedBy IS NOT NULL AND (item.ownerID IS NULL OR tic.lockedBy <> item.ownerID) + WITH collect(tic.id) as lockedIDs + RETURN lockedIDs + } + CALL apoc.util.validate(size(lockedIDs) > 0, apoc.convert.toJson({code: 409, ids: lockedIDs}), null) + + UNWIND $in as item + MATCH (ti: TypeInstance {id: item.id}) + CALL { + WITH ti + MATCH (ti)-[:CONTAINS]->(latestRevision:TypeInstanceResourceVersion) + RETURN latestRevision + ORDER BY latestRevision.resourceVersion DESC LIMIT 1 + } + + CREATE (tir: TypeInstanceResourceVersion {resourceVersion: latestRevision.resourceVersion + 1, createdBy: item.createdBy}) + CREATE (ti)-[:CONTAINS]->(tir) + + // Handle the `spec.value` property + CREATE (spec: TypeInstanceResourceVersionSpec) + CREATE (tir)-[:SPECIFIED_BY]->(spec) + + WITH ti, tir, spec, latestRevision, item + CALL apoc.do.when( + item.typeInstance.value IS NOT NULL, + ' + SET spec.value = apoc.convert.toJson(item.typeInstance.value) RETURN spec + ', + ' + MATCH (latestRevision)-[:SPECIFIED_BY]->(latestSpec: TypeInstanceResourceVersionSpec) + SET spec.value = latestSpec.value RETURN spec + ', + {spec:spec, latestRevision: latestRevision, item: item}) YIELD value + + // Handle the `metadata.attributes` property + CREATE (metadata: TypeInstanceResourceVersionMetadata) + CREATE (tir)-[:DESCRIBED_BY]->(metadata) + + WITH ti, tir, latestRevision, metadata, item + CALL apoc.do.when( + item.typeInstance.attributes IS NOT NULL, + ' + FOREACH (attr in item.typeInstance.attributes | MERGE (attrRef: AttributeReference {path: attr.path, revision: attr.revision}) CREATE (metadata)-[:CHARACTERIZED_BY]->(attrRef) - ) - - RETURN metadata - ', - ' - OPTIONAL MATCH (latestRevision)-[:DESCRIBED_BY]->(TypeInstanceResourceVersionMetadata)-[:CHARACTERIZED_BY]->(latestAttrRef: AttributeReference) - WHERE latestAttrRef IS NOT NULL - WITH *, COLLECT(latestAttrRef) AS latestAttrRefs - FOREACH (attr in latestAttrRefs | + ) + + RETURN metadata + ', + ' + OPTIONAL MATCH (latestRevision)-[:DESCRIBED_BY]->(TypeInstanceResourceVersionMetadata)-[:CHARACTERIZED_BY]->(latestAttrRef: AttributeReference) + WHERE latestAttrRef IS NOT NULL + WITH *, COLLECT(latestAttrRef) AS latestAttrRefs + FOREACH (attr in latestAttrRefs | CREATE (metadata)-[:CHARACTERIZED_BY]->(attr) - ) - - RETURN metadata - ', - {metadata: metadata, latestRevision: latestRevision, item: item} - ) YIELD value - - RETURN ti - """ - ) - - deleteTypeInstance(id: ID!, ownerID: LockOwnerID): ID! - - """ - Mark given TypeInstances as locked by a given owner. - If at least one TypeInstance is already locked with different OwnerID, an error is returned. - """ - lockTypeInstances(in: LockTypeInstancesInput!): [ID!]! - - """ - Remove lock from given TypeInstances. - If at least one TypeInstance was not locked by a given owner, an error is returned. - """ - unlockTypeInstances(in: UnlockTypeInstancesInput!): [ID!]! + ) + + RETURN metadata + ', + {metadata: metadata, latestRevision: latestRevision, item: item} + ) YIELD value + + RETURN ti + """ + ) + + deleteTypeInstance(id: ID!, ownerID: LockOwnerID): ID! + + """ + Mark given TypeInstances as locked by a given owner. + If at least one TypeInstance is already locked with different OwnerID, an error is returned. + """ + lockTypeInstances(in: LockTypeInstancesInput!): [ID!]! + + """ + Remove lock from given TypeInstances. + If at least one TypeInstance was not locked by a given owner, an error is returned. + """ + unlockTypeInstances(in: UnlockTypeInstancesInput!): [ID!]! } # TODO: Prepare directive for user authorization in https://github.com/capactio/capact/issues/508 diff --git a/internal/k8s-engine/controller/suite_test.go b/internal/k8s-engine/controller/suite_test.go index f0c72829a..308e4a343 100644 --- a/internal/k8s-engine/controller/suite_test.go +++ b/internal/k8s-engine/controller/suite_test.go @@ -4,9 +4,9 @@ package controller import ( + "capact.io/capact/internal/logger" "capact.io/capact/pkg/hub/client/local" "context" - "io/ioutil" "path/filepath" "strings" "testing" @@ -83,7 +83,7 @@ var _ = BeforeSuite(func(done Done) { }, } - svc := NewActionService(zap.NewRaw(zap.WriteTo(ioutil.Discard)), mgr.GetClient(), + svc := NewActionService(logger.Noop(), mgr.GetClient(), &argoRendererFake{}, &actionValidatorFake{}, &policyServiceFake{}, policy.MergeOrder{policy.Action, policy.Global}, &typeInstanceLockerFake{}, &typeInstanceGetterFake{}, cfg) diff --git a/internal/k8s-engine/graphql/domain/policy/converter.go b/internal/k8s-engine/graphql/domain/policy/converter.go index 21980bd4b..8732b1066 100644 --- a/internal/k8s-engine/graphql/domain/policy/converter.go +++ b/internal/k8s-engine/graphql/domain/policy/converter.go @@ -22,8 +22,11 @@ func (c *Converter) FromGraphQLInput(in graphql.PolicyInput) (policy.Policy, err return policy.Policy{}, err } + typeInstanceRules := c.typeInstanceFromGraphQLInput(in.TypeInstance) + return policy.Policy{ - Interface: ifaceRules, + Interface: ifaceRules, + TypeInstance: typeInstanceRules, }, nil } @@ -31,7 +34,7 @@ func (c *Converter) interfaceFromGraphQLInput(in *graphql.InterfacePolicyInput) if in == nil { return policy.InterfacePolicy{}, nil } - var rules policy.RulesList + var rules policy.InterfaceRulesList for _, gqlRule := range in.Rules { iface := c.manifestRefFromGraphQLInput(gqlRule.Interface) @@ -49,10 +52,58 @@ func (c *Converter) interfaceFromGraphQLInput(in *graphql.InterfacePolicyInput) return policy.InterfacePolicy{Rules: rules}, nil } +func (c *Converter) typeInstanceFromGraphQLInput(in *graphql.TypeInstancePolicyInput) policy.TypeInstancePolicy { + if in == nil { + return policy.TypeInstancePolicy{} + } + var rules []policy.RulesForTypeInstance + + for _, gqlRule := range in.Rules { + gqlRef, gqlBackend := gqlRule.TypeRef, gqlRule.Backend + if gqlRef == nil || gqlBackend == nil { + continue + } + + ref := types.ManifestRefWithOptRevision(*gqlRef) + rules = append(rules, policy.RulesForTypeInstance{ + TypeRef: ref, + Backend: policy.TypeInstanceBackend{ + TypeInstanceReference: policy.TypeInstanceReference{ + ID: gqlBackend.ID, + Description: gqlBackend.Description, + }, + }, + }) + } + + return policy.TypeInstancePolicy{Rules: rules} +} + // ToGraphQL converts Policy model representation to GraphQL DTO. func (c *Converter) ToGraphQL(in policy.Policy) graphql.Policy { return graphql.Policy{ - Interface: c.interfaceToGraphQL(in.Interface), + Interface: c.interfaceToGraphQL(in.Interface), + TypeInstance: c.typeInstanceToGraphQL(in.TypeInstance), + } +} + +func (c *Converter) typeInstanceToGraphQL(in policy.TypeInstancePolicy) *graphql.TypeInstancePolicy { + var gqlRules []*graphql.RulesForTypeInstance + + for _, rule := range in.Rules { + ref := graphql.ManifestReferenceWithOptionalRevision(rule.TypeRef) + + gqlRules = append(gqlRules, &graphql.RulesForTypeInstance{ + TypeRef: &ref, + Backend: &graphql.TypeInstanceBackend{ + ID: rule.Backend.ID, + Description: rule.Backend.Description, + }, + }) + } + + return &graphql.TypeInstancePolicy{ + Rules: gqlRules, } } @@ -243,7 +294,7 @@ func (c *Converter) requiredTypeInstancesToInjectFromGraphQLInput(in []*graphql. var out []policy.RequiredTypeInstanceToInject for _, item := range in { out = append(out, policy.RequiredTypeInstanceToInject{ - RequiredTypeInstanceReference: policy.RequiredTypeInstanceReference{ + TypeInstanceReference: policy.TypeInstanceReference{ ID: item.ID, Description: item.Description, }, diff --git a/internal/k8s-engine/graphql/domain/policy/fixtures_test.go b/internal/k8s-engine/graphql/domain/policy/fixtures_test.go index 1d1cf4a70..9fc66a59c 100644 --- a/internal/k8s-engine/graphql/domain/policy/fixtures_test.go +++ b/internal/k8s-engine/graphql/domain/policy/fixtures_test.go @@ -73,6 +73,37 @@ func fixGQLInput() graphql.PolicyInput { }, }, }, + TypeInstance: &graphql.TypeInstancePolicyInput{ + Rules: []*graphql.RulesForTypeInstanceInput{ + { + TypeRef: &graphql.ManifestReferenceInput{ + Path: "cap.type.aws.auth.credentials", + Revision: ptr.String("0.1.0"), + }, + Backend: &graphql.TypeInstanceBackendInput{ + ID: "00fd161c-01bd-47a6-9872-47490e11f996", + Description: ptr.String("Vault TI"), + }, + }, + { + TypeRef: &graphql.ManifestReferenceInput{ + Path: "cap.type.aws.*", + }, + Backend: &graphql.TypeInstanceBackendInput{ + ID: "31bb8355-10d7-49ce-a739-4554d8a40b63", + }, + }, + { + TypeRef: &graphql.ManifestReferenceInput{ + Path: "cap.*", + }, + Backend: &graphql.TypeInstanceBackendInput{ + ID: "a36ed738-dfe7-45ec-acd1-8e44e8db893b", + Description: ptr.String("Default Capact PostgreSQL backend"), + }, + }, + }, + }, } } @@ -142,13 +173,44 @@ func fixGQL() graphql.Policy { }, }, }, + TypeInstance: &graphql.TypeInstancePolicy{ + Rules: []*graphql.RulesForTypeInstance{ + { + TypeRef: &graphql.ManifestReferenceWithOptionalRevision{ + Path: "cap.type.aws.auth.credentials", + Revision: ptr.String("0.1.0"), + }, + Backend: &graphql.TypeInstanceBackend{ + ID: "00fd161c-01bd-47a6-9872-47490e11f996", + Description: ptr.String("Vault TI"), + }, + }, + { + TypeRef: &graphql.ManifestReferenceWithOptionalRevision{ + Path: "cap.type.aws.*", + }, + Backend: &graphql.TypeInstanceBackend{ + ID: "31bb8355-10d7-49ce-a739-4554d8a40b63", + }, + }, + { + TypeRef: &graphql.ManifestReferenceWithOptionalRevision{ + Path: "cap.*", + }, + Backend: &graphql.TypeInstanceBackend{ + ID: "a36ed738-dfe7-45ec-acd1-8e44e8db893b", + Description: ptr.String("Default Capact PostgreSQL backend"), + }, + }, + }, + }, } } func fixModel() policy.Policy { return policy.Policy{ Interface: policy.InterfacePolicy{ - Rules: policy.RulesList{ + Rules: policy.InterfaceRulesList{ { Interface: types.ManifestRefWithOptRevision{ Path: "cap.interface.database.postgresql.install", @@ -172,7 +234,7 @@ func fixModel() policy.Policy { Inject: &policy.InjectData{ RequiredTypeInstances: []policy.RequiredTypeInstanceToInject{ { - RequiredTypeInstanceReference: policy.RequiredTypeInstanceReference{ + TypeInstanceReference: policy.TypeInstanceReference{ ID: "c268d3f5-8834-434b-bea2-b677793611c5", Description: ptr.String("Sample description"), }, @@ -215,5 +277,42 @@ func fixModel() policy.Policy { }, }, }, + TypeInstance: policy.TypeInstancePolicy{ + Rules: []policy.RulesForTypeInstance{ + { + TypeRef: types.ManifestRefWithOptRevision{ + Path: "cap.type.aws.auth.credentials", + Revision: ptr.String("0.1.0"), + }, + Backend: policy.TypeInstanceBackend{ + TypeInstanceReference: policy.TypeInstanceReference{ + ID: "00fd161c-01bd-47a6-9872-47490e11f996", + Description: ptr.String("Vault TI"), + }, + }, + }, + { + TypeRef: types.ManifestRefWithOptRevision{ + Path: "cap.type.aws.*", + }, + Backend: policy.TypeInstanceBackend{ + TypeInstanceReference: policy.TypeInstanceReference{ + ID: "31bb8355-10d7-49ce-a739-4554d8a40b63", + }, + }, + }, + { + TypeRef: types.ManifestRefWithOptRevision{ + Path: "cap.*", + }, + Backend: policy.TypeInstanceBackend{ + TypeInstanceReference: policy.TypeInstanceReference{ + ID: "a36ed738-dfe7-45ec-acd1-8e44e8db893b", + Description: ptr.String("Default Capact PostgreSQL backend"), + }, + }, + }, + }, + }, } } diff --git a/internal/k8s-engine/policy/fixtures_test.go b/internal/k8s-engine/policy/fixtures_test.go index 9e1f8064a..dfd005ead 100644 --- a/internal/k8s-engine/policy/fixtures_test.go +++ b/internal/k8s-engine/policy/fixtures_test.go @@ -33,7 +33,7 @@ func fixCfgMap(t *testing.T, in policy.Policy) *v1.ConfigMap { func fixModel() policy.Policy { return policy.Policy{ Interface: policy.InterfacePolicy{ - Rules: policy.RulesList{ + Rules: policy.InterfaceRulesList{ { Interface: types.ManifestRefWithOptRevision{ Path: "cap.interface.database.postgresql.install", @@ -57,7 +57,7 @@ func fixModel() policy.Policy { Inject: &policy.InjectData{ RequiredTypeInstances: []policy.RequiredTypeInstanceToInject{ { - RequiredTypeInstanceReference: policy.RequiredTypeInstanceReference{ + TypeInstanceReference: policy.TypeInstanceReference{ ID: "c268d3f5-8834-434b-bea2-b677793611c5", Description: ptr.String("Sample description"), }, diff --git a/internal/logger/noop.go b/internal/logger/noop.go new file mode 100644 index 000000000..1c38cfd43 --- /dev/null +++ b/internal/logger/noop.go @@ -0,0 +1,13 @@ +package logger + +import ( + "io/ioutil" + + uberzap "go.uber.org/zap" + "sigs.k8s.io/controller-runtime/pkg/log/zap" +) + +// Noop discards all logged messages. +func Noop() *uberzap.Logger { + return zap.NewRaw(zap.WriteTo(ioutil.Discard)) +} diff --git a/pkg/engine/api/graphql/models_gen.go b/pkg/engine/api/graphql/models_gen.go index 9e507c82d..a87db8d73 100644 --- a/pkg/engine/api/graphql/models_gen.go +++ b/pkg/engine/api/graphql/models_gen.go @@ -164,11 +164,13 @@ type OutputTypeInstanceDetails struct { } type Policy struct { - Interface *InterfacePolicy `json:"interface"` + Interface *InterfacePolicy `json:"interface"` + TypeInstance *TypeInstancePolicy `json:"typeInstance"` } type PolicyInput struct { - Interface *InterfacePolicyInput `json:"interface"` + Interface *InterfacePolicyInput `json:"interface"` + TypeInstance *TypeInstancePolicyInput `json:"typeInstance"` } type PolicyRuleImplementationConstraintsInput struct { @@ -206,12 +208,40 @@ type RulesForInterfaceInput struct { OneOf []*PolicyRuleInput `json:"oneOf"` } +type RulesForTypeInstance struct { + TypeRef *ManifestReferenceWithOptionalRevision `json:"typeRef"` + Backend *TypeInstanceBackend `json:"backend"` +} + +type RulesForTypeInstanceInput struct { + TypeRef *ManifestReferenceInput `json:"typeRef"` + Backend *TypeInstanceBackendInput `json:"backend"` +} + // Additional Action status from the Runner type RunnerStatus struct { // Status of a given Runner e.g. Argo Workflow Runner status object with argoWorkflowRef field Status interface{} `json:"status"` } +type TypeInstanceBackend struct { + ID string `json:"id"` + Description *string `json:"description"` +} + +type TypeInstanceBackendInput struct { + ID string `json:"id"` + Description *string `json:"description"` +} + +type TypeInstancePolicy struct { + Rules []*RulesForTypeInstance `json:"rules"` +} + +type TypeInstancePolicyInput struct { + Rules []*RulesForTypeInstanceInput `json:"rules"` +} + // Stores user information type UserInfo struct { Username string `json:"username"` diff --git a/pkg/engine/api/graphql/schema.graphql b/pkg/engine/api/graphql/schema.graphql index ca7d4c5b9..3885a00db 100644 --- a/pkg/engine/api/graphql/schema.graphql +++ b/pkg/engine/api/graphql/schema.graphql @@ -304,8 +304,25 @@ enum ActionStatusPhase { input PolicyInput { interface: InterfacePolicyInput + typeInstance: TypeInstancePolicyInput } +# TypeInstance Policy Input +input TypeInstancePolicyInput { + rules: [RulesForTypeInstanceInput!]! +} + +input RulesForTypeInstanceInput { + typeRef: ManifestReferenceInput! + backend: TypeInstanceBackendInput! +} + +input TypeInstanceBackendInput { + id: ID! + description: String +} + +# Interface Policy Input input InterfacePolicyInput { rules: [RulesForInterfaceInput!]! } @@ -350,8 +367,25 @@ input PolicyRuleImplementationConstraintsInput { type Policy { interface: InterfacePolicy + typeInstance: TypeInstancePolicy +} + +# TypeInstance Policy +type TypeInstancePolicy { + rules: [RulesForTypeInstance!]! +} + +type RulesForTypeInstance { + typeRef: ManifestReferenceWithOptionalRevision! + backend: TypeInstanceBackend! +} + +type TypeInstanceBackend { + id: ID! + description: String } +# Interface Policy type InterfacePolicy { rules: [RulesForInterface!]! } diff --git a/pkg/engine/api/graphql/schema_gen.go b/pkg/engine/api/graphql/schema_gen.go index f3134b4ce..5490da85c 100644 --- a/pkg/engine/api/graphql/schema_gen.go +++ b/pkg/engine/api/graphql/schema_gen.go @@ -132,7 +132,8 @@ type ComplexityRoot struct { } Policy struct { - Interface func(childComplexity int) int + Interface func(childComplexity int) int + TypeInstance func(childComplexity int) int } PolicyRule struct { @@ -168,10 +169,24 @@ type ComplexityRoot struct { OneOf func(childComplexity int) int } + RulesForTypeInstance struct { + Backend func(childComplexity int) int + TypeRef func(childComplexity int) int + } + RunnerStatus struct { Status func(childComplexity int) int } + TypeInstanceBackend struct { + Description func(childComplexity int) int + ID func(childComplexity int) int + } + + TypeInstancePolicy struct { + Rules func(childComplexity int) int + } + UserInfo struct { Extra func(childComplexity int) int Groups func(childComplexity int) int @@ -580,6 +595,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Policy.Interface(childComplexity), true + case "Policy.typeInstance": + if e.complexity.Policy.TypeInstance == nil { + break + } + + return e.complexity.Policy.TypeInstance(childComplexity), true + case "PolicyRule.implementationConstraints": if e.complexity.PolicyRule.ImplementationConstraints == nil { break @@ -695,6 +717,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.RulesForInterface.OneOf(childComplexity), true + case "RulesForTypeInstance.backend": + if e.complexity.RulesForTypeInstance.Backend == nil { + break + } + + return e.complexity.RulesForTypeInstance.Backend(childComplexity), true + + case "RulesForTypeInstance.typeRef": + if e.complexity.RulesForTypeInstance.TypeRef == nil { + break + } + + return e.complexity.RulesForTypeInstance.TypeRef(childComplexity), true + case "RunnerStatus.status": if e.complexity.RunnerStatus.Status == nil { break @@ -702,6 +738,27 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.RunnerStatus.Status(childComplexity), true + case "TypeInstanceBackend.description": + if e.complexity.TypeInstanceBackend.Description == nil { + break + } + + return e.complexity.TypeInstanceBackend.Description(childComplexity), true + + case "TypeInstanceBackend.id": + if e.complexity.TypeInstanceBackend.ID == nil { + break + } + + return e.complexity.TypeInstanceBackend.ID(childComplexity), true + + case "TypeInstancePolicy.rules": + if e.complexity.TypeInstancePolicy.Rules == nil { + break + } + + return e.complexity.TypeInstancePolicy.Rules(childComplexity), true + case "UserInfo.extra": if e.complexity.UserInfo.Extra == nil { break @@ -1093,8 +1150,25 @@ enum ActionStatusPhase { input PolicyInput { interface: InterfacePolicyInput + typeInstance: TypeInstancePolicyInput +} + +# TypeInstance Policy Input +input TypeInstancePolicyInput { + rules: [RulesForTypeInstanceInput!]! } +input RulesForTypeInstanceInput { + typeRef: ManifestReferenceInput! + backend: TypeInstanceBackendInput! +} + +input TypeInstanceBackendInput { + id: ID! + description: String +} + +# Interface Policy Input input InterfacePolicyInput { rules: [RulesForInterfaceInput!]! } @@ -1139,8 +1213,25 @@ input PolicyRuleImplementationConstraintsInput { type Policy { interface: InterfacePolicy + typeInstance: TypeInstancePolicy } +# TypeInstance Policy +type TypeInstancePolicy { + rules: [RulesForTypeInstance!]! +} + +type RulesForTypeInstance { + typeRef: ManifestReferenceWithOptionalRevision! + backend: TypeInstanceBackend! +} + +type TypeInstanceBackend { + id: ID! + description: String +} + +# Interface Policy type InterfacePolicy { rules: [RulesForInterface!]! } @@ -3110,6 +3201,38 @@ func (ec *executionContext) _Policy_interface(ctx context.Context, field graphql return ec.marshalOInterfacePolicy2ᚖcapactᚗioᚋcapactᚋpkgᚋengineᚋapiᚋgraphqlᚐInterfacePolicy(ctx, field.Selections, res) } +func (ec *executionContext) _Policy_typeInstance(ctx context.Context, field graphql.CollectedField, obj *Policy) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Policy", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.TypeInstance, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*TypeInstancePolicy) + fc.Result = res + return ec.marshalOTypeInstancePolicy2ᚖcapactᚗioᚋcapactᚋpkgᚋengineᚋapiᚋgraphqlᚐTypeInstancePolicy(ctx, field.Selections, res) +} + func (ec *executionContext) _PolicyRule_implementationConstraints(ctx context.Context, field graphql.CollectedField, obj *PolicyRule) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -3690,6 +3813,76 @@ func (ec *executionContext) _RulesForInterface_oneOf(ctx context.Context, field return ec.marshalNPolicyRule2ᚕᚖcapactᚗioᚋcapactᚋpkgᚋengineᚋapiᚋgraphqlᚐPolicyRuleᚄ(ctx, field.Selections, res) } +func (ec *executionContext) _RulesForTypeInstance_typeRef(ctx context.Context, field graphql.CollectedField, obj *RulesForTypeInstance) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "RulesForTypeInstance", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.TypeRef, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*ManifestReferenceWithOptionalRevision) + fc.Result = res + return ec.marshalNManifestReferenceWithOptionalRevision2ᚖcapactᚗioᚋcapactᚋpkgᚋengineᚋapiᚋgraphqlᚐManifestReferenceWithOptionalRevision(ctx, field.Selections, res) +} + +func (ec *executionContext) _RulesForTypeInstance_backend(ctx context.Context, field graphql.CollectedField, obj *RulesForTypeInstance) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "RulesForTypeInstance", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Backend, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*TypeInstanceBackend) + fc.Result = res + return ec.marshalNTypeInstanceBackend2ᚖcapactᚗioᚋcapactᚋpkgᚋengineᚋapiᚋgraphqlᚐTypeInstanceBackend(ctx, field.Selections, res) +} + func (ec *executionContext) _RunnerStatus_status(ctx context.Context, field graphql.CollectedField, obj *RunnerStatus) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -3722,6 +3915,108 @@ func (ec *executionContext) _RunnerStatus_status(ctx context.Context, field grap return ec.marshalOAny2interface(ctx, field.Selections, res) } +func (ec *executionContext) _TypeInstanceBackend_id(ctx context.Context, field graphql.CollectedField, obj *TypeInstanceBackend) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "TypeInstanceBackend", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.ID, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNID2string(ctx, field.Selections, res) +} + +func (ec *executionContext) _TypeInstanceBackend_description(ctx context.Context, field graphql.CollectedField, obj *TypeInstanceBackend) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "TypeInstanceBackend", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Description, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + +func (ec *executionContext) _TypeInstancePolicy_rules(ctx context.Context, field graphql.CollectedField, obj *TypeInstancePolicy) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "TypeInstancePolicy", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Rules, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.([]*RulesForTypeInstance) + fc.Result = res + return ec.marshalNRulesForTypeInstance2ᚕᚖcapactᚗioᚋcapactᚋpkgᚋengineᚋapiᚋgraphqlᚐRulesForTypeInstanceᚄ(ctx, field.Selections, res) +} + func (ec *executionContext) _UserInfo_username(ctx context.Context, field graphql.CollectedField, obj *UserInfo) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -5209,6 +5504,14 @@ func (ec *executionContext) unmarshalInputPolicyInput(ctx context.Context, obj i if err != nil { return it, err } + case "typeInstance": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("typeInstance")) + it.TypeInstance, err = ec.unmarshalOTypeInstancePolicyInput2ᚖcapactᚗioᚋcapactᚋpkgᚋengineᚋapiᚋgraphqlᚐTypeInstancePolicyInput(ctx, v) + if err != nil { + return it, err + } } } @@ -5371,6 +5674,82 @@ func (ec *executionContext) unmarshalInputRulesForInterfaceInput(ctx context.Con return it, nil } +func (ec *executionContext) unmarshalInputRulesForTypeInstanceInput(ctx context.Context, obj interface{}) (RulesForTypeInstanceInput, error) { + var it RulesForTypeInstanceInput + var asMap = obj.(map[string]interface{}) + + for k, v := range asMap { + switch k { + case "typeRef": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("typeRef")) + it.TypeRef, err = ec.unmarshalNManifestReferenceInput2ᚖcapactᚗioᚋcapactᚋpkgᚋengineᚋapiᚋgraphqlᚐManifestReferenceInput(ctx, v) + if err != nil { + return it, err + } + case "backend": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("backend")) + it.Backend, err = ec.unmarshalNTypeInstanceBackendInput2ᚖcapactᚗioᚋcapactᚋpkgᚋengineᚋapiᚋgraphqlᚐTypeInstanceBackendInput(ctx, v) + if err != nil { + return it, err + } + } + } + + return it, nil +} + +func (ec *executionContext) unmarshalInputTypeInstanceBackendInput(ctx context.Context, obj interface{}) (TypeInstanceBackendInput, error) { + var it TypeInstanceBackendInput + var asMap = obj.(map[string]interface{}) + + for k, v := range asMap { + switch k { + case "id": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("id")) + it.ID, err = ec.unmarshalNID2string(ctx, v) + if err != nil { + return it, err + } + case "description": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("description")) + it.Description, err = ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + } + } + + return it, nil +} + +func (ec *executionContext) unmarshalInputTypeInstancePolicyInput(ctx context.Context, obj interface{}) (TypeInstancePolicyInput, error) { + var it TypeInstancePolicyInput + var asMap = obj.(map[string]interface{}) + + for k, v := range asMap { + switch k { + case "rules": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("rules")) + it.Rules, err = ec.unmarshalNRulesForTypeInstanceInput2ᚕᚖcapactᚗioᚋcapactᚋpkgᚋengineᚋapiᚋgraphqlᚐRulesForTypeInstanceInputᚄ(ctx, v) + if err != nil { + return it, err + } + } + } + + return it, nil +} + // endregion **************************** input.gotpl ***************************** // region ************************** interface.gotpl *************************** @@ -5897,6 +6276,8 @@ func (ec *executionContext) _Policy(ctx context.Context, sel ast.SelectionSet, o out.Values[i] = graphql.MarshalString("Policy") case "interface": out.Values[i] = ec._Policy_interface(ctx, field, obj) + case "typeInstance": + out.Values[i] = ec._Policy_typeInstance(ctx, field, obj) default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -6120,6 +6501,38 @@ func (ec *executionContext) _RulesForInterface(ctx context.Context, sel ast.Sele return out } +var rulesForTypeInstanceImplementors = []string{"RulesForTypeInstance"} + +func (ec *executionContext) _RulesForTypeInstance(ctx context.Context, sel ast.SelectionSet, obj *RulesForTypeInstance) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, rulesForTypeInstanceImplementors) + + out := graphql.NewFieldSet(fields) + var invalids uint32 + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("RulesForTypeInstance") + case "typeRef": + out.Values[i] = ec._RulesForTypeInstance_typeRef(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + case "backend": + out.Values[i] = ec._RulesForTypeInstance_backend(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch() + if invalids > 0 { + return graphql.Null + } + return out +} + var runnerStatusImplementors = []string{"RunnerStatus"} func (ec *executionContext) _RunnerStatus(ctx context.Context, sel ast.SelectionSet, obj *RunnerStatus) graphql.Marshaler { @@ -6144,6 +6557,62 @@ func (ec *executionContext) _RunnerStatus(ctx context.Context, sel ast.Selection return out } +var typeInstanceBackendImplementors = []string{"TypeInstanceBackend"} + +func (ec *executionContext) _TypeInstanceBackend(ctx context.Context, sel ast.SelectionSet, obj *TypeInstanceBackend) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, typeInstanceBackendImplementors) + + out := graphql.NewFieldSet(fields) + var invalids uint32 + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("TypeInstanceBackend") + case "id": + out.Values[i] = ec._TypeInstanceBackend_id(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + case "description": + out.Values[i] = ec._TypeInstanceBackend_description(ctx, field, obj) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch() + if invalids > 0 { + return graphql.Null + } + return out +} + +var typeInstancePolicyImplementors = []string{"TypeInstancePolicy"} + +func (ec *executionContext) _TypeInstancePolicy(ctx context.Context, sel ast.SelectionSet, obj *TypeInstancePolicy) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, typeInstancePolicyImplementors) + + out := graphql.NewFieldSet(fields) + var invalids uint32 + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("TypeInstancePolicy") + case "rules": + out.Values[i] = ec._TypeInstancePolicy_rules(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch() + if invalids > 0 { + return graphql.Null + } + return out +} + var userInfoImplementors = []string{"UserInfo"} func (ec *executionContext) _UserInfo(ctx context.Context, sel ast.SelectionSet, obj *UserInfo) graphql.Marshaler { @@ -6941,6 +7410,79 @@ func (ec *executionContext) unmarshalNRulesForInterfaceInput2ᚖcapactᚗioᚋca return &res, graphql.ErrorOnPath(ctx, err) } +func (ec *executionContext) marshalNRulesForTypeInstance2ᚕᚖcapactᚗioᚋcapactᚋpkgᚋengineᚋapiᚋgraphqlᚐRulesForTypeInstanceᚄ(ctx context.Context, sel ast.SelectionSet, v []*RulesForTypeInstance) graphql.Marshaler { + ret := make(graphql.Array, len(v)) + var wg sync.WaitGroup + isLen1 := len(v) == 1 + if !isLen1 { + wg.Add(len(v)) + } + for i := range v { + i := i + fc := &graphql.FieldContext{ + Index: &i, + Result: &v[i], + } + ctx := graphql.WithFieldContext(ctx, fc) + f := func(i int) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + if !isLen1 { + defer wg.Done() + } + ret[i] = ec.marshalNRulesForTypeInstance2ᚖcapactᚗioᚋcapactᚋpkgᚋengineᚋapiᚋgraphqlᚐRulesForTypeInstance(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + return ret +} + +func (ec *executionContext) marshalNRulesForTypeInstance2ᚖcapactᚗioᚋcapactᚋpkgᚋengineᚋapiᚋgraphqlᚐRulesForTypeInstance(ctx context.Context, sel ast.SelectionSet, v *RulesForTypeInstance) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + return ec._RulesForTypeInstance(ctx, sel, v) +} + +func (ec *executionContext) unmarshalNRulesForTypeInstanceInput2ᚕᚖcapactᚗioᚋcapactᚋpkgᚋengineᚋapiᚋgraphqlᚐRulesForTypeInstanceInputᚄ(ctx context.Context, v interface{}) ([]*RulesForTypeInstanceInput, error) { + var vSlice []interface{} + if v != nil { + if tmp1, ok := v.([]interface{}); ok { + vSlice = tmp1 + } else { + vSlice = []interface{}{v} + } + } + var err error + res := make([]*RulesForTypeInstanceInput, len(vSlice)) + for i := range vSlice { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithIndex(i)) + res[i], err = ec.unmarshalNRulesForTypeInstanceInput2ᚖcapactᚗioᚋcapactᚋpkgᚋengineᚋapiᚋgraphqlᚐRulesForTypeInstanceInput(ctx, vSlice[i]) + if err != nil { + return nil, err + } + } + return res, nil +} + +func (ec *executionContext) unmarshalNRulesForTypeInstanceInput2ᚖcapactᚗioᚋcapactᚋpkgᚋengineᚋapiᚋgraphqlᚐRulesForTypeInstanceInput(ctx context.Context, v interface{}) (*RulesForTypeInstanceInput, error) { + res, err := ec.unmarshalInputRulesForTypeInstanceInput(ctx, v) + return &res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) unmarshalNString2string(ctx context.Context, v interface{}) (string, error) { res, err := graphql.UnmarshalString(v) return res, graphql.ErrorOnPath(ctx, err) @@ -6996,6 +7538,21 @@ func (ec *executionContext) marshalNTimestamp2capactᚗioᚋcapactᚋpkgᚋengin return v } +func (ec *executionContext) marshalNTypeInstanceBackend2ᚖcapactᚗioᚋcapactᚋpkgᚋengineᚋapiᚋgraphqlᚐTypeInstanceBackend(ctx context.Context, sel ast.SelectionSet, v *TypeInstanceBackend) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + return ec._TypeInstanceBackend(ctx, sel, v) +} + +func (ec *executionContext) unmarshalNTypeInstanceBackendInput2ᚖcapactᚗioᚋcapactᚋpkgᚋengineᚋapiᚋgraphqlᚐTypeInstanceBackendInput(ctx context.Context, v interface{}) (*TypeInstanceBackendInput, error) { + res, err := ec.unmarshalInputTypeInstanceBackendInput(ctx, v) + return &res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) unmarshalNVersion2string(ctx context.Context, v interface{}) (string, error) { res, err := graphql.UnmarshalString(v) return res, graphql.ErrorOnPath(ctx, err) @@ -7779,6 +8336,21 @@ func (ec *executionContext) marshalOString2ᚖstring(ctx context.Context, sel as return graphql.MarshalString(*v) } +func (ec *executionContext) marshalOTypeInstancePolicy2ᚖcapactᚗioᚋcapactᚋpkgᚋengineᚋapiᚋgraphqlᚐTypeInstancePolicy(ctx context.Context, sel ast.SelectionSet, v *TypeInstancePolicy) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._TypeInstancePolicy(ctx, sel, v) +} + +func (ec *executionContext) unmarshalOTypeInstancePolicyInput2ᚖcapactᚗioᚋcapactᚋpkgᚋengineᚋapiᚋgraphqlᚐTypeInstancePolicyInput(ctx context.Context, v interface{}) (*TypeInstancePolicyInput, error) { + if v == nil { + return nil, nil + } + res, err := ec.unmarshalInputTypeInstancePolicyInput(ctx, v) + return &res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) marshalOUserInfo2ᚖcapactᚗioᚋcapactᚋpkgᚋengineᚋapiᚋgraphqlᚐUserInfo(ctx context.Context, sel ast.SelectionSet, v *UserInfo) graphql.Marshaler { if v == nil { return graphql.Null diff --git a/pkg/engine/client/action.go b/pkg/engine/client/action.go new file mode 100644 index 000000000..bd66b2206 --- /dev/null +++ b/pkg/engine/client/action.go @@ -0,0 +1,137 @@ +package client + +import ( + "context" + "fmt" + + "capact.io/capact/internal/k8s-engine/graphql/namespace" + gqlengine "capact.io/capact/pkg/engine/api/graphql" + + "github.com/machinebox/graphql" + "github.com/pkg/errors" +) + +// Action knows how to execute GraphQL queries and mutations for Action type. +type Action struct { + client *graphql.Client +} + +// CreateAction creates Action in the Namespace extracted from a given ctx. +func (c *Action) CreateAction(ctx context.Context, in *gqlengine.ActionDetailsInput) (*gqlengine.Action, error) { + req := graphql.NewRequest(fmt.Sprintf(`mutation($in: ActionDetailsInput) { + createAction( + in: $in + ) { + %s + } + }`, actionFields)) + + c.enrichWithNamespace(ctx, req) + req.Var("in", in) + + var resp struct { + Action gqlengine.Action `json:"createAction"` + } + if err := c.client.Run(ctx, req, &resp); err != nil { + return nil, errors.Wrap(err, "while executing mutation to create Action") + } + + return &resp.Action, nil +} + +// GetAction returns Action with a given name from Namespace extracted from a given ctx. +func (c *Action) GetAction(ctx context.Context, name string) (*gqlengine.Action, error) { + req := graphql.NewRequest(fmt.Sprintf(`query($name: String!) { + action(name: $name) { + %s + } + }`, actionFields)) + + c.enrichWithNamespace(ctx, req) + req.Var("name", name) + + var resp struct { + Action *gqlengine.Action `json:"action"` + } + if err := c.client.Run(ctx, req, &resp); err != nil { + return nil, errors.Wrap(err, "while executing query to get Action") + } + + return resp.Action, nil +} + +// ListActions returns all Actions which meet filter criteria. +// Namespace extracted from a given ctx. +func (c *Action) ListActions(ctx context.Context, filter *gqlengine.ActionFilter) ([]*gqlengine.Action, error) { + req := graphql.NewRequest(fmt.Sprintf(`query($filter: ActionFilter) { + actions(filter: $filter) { + %s + } + }`, actionFields)) + + c.enrichWithNamespace(ctx, req) + req.Var("filter", filter) + + var resp struct { + Actions []*gqlengine.Action `json:"actions"` + } + if err := c.client.Run(ctx, req, &resp); err != nil { + return nil, errors.Wrap(err, "while executing query to get Action") + } + + return resp.Actions, nil +} + +// RunAction executes a given Action. +func (c *Action) RunAction(ctx context.Context, name string) error { + req := graphql.NewRequest(fmt.Sprintf(`mutation($name: String!) { + runAction( + name: $name + ) { + %s + } + }`, actionFields)) + + c.enrichWithNamespace(ctx, req) + req.Var("name", name) + + var resp struct { + Action gqlengine.Action `json:"runAction"` + } + if err := c.client.Run(ctx, req, &resp); err != nil { + return errors.Wrap(err, "while executing mutation to run Action") + } + + return nil +} + +// DeleteAction deletes a given Action. +func (c *Action) DeleteAction(ctx context.Context, name string) error { + req := graphql.NewRequest(fmt.Sprintf(`mutation($name: String!) { + deleteAction( + name: $name + ) { + %s + } + }`, actionFields)) + + c.enrichWithNamespace(ctx, req) + req.Var("name", name) + + var resp struct { + Action gqlengine.Action `json:"deleteAction"` + } + if err := c.client.Run(ctx, req, &resp); err != nil { + return errors.Wrap(err, "while executing mutation to delete Action") + } + + return nil +} + +func (c *Action) enrichWithNamespace(ctx context.Context, req *graphql.Request) { + ns, err := namespace.FromContext(ctx) + if err != nil { + return + } + req.Header.Add(namespace.NamespaceHeaderName, ns) +} diff --git a/pkg/engine/client/client.go b/pkg/engine/client/client.go index b631283f6..e1a376d58 100644 --- a/pkg/engine/client/client.go +++ b/pkg/engine/client/client.go @@ -1,183 +1,19 @@ package client import ( - "context" - "fmt" - - "capact.io/capact/internal/k8s-engine/graphql/namespace" - enginegraphql "capact.io/capact/pkg/engine/api/graphql" - "github.com/machinebox/graphql" - "github.com/pkg/errors" ) // Client used to communicate with the Capact Engine GraphQL API type Client struct { - client *graphql.Client + Action + Policy } // New returns a new Client instance. func New(gqlClient *graphql.Client) *Client { return &Client{ - client: gqlClient, - } -} - -// CreateAction creates Action in the Namespace extracted from a given ctx. -func (c *Client) CreateAction(ctx context.Context, in *enginegraphql.ActionDetailsInput) (*enginegraphql.Action, error) { - req := graphql.NewRequest(fmt.Sprintf(`mutation($in: ActionDetailsInput) { - createAction( - in: $in - ) { - %s - } - }`, actionFields)) - - c.enrichWithNamespace(ctx, req) - req.Var("in", in) - - var resp struct { - Action enginegraphql.Action `json:"createAction"` - } - if err := c.client.Run(ctx, req, &resp); err != nil { - return nil, errors.Wrap(err, "while executing mutation to create Action") - } - - return &resp.Action, nil -} - -// GetAction returns Action with a given name from Namespace extracted from a given ctx. -func (c *Client) GetAction(ctx context.Context, name string) (*enginegraphql.Action, error) { - req := graphql.NewRequest(fmt.Sprintf(`query($name: String!) { - action(name: $name) { - %s - } - }`, actionFields)) - - c.enrichWithNamespace(ctx, req) - req.Var("name", name) - - var resp struct { - Action *enginegraphql.Action `json:"action"` - } - if err := c.client.Run(ctx, req, &resp); err != nil { - return nil, errors.Wrap(err, "while executing query to get Action") - } - - return resp.Action, nil -} - -// ListActions returns all Actions which meet filter criteria. -// Namespace extracted from a given ctx. -func (c *Client) ListActions(ctx context.Context, filter *enginegraphql.ActionFilter) ([]*enginegraphql.Action, error) { - req := graphql.NewRequest(fmt.Sprintf(`query($filter: ActionFilter) { - actions(filter: $filter) { - %s - } - }`, actionFields)) - - c.enrichWithNamespace(ctx, req) - req.Var("filter", filter) - - var resp struct { - Actions []*enginegraphql.Action `json:"actions"` - } - if err := c.client.Run(ctx, req, &resp); err != nil { - return nil, errors.Wrap(err, "while executing query to get Action") - } - - return resp.Actions, nil -} - -// RunAction executes a given Action. -func (c *Client) RunAction(ctx context.Context, name string) error { - req := graphql.NewRequest(fmt.Sprintf(`mutation($name: String!) { - runAction( - name: $name - ) { - %s - } - }`, actionFields)) - - c.enrichWithNamespace(ctx, req) - req.Var("name", name) - - var resp struct { - Action enginegraphql.Action `json:"runAction"` - } - if err := c.client.Run(ctx, req, &resp); err != nil { - return errors.Wrap(err, "while executing mutation to run Action") - } - - return nil -} - -// DeleteAction deletes a given Action. -func (c *Client) DeleteAction(ctx context.Context, name string) error { - req := graphql.NewRequest(fmt.Sprintf(`mutation($name: String!) { - deleteAction( - name: $name - ) { - %s - } - }`, actionFields)) - - c.enrichWithNamespace(ctx, req) - req.Var("name", name) - - var resp struct { - Action enginegraphql.Action `json:"deleteAction"` - } - if err := c.client.Run(ctx, req, &resp); err != nil { - return errors.Wrap(err, "while executing mutation to delete Action") - } - - return nil -} - -// UpdatePolicy updates Capact Policy on cluster side. -func (c *Client) UpdatePolicy(ctx context.Context, policy *enginegraphql.PolicyInput) (*enginegraphql.Policy, error) { - req := graphql.NewRequest(fmt.Sprintf(`mutation($in: PolicyInput!) { - updatePolicy( - in: $in - ) { - %s - } - }`, policyFields)) - req.Var("in", policy) - - var resp struct { - Policy *enginegraphql.Policy `json:"updatePolicy"` - } - if err := c.client.Run(ctx, req, nil); err != nil { - return nil, errors.Wrap(err, "while executing mutation to update Policy") - } - - return resp.Policy, nil -} - -// GetPolicy returns current Capact Policy. -func (c *Client) GetPolicy(ctx context.Context) (*enginegraphql.Policy, error) { - req := graphql.NewRequest(fmt.Sprintf(`query { - policy{ - %s - } - }`, policyFields)) - - var resp struct { - Policy *enginegraphql.Policy `json:"policy"` - } - if err := c.client.Run(ctx, req, &resp); err != nil { - return nil, errors.Wrap(err, "while executing query to get Policy") - } - - return resp.Policy, nil -} - -func (c *Client) enrichWithNamespace(ctx context.Context, req *graphql.Request) { - ns, err := namespace.FromContext(ctx) - if err != nil { - return + Action: Action{client: gqlClient}, + Policy: Policy{client: gqlClient}, } - req.Header.Add(namespace.NamespaceHeaderName, ns) } diff --git a/pkg/engine/client/fields.go b/pkg/engine/client/fields.go index 8653d349d..14597f4b9 100644 --- a/pkg/engine/client/fields.go +++ b/pkg/engine/client/fields.go @@ -104,4 +104,16 @@ const policyFields = ` } } } + typeInstance { + rules { + typeRef { + path + revision + } + backend { + id + description + } + } + } ` diff --git a/pkg/engine/client/policy.go b/pkg/engine/client/policy.go new file mode 100644 index 000000000..2bc9b768c --- /dev/null +++ b/pkg/engine/client/policy.go @@ -0,0 +1,55 @@ +package client + +import ( + "context" + "fmt" + + gqlengine "capact.io/capact/pkg/engine/api/graphql" + + "github.com/machinebox/graphql" + "github.com/pkg/errors" +) + +// Policy knows how to execute GraphQL queries and mutations for Policy type. +type Policy struct { + client *graphql.Client +} + +// UpdatePolicy updates Capact Policy on cluster side. +func (c *Policy) UpdatePolicy(ctx context.Context, policy *gqlengine.PolicyInput) (*gqlengine.Policy, error) { + req := graphql.NewRequest(fmt.Sprintf(`mutation($in: PolicyInput!) { + updatePolicy( + in: $in + ) { + %s + } + }`, policyFields)) + req.Var("in", policy) + + var resp struct { + Policy *gqlengine.Policy `json:"updatePolicy"` + } + if err := c.client.Run(ctx, req, nil); err != nil { + return nil, errors.Wrap(err, "while executing mutation to update Policy") + } + + return resp.Policy, nil +} + +// GetPolicy returns current Capact Policy. +func (c *Policy) GetPolicy(ctx context.Context) (*gqlengine.Policy, error) { + req := graphql.NewRequest(fmt.Sprintf(`query { + policy{ + %s + } + }`, policyFields)) + + var resp struct { + Policy *gqlengine.Policy `json:"policy"` + } + if err := c.client.Run(ctx, req, &resp); err != nil { + return nil, errors.Wrap(err, "while executing query to get Policy") + } + + return resp.Policy, nil +} diff --git a/pkg/engine/k8s/policy/fixtures_test.go b/pkg/engine/k8s/policy/fixtures_test.go index f94c9a0e1..f3b3e7825 100644 --- a/pkg/engine/k8s/policy/fixtures_test.go +++ b/pkg/engine/k8s/policy/fixtures_test.go @@ -9,7 +9,7 @@ import ( func fixPolicyWithTypeRef() policy.Policy { return policy.Policy{ Interface: policy.InterfacePolicy{ - Rules: policy.RulesList{ + Rules: policy.InterfaceRulesList{ { Interface: types.ManifestRefWithOptRevision{ Path: "cap.*", @@ -20,22 +20,22 @@ func fixPolicyWithTypeRef() policy.Policy { Inject: &policy.InjectData{ RequiredTypeInstances: []policy.RequiredTypeInstanceToInject{ { - RequiredTypeInstanceReference: policy.RequiredTypeInstanceReference{ + TypeInstanceReference: policy.TypeInstanceReference{ ID: "id", - }, - TypeRef: &types.ManifestRef{ - Path: "cap.type.sample", - Revision: "0.1.0", + TypeRef: &types.TypeRef{ + Path: "cap.type.sample", + Revision: "0.1.0", + }, }, }, { - RequiredTypeInstanceReference: policy.RequiredTypeInstanceReference{ + TypeInstanceReference: policy.TypeInstanceReference{ ID: "id2", Description: ptr.String("ID 2"), - }, - TypeRef: &types.ManifestRef{ - Path: "cap.type.sample2", - Revision: "0.2.0", + TypeRef: &types.TypeRef{ + Path: "cap.type.sample2", + Revision: "0.2.0", + }, }, }, }, diff --git a/pkg/engine/k8s/policy/from_yaml_test.go b/pkg/engine/k8s/policy/from_yaml_test.go index 38b4c96df..138704563 100644 --- a/pkg/engine/k8s/policy/from_yaml_test.go +++ b/pkg/engine/k8s/policy/from_yaml_test.go @@ -48,7 +48,7 @@ func loadInput(t *testing.T, path string) string { func fixValidPolicy() policy.Policy { return policy.Policy{ Interface: policy.InterfacePolicy{ - Rules: policy.RulesList{ + Rules: policy.InterfaceRulesList{ { Interface: types.ManifestRefWithOptRevision{ Path: "cap.interface.database.postgresql.install", @@ -77,7 +77,7 @@ func fixValidPolicy() policy.Policy { Inject: &policy.InjectData{ RequiredTypeInstances: []policy.RequiredTypeInstanceToInject{ { - RequiredTypeInstanceReference: policy.RequiredTypeInstanceReference{ + TypeInstanceReference: policy.TypeInstanceReference{ ID: "sample-uuid", Description: ptr.String("Google Cloud Platform Service Account"), }, diff --git a/pkg/engine/k8s/policy/metadata/fixtures_test.go b/pkg/engine/k8s/policy/metadata/fixtures_test.go index 3ff0a27b8..da34d40a7 100644 --- a/pkg/engine/k8s/policy/metadata/fixtures_test.go +++ b/pkg/engine/k8s/policy/metadata/fixtures_test.go @@ -3,19 +3,23 @@ package metadata_test import ( "context" "fmt" + "regexp" "strings" "capact.io/capact/internal/ptr" "capact.io/capact/pkg/engine/k8s/policy" gqllocalapi "capact.io/capact/pkg/hub/api/graphql/local" + hubpublicgraphql "capact.io/capact/pkg/hub/api/graphql/public" + "capact.io/capact/pkg/hub/client/public" "capact.io/capact/pkg/sdk/apis/0.0.1/types" + "github.com/pkg/errors" ) func fixComplexPolicyWithoutTypeRef() *policy.Policy { return &policy.Policy{ Interface: policy.InterfacePolicy{ - Rules: policy.RulesList{ + Rules: policy.InterfaceRulesList{ { Interface: types.ManifestRefWithOptRevision{ Path: "cap.*", @@ -26,12 +30,12 @@ func fixComplexPolicyWithoutTypeRef() *policy.Policy { Inject: &policy.InjectData{ RequiredTypeInstances: []policy.RequiredTypeInstanceToInject{ { - RequiredTypeInstanceReference: policy.RequiredTypeInstanceReference{ + TypeInstanceReference: policy.TypeInstanceReference{ ID: "id1", }, }, { - RequiredTypeInstanceReference: policy.RequiredTypeInstanceReference{ + TypeInstanceReference: policy.TypeInstanceReference{ ID: "id2", Description: ptr.String("ID 2"), }, @@ -58,7 +62,7 @@ func fixComplexPolicyWithoutTypeRef() *policy.Policy { Inject: &policy.InjectData{ RequiredTypeInstances: []policy.RequiredTypeInstanceToInject{ { - RequiredTypeInstanceReference: policy.RequiredTypeInstanceReference{ + TypeInstanceReference: policy.TypeInstanceReference{ ID: "id4", }, }, @@ -91,7 +95,7 @@ func fixComplexPolicyWithoutTypeRef() *policy.Policy { Inject: &policy.InjectData{ RequiredTypeInstances: []policy.RequiredTypeInstanceToInject{ { - RequiredTypeInstanceReference: policy.RequiredTypeInstanceReference{ + TypeInstanceReference: policy.TypeInstanceReference{ ID: "id7", }, }, @@ -110,13 +114,50 @@ func fixComplexPolicyWithoutTypeRef() *policy.Policy { }, }, }, + TypeInstance: policy.TypeInstancePolicy{ + Rules: []policy.RulesForTypeInstance{ + { + TypeRef: types.ManifestRefWithOptRevision{ + Path: "cap.type.aws.auth.credentials", + Revision: ptr.String("0.1.0"), + }, + Backend: policy.TypeInstanceBackend{ + TypeInstanceReference: policy.TypeInstanceReference{ + ID: "id9", + Description: ptr.String("ID 9"), + }, + }, + }, + { + TypeRef: types.ManifestRefWithOptRevision{ + Path: "cap.type.aws.*", + }, + Backend: policy.TypeInstanceBackend{ + TypeInstanceReference: policy.TypeInstanceReference{ + ID: "id10", + }, + }, + }, + { + TypeRef: types.ManifestRefWithOptRevision{ + Path: "cap.*", + }, + Backend: policy.TypeInstanceBackend{ + TypeInstanceReference: policy.TypeInstanceReference{ + ID: "id11", + Description: ptr.String("ID 11"), + }, + }, + }, + }, + }, } } func fixComplexPolicyWithTypeRef() *policy.Policy { return &policy.Policy{ Interface: policy.InterfacePolicy{ - Rules: policy.RulesList{ + Rules: policy.InterfaceRulesList{ { Interface: types.ManifestRefWithOptRevision{ Path: "cap.*", @@ -127,22 +168,22 @@ func fixComplexPolicyWithTypeRef() *policy.Policy { Inject: &policy.InjectData{ RequiredTypeInstances: []policy.RequiredTypeInstanceToInject{ { - RequiredTypeInstanceReference: policy.RequiredTypeInstanceReference{ + TypeInstanceReference: policy.TypeInstanceReference{ ID: "id1", - }, - TypeRef: &types.ManifestRef{ - Path: "cap.type.type1", - Revision: "0.1.0", + TypeRef: &types.TypeRef{ + Path: "cap.type.type1", + Revision: "0.1.0", + }, }, }, { - RequiredTypeInstanceReference: policy.RequiredTypeInstanceReference{ + TypeInstanceReference: policy.TypeInstanceReference{ ID: "id2", Description: ptr.String("ID 2"), - }, - TypeRef: &types.ManifestRef{ - Path: "cap.type.type2", - Revision: "0.2.0", + TypeRef: &types.TypeRef{ + Path: "cap.type.type2", + Revision: "0.2.0", + }, }, }, }, @@ -175,12 +216,12 @@ func fixComplexPolicyWithTypeRef() *policy.Policy { Inject: &policy.InjectData{ RequiredTypeInstances: []policy.RequiredTypeInstanceToInject{ { - RequiredTypeInstanceReference: policy.RequiredTypeInstanceReference{ + TypeInstanceReference: policy.TypeInstanceReference{ ID: "id4", - }, - TypeRef: &types.ManifestRef{ - Path: "cap.type.type4", - Revision: "0.4.0", + TypeRef: &types.TypeRef{ + Path: "cap.type.type4", + Revision: "0.4.0", + }, }, }, }, @@ -220,12 +261,12 @@ func fixComplexPolicyWithTypeRef() *policy.Policy { Inject: &policy.InjectData{ RequiredTypeInstances: []policy.RequiredTypeInstanceToInject{ { - RequiredTypeInstanceReference: policy.RequiredTypeInstanceReference{ + TypeInstanceReference: policy.TypeInstanceReference{ ID: "id7", - }, - TypeRef: &types.ManifestRef{ - Path: "cap.type.type7", - Revision: "0.7.0", + TypeRef: &types.TypeRef{ + Path: "cap.type.type7", + Revision: "0.7.0", + }, }, }, }, @@ -247,6 +288,55 @@ func fixComplexPolicyWithTypeRef() *policy.Policy { }, }, }, + TypeInstance: policy.TypeInstancePolicy{ + Rules: []policy.RulesForTypeInstance{ + { + TypeRef: types.ManifestRefWithOptRevision{ + Path: "cap.type.aws.auth.credentials", + Revision: ptr.String("0.1.0"), + }, + Backend: policy.TypeInstanceBackend{ + TypeInstanceReference: policy.TypeInstanceReference{ + ID: "id9", + Description: ptr.String("ID 9"), + TypeRef: &types.TypeRef{ + Path: "cap.type.type9", + Revision: "0.9.0", + }, + }, + }, + }, + { + TypeRef: types.ManifestRefWithOptRevision{ + Path: "cap.type.aws.*", + }, + Backend: policy.TypeInstanceBackend{ + TypeInstanceReference: policy.TypeInstanceReference{ + ID: "id10", + TypeRef: &types.TypeRef{ + Path: "cap.type.type10", + Revision: "0.10.0", + }, + }, + }, + }, + { + TypeRef: types.ManifestRefWithOptRevision{ + Path: "cap.*", + }, + Backend: policy.TypeInstanceBackend{ + TypeInstanceReference: policy.TypeInstanceReference{ + ID: "id11", + Description: ptr.String("ID 11"), + TypeRef: &types.TypeRef{ + Path: "cap.type.type11", + Revision: "0.11.0", + }, + }, + }, + }, + }, + }, } } @@ -284,3 +374,40 @@ func (f *fakeHub) FindTypeInstancesTypeRef(_ context.Context, ids []string) (map return res, nil } + +func (f *fakeHub) ListTypes(_ context.Context, opts ...public.TypeOption) ([]*hubpublicgraphql.Type, error) { + if !f.ShouldRun { + return nil, errors.New("shouldn't run") + } + + var allTypes []*hubpublicgraphql.Type + for id := 0; id < f.ExpectedIDLen; id++ { + allTypes = append(allTypes, &hubpublicgraphql.Type{ + Path: fmt.Sprintf("cap.type.type%d", id), + Revision: &hubpublicgraphql.TypeRevision{ + Revision: fmt.Sprintf("0.%d.0", id), + }, + }) + } + + typeOpts := &public.TypeOptions{} + typeOpts.Apply(opts...) + + if typeOpts.Filter.PathPattern == nil { + return allTypes, nil + } + + var out []*hubpublicgraphql.Type + for _, item := range allTypes { + matched, err := regexp.MatchString(*typeOpts.Filter.PathPattern, item.Path) + if err != nil { + return nil, err + } + if !matched { + continue + } + out = append(out, item) + } + + return out, nil +} diff --git a/pkg/engine/k8s/policy/metadata/metadata.go b/pkg/engine/k8s/policy/metadata/metadata.go index 6485394e5..be9185317 100644 --- a/pkg/engine/k8s/policy/metadata/metadata.go +++ b/pkg/engine/k8s/policy/metadata/metadata.go @@ -12,6 +12,7 @@ type typeInstanceKind string const ( requiredTypeInstance typeInstanceKind = "RequiredTypeInstance" additionalTypeInstance typeInstanceKind = "AdditionalTypeInstance" + backendTypeInstance typeInstanceKind = "BackendTypeInstance" ) // TypeInstanceMetadata defines metadata for required and additional TypeInstances defined in Policy. @@ -55,6 +56,19 @@ func TypeInstanceIDsWithUnresolvedMetadataForPolicy(in policy.Policy) []TypeInst } } + // TypeInstances backends + for _, rule := range in.TypeInstance.Rules { + if rule.Backend.TypeRef != nil && rule.Backend.TypeRef.Path != "" && rule.Backend.TypeRef.Revision != "" { + continue + } + + tis = append(tis, TypeInstanceMetadata{ + ID: rule.Backend.ID, + Description: rule.Backend.Description, + Kind: backendTypeInstance, + }) + } + return tis } diff --git a/pkg/engine/k8s/policy/metadata/metadata_resolver.go b/pkg/engine/k8s/policy/metadata/metadata_resolver.go index ea94ace28..851d3eaee 100644 --- a/pkg/engine/k8s/policy/metadata/metadata_resolver.go +++ b/pkg/engine/k8s/policy/metadata/metadata_resolver.go @@ -3,19 +3,24 @@ package metadata import ( "context" "fmt" + "strings" "capact.io/capact/internal/multierror" - "capact.io/capact/pkg/engine/k8s/policy" hublocalgraphql "capact.io/capact/pkg/hub/api/graphql/local" + hubpublicgraphql "capact.io/capact/pkg/hub/api/graphql/public" + "capact.io/capact/pkg/hub/client/public" + typesutil "capact.io/capact/pkg/hub/client/public/facade/types" "capact.io/capact/pkg/sdk/apis/0.0.1/types" multierr "github.com/hashicorp/go-multierror" + "github.com/pkg/errors" ) // HubClient defines Hub client which is able to find TypeInstance Type references. type HubClient interface { FindTypeInstancesTypeRef(ctx context.Context, ids []string) (map[string]hublocalgraphql.TypeInstanceTypeReference, error) + ListTypes(ctx context.Context, opts ...public.TypeOption) ([]*hubpublicgraphql.Type, error) } // Resolver resolves Policy metadata against Hub. @@ -49,7 +54,7 @@ func (r *Resolver) ResolveTypeInstanceMetadata(ctx context.Context, policy *poli return nil } - res, err := r.hubCli.FindTypeInstancesTypeRef(ctx, idsToQuery) + resolvedTypeRefs, err := r.hubCli.FindTypeInstancesTypeRef(ctx, idsToQuery) if err != nil { return errors.Wrap(err, "while finding TypeRef for TypeInstances") } @@ -57,7 +62,7 @@ func (r *Resolver) ResolveTypeInstanceMetadata(ctx context.Context, policy *poli // verify if all TypeInstances are resolved multiErr := multierror.New() for _, ti := range unresolvedTIs { - if typeRef, exists := res[ti.ID]; exists && typeRef.Path != "" && typeRef.Revision != "" { + if typeRef, exists := resolvedTypeRefs[ti.ID]; exists && typeRef.Path != "" && typeRef.Revision != "" { continue } @@ -67,13 +72,51 @@ func (r *Resolver) ResolveTypeInstanceMetadata(ctx context.Context, policy *poli return multiErr } - r.setTypeRefsForRequiredTypeInstances(policy, res) - r.setTypeRefsForAdditionalTypeInstances(policy, res) + typeRefWithParentNodes, err := r.enrichWithParentNodes(ctx, resolvedTypeRefs) + if err != nil { + return errors.Wrap(err, "while resolving parent nodes for TypeRefs") + } + + r.setTypeRefsForAdditionalTypeInstances(policy, typeRefWithParentNodes) + r.setTypeRefsForRequiredTypeInstances(policy, typeRefWithParentNodes) + r.setTypeRefsForBackendTypeInstances(policy, typeRefWithParentNodes) return nil } -func (r *Resolver) setTypeRefsForRequiredTypeInstances(policy *policy.Policy, typeRefs map[string]hublocalgraphql.TypeInstanceTypeReference) { +// TypeRefWithAdditionalRefs holds TypeRef associated with its additional references (parent nodes). +type TypeRefWithAdditionalRefs struct { + types.TypeRef + AdditionalRefs []string +} + +func (r *Resolver) enrichWithParentNodes(ctx context.Context, refs map[string]hublocalgraphql.TypeInstanceTypeReference) (map[string]TypeRefWithAdditionalRefs, error) { + typesPath := r.mapToTypeRefs(refs) + gotAttachedTypes, err := typesutil.ListAdditionalRefs(ctx, r.hubCli, typesPath) + if err != nil { + return nil, errors.Wrap(err, "while fetching Types") + } + + out := map[string]TypeRefWithAdditionalRefs{} + for id, ref := range refs { + out[id] = TypeRefWithAdditionalRefs{ + TypeRef: types.TypeRef(ref), + AdditionalRefs: gotAttachedTypes[types.TypeRef(ref)], + } + } + + return out, nil +} + +func (r *Resolver) mapToTypeRefs(in map[string]hublocalgraphql.TypeInstanceTypeReference) []types.TypeRef { + var out []types.TypeRef + for _, expType := range in { + out = append(out, types.TypeRef(expType)) + } + return out +} + +func (r *Resolver) setTypeRefsForRequiredTypeInstances(policy *policy.Policy, typeRefs map[string]TypeRefWithAdditionalRefs) { for ruleIdx, rule := range policy.Interface.Rules { for ruleItemIdx, ruleItem := range rule.OneOf { if ruleItem.Inject == nil { @@ -85,16 +128,17 @@ func (r *Resolver) setTypeRefsForRequiredTypeInstances(policy *policy.Policy, ty continue } - policy.Interface.Rules[ruleIdx].OneOf[ruleItemIdx].Inject.RequiredTypeInstances[reqTIIdx].TypeRef = &types.ManifestRef{ + policy.Interface.Rules[ruleIdx].OneOf[ruleItemIdx].Inject.RequiredTypeInstances[reqTIIdx].TypeRef = &types.TypeRef{ Path: typeRef.Path, Revision: typeRef.Revision, } + policy.Interface.Rules[ruleIdx].OneOf[ruleItemIdx].Inject.RequiredTypeInstances[reqTIIdx].ExtendsHubStorage = r.isExtendingHubStorage(typeRef) } } } } -func (r *Resolver) setTypeRefsForAdditionalTypeInstances(policy *policy.Policy, typeRefs map[string]hublocalgraphql.TypeInstanceTypeReference) { +func (r *Resolver) setTypeRefsForAdditionalTypeInstances(policy *policy.Policy, typeRefs map[string]TypeRefWithAdditionalRefs) { for ruleIdx, rule := range policy.Interface.Rules { for ruleItemIdx, ruleItem := range rule.OneOf { if ruleItem.Inject == nil { @@ -114,3 +158,33 @@ func (r *Resolver) setTypeRefsForAdditionalTypeInstances(policy *policy.Policy, } } } + +func (r *Resolver) setTypeRefsForBackendTypeInstances(policy *policy.Policy, typeRefs map[string]TypeRefWithAdditionalRefs) { + for ruleIdx, rule := range policy.TypeInstance.Rules { + typeRef, exists := typeRefs[rule.Backend.ID] + if !exists { + continue + } + + policy.TypeInstance.Rules[ruleIdx].Backend.TypeRef = &types.TypeRef{ + Path: typeRef.Path, + Revision: typeRef.Revision, + } + policy.TypeInstance.Rules[ruleIdx].Backend.ExtendsHubStorage = r.isExtendingHubStorage(typeRef) + } +} + +func (r *Resolver) isExtendingHubStorage(ref TypeRefWithAdditionalRefs) bool { + // Check if it's a core Hub storage + if strings.HasPrefix(ref.Path, types.HubBackendParentNodeName) { + return true + } + + // if not, check if extends core Hub storage type + for _, ref := range ref.AdditionalRefs { + if ref == types.HubBackendParentNodeName { + return true + } + } + return false +} diff --git a/pkg/engine/k8s/policy/metadata/metadata_resolver_test.go b/pkg/engine/k8s/policy/metadata/metadata_resolver_test.go index 54e8caa74..9d012bf5d 100644 --- a/pkg/engine/k8s/policy/metadata/metadata_resolver_test.go +++ b/pkg/engine/k8s/policy/metadata/metadata_resolver_test.go @@ -37,19 +37,26 @@ func TestResolveTypeInstanceMetadata(t *testing.T) { { Name: "Unresolved TypeRefs", Input: fixComplexPolicyWithoutTypeRef(), - HubCli: &fakeHub{ShouldRun: true, ExpectedIDLen: 9}, + HubCli: &fakeHub{ShouldRun: true, ExpectedIDLen: 12}, Expected: fixComplexPolicyWithTypeRef(), }, { - Name: "Partial result", - Input: fixComplexPolicyWithoutTypeRef(), - HubCli: &fakeHub{ShouldRun: true, ExpectedIDLen: 9, IgnoreIDs: map[string]struct{}{"id2": {}, "id4": {}, "id8": {}}}, + Name: "Partial result", + Input: fixComplexPolicyWithoutTypeRef(), + HubCli: &fakeHub{ShouldRun: true, ExpectedIDLen: 12, IgnoreIDs: map[string]struct{}{ + "id2": {}, "id4": {}, // required + "id8": {}, // additional + "id9": {}, "id11": {}, // backend + }, + }, ExpectedErrMessage: ptr.String( heredoc.Doc(` - 3 errors occurred: + 5 errors occurred: * missing Type reference for RequiredTypeInstance (ID: "id2", description: "ID 2") * missing Type reference for RequiredTypeInstance (ID: "id4") - * missing Type reference for AdditionalTypeInstance (ID: "id8", name: "ID8")`, + * missing Type reference for AdditionalTypeInstance (ID: "id8", name: "ID8") + * missing Type reference for BackendTypeInstance (ID: "id9", description: "ID 9") + * missing Type reference for BackendTypeInstance (ID: "id11", description: "ID 11")`, ), ), }, diff --git a/pkg/engine/k8s/policy/predefined.go b/pkg/engine/k8s/policy/predefined.go index 8b6949420..16b49d002 100644 --- a/pkg/engine/k8s/policy/predefined.go +++ b/pkg/engine/k8s/policy/predefined.go @@ -15,7 +15,7 @@ func NewDenyAll() Policy { func NewAllowAll() Policy { return Policy{ Interface: InterfacePolicy{ - Rules: RulesList{ + Rules: InterfaceRulesList{ { Interface: types.ManifestRefWithOptRevision{ Path: "cap.*", diff --git a/pkg/engine/k8s/policy/type_instance.go b/pkg/engine/k8s/policy/type_instance.go new file mode 100644 index 000000000..68452757a --- /dev/null +++ b/pkg/engine/k8s/policy/type_instance.go @@ -0,0 +1,136 @@ +package policy + +import ( + "fmt" + + "capact.io/capact/internal/ptr" + + "capact.io/capact/pkg/sdk/apis/0.0.1/types" +) + +// maxBackendLookupForTypeRef defines maximum number of iteration to find a matching backend based on TypeRef path pattern. +const maxBackendLookupForTypeRef = 30 + +// TypeInstancePolicy holds the Policy for TypeInstance. +type TypeInstancePolicy struct { + Rules []RulesForTypeInstance `json:"rules"` +} + +// RulesForTypeInstance holds a single policy rule for a TypeInstance. +// +kubebuilder:object:generate=true +type RulesForTypeInstance struct { + TypeRef types.ManifestRefWithOptRevision `json:"typeRef"` + Backend TypeInstanceBackend `json:"backend"` +} + +// TypeInstanceBackend holds a Backend description to be used for storing a given TypeInstance. +// +kubebuilder:object:generate=true +type TypeInstanceBackend struct { + TypeInstanceReference `json:",inline"` +} + +// TypeInstanceBackendCollection knows which Backend should be used for a given TypeInstance based on the TypeRef. +type TypeInstanceBackendCollection struct { + byTypeRef map[string]TypeInstanceBackend + byAlias map[string]TypeInstanceBackend + defaultBackend TypeInstanceBackend +} + +// SetDefault sets the default storage backend. +// TODO(storage): probably not needed anymore. +func (t *TypeInstanceBackendCollection) SetDefault(backend TypeInstanceBackend) { + t.defaultBackend = backend +} + +// SetByTypeRef associates a given TypeRef with a given storage backend instance. +func (t *TypeInstanceBackendCollection) SetByTypeRef(ref types.ManifestRefWithOptRevision, backend TypeInstanceBackend) { + if t.byTypeRef == nil { + t.byTypeRef = map[string]TypeInstanceBackend{} + } + t.byTypeRef[t.key(ref)] = backend +} + +// GetByTypeRef returns storage backend for a given TypeRef. +// If backend for an explicit TypeRef is not found, the pattern matching is used. +// +// For example, if TypeRef is `cap.type.capactio.examples.message:0.1.0`: +// - cap.type.capactio.examples.*:0.1.0 +// - cap.type.capactio.examples.* +// - cap.type.capactio.*:0.1.0 +// - cap.type.capactio.* +// - cap.type.*:0.1.0 +// - cap.type.* +// - cap.*:0.1.0 +// - cap.* +// +// If both methods fail, default backend is returned. +func (t TypeInstanceBackendCollection) GetByTypeRef(typeRef types.TypeRef) TypeInstanceBackend { + // 1. Try the explicit TypeRef + backend, found := t.byTypeRef[t.key(types.ManifestRefWithOptRevision{ + Path: typeRef.Path, + Revision: ptr.String(typeRef.Revision), + })] + if found { + return backend + } + + // 2. Try to find matching pattern for a given TypeRef. + var ( + subPath = typeRef.Path + iterations = 0 + ) + + for { + if fmt.Sprintf("%s.", subPath) == types.OCFPathPrefix || iterations > maxBackendLookupForTypeRef { + break + } + subPath = types.TrimLastNodeFromOCFPath(subPath) + + keyPatterns := []string{ + fmt.Sprintf("%s.*:%s", subPath, typeRef.Revision), // first try to match with revision + fmt.Sprintf("%s.*", subPath), // later check for path pattern only + } + for _, pattern := range keyPatterns { + backend, found := t.byTypeRef[pattern] + if found { + return backend + } + } + iterations++ + } + + return t.defaultBackend +} + +// SetByAlias associates a given alias with a given storage backend instance. +func (t *TypeInstanceBackendCollection) SetByAlias(name string, backend TypeInstanceBackend) { + if t.byAlias == nil { + t.byAlias = map[string]TypeInstanceBackend{} + } + t.byAlias[name] = backend +} + +// GetByAlias returns backend associated with a given alias. +func (t *TypeInstanceBackendCollection) GetByAlias(name string) (TypeInstanceBackend, bool) { + backend, found := t.byAlias[name] + return backend, found +} + +// GetAll returns all registered storage backends both for aliases and TypeRefs. +func (t *TypeInstanceBackendCollection) GetAll() map[string]TypeInstanceBackend { + out := map[string]TypeInstanceBackend{} + for k, v := range t.byAlias { + out[k] = v + } + for k, v := range t.byTypeRef { + out[k] = v + } + return out +} + +func (t TypeInstanceBackendCollection) key(typeRef types.ManifestRefWithOptRevision) string { + if typeRef.Revision != nil && *typeRef.Revision != "" { + return fmt.Sprintf("%s:%s", typeRef.Path, *typeRef.Revision) + } + return typeRef.Path +} diff --git a/pkg/engine/k8s/policy/type_instance_test.go b/pkg/engine/k8s/policy/type_instance_test.go new file mode 100644 index 000000000..cd1ab198d --- /dev/null +++ b/pkg/engine/k8s/policy/type_instance_test.go @@ -0,0 +1,131 @@ +package policy_test + +import ( + "strings" + "testing" + + "capact.io/capact/internal/ptr" + "capact.io/capact/pkg/engine/k8s/policy" + "capact.io/capact/pkg/sdk/apis/0.0.1/types" + + "github.com/stretchr/testify/assert" +) + +func TestTypeInstanceBackendCollection_GetByTypeRef(t *testing.T) { + data := policy.TypeInstanceBackendCollection{} + + data.SetByTypeRef(fixTypeRef("cap.type.capactio.examples.message:0.1.0"), fixTypeInstanceBackend("ID1")) + data.SetByTypeRef(fixTypeRef("cap.type.capactio.examples.*"), fixTypeInstanceBackend("ID2")) + data.SetByTypeRef(fixTypeRef("cap.type.capactio.examples.*:0.2.0"), fixTypeInstanceBackend("ID3")) + data.SetByTypeRef(fixTypeRef("cap.*"), fixTypeInstanceBackend("ID4")) + data.SetByTypeRef(fixTypeRef("cap.*:0.1.0"), fixTypeInstanceBackend("ID5")) + + data.SetByAlias("aws-secret-manager", fixTypeInstanceBackend("ID333")) // ensure that Alias does not affect proper selection + + tests := map[string]struct { + givenTypeRef types.TypeRef + expBackend policy.TypeInstanceBackend + }{ + "Should match exact type ref": { + givenTypeRef: types.TypeRef{ + Path: "cap.type.capactio.examples.message", + Revision: "0.1.0", + }, + expBackend: fixTypeInstanceBackend("ID1"), + }, + "Should match pattern cap.type.capactio.examples.*": { + givenTypeRef: types.TypeRef{ + Path: "cap.type.capactio.examples.other-that-message", + Revision: "0.1.0", + }, + expBackend: fixTypeInstanceBackend("ID2"), + }, + "Should match pattern cap.type.capactio.examples.*:0.2.0": { + givenTypeRef: types.TypeRef{ + Path: "cap.type.capactio.examples.other-that-message", + Revision: "0.2.0", + }, + expBackend: fixTypeInstanceBackend("ID3"), + }, + "Should match generic cap.* with revision": { + givenTypeRef: types.TypeRef{ + Path: "cap.type.aws.examples", + Revision: "0.1.0", + }, + expBackend: fixTypeInstanceBackend("ID5"), + }, + "Should match generic cap.* with different revision": { + givenTypeRef: types.TypeRef{ + Path: "cap.type.aws.examples", + Revision: "0.2.0", + }, + expBackend: fixTypeInstanceBackend("ID4"), + }, + } + for tn, tc := range tests { + t.Run(tn, func(t *testing.T) { + gotBackend := data.GetByTypeRef(tc.givenTypeRef) + assert.Equal(t, tc.expBackend, gotBackend) + }) + } +} + +func TestTypeInstanceBackendCollection_GetByAlias(t *testing.T) { + data := policy.TypeInstanceBackendCollection{} + + data.SetByTypeRef(fixTypeRef("cap.type.capactio.examples.message:0.1.0"), fixTypeInstanceBackend("ID1")) // ensure that TypeRef does not affect proper selection + data.SetByAlias("helm-storage", fixTypeInstanceBackend("ID2")) + data.SetByAlias("aws-secret-manager", fixTypeInstanceBackend("ID3")) + + tests := map[string]struct { + givenAlias string + + expFound bool + expBackend policy.TypeInstanceBackend + }{ + "Should match helm-storage": { + givenAlias: "helm-storage", + + expFound: true, + expBackend: fixTypeInstanceBackend("ID2"), + }, + "Should match aws-secret-manager": { + givenAlias: "aws-secret-manager", + + expFound: true, + expBackend: fixTypeInstanceBackend("ID3"), + }, + "Should not found valut": { + givenAlias: "vault", + + expFound: false, + }, + } + for tn, tc := range tests { + t.Run(tn, func(t *testing.T) { + gotBackend, found := data.GetByAlias(tc.givenAlias) + assert.Equal(t, tc.expFound, found) + assert.Equal(t, tc.expBackend, gotBackend) + }) + } +} + +func fixTypeRef(in string) types.ManifestRefWithOptRevision { + var rev *string + parts := strings.Split(in, ":") + if len(parts) > 1 { + rev = ptr.String(parts[1]) + } + return types.ManifestRefWithOptRevision{ + Path: parts[0], + Revision: rev, + } +} + +func fixTypeInstanceBackend(id string) policy.TypeInstanceBackend { + return policy.TypeInstanceBackend{ + TypeInstanceReference: policy.TypeInstanceReference{ + ID: id, + }, + } +} diff --git a/pkg/engine/k8s/policy/types.go b/pkg/engine/k8s/policy/types.go index 8f0915e24..327059670 100644 --- a/pkg/engine/k8s/policy/types.go +++ b/pkg/engine/k8s/policy/types.go @@ -30,19 +30,20 @@ const ( // Policy holds the policy properties. type Policy struct { - Interface InterfacePolicy `json:"interface"` + Interface InterfacePolicy `json:"interface"` + TypeInstance TypeInstancePolicy `json:"typeInstance"` } // InterfacePolicy holds the Policy for Interfaces. type InterfacePolicy struct { - Rules RulesList `json:"rules"` + Rules InterfaceRulesList `json:"rules"` } // ActionPolicy holds the Policy injected during Action creation properties. type ActionPolicy Policy -// RulesList holds the list of the rules in the policy. -type RulesList []RulesForInterface +// InterfaceRulesList holds the list of the rules in the Interface policy. +type InterfaceRulesList []RulesForInterface // RulesForInterface holds a single policy rule for an Interface. // +kubebuilder:object:generate=true @@ -110,33 +111,24 @@ type ImplementationConstraints struct { // RequiredTypeInstanceToInject holds a RequiredTypeInstances to be injected to the Action. // +kubebuilder:object:generate=true type RequiredTypeInstanceToInject struct { - // RequiredTypeInstanceReference is a reference to TypeInstance provided by user. - RequiredTypeInstanceReference `json:",inline"` - - // TypeRef refers to a given Type. - TypeRef *types.ManifestRef `json:"typeRef"` + TypeInstanceReference `json:",inline"` } -// RequiredTypeInstanceReference is a reference to TypeInstance provided by user. +// TypeInstanceReference holds TypeInstance ID with TypeRef that is resolved in runtime. // +kubebuilder:object:generate=true -type RequiredTypeInstanceReference struct { +type TypeInstanceReference struct { // ID is the TypeInstance identifier. ID string `json:"id"` - // Description contains user's description for a given RequiredTypeInstanceToInject. + // Description contains user's description for a given TypeInstance. Description *string `json:"description,omitempty"` -} -// UnmarshalJSON unmarshalls RequiredTypeInstanceToInject from bytes. It ignores all fields apart from RequiredTypeInstanceReference files. -func (in *RequiredTypeInstanceToInject) UnmarshalJSON(bytes []byte) error { - var out RequiredTypeInstanceReference - if err := json.Unmarshal(bytes, &out); err != nil { - return err - } + // TypeRef refers to a given Type. Ignores if present in Policy as this is resolved only in runtime + // based on real data stored in Hub. + TypeRef *types.TypeRef `json:"-"` - in.RequiredTypeInstanceReference = out - - return nil + // ExtendsHubStorage must be set to `true` if TypeRef is a child of `cap.core.type.hub.storage` node. + ExtendsHubStorage bool `json:"-"` } // AdditionalTypeInstanceToInject is used to represent additional TypeInstance injection for a given Implementation. diff --git a/pkg/engine/k8s/policy/types_test.go b/pkg/engine/k8s/policy/types_test.go index 045c280f8..67e2f34ac 100644 --- a/pkg/engine/k8s/policy/types_test.go +++ b/pkg/engine/k8s/policy/types_test.go @@ -26,22 +26,22 @@ func TestRule_RequiredTypeInstancesToInject(t *testing.T) { Input: fixPolicyWithTypeRef().Interface.Rules[0].OneOf[0], Expected: []policy.RequiredTypeInstanceToInject{ { - RequiredTypeInstanceReference: policy.RequiredTypeInstanceReference{ + TypeInstanceReference: policy.TypeInstanceReference{ ID: "id", - }, - TypeRef: &types.ManifestRef{ - Path: "cap.type.sample", - Revision: "0.1.0", + TypeRef: &types.TypeRef{ + Path: "cap.type.sample", + Revision: "0.1.0", + }, }, }, { - RequiredTypeInstanceReference: policy.RequiredTypeInstanceReference{ + TypeInstanceReference: policy.TypeInstanceReference{ ID: "id2", Description: ptr.String("ID 2"), - }, - TypeRef: &types.ManifestRef{ - Path: "cap.type.sample2", - Revision: "0.2.0", + TypeRef: &types.TypeRef{ + Path: "cap.type.sample2", + Revision: "0.2.0", + }, }, }, }, diff --git a/pkg/engine/k8s/policy/workflow_test.go b/pkg/engine/k8s/policy/workflow_test.go index f07d47776..0b160c8a7 100644 --- a/pkg/engine/k8s/policy/workflow_test.go +++ b/pkg/engine/k8s/policy/workflow_test.go @@ -54,7 +54,7 @@ func policyWithAdditionalInput(input map[string]interface{}) Policy { implementation := "cap.implementation.bitnami.postgresql.install" return Policy{ Interface: InterfacePolicy{ - Rules: RulesList{ + Rules: InterfaceRulesList{ RulesForInterface{ Interface: types.ManifestRefWithOptRevision{ Path: "cap.interface.database.postgresql.install", diff --git a/pkg/engine/k8s/policy/zz_generated.deepcopy.go b/pkg/engine/k8s/policy/zz_generated.deepcopy.go index b298356d1..e6c5d5e17 100644 --- a/pkg/engine/k8s/policy/zz_generated.deepcopy.go +++ b/pkg/engine/k8s/policy/zz_generated.deepcopy.go @@ -87,35 +87,10 @@ func (in *ImplementationConstraints) DeepCopy() *ImplementationConstraints { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RequiredTypeInstanceReference) DeepCopyInto(out *RequiredTypeInstanceReference) { - *out = *in - if in.Description != nil { - in, out := &in.Description, &out.Description - *out = new(string) - **out = **in - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RequiredTypeInstanceReference. -func (in *RequiredTypeInstanceReference) DeepCopy() *RequiredTypeInstanceReference { - if in == nil { - return nil - } - out := new(RequiredTypeInstanceReference) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RequiredTypeInstanceToInject) DeepCopyInto(out *RequiredTypeInstanceToInject) { *out = *in - in.RequiredTypeInstanceReference.DeepCopyInto(&out.RequiredTypeInstanceReference) - if in.TypeRef != nil { - in, out := &in.TypeRef, &out.TypeRef - *out = new(types.ManifestRef) - **out = **in - } + in.TypeInstanceReference.DeepCopyInto(&out.TypeInstanceReference) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RequiredTypeInstanceToInject. @@ -170,3 +145,61 @@ func (in *RulesForInterface) DeepCopy() *RulesForInterface { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RulesForTypeInstance) DeepCopyInto(out *RulesForTypeInstance) { + *out = *in + in.TypeRef.DeepCopyInto(&out.TypeRef) + in.Backend.DeepCopyInto(&out.Backend) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RulesForTypeInstance. +func (in *RulesForTypeInstance) DeepCopy() *RulesForTypeInstance { + if in == nil { + return nil + } + out := new(RulesForTypeInstance) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TypeInstanceBackend) DeepCopyInto(out *TypeInstanceBackend) { + *out = *in + in.TypeInstanceReference.DeepCopyInto(&out.TypeInstanceReference) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TypeInstanceBackend. +func (in *TypeInstanceBackend) DeepCopy() *TypeInstanceBackend { + if in == nil { + return nil + } + out := new(TypeInstanceBackend) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TypeInstanceReference) DeepCopyInto(out *TypeInstanceReference) { + *out = *in + if in.Description != nil { + in, out := &in.Description, &out.Description + *out = new(string) + **out = **in + } + if in.TypeRef != nil { + in, out := &in.TypeRef, &out.TypeRef + *out = new(types.TypeRef) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TypeInstanceReference. +func (in *TypeInstanceReference) DeepCopy() *TypeInstanceReference { + if in == nil { + return nil + } + out := new(TypeInstanceReference) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/hub/api/graphql/local/models_gen.go b/pkg/hub/api/graphql/local/models_gen.go index 6936e66de..ee96578d6 100644 --- a/pkg/hub/api/graphql/local/models_gen.go +++ b/pkg/hub/api/graphql/local/models_gen.go @@ -31,7 +31,9 @@ type CreateTypeInstanceInput struct { CreatedBy *string `json:"createdBy"` TypeRef *TypeInstanceTypeReferenceInput `json:"typeRef"` Attributes []*AttributeReferenceInput `json:"attributes"` - Value interface{} `json:"value"` + // If not provided, TypeInstance value is stored as static value in Local Hub core storage. + Backend *TypeInstanceBackendInput `json:"backend"` + Value interface{} `json:"value"` } type CreateTypeInstanceOutput struct { @@ -63,6 +65,14 @@ type TypeInstance struct { ResourceVersions []*TypeInstanceResourceVersion `json:"resourceVersions"` } +type TypeInstanceBackend struct { + ID string `json:"id"` +} + +type TypeInstanceBackendInput struct { + ID string `json:"id"` +} + type TypeInstanceFilter struct { Attributes []*AttributeFilterInput `json:"attributes"` TypeRef *TypeRefFilterInput `json:"typeRef"` @@ -99,6 +109,7 @@ type TypeInstanceResourceVersion struct { CreatedBy *string `json:"createdBy"` Metadata *TypeInstanceResourceVersionMetadata `json:"metadata"` Spec *TypeInstanceResourceVersionSpec `json:"spec"` + Backend *TypeInstanceBackend `json:"backend"` } type TypeInstanceResourceVersionMetadata struct { diff --git a/pkg/hub/api/graphql/local/schema_gen.go b/pkg/hub/api/graphql/local/schema_gen.go index d49131e73..cdd9a1ef0 100644 --- a/pkg/hub/api/graphql/local/schema_gen.go +++ b/pkg/hub/api/graphql/local/schema_gen.go @@ -84,6 +84,10 @@ type ComplexityRoot struct { Uses func(childComplexity int) int } + TypeInstanceBackend struct { + ID func(childComplexity int) int + } + TypeInstanceInstrumentation struct { Health func(childComplexity int) int Metrics func(childComplexity int) int @@ -106,6 +110,7 @@ type ComplexityRoot struct { } TypeInstanceResourceVersion struct { + Backend func(childComplexity int) int CreatedBy func(childComplexity int) int Metadata func(childComplexity int) int ResourceVersion func(childComplexity int) int @@ -354,6 +359,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.TypeInstance.Uses(childComplexity), true + case "TypeInstanceBackend.id": + if e.complexity.TypeInstanceBackend.ID == nil { + break + } + + return e.complexity.TypeInstanceBackend.ID(childComplexity), true + case "TypeInstanceInstrumentation.health": if e.complexity.TypeInstanceInstrumentation.Health == nil { break @@ -417,6 +429,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.TypeInstanceInstrumentationMetricsDashboard.URL(childComplexity), true + case "TypeInstanceResourceVersion.backend": + if e.complexity.TypeInstanceResourceVersion.Backend == nil { + break + } + + return e.complexity.TypeInstanceResourceVersion.Backend(childComplexity), true + case "TypeInstanceResourceVersion.createdBy": if e.complexity.TypeInstanceResourceVersion.CreatedBy == nil { break @@ -548,10 +567,10 @@ var sources = []*ast.Source{ # To make it work for other graphql client we need to add them to the schema manually, based on: # https://github.com/neo4j-graphql/neo4j-graphql-js/blob/master/src/augment/directives.js directive @relation( - name: String - direction: String - from: String - to: String + name: String + direction: String + from: String + to: String ) on FIELD_DEFINITION | OBJECT directive @cypher(statement: String) on FIELD_DEFINITION @@ -579,440 +598,455 @@ LockOwner defines owner name who locked a given TypeInstance scalar LockOwnerID type TypeInstance { - id: ID! @id - - lockedBy: LockOwnerID - - """ - Common properties for all TypeInstances which cannot be changed - """ - typeRef: TypeInstanceTypeReference! - @relation(name: "OF_TYPE", direction: "OUT") - uses: [TypeInstance!]! @relation(name: "USES", direction: "OUT") - usedBy: [TypeInstance!]! @relation(name: "USES", direction: "IN") - - latestResourceVersion: TypeInstanceResourceVersion - @cypher( - statement: "MATCH (this)-[:CONTAINS]->(tir:TypeInstanceResourceVersion) RETURN tir ORDER BY tir.resourceVersion DESC LIMIT 1" - ) - firstResourceVersion: TypeInstanceResourceVersion - @cypher( - statement: "MATCH (this)-[:CONTAINS]->(tir:TypeInstanceResourceVersion) RETURN tir ORDER BY tir.resourceVersion ASC LIMIT 1" - ) - previousResourceVersion: TypeInstanceResourceVersion - @cypher( - statement: "MATCH (this)-[:CONTAINS]->(tir:TypeInstanceResourceVersion) RETURN tir ORDER BY tir.resourceVersion DESC SKIP 1 LIMIT 1" - ) - resourceVersion(resourceVersion: Int!): TypeInstanceResourceVersion - @cypher( - statement: "MATCH (this)-[:CONTAINS]->(tir:TypeInstanceResourceVersion {resourceVersion: $resourceVersion}) RETURN tir" - ) - resourceVersions: [TypeInstanceResourceVersion!]! - @relation(name: "CONTAINS", direction: "OUT") + id: ID! @id + + lockedBy: LockOwnerID + + """ + Common properties for all TypeInstances which cannot be changed + """ + typeRef: TypeInstanceTypeReference! + @relation(name: "OF_TYPE", direction: "OUT") + uses: [TypeInstance!]! @relation(name: "USES", direction: "OUT") + usedBy: [TypeInstance!]! @relation(name: "USES", direction: "IN") + + latestResourceVersion: TypeInstanceResourceVersion + @cypher( + statement: "MATCH (this)-[:CONTAINS]->(tir:TypeInstanceResourceVersion) RETURN tir ORDER BY tir.resourceVersion DESC LIMIT 1" + ) + firstResourceVersion: TypeInstanceResourceVersion + @cypher( + statement: "MATCH (this)-[:CONTAINS]->(tir:TypeInstanceResourceVersion) RETURN tir ORDER BY tir.resourceVersion ASC LIMIT 1" + ) + previousResourceVersion: TypeInstanceResourceVersion + @cypher( + statement: "MATCH (this)-[:CONTAINS]->(tir:TypeInstanceResourceVersion) RETURN tir ORDER BY tir.resourceVersion DESC SKIP 1 LIMIT 1" + ) + resourceVersion(resourceVersion: Int!): TypeInstanceResourceVersion + @cypher( + statement: "MATCH (this)-[:CONTAINS]->(tir:TypeInstanceResourceVersion {resourceVersion: $resourceVersion}) RETURN tir" + ) + resourceVersions: [TypeInstanceResourceVersion!]! + @relation(name: "CONTAINS", direction: "OUT") } type TypeInstanceResourceVersion { - resourceVersion: Int! @index - createdBy: String + resourceVersion: Int! @index + createdBy: String + + metadata: TypeInstanceResourceVersionMetadata! + @relation(name: "DESCRIBED_BY", direction: "OUT") + spec: TypeInstanceResourceVersionSpec! + @relation(name: "SPECIFIED_BY", direction: "OUT") - metadata: TypeInstanceResourceVersionMetadata! - @relation(name: "DESCRIBED_BY", direction: "OUT") - spec: TypeInstanceResourceVersionSpec! - @relation(name: "SPECIFIED_BY", direction: "OUT") + # TODO: can be changed to TypeInstance, and a proper relation set + backend: TypeInstanceBackend! } type TypeInstanceResourceVersionMetadata { - attributes: [AttributeReference!] - @relation(name: "CHARACTERIZED_BY", direction: "OUT") + attributes: [AttributeReference!] + @relation(name: "CHARACTERIZED_BY", direction: "OUT") } type TypeInstanceResourceVersionSpec { - value: Any! - @cypher( - statement: """ - RETURN apoc.convert.fromJsonMap(this.value) - """ - ) + value: Any! + @cypher( + statement: """ + RETURN apoc.convert.fromJsonMap(this.value) + """ + ) + + """ + CURRENTLY NOT IMPLEMENTED + """ + instrumentation: TypeInstanceInstrumentation + @relation(name: "INSTRUMENTED_WITH", direction: "OUT") +} - """ - CURRENTLY NOT IMPLEMENTED - """ - instrumentation: TypeInstanceInstrumentation - @relation(name: "INSTRUMENTED_WITH", direction: "OUT") +type TypeInstanceBackend { + id: String! } type TypeInstanceTypeReference { - path: NodePath! - revision: Version! + path: NodePath! + revision: Version! } input AttributeReferenceInput { - path: NodePath! - revision: Version! + path: NodePath! + revision: Version! } type AttributeReference { - path: NodePath! - revision: Version! + path: NodePath! + revision: Version! } """ CURRENTLY NOT IMPLEMENTED """ type TypeInstanceInstrumentation { - metrics: TypeInstanceInstrumentationMetrics - @relation(name: "MEASURED_BY", direction: "OUT") - health: TypeInstanceInstrumentationHealth - @relation(name: "INDICATED_BY", direction: "OUT") + metrics: TypeInstanceInstrumentationMetrics + @relation(name: "MEASURED_BY", direction: "OUT") + health: TypeInstanceInstrumentationHealth + @relation(name: "INDICATED_BY", direction: "OUT") } """ CURRENTLY NOT IMPLEMENTED """ type TypeInstanceInstrumentationMetrics { - endpoint: String - regex: String # optional regex for scraping metrics - dashboards: [TypeInstanceInstrumentationMetricsDashboard!]! - @relation(name: "ON", direction: "OUT") + endpoint: String + regex: String # optional regex for scraping metrics + dashboards: [TypeInstanceInstrumentationMetricsDashboard!]! + @relation(name: "ON", direction: "OUT") } """ CURRENTLY NOT IMPLEMENTED """ type TypeInstanceInstrumentationMetricsDashboard { - url: String! + url: String! } """ CURRENTLY NOT IMPLEMENTED """ type TypeInstanceInstrumentationHealth { - url: String - method: HTTPRequestMethod + url: String + method: HTTPRequestMethod - # resolver, which does a HTTP call on a given URL - # and expects status code greater than or equal to 200 - # and less than 400 - # TODO implement TypeInstance health check, for resolution of this field - status: TypeInstanceInstrumentationHealthStatus + # resolver, which does a HTTP call on a given URL + # and expects status code greater than or equal to 200 + # and less than 400 + # TODO implement TypeInstance health check, for resolution of this field + status: TypeInstanceInstrumentationHealthStatus } """ CURRENTLY NOT IMPLEMENTED """ enum TypeInstanceInstrumentationHealthStatus { - UNKNOWN - READY - FAILING + UNKNOWN + READY + FAILING } enum HTTPRequestMethod { - GET - POST + GET + POST } input AttributeFilterInput { - path: NodePath! - rule: FilterRule = INCLUDE + path: NodePath! + rule: FilterRule = INCLUDE - """ - If not provided, any revision of the Attribute applies to this filter - """ - revision: Version + """ + If not provided, any revision of the Attribute applies to this filter + """ + revision: Version } enum FilterRule { - INCLUDE - EXCLUDE + INCLUDE + EXCLUDE } input TypeInstanceFilter { - attributes: [AttributeFilterInput] - typeRef: TypeRefFilterInput - createdBy: String + attributes: [AttributeFilterInput] + typeRef: TypeRefFilterInput + createdBy: String } input TypeRefFilterInput { - path: NodePath! + path: NodePath! - """ - If not provided, it returns TypeInstances for all revisions of given Type - """ - revision: Version + """ + If not provided, it returns TypeInstances for all revisions of given Type + """ + revision: Version } input TypeInstanceTypeReferenceInput { - path: NodePath! - revision: Version! + path: NodePath! + revision: Version! +} + +input TypeInstanceBackendInput { + id: String! } input CreateTypeInstanceInput { - """ - Used to define the relationships, between the created TypeInstances - """ - alias: String + """ + Used to define the relationships, between the created TypeInstances + """ + alias: String - createdBy: String - typeRef: TypeInstanceTypeReferenceInput! - attributes: [AttributeReferenceInput!] - value: Any + createdBy: String + typeRef: TypeInstanceTypeReferenceInput! + attributes: [AttributeReferenceInput!] + """ + If not provided, TypeInstance value is stored as static value in Local Hub core storage. + """ + backend: TypeInstanceBackendInput + value: Any } input TypeInstanceUsesRelationInput { - """ - Can be existing TypeInstance ID or alias of a TypeInstance from typeInstances list - """ - from: String! + """ + Can be existing TypeInstance ID or alias of a TypeInstance from typeInstances list + """ + from: String! - """ - Can be existing TypeInstance ID or alias of a TypeInstance from typeInstances list - """ - to: String! + """ + Can be existing TypeInstance ID or alias of a TypeInstance from typeInstances list + """ + to: String! } input CreateTypeInstancesInput { - typeInstances: [CreateTypeInstanceInput!]! - usesRelations: [TypeInstanceUsesRelationInput!]! + typeInstances: [CreateTypeInstanceInput!]! + usesRelations: [TypeInstanceUsesRelationInput!]! } type CreateTypeInstanceOutput { - id: ID! - alias: String! + id: ID! + alias: String! } """ At least one property needs to be specified. """ input UpdateTypeInstanceInput { - """ - The attributes property is optional. If not provided, previous value is used. - """ - attributes: [AttributeReferenceInput!] + """ + The attributes property is optional. If not provided, previous value is used. + """ + attributes: [AttributeReferenceInput!] - """ - The value property is optional. If not provided, previous value is used. - """ - value: Any + """ + The value property is optional. If not provided, previous value is used. + """ + value: Any } input UpdateTypeInstancesInput { - """ - Allows you to update TypeInstances which are locked by a given ownerID. If not provided, - you can update only those TypeInstances which are not locked. - """ - ownerID: LockOwnerID - createdBy: String + """ + Allows you to update TypeInstances which are locked by a given ownerID. If not provided, + you can update only those TypeInstances which are not locked. + """ + ownerID: LockOwnerID + createdBy: String - id: ID! - typeInstance: UpdateTypeInstanceInput! + id: ID! + typeInstance: UpdateTypeInstanceInput! } input LockTypeInstancesInput { - ids: [ID!]! - ownerID: LockOwnerID! + ids: [ID!]! + ownerID: LockOwnerID! } input UnlockTypeInstancesInput { - ids: [ID!]! - ownerID: LockOwnerID! + ids: [ID!]! + ownerID: LockOwnerID! } type Query { - typeInstances(filter: TypeInstanceFilter = {}): [TypeInstance!]! - @cypher( - statement: """ - WITH [x IN $filter.attributes WHERE x.rule = "EXCLUDE" | x ] AS excluded, - [x IN $filter.attributes WHERE x.rule = "INCLUDE" | x ] AS included - - CALL { - WITH excluded - UNWIND excluded AS f - MATCH (ex:AttributeReference {path: f.path}) - WHERE (f.revision IS NULL) OR (ex.revision = f.revision) - RETURN collect(ex) as excludedAttributes - } - - MATCH (tir:TypeInstanceResourceVersion)-[:DESCRIBED_BY]->(meta:TypeInstanceResourceVersionMetadata) - OPTIONAL MATCH (meta)-[:CHARACTERIZED_BY]->(attr:AttributeReference) - MATCH (ti:TypeInstance)-[:OF_TYPE]->(typeRef:TypeInstanceTypeReference) - MATCH (ti:TypeInstance)-[:CONTAINS]->(tir) - WHERE - $filter = {} OR - ( - ( - $filter.typeRef IS NULL - OR - ( + typeInstances(filter: TypeInstanceFilter = {}): [TypeInstance!]! + @cypher( + statement: """ + WITH [x IN $filter.attributes WHERE x.rule = "EXCLUDE" | x ] AS excluded, + [x IN $filter.attributes WHERE x.rule = "INCLUDE" | x ] AS included + + CALL { + WITH excluded + UNWIND excluded AS f + MATCH (ex:AttributeReference {path: f.path}) + WHERE (f.revision IS NULL) OR (ex.revision = f.revision) + RETURN collect(ex) as excludedAttributes + } + + MATCH (tir:TypeInstanceResourceVersion)-[:DESCRIBED_BY]->(meta:TypeInstanceResourceVersionMetadata) + OPTIONAL MATCH (meta)-[:CHARACTERIZED_BY]->(attr:AttributeReference) + MATCH (ti:TypeInstance)-[:OF_TYPE]->(typeRef:TypeInstanceTypeReference) + MATCH (ti:TypeInstance)-[:CONTAINS]->(tir) + WHERE + $filter = {} OR + ( + ( + $filter.typeRef IS NULL + OR + ( ($filter.typeRef.revision IS NULL AND typeRef.path = $filter.typeRef.path) OR (typeRef.path = $filter.typeRef.path AND typeRef.revision = $filter.typeRef.revision) - ) - ) - AND - ($filter.createdBy IS NULL OR tir.createdBy = $filter.createdBy) - AND - ( - $filter.attributes IS NULL - OR - ( + ) + ) + AND + ($filter.createdBy IS NULL OR tir.createdBy = $filter.createdBy) + AND + ( + $filter.attributes IS NULL + OR + ( all(inc IN included WHERE - (tir)-[:DESCRIBED_BY]->(meta:TypeInstanceResourceVersionMetadata)-[:CHARACTERIZED_BY]->(attr:AttributeReference {path: inc.path}) - AND - (inc.revision IS NULL OR attr.revision = inc.revision) + (tir)-[:DESCRIBED_BY]->(meta:TypeInstanceResourceVersionMetadata)-[:CHARACTERIZED_BY]->(attr:AttributeReference {path: inc.path}) + AND + (inc.revision IS NULL OR attr.revision = inc.revision) ) AND none(exc IN excludedAttributes WHERE (tir)-[:DESCRIBED_BY]->(meta:TypeInstanceResourceVersionMetadata)-[:CHARACTERIZED_BY]->(exc)) - ) - ) - ) + ) + ) + ) - RETURN DISTINCT ti - """ - ) + RETURN DISTINCT ti + """ + ) - typeInstance(id: ID!): TypeInstance - @cypher( - statement: """ - MATCH (this:TypeInstance {id: $id}) - RETURN this - """ - ) + typeInstance(id: ID!): TypeInstance + @cypher( + statement: """ + MATCH (this:TypeInstance {id: $id}) + RETURN this + """ + ) } type Mutation { - createTypeInstances( - in: CreateTypeInstancesInput! - ): [CreateTypeInstanceOutput!]! - - # TODO extend input with TypeInstanceInstrumentation - createTypeInstance(in: CreateTypeInstanceInput!): TypeInstance! - @cypher( - statement: """ - WITH apoc.convert.toJson($in.value) as value - MERGE (typeRef:TypeInstanceTypeReference {path: $in.typeRef.path, revision: $in.typeRef.revision}) - - CREATE (ti:TypeInstance {id: apoc.create.uuid()}) - CREATE (ti)-[:OF_TYPE]->(typeRef) - - CREATE (tir: TypeInstanceResourceVersion {resourceVersion: 1, createdBy: $in.createdBy}) - CREATE (ti)-[:CONTAINS]->(tir) - - CREATE (tir)-[:DESCRIBED_BY]->(metadata: TypeInstanceResourceVersionMetadata) - CREATE (tir)-[:SPECIFIED_BY]->(spec: TypeInstanceResourceVersionSpec {value: value}) - - FOREACH (attr in $in.attributes | - MERGE (attrRef: AttributeReference {path: attr.path, revision: attr.revision}) - CREATE (metadata)-[:CHARACTERIZED_BY]->(attrRef) - ) - - RETURN ti - """ - ) - - updateTypeInstances(in: [UpdateTypeInstancesInput]!): [TypeInstance!]! - @cypher( - statement: """ - CALL { - UNWIND $in AS item - RETURN collect(item.id) as allInputIDs - } - - // Check if all TypeInstances were found - WITH * - CALL { - WITH allInputIDs - MATCH (ti:TypeInstance) - WHERE ti.id IN allInputIDs - WITH collect(ti.id) as foundIDs - RETURN foundIDs - } - CALL apoc.util.validate(size(foundIDs) < size(allInputIDs), apoc.convert.toJson({code: 404, ids: foundIDs}), null) - - // Check if given TypeInstances are not already locked by others - WITH * - CALL { - WITH * - UNWIND $in AS item - MATCH (tic:TypeInstance {id: item.id}) - WHERE tic.lockedBy IS NOT NULL AND (item.ownerID IS NULL OR tic.lockedBy <> item.ownerID) - WITH collect(tic.id) as lockedIDs - RETURN lockedIDs - } - CALL apoc.util.validate(size(lockedIDs) > 0, apoc.convert.toJson({code: 409, ids: lockedIDs}), null) - - UNWIND $in as item - MATCH (ti: TypeInstance {id: item.id}) - CALL { - WITH ti - MATCH (ti)-[:CONTAINS]->(latestRevision:TypeInstanceResourceVersion) - RETURN latestRevision - ORDER BY latestRevision.resourceVersion DESC LIMIT 1 - } - - CREATE (tir: TypeInstanceResourceVersion {resourceVersion: latestRevision.resourceVersion + 1, createdBy: item.createdBy}) - CREATE (ti)-[:CONTAINS]->(tir) - - // Handle the ` + "`" + `spec.value` + "`" + ` property - CREATE (spec: TypeInstanceResourceVersionSpec) - CREATE (tir)-[:SPECIFIED_BY]->(spec) - - WITH ti, tir, spec, latestRevision, item - CALL apoc.do.when( - item.typeInstance.value IS NOT NULL, - ' - SET spec.value = apoc.convert.toJson(item.typeInstance.value) RETURN spec - ', - ' - MATCH (latestRevision)-[:SPECIFIED_BY]->(latestSpec: TypeInstanceResourceVersionSpec) - SET spec.value = latestSpec.value RETURN spec - ', - {spec:spec, latestRevision: latestRevision, item: item}) YIELD value - - // Handle the ` + "`" + `metadata.attributes` + "`" + ` property - CREATE (metadata: TypeInstanceResourceVersionMetadata) - CREATE (tir)-[:DESCRIBED_BY]->(metadata) - - WITH ti, tir, latestRevision, metadata, item - CALL apoc.do.when( - item.typeInstance.attributes IS NOT NULL, - ' - FOREACH (attr in item.typeInstance.attributes | + createTypeInstances( + in: CreateTypeInstancesInput! + ): [CreateTypeInstanceOutput!]! + + # TODO extend input with TypeInstanceInstrumentation + createTypeInstance(in: CreateTypeInstanceInput!): TypeInstance! + @cypher( + statement: """ + WITH apoc.convert.toJson($in.value) as value + MERGE (typeRef:TypeInstanceTypeReference {path: $in.typeRef.path, revision: $in.typeRef.revision}) + + CREATE (ti:TypeInstance {id: apoc.create.uuid()}) + CREATE (ti)-[:OF_TYPE]->(typeRef) + + CREATE (tir: TypeInstanceResourceVersion {resourceVersion: 1, createdBy: $in.createdBy}) + CREATE (ti)-[:CONTAINS]->(tir) + + CREATE (tir)-[:DESCRIBED_BY]->(metadata: TypeInstanceResourceVersionMetadata) + CREATE (tir)-[:SPECIFIED_BY]->(spec: TypeInstanceResourceVersionSpec {value: value}) + + FOREACH (attr in $in.attributes | MERGE (attrRef: AttributeReference {path: attr.path, revision: attr.revision}) CREATE (metadata)-[:CHARACTERIZED_BY]->(attrRef) - ) - - RETURN metadata - ', - ' - OPTIONAL MATCH (latestRevision)-[:DESCRIBED_BY]->(TypeInstanceResourceVersionMetadata)-[:CHARACTERIZED_BY]->(latestAttrRef: AttributeReference) - WHERE latestAttrRef IS NOT NULL - WITH *, COLLECT(latestAttrRef) AS latestAttrRefs - FOREACH (attr in latestAttrRefs | + ) + + RETURN ti + """ + ) + + updateTypeInstances(in: [UpdateTypeInstancesInput]!): [TypeInstance!]! + @cypher( + statement: """ + CALL { + UNWIND $in AS item + RETURN collect(item.id) as allInputIDs + } + + // Check if all TypeInstances were found + WITH * + CALL { + WITH allInputIDs + MATCH (ti:TypeInstance) + WHERE ti.id IN allInputIDs + WITH collect(ti.id) as foundIDs + RETURN foundIDs + } + CALL apoc.util.validate(size(foundIDs) < size(allInputIDs), apoc.convert.toJson({code: 404, ids: foundIDs}), null) + + // Check if given TypeInstances are not already locked by others + WITH * + CALL { + WITH * + UNWIND $in AS item + MATCH (tic:TypeInstance {id: item.id}) + WHERE tic.lockedBy IS NOT NULL AND (item.ownerID IS NULL OR tic.lockedBy <> item.ownerID) + WITH collect(tic.id) as lockedIDs + RETURN lockedIDs + } + CALL apoc.util.validate(size(lockedIDs) > 0, apoc.convert.toJson({code: 409, ids: lockedIDs}), null) + + UNWIND $in as item + MATCH (ti: TypeInstance {id: item.id}) + CALL { + WITH ti + MATCH (ti)-[:CONTAINS]->(latestRevision:TypeInstanceResourceVersion) + RETURN latestRevision + ORDER BY latestRevision.resourceVersion DESC LIMIT 1 + } + + CREATE (tir: TypeInstanceResourceVersion {resourceVersion: latestRevision.resourceVersion + 1, createdBy: item.createdBy}) + CREATE (ti)-[:CONTAINS]->(tir) + + // Handle the ` + "`" + `spec.value` + "`" + ` property + CREATE (spec: TypeInstanceResourceVersionSpec) + CREATE (tir)-[:SPECIFIED_BY]->(spec) + + WITH ti, tir, spec, latestRevision, item + CALL apoc.do.when( + item.typeInstance.value IS NOT NULL, + ' + SET spec.value = apoc.convert.toJson(item.typeInstance.value) RETURN spec + ', + ' + MATCH (latestRevision)-[:SPECIFIED_BY]->(latestSpec: TypeInstanceResourceVersionSpec) + SET spec.value = latestSpec.value RETURN spec + ', + {spec:spec, latestRevision: latestRevision, item: item}) YIELD value + + // Handle the ` + "`" + `metadata.attributes` + "`" + ` property + CREATE (metadata: TypeInstanceResourceVersionMetadata) + CREATE (tir)-[:DESCRIBED_BY]->(metadata) + + WITH ti, tir, latestRevision, metadata, item + CALL apoc.do.when( + item.typeInstance.attributes IS NOT NULL, + ' + FOREACH (attr in item.typeInstance.attributes | + MERGE (attrRef: AttributeReference {path: attr.path, revision: attr.revision}) + CREATE (metadata)-[:CHARACTERIZED_BY]->(attrRef) + ) + + RETURN metadata + ', + ' + OPTIONAL MATCH (latestRevision)-[:DESCRIBED_BY]->(TypeInstanceResourceVersionMetadata)-[:CHARACTERIZED_BY]->(latestAttrRef: AttributeReference) + WHERE latestAttrRef IS NOT NULL + WITH *, COLLECT(latestAttrRef) AS latestAttrRefs + FOREACH (attr in latestAttrRefs | CREATE (metadata)-[:CHARACTERIZED_BY]->(attr) - ) + ) - RETURN metadata - ', - {metadata: metadata, latestRevision: latestRevision, item: item} - ) YIELD value + RETURN metadata + ', + {metadata: metadata, latestRevision: latestRevision, item: item} + ) YIELD value - RETURN ti - """ - ) + RETURN ti + """ + ) - deleteTypeInstance(id: ID!, ownerID: LockOwnerID): ID! + deleteTypeInstance(id: ID!, ownerID: LockOwnerID): ID! - """ - Mark given TypeInstances as locked by a given owner. - If at least one TypeInstance is already locked with different OwnerID, an error is returned. - """ - lockTypeInstances(in: LockTypeInstancesInput!): [ID!]! + """ + Mark given TypeInstances as locked by a given owner. + If at least one TypeInstance is already locked with different OwnerID, an error is returned. + """ + lockTypeInstances(in: LockTypeInstancesInput!): [ID!]! - """ - Remove lock from given TypeInstances. - If at least one TypeInstance was not locked by a given owner, an error is returned. - """ - unlockTypeInstances(in: UnlockTypeInstancesInput!): [ID!]! + """ + Remove lock from given TypeInstances. + If at least one TypeInstance was not locked by a given owner, an error is returned. + """ + unlockTypeInstances(in: UnlockTypeInstancesInput!): [ID!]! } # TODO: Prepare directive for user authorization in https://github.com/capactio/capact/issues/508 @@ -1489,7 +1523,7 @@ func (ec *executionContext) _Mutation_createTypeInstance(ctx context.Context, fi return ec.resolvers.Mutation().CreateTypeInstance(rctx, args["in"].(CreateTypeInstanceInput)) } directive1 := func(ctx context.Context) (interface{}, error) { - statement, err := ec.unmarshalOString2ᚖstring(ctx, "WITH apoc.convert.toJson($in.value) as value\nMERGE (typeRef:TypeInstanceTypeReference {path: $in.typeRef.path, revision: $in.typeRef.revision})\n\nCREATE (ti:TypeInstance {id: apoc.create.uuid()})\nCREATE (ti)-[:OF_TYPE]->(typeRef)\n\nCREATE (tir: TypeInstanceResourceVersion {resourceVersion: 1, createdBy: $in.createdBy})\nCREATE (ti)-[:CONTAINS]->(tir)\n\nCREATE (tir)-[:DESCRIBED_BY]->(metadata: TypeInstanceResourceVersionMetadata)\nCREATE (tir)-[:SPECIFIED_BY]->(spec: TypeInstanceResourceVersionSpec {value: value})\n\nFOREACH (attr in $in.attributes |\n MERGE (attrRef: AttributeReference {path: attr.path, revision: attr.revision})\n CREATE (metadata)-[:CHARACTERIZED_BY]->(attrRef)\n)\n\nRETURN ti") + statement, err := ec.unmarshalOString2ᚖstring(ctx, "WITH apoc.convert.toJson($in.value) as value\nMERGE (typeRef:TypeInstanceTypeReference {path: $in.typeRef.path, revision: $in.typeRef.revision})\n\nCREATE (ti:TypeInstance {id: apoc.create.uuid()})\nCREATE (ti)-[:OF_TYPE]->(typeRef)\n\nCREATE (tir: TypeInstanceResourceVersion {resourceVersion: 1, createdBy: $in.createdBy})\nCREATE (ti)-[:CONTAINS]->(tir)\n\nCREATE (tir)-[:DESCRIBED_BY]->(metadata: TypeInstanceResourceVersionMetadata)\nCREATE (tir)-[:SPECIFIED_BY]->(spec: TypeInstanceResourceVersionSpec {value: value})\n\nFOREACH (attr in $in.attributes |\nMERGE (attrRef: AttributeReference {path: attr.path, revision: attr.revision})\nCREATE (metadata)-[:CHARACTERIZED_BY]->(attrRef)\n)\n\nRETURN ti") if err != nil { return nil, err } @@ -1555,7 +1589,7 @@ func (ec *executionContext) _Mutation_updateTypeInstances(ctx context.Context, f return ec.resolvers.Mutation().UpdateTypeInstances(rctx, args["in"].([]*UpdateTypeInstancesInput)) } directive1 := func(ctx context.Context) (interface{}, error) { - statement, err := ec.unmarshalOString2ᚖstring(ctx, "CALL {\n UNWIND $in AS item\n RETURN collect(item.id) as allInputIDs\n}\n\n// Check if all TypeInstances were found\nWITH *\nCALL {\n WITH allInputIDs\n MATCH (ti:TypeInstance)\n WHERE ti.id IN allInputIDs\n WITH collect(ti.id) as foundIDs\n RETURN foundIDs\n}\nCALL apoc.util.validate(size(foundIDs) < size(allInputIDs), apoc.convert.toJson({code: 404, ids: foundIDs}), null)\n\n// Check if given TypeInstances are not already locked by others\nWITH *\nCALL {\n WITH *\n UNWIND $in AS item\n MATCH (tic:TypeInstance {id: item.id})\n WHERE tic.lockedBy IS NOT NULL AND (item.ownerID IS NULL OR tic.lockedBy <> item.ownerID)\n WITH collect(tic.id) as lockedIDs\n RETURN lockedIDs\n}\nCALL apoc.util.validate(size(lockedIDs) > 0, apoc.convert.toJson({code: 409, ids: lockedIDs}), null)\n\nUNWIND $in as item\nMATCH (ti: TypeInstance {id: item.id})\nCALL {\n WITH ti\n MATCH (ti)-[:CONTAINS]->(latestRevision:TypeInstanceResourceVersion)\n RETURN latestRevision\n ORDER BY latestRevision.resourceVersion DESC LIMIT 1\n}\n\nCREATE (tir: TypeInstanceResourceVersion {resourceVersion: latestRevision.resourceVersion + 1, createdBy: item.createdBy})\nCREATE (ti)-[:CONTAINS]->(tir)\n\n// Handle the `spec.value` property\nCREATE (spec: TypeInstanceResourceVersionSpec)\nCREATE (tir)-[:SPECIFIED_BY]->(spec)\n\nWITH ti, tir, spec, latestRevision, item\nCALL apoc.do.when(\n item.typeInstance.value IS NOT NULL,\n '\n SET spec.value = apoc.convert.toJson(item.typeInstance.value) RETURN spec\n ',\n '\n MATCH (latestRevision)-[:SPECIFIED_BY]->(latestSpec: TypeInstanceResourceVersionSpec)\n SET spec.value = latestSpec.value RETURN spec\n ',\n {spec:spec, latestRevision: latestRevision, item: item}) YIELD value\n\n// Handle the `metadata.attributes` property\nCREATE (metadata: TypeInstanceResourceVersionMetadata)\nCREATE (tir)-[:DESCRIBED_BY]->(metadata)\n\nWITH ti, tir, latestRevision, metadata, item\nCALL apoc.do.when(\n item.typeInstance.attributes IS NOT NULL,\n '\n FOREACH (attr in item.typeInstance.attributes |\n MERGE (attrRef: AttributeReference {path: attr.path, revision: attr.revision})\n CREATE (metadata)-[:CHARACTERIZED_BY]->(attrRef)\n )\n\n RETURN metadata\n ',\n '\n OPTIONAL MATCH (latestRevision)-[:DESCRIBED_BY]->(TypeInstanceResourceVersionMetadata)-[:CHARACTERIZED_BY]->(latestAttrRef: AttributeReference)\n WHERE latestAttrRef IS NOT NULL\n WITH *, COLLECT(latestAttrRef) AS latestAttrRefs\n FOREACH (attr in latestAttrRefs |\n CREATE (metadata)-[:CHARACTERIZED_BY]->(attr)\n )\n\n RETURN metadata\n ',\n {metadata: metadata, latestRevision: latestRevision, item: item}\n) YIELD value\n\nRETURN ti") + statement, err := ec.unmarshalOString2ᚖstring(ctx, "CALL {\nUNWIND $in AS item\nRETURN collect(item.id) as allInputIDs\n}\n\n// Check if all TypeInstances were found\nWITH *\nCALL {\nWITH allInputIDs\nMATCH (ti:TypeInstance)\nWHERE ti.id IN allInputIDs\nWITH collect(ti.id) as foundIDs\nRETURN foundIDs\n}\nCALL apoc.util.validate(size(foundIDs) < size(allInputIDs), apoc.convert.toJson({code: 404, ids: foundIDs}), null)\n\n// Check if given TypeInstances are not already locked by others\nWITH *\nCALL {\nWITH *\nUNWIND $in AS item\nMATCH (tic:TypeInstance {id: item.id})\nWHERE tic.lockedBy IS NOT NULL AND (item.ownerID IS NULL OR tic.lockedBy <> item.ownerID)\nWITH collect(tic.id) as lockedIDs\nRETURN lockedIDs\n}\nCALL apoc.util.validate(size(lockedIDs) > 0, apoc.convert.toJson({code: 409, ids: lockedIDs}), null)\n\nUNWIND $in as item\nMATCH (ti: TypeInstance {id: item.id})\nCALL {\nWITH ti\nMATCH (ti)-[:CONTAINS]->(latestRevision:TypeInstanceResourceVersion)\nRETURN latestRevision\nORDER BY latestRevision.resourceVersion DESC LIMIT 1\n}\n\nCREATE (tir: TypeInstanceResourceVersion {resourceVersion: latestRevision.resourceVersion + 1, createdBy: item.createdBy})\nCREATE (ti)-[:CONTAINS]->(tir)\n\n// Handle the `spec.value` property\nCREATE (spec: TypeInstanceResourceVersionSpec)\nCREATE (tir)-[:SPECIFIED_BY]->(spec)\n\nWITH ti, tir, spec, latestRevision, item\nCALL apoc.do.when(\nitem.typeInstance.value IS NOT NULL,\n'\nSET spec.value = apoc.convert.toJson(item.typeInstance.value) RETURN spec\n',\n'\nMATCH (latestRevision)-[:SPECIFIED_BY]->(latestSpec: TypeInstanceResourceVersionSpec)\nSET spec.value = latestSpec.value RETURN spec\n',\n{spec:spec, latestRevision: latestRevision, item: item}) YIELD value\n\n// Handle the `metadata.attributes` property\nCREATE (metadata: TypeInstanceResourceVersionMetadata)\nCREATE (tir)-[:DESCRIBED_BY]->(metadata)\n\nWITH ti, tir, latestRevision, metadata, item\nCALL apoc.do.when(\nitem.typeInstance.attributes IS NOT NULL,\n'\nFOREACH (attr in item.typeInstance.attributes |\nMERGE (attrRef: AttributeReference {path: attr.path, revision: attr.revision})\nCREATE (metadata)-[:CHARACTERIZED_BY]->(attrRef)\n)\n\nRETURN metadata\n',\n'\nOPTIONAL MATCH (latestRevision)-[:DESCRIBED_BY]->(TypeInstanceResourceVersionMetadata)-[:CHARACTERIZED_BY]->(latestAttrRef: AttributeReference)\nWHERE latestAttrRef IS NOT NULL\nWITH *, COLLECT(latestAttrRef) AS latestAttrRefs\nFOREACH (attr in latestAttrRefs |\nCREATE (metadata)-[:CHARACTERIZED_BY]->(attr)\n)\n\nRETURN metadata\n',\n{metadata: metadata, latestRevision: latestRevision, item: item}\n) YIELD value\n\nRETURN ti") if err != nil { return nil, err } @@ -1747,7 +1781,7 @@ func (ec *executionContext) _Query_typeInstances(ctx context.Context, field grap return ec.resolvers.Query().TypeInstances(rctx, args["filter"].(*TypeInstanceFilter)) } directive1 := func(ctx context.Context) (interface{}, error) { - statement, err := ec.unmarshalOString2ᚖstring(ctx, "WITH [x IN $filter.attributes WHERE x.rule = \"EXCLUDE\" | x ] AS excluded,\n [x IN $filter.attributes WHERE x.rule = \"INCLUDE\" | x ] AS included\n\nCALL {\n WITH excluded\n UNWIND excluded AS f\n MATCH (ex:AttributeReference {path: f.path})\n WHERE (f.revision IS NULL) OR (ex.revision = f.revision)\n RETURN collect(ex) as excludedAttributes\n}\n\nMATCH (tir:TypeInstanceResourceVersion)-[:DESCRIBED_BY]->(meta:TypeInstanceResourceVersionMetadata)\nOPTIONAL MATCH (meta)-[:CHARACTERIZED_BY]->(attr:AttributeReference)\nMATCH (ti:TypeInstance)-[:OF_TYPE]->(typeRef:TypeInstanceTypeReference)\nMATCH (ti:TypeInstance)-[:CONTAINS]->(tir)\nWHERE\n$filter = {} OR\n(\n (\n $filter.typeRef IS NULL\n OR\n (\n ($filter.typeRef.revision IS NULL AND typeRef.path = $filter.typeRef.path)\n OR\n (typeRef.path = $filter.typeRef.path AND typeRef.revision = $filter.typeRef.revision)\n )\n )\n AND\n ($filter.createdBy IS NULL OR tir.createdBy = $filter.createdBy)\n AND\n (\n \t$filter.attributes IS NULL\n OR\n (\n all(inc IN included WHERE\n (tir)-[:DESCRIBED_BY]->(meta:TypeInstanceResourceVersionMetadata)-[:CHARACTERIZED_BY]->(attr:AttributeReference {path: inc.path})\n AND\n (inc.revision IS NULL OR attr.revision = inc.revision)\n )\n AND\n none(exc IN excludedAttributes WHERE (tir)-[:DESCRIBED_BY]->(meta:TypeInstanceResourceVersionMetadata)-[:CHARACTERIZED_BY]->(exc))\n )\n )\n)\n\nRETURN DISTINCT ti") + statement, err := ec.unmarshalOString2ᚖstring(ctx, "WITH [x IN $filter.attributes WHERE x.rule = \"EXCLUDE\" | x ] AS excluded,\n[x IN $filter.attributes WHERE x.rule = \"INCLUDE\" | x ] AS included\n\nCALL {\nWITH excluded\nUNWIND excluded AS f\nMATCH (ex:AttributeReference {path: f.path})\nWHERE (f.revision IS NULL) OR (ex.revision = f.revision)\nRETURN collect(ex) as excludedAttributes\n}\n\nMATCH (tir:TypeInstanceResourceVersion)-[:DESCRIBED_BY]->(meta:TypeInstanceResourceVersionMetadata)\nOPTIONAL MATCH (meta)-[:CHARACTERIZED_BY]->(attr:AttributeReference)\nMATCH (ti:TypeInstance)-[:OF_TYPE]->(typeRef:TypeInstanceTypeReference)\nMATCH (ti:TypeInstance)-[:CONTAINS]->(tir)\nWHERE\n$filter = {} OR\n(\n(\n$filter.typeRef IS NULL\nOR\n(\n($filter.typeRef.revision IS NULL AND typeRef.path = $filter.typeRef.path)\nOR\n(typeRef.path = $filter.typeRef.path AND typeRef.revision = $filter.typeRef.revision)\n)\n)\nAND\n($filter.createdBy IS NULL OR tir.createdBy = $filter.createdBy)\nAND\n(\n$filter.attributes IS NULL\nOR\n(\nall(inc IN included WHERE\n(tir)-[:DESCRIBED_BY]->(meta:TypeInstanceResourceVersionMetadata)-[:CHARACTERIZED_BY]->(attr:AttributeReference {path: inc.path})\nAND\n(inc.revision IS NULL OR attr.revision = inc.revision)\n)\nAND\nnone(exc IN excludedAttributes WHERE (tir)-[:DESCRIBED_BY]->(meta:TypeInstanceResourceVersionMetadata)-[:CHARACTERIZED_BY]->(exc))\n)\n)\n)\n\nRETURN DISTINCT ti") if err != nil { return nil, err } @@ -2488,6 +2522,41 @@ func (ec *executionContext) _TypeInstance_resourceVersions(ctx context.Context, return ec.marshalNTypeInstanceResourceVersion2ᚕᚖcapactᚗioᚋcapactᚋpkgᚋhubᚋapiᚋgraphqlᚋlocalᚐTypeInstanceResourceVersionᚄ(ctx, field.Selections, res) } +func (ec *executionContext) _TypeInstanceBackend_id(ctx context.Context, field graphql.CollectedField, obj *TypeInstanceBackend) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "TypeInstanceBackend", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.ID, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + func (ec *executionContext) _TypeInstanceInstrumentation_metrics(ctx context.Context, field graphql.CollectedField, obj *TypeInstanceInstrumentation) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -3079,6 +3148,41 @@ func (ec *executionContext) _TypeInstanceResourceVersion_spec(ctx context.Contex return ec.marshalNTypeInstanceResourceVersionSpec2ᚖcapactᚗioᚋcapactᚋpkgᚋhubᚋapiᚋgraphqlᚋlocalᚐTypeInstanceResourceVersionSpec(ctx, field.Selections, res) } +func (ec *executionContext) _TypeInstanceResourceVersion_backend(ctx context.Context, field graphql.CollectedField, obj *TypeInstanceResourceVersion) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "TypeInstanceResourceVersion", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Backend, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*TypeInstanceBackend) + fc.Result = res + return ec.marshalNTypeInstanceBackend2ᚖcapactᚗioᚋcapactᚋpkgᚋhubᚋapiᚋgraphqlᚋlocalᚐTypeInstanceBackend(ctx, field.Selections, res) +} + func (ec *executionContext) _TypeInstanceResourceVersionMetadata_attributes(ctx context.Context, field graphql.CollectedField, obj *TypeInstanceResourceVersionMetadata) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -4521,6 +4625,14 @@ func (ec *executionContext) unmarshalInputCreateTypeInstanceInput(ctx context.Co if err != nil { return it, err } + case "backend": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("backend")) + it.Backend, err = ec.unmarshalOTypeInstanceBackendInput2ᚖcapactᚗioᚋcapactᚋpkgᚋhubᚋapiᚋgraphqlᚋlocalᚐTypeInstanceBackendInput(ctx, v) + if err != nil { + return it, err + } case "value": var err error @@ -4591,6 +4703,26 @@ func (ec *executionContext) unmarshalInputLockTypeInstancesInput(ctx context.Con return it, nil } +func (ec *executionContext) unmarshalInputTypeInstanceBackendInput(ctx context.Context, obj interface{}) (TypeInstanceBackendInput, error) { + var it TypeInstanceBackendInput + var asMap = obj.(map[string]interface{}) + + for k, v := range asMap { + switch k { + case "id": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("id")) + it.ID, err = ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputTypeInstanceFilter(ctx context.Context, obj interface{}) (TypeInstanceFilter, error) { var it TypeInstanceFilter var asMap = obj.(map[string]interface{}) @@ -5051,6 +5183,33 @@ func (ec *executionContext) _TypeInstance(ctx context.Context, sel ast.Selection return out } +var typeInstanceBackendImplementors = []string{"TypeInstanceBackend"} + +func (ec *executionContext) _TypeInstanceBackend(ctx context.Context, sel ast.SelectionSet, obj *TypeInstanceBackend) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, typeInstanceBackendImplementors) + + out := graphql.NewFieldSet(fields) + var invalids uint32 + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("TypeInstanceBackend") + case "id": + out.Values[i] = ec._TypeInstanceBackend_id(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch() + if invalids > 0 { + return graphql.Null + } + return out +} + var typeInstanceInstrumentationImplementors = []string{"TypeInstanceInstrumentation"} func (ec *executionContext) _TypeInstanceInstrumentation(ctx context.Context, sel ast.SelectionSet, obj *TypeInstanceInstrumentation) graphql.Marshaler { @@ -5191,6 +5350,11 @@ func (ec *executionContext) _TypeInstanceResourceVersion(ctx context.Context, se if out.Values[i] == graphql.Null { invalids++ } + case "backend": + out.Values[i] = ec._TypeInstanceResourceVersion_backend(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -5827,6 +5991,16 @@ func (ec *executionContext) marshalNTypeInstance2ᚖcapactᚗioᚋcapactᚋpkg return ec._TypeInstance(ctx, sel, v) } +func (ec *executionContext) marshalNTypeInstanceBackend2ᚖcapactᚗioᚋcapactᚋpkgᚋhubᚋapiᚋgraphqlᚋlocalᚐTypeInstanceBackend(ctx context.Context, sel ast.SelectionSet, v *TypeInstanceBackend) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + return ec._TypeInstanceBackend(ctx, sel, v) +} + func (ec *executionContext) marshalNTypeInstanceInstrumentationMetricsDashboard2ᚕᚖcapactᚗioᚋcapactᚋpkgᚋhubᚋapiᚋgraphqlᚋlocalᚐTypeInstanceInstrumentationMetricsDashboardᚄ(ctx context.Context, sel ast.SelectionSet, v []*TypeInstanceInstrumentationMetricsDashboard) graphql.Marshaler { ret := make(graphql.Array, len(v)) var wg sync.WaitGroup @@ -6470,6 +6644,14 @@ func (ec *executionContext) marshalOTypeInstance2ᚖcapactᚗioᚋcapactᚋpkg return ec._TypeInstance(ctx, sel, v) } +func (ec *executionContext) unmarshalOTypeInstanceBackendInput2ᚖcapactᚗioᚋcapactᚋpkgᚋhubᚋapiᚋgraphqlᚋlocalᚐTypeInstanceBackendInput(ctx context.Context, v interface{}) (*TypeInstanceBackendInput, error) { + if v == nil { + return nil, nil + } + res, err := ec.unmarshalInputTypeInstanceBackendInput(ctx, v) + return &res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) unmarshalOTypeInstanceFilter2ᚖcapactᚗioᚋcapactᚋpkgᚋhubᚋapiᚋgraphqlᚋlocalᚐTypeInstanceFilter(ctx context.Context, v interface{}) (*TypeInstanceFilter, error) { if v == nil { return nil, nil diff --git a/pkg/hub/client/policy_enforced_client.go b/pkg/hub/client/policy_enforced_client.go index ee9df9de7..d37858613 100644 --- a/pkg/hub/client/policy_enforced_client.go +++ b/pkg/hub/client/policy_enforced_client.go @@ -5,14 +5,14 @@ import ( "fmt" "sync" - "capact.io/capact/pkg/engine/k8s/policy/metadata" - "capact.io/capact/pkg/sdk/validation" - + "capact.io/capact/internal/ptr" "capact.io/capact/pkg/engine/k8s/policy" + "capact.io/capact/pkg/engine/k8s/policy/metadata" hublocalgraphql "capact.io/capact/pkg/hub/api/graphql/local" hubpublicgraphql "capact.io/capact/pkg/hub/api/graphql/public" "capact.io/capact/pkg/hub/client/public" "capact.io/capact/pkg/sdk/apis/0.0.1/types" + "capact.io/capact/pkg/sdk/validation" "github.com/pkg/errors" "sigs.k8s.io/yaml" @@ -25,6 +25,7 @@ type HubClient interface { ListTypeInstancesTypeRef(ctx context.Context) ([]hublocalgraphql.TypeInstanceTypeReference, error) FindInterfaceRevision(ctx context.Context, ref hubpublicgraphql.InterfaceReference, opts ...public.InterfaceRevisionOption) (*hubpublicgraphql.InterfaceRevision, error) FindTypeInstancesTypeRef(ctx context.Context, ids []string) (map[string]hublocalgraphql.TypeInstanceTypeReference, error) + ListTypes(ctx context.Context, opts ...public.TypeOption) ([]*hubpublicgraphql.Type, error) } // PolicyIOValidator defines validator used for PolicyEnforcedClient. @@ -33,7 +34,7 @@ type PolicyIOValidator interface { ValidateTypeInstancesMetadata(in policy.Policy) validation.Result ValidateTypeInstancesMetadataForRule(in policy.Rule) validation.Result ValidateAdditionalTypeInstances(additionalTIsInPolicy []policy.AdditionalTypeInstanceToInject, implRev hubpublicgraphql.ImplementationRevision) validation.Result - IsTypeRefInjectableAndEqualToImplReq(typeRef *types.ManifestRef, reqItem *hubpublicgraphql.ImplementationRequirementItem) bool + IsTypeRefInjectableAndEqualToImplReq(typeRef *types.TypeRef, reqItem *hubpublicgraphql.ImplementationRequirementItem) bool LoadAdditionalInputParametersSchemas(context.Context, hubpublicgraphql.ImplementationRevision) (validation.SchemaCollection, error) ValidateAdditionalInputParameters(ctx context.Context, paramsSchemas validation.SchemaCollection, parameters types.ParametersCollection) (validation.Result, error) } @@ -104,9 +105,65 @@ func (e *PolicyEnforcedClient) ListImplementationRevisionForInterface(ctx contex return implementations, rule, nil } +// ListTypeInstancesBackendsBasedOnPolicy returns default backends defined in Policy and those specified explicitly in a given policy rule. +func (e *PolicyEnforcedClient) ListTypeInstancesBackendsBasedOnPolicy(_ context.Context, rule policy.Rule, implRev hubpublicgraphql.ImplementationRevision) (policy.TypeInstanceBackendCollection, error) { + out := policy.TypeInstanceBackendCollection{} + + // TODO(storage): set default Hub storage + // TODO(storage): but should we really set the default? In this case it's always required on type instance creation. + // if not, it can be empty and Local Hub will save it in own core storage. + out.SetDefault(policy.TypeInstanceBackend{ + TypeInstanceReference: policy.TypeInstanceReference{ + ID: "123123123123", + Description: ptr.String("Default Capact Hub storage"), + }, + }) + + // 1. Global Defaults based on TypeRefs + for _, rule := range e.mergedPolicy.TypeInstance.Rules { + out.SetByTypeRef(rule.TypeRef, rule.Backend) + } + + // TODO(https://github.com/capactio/capact/issues/624): + // 2. Global defaults based on required TypeInstance injection + // e.mergedPolicy.Interface.Defaults + + //3. Override defaults with specific Interface Policy rule + inject, err := e.listRequiredTypeInstancesToInjectBasedOnPolicy(rule, implRev) + if err != nil { + return policy.TypeInstanceBackendCollection{}, err + } + + for alias, rule := range inject { + out.SetByAlias(alias, policy.TypeInstanceBackend(rule)) + } + + return out, nil +} + // ListRequiredTypeInstancesToInjectBasedOnPolicy returns the required TypeInstance references, // which have to be injected into the Action, based on the current policy rules. func (e *PolicyEnforcedClient) ListRequiredTypeInstancesToInjectBasedOnPolicy(policyRule policy.Rule, implRev hubpublicgraphql.ImplementationRevision) ([]types.InputTypeInstanceRef, error) { + var typeInstancesToInject []types.InputTypeInstanceRef + inject, err := e.listRequiredTypeInstancesToInjectBasedOnPolicy(policyRule, implRev) + if err != nil { + return nil, err + } + + for alias, typeInstance := range inject { + typeInstancesToInject = append(typeInstancesToInject, types.InputTypeInstanceRef{ + Name: alias, + ID: typeInstance.ID, + }) + } + + return typeInstancesToInject, nil +} + +// requiredTypeInstanceToInject holds required TypeInstances for injection indexed by alias. +type requiredTypeInstanceToInject map[string]policy.RequiredTypeInstanceToInject + +func (e *PolicyEnforcedClient) listRequiredTypeInstancesToInjectBasedOnPolicy(policyRule policy.Rule, implRev hubpublicgraphql.ImplementationRevision) (requiredTypeInstanceToInject, error) { requiredTIs := policyRule.RequiredTypeInstancesToInject() if len(requiredTIs) == 0 { return nil, nil @@ -116,20 +173,18 @@ func (e *PolicyEnforcedClient) ListRequiredTypeInstancesToInjectBasedOnPolicy(po return nil, e.wrapValidationResultError(res.ErrorOrNil(), "while validating Policy rule") } - var typeInstancesToInject []types.InputTypeInstanceRef + typeInstancesToInject := requiredTypeInstanceToInject{} for _, typeInstance := range requiredTIs { alias, found := e.findAliasForTypeInstance(typeInstance, implRev) if !found { // Implementation doesn't require such TypeInstance, skip injecting it continue } - - typeInstanceToInject := types.InputTypeInstanceRef{ - Name: alias, - ID: typeInstance.ID, + if _, found := typeInstancesToInject[alias]; found { + return nil, fmt.Errorf("found duplicated alias %q entry under requires property", alias) } - typeInstancesToInject = append(typeInstancesToInject, typeInstanceToInject) + typeInstancesToInject[alias] = typeInstance } return typeInstancesToInject, nil diff --git a/pkg/hub/client/policy_enforced_client_test.go b/pkg/hub/client/policy_enforced_client_test.go index b8469dafd..458a51402 100644 --- a/pkg/hub/client/policy_enforced_client_test.go +++ b/pkg/hub/client/policy_enforced_client_test.go @@ -79,13 +79,13 @@ func TestPolicyEnforcedClient_ListRequiredTypeInstancesToInjectBasedOnPolicy(t * Inject: &policy.InjectData{ RequiredTypeInstances: []policy.RequiredTypeInstanceToInject{ { - RequiredTypeInstanceReference: policy.RequiredTypeInstanceReference{ + TypeInstanceReference: policy.TypeInstanceReference{ ID: "my-uuid", Description: ptr.String("My UUID"), - }, - TypeRef: &types.ManifestRef{ - Path: "cap.type.gcp.auth.service-account", - Revision: "0.1.1", + TypeRef: &types.TypeRef{ + Path: "cap.type.gcp.auth.service-account", + Revision: "0.1.1", + }, }, }, }, @@ -110,7 +110,7 @@ func TestPolicyEnforcedClient_ListRequiredTypeInstancesToInjectBasedOnPolicy(t * Inject: &policy.InjectData{ RequiredTypeInstances: []policy.RequiredTypeInstanceToInject{ { - RequiredTypeInstanceReference: policy.RequiredTypeInstanceReference{ + TypeInstanceReference: policy.TypeInstanceReference{ ID: "my-uuid", Description: ptr.String("My UUID"), }, @@ -142,13 +142,13 @@ func TestPolicyEnforcedClient_ListRequiredTypeInstancesToInjectBasedOnPolicy(t * Inject: &policy.InjectData{ RequiredTypeInstances: []policy.RequiredTypeInstanceToInject{ { - RequiredTypeInstanceReference: policy.RequiredTypeInstanceReference{ + TypeInstanceReference: policy.TypeInstanceReference{ ID: "my-uuid", Description: ptr.String("My UUID"), - }, - TypeRef: &types.ManifestRef{ - Path: "cap.type.gcp.auth.service-account", - Revision: "0.1.1", + TypeRef: &types.TypeRef{ + Path: "cap.type.gcp.auth.service-account", + Revision: "0.1.1", + }, }, }, }, @@ -469,6 +469,167 @@ func TestPolicyEnforcedClient_ListAdditionalInputToInjectBasedOnPolicy(t *testin } } +func TestPolicyEnforcedClient_ListTypeInstancesBackendsBasedOnPolicy(t *testing.T) { + tests := []struct { + name string + + implRev gqlpublicapi.ImplementationRevision + policyRule policy.Rule + expectedBackends map[string]policy.TypeInstanceBackend + expectedErrMessage *string + globalPolicy policy.Policy + }{ + { + name: "Empty inject in policy rule", + implRev: fixImplementationRevisionWithRequire(gqlpublicapi.ImplementationRequirement{ + AnyOf: []*gqlpublicapi.ImplementationRequirementItem{ + { + TypeRef: &gqlpublicapi.TypeReference{ + Path: "cap.type.gcp.sa", + Revision: "0.1.1", + }, + }, + }, + }), + policyRule: policy.Rule{ + Inject: &policy.InjectData{ + RequiredTypeInstances: []policy.RequiredTypeInstanceToInject{}, + }, + }, + globalPolicy: policy.Policy{ + TypeInstance: policy.TypeInstancePolicy{ + Rules: []policy.RulesForTypeInstance{ + { + TypeRef: types.ManifestRefWithOptRevision{ + Path: "cap.type.capactio.examples.message", + }, + Backend: policy.TypeInstanceBackend{ + TypeInstanceReference: policy.TypeInstanceReference{ + ID: "ID1", + TypeRef: &types.TypeRef{ + Path: "cap.type.aws.secret-manager.storage", + Revision: "0.1.0", + }, + ExtendsHubStorage: true, + }, + }, + }, + }, + }, + }, + expectedBackends: map[string]policy.TypeInstanceBackend{ + "cap.type.capactio.examples.message": { + TypeInstanceReference: policy.TypeInstanceReference{ + ID: "ID1", + TypeRef: &types.TypeRef{ + Path: "cap.type.aws.secret-manager.storage", + Revision: "0.1.0", + }, + ExtendsHubStorage: true, + }}, + }, + }, + { + name: "Inject Helm storage", + implRev: fixImplementationRevisionWithRequire(gqlpublicapi.ImplementationRequirement{ + AllOf: []*gqlpublicapi.ImplementationRequirementItem{ + { + Alias: ptr.String("helm-storage"), + TypeRef: &gqlpublicapi.TypeReference{ + Path: "cap.type.helm.storage", + Revision: "0.1.1", + }, + }, + }, + }), + policyRule: policy.Rule{ + Inject: &policy.InjectData{ + RequiredTypeInstances: []policy.RequiredTypeInstanceToInject{ + { + TypeInstanceReference: policy.TypeInstanceReference{ + ID: "ID2", + Description: ptr.String("ID2"), + TypeRef: &types.TypeRef{ + Path: "cap.type.helm.storage", + Revision: "0.1.1", + }, + ExtendsHubStorage: true, + }, + }, + }, + }, + }, + globalPolicy: policy.Policy{ + TypeInstance: policy.TypeInstancePolicy{ + Rules: []policy.RulesForTypeInstance{ + { + TypeRef: types.ManifestRefWithOptRevision{ + Path: "cap.type.capactio.examples.message", + }, + Backend: policy.TypeInstanceBackend{ + TypeInstanceReference: policy.TypeInstanceReference{ + ID: "ID1", + TypeRef: &types.TypeRef{ + Path: "cap.type.aws.secret-manager.storage", + Revision: "0.1.0", + }, + ExtendsHubStorage: true, + }, + }, + }, + }, + }, + }, + expectedBackends: map[string]policy.TypeInstanceBackend{ + "cap.type.capactio.examples.message": { + TypeInstanceReference: policy.TypeInstanceReference{ + ID: "ID1", + TypeRef: &types.TypeRef{ + Path: "cap.type.aws.secret-manager.storage", + Revision: "0.1.0", + }, + ExtendsHubStorage: true, + }, + }, + "helm-storage": { + TypeInstanceReference: policy.TypeInstanceReference{ + ID: "ID2", + Description: ptr.String("ID2"), + TypeRef: &types.TypeRef{ + Path: "cap.type.helm.storage", + Revision: "0.1.1", + }, + ExtendsHubStorage: true, + }, + }, + }, + }, + } + for _, test := range tests { + tt := test + t.Run(tt.name, func(t *testing.T) { + // given + hubCli := &fake.FileSystemClient{} + + validator := policyvalidation.NewValidator(hubCli) + cli := client.NewPolicyEnforcedClient(hubCli, validator) + cli.SetGlobalPolicy(tt.globalPolicy) + + // when + actual, err := cli.ListTypeInstancesBackendsBasedOnPolicy(context.Background(), tt.policyRule, tt.implRev) + + // then + if tt.expectedErrMessage != nil { + require.Error(t, err) + assert.EqualError(t, err, *tt.expectedErrMessage) + } else { + require.NoError(t, err) + assert.Equal(t, tt.expectedBackends, actual.GetAll()) + } + }) + } +} + func fixImplementationRevisionWithRequire(req gqlpublicapi.ImplementationRequirement) gqlpublicapi.ImplementationRevision { impl := fixImplementationRevision("impl", "0.0.1") impl.Spec.Requires = []*gqlpublicapi.ImplementationRequirement{ diff --git a/pkg/hub/client/policy_merger.go b/pkg/hub/client/policy_merger.go index 88e944754..986a1737b 100644 --- a/pkg/hub/client/policy_merger.go +++ b/pkg/hub/client/policy_merger.go @@ -4,7 +4,7 @@ import ( "reflect" "capact.io/capact/internal/maps" - + "capact.io/capact/internal/ptr" "capact.io/capact/pkg/engine/k8s/policy" ) @@ -15,10 +15,14 @@ func (e *PolicyEnforcedClient) mergePolicies() { switch p { case policy.Global: applyInterfacePolicy(¤tPolicy.Interface, e.globalPolicy.Interface) + applyTypeInstancePolicy(¤tPolicy.TypeInstance, e.globalPolicy.TypeInstance) case policy.Action: applyInterfacePolicy(¤tPolicy.Interface, e.actionPolicy.Interface) + applyTypeInstancePolicy(¤tPolicy.TypeInstance, e.actionPolicy.TypeInstance) case policy.Workflow: for _, wp := range e.workflowStepPolicies { + // ignore TypeInstance Policy on Workflow as it's not supported, + // see: policy.WorkflowPolicy type. applyInterfacePolicy(¤tPolicy.Interface, wp.Interface) } } @@ -31,7 +35,7 @@ func (e *PolicyEnforcedClient) mergePolicies() { // current policy is a higher priority policy func applyInterfacePolicy(currentPolicy *policy.InterfacePolicy, newPolicy policy.InterfacePolicy) { for _, newRuleForInterface := range newPolicy.Rules { - policyRuleIndex := getIndexOfPolicyRule(currentPolicy, newRuleForInterface) + policyRuleIndex := getIndexOfInterfacePolicyRule(currentPolicy, newRuleForInterface) if policyRuleIndex == -1 { newRuleForInterface := newRuleForInterface.DeepCopy() currentPolicy.Rules = append(currentPolicy.Rules, *newRuleForInterface) @@ -71,7 +75,7 @@ func mergeRules(rule *policy.Rule, newRule policy.Rule) { } } -func getIndexOfPolicyRule(p *policy.InterfacePolicy, rule policy.RulesForInterface) int { +func getIndexOfInterfacePolicyRule(p *policy.InterfacePolicy, rule policy.RulesForInterface) int { for i, ruleForInterface := range p.Rules { if isForSameInterface(ruleForInterface, rule) { return i @@ -162,3 +166,35 @@ func mergeAdditionalParameters(current, overwrite []policy.AdditionalParametersT } return out } + +func applyTypeInstancePolicy(currentPolicy *policy.TypeInstancePolicy, newPolicy policy.TypeInstancePolicy) { + for _, newRule := range newPolicy.Rules { + policyRuleIndex := getIndexOfTypeInstancePolicyRule(currentPolicy, newRule) + if policyRuleIndex == -1 { + newRuleForInterface := newRule.DeepCopy() + currentPolicy.Rules = append(currentPolicy.Rules, *newRuleForInterface) + continue + } + + // override + currentPolicy.Rules[policyRuleIndex] = newRule + } +} + +func getIndexOfTypeInstancePolicyRule(current *policy.TypeInstancePolicy, newRule policy.RulesForTypeInstance) int { + for i, currentRule := range current.Rules { + if currentRule.TypeRef.Path != newRule.TypeRef.Path { + continue + } + curRev := ptr.StringPtrToString(currentRule.TypeRef.Revision) + newRev := ptr.StringPtrToString(newRule.TypeRef.Revision) + + if curRev != newRev { + continue + } + + return i + } + + return -1 +} diff --git a/pkg/hub/client/policy_merger_test.go b/pkg/hub/client/policy_merger_test.go index fdb592c72..51d0b7def 100644 --- a/pkg/hub/client/policy_merger_test.go +++ b/pkg/hub/client/policy_merger_test.go @@ -28,7 +28,7 @@ func TestPolicyEnforcedClient_mergePolicies(t *testing.T) { name: "only global policy", global: policy.Policy{ Interface: policy.InterfacePolicy{ - Rules: policy.RulesList{ + Rules: policy.InterfaceRulesList{ policy.RulesForInterface{ Interface: types.ManifestRefWithOptRevision{ Path: interfacePath, @@ -51,13 +51,13 @@ func TestPolicyEnforcedClient_mergePolicies(t *testing.T) { }, RequiredTypeInstances: []policy.RequiredTypeInstanceToInject{ { - RequiredTypeInstanceReference: policy.RequiredTypeInstanceReference{ + TypeInstanceReference: policy.TypeInstanceReference{ ID: "1314-142-123", Description: ptr.String("Sample TI"), - }, - TypeRef: &types.ManifestRef{ - Path: "cap.type.gcp.auth.service-account", - Revision: "0.1.0", + TypeRef: &types.TypeRef{ + Path: "cap.type.gcp.auth.service-account", + Revision: "0.1.0", + }, }, }, }, @@ -83,7 +83,7 @@ func TestPolicyEnforcedClient_mergePolicies(t *testing.T) { action: policy.ActionPolicy{}, expected: policy.Policy{ Interface: policy.InterfacePolicy{ - Rules: policy.RulesList{ + Rules: policy.InterfaceRulesList{ policy.RulesForInterface{ Interface: types.ManifestRefWithOptRevision{ Path: interfacePath, @@ -106,13 +106,13 @@ func TestPolicyEnforcedClient_mergePolicies(t *testing.T) { }, RequiredTypeInstances: []policy.RequiredTypeInstanceToInject{ { - RequiredTypeInstanceReference: policy.RequiredTypeInstanceReference{ + TypeInstanceReference: policy.TypeInstanceReference{ ID: "1314-142-123", Description: ptr.String("Sample TI"), - }, - TypeRef: &types.ManifestRef{ - Path: "cap.type.gcp.auth.service-account", - Revision: "0.1.0", + TypeRef: &types.TypeRef{ + Path: "cap.type.gcp.auth.service-account", + Revision: "0.1.0", + }, }, }, }, @@ -141,7 +141,7 @@ func TestPolicyEnforcedClient_mergePolicies(t *testing.T) { name: "only action policy", action: policy.ActionPolicy{ Interface: policy.InterfacePolicy{ - Rules: policy.RulesList{ + Rules: policy.InterfaceRulesList{ policy.RulesForInterface{ Interface: types.ManifestRefWithOptRevision{ Path: interfacePath, @@ -164,12 +164,12 @@ func TestPolicyEnforcedClient_mergePolicies(t *testing.T) { }, RequiredTypeInstances: []policy.RequiredTypeInstanceToInject{ { - RequiredTypeInstanceReference: policy.RequiredTypeInstanceReference{ + TypeInstanceReference: policy.TypeInstanceReference{ ID: "1314-142-123", Description: ptr.String("Sample TI"), - }, - TypeRef: &types.ManifestRef{ - Path: "cap.type.gcp.auth.service-account", + TypeRef: &types.TypeRef{ + Path: "cap.type.gcp.auth.service-account", + }, }, }, }, @@ -195,7 +195,7 @@ func TestPolicyEnforcedClient_mergePolicies(t *testing.T) { global: policy.Policy{}, expected: policy.Policy{ Interface: policy.InterfacePolicy{ - Rules: policy.RulesList{ + Rules: policy.InterfaceRulesList{ policy.RulesForInterface{ Interface: types.ManifestRefWithOptRevision{ Path: interfacePath, @@ -218,12 +218,12 @@ func TestPolicyEnforcedClient_mergePolicies(t *testing.T) { }, RequiredTypeInstances: []policy.RequiredTypeInstanceToInject{ { - RequiredTypeInstanceReference: policy.RequiredTypeInstanceReference{ + TypeInstanceReference: policy.TypeInstanceReference{ ID: "1314-142-123", Description: ptr.String("Sample TI"), - }, - TypeRef: &types.ManifestRef{ - Path: "cap.type.gcp.auth.service-account", + TypeRef: &types.TypeRef{ + Path: "cap.type.gcp.auth.service-account", + }, }, }, }, @@ -252,7 +252,7 @@ func TestPolicyEnforcedClient_mergePolicies(t *testing.T) { name: "action first then global for the same interface", action: policy.ActionPolicy{ Interface: policy.InterfacePolicy{ - Rules: policy.RulesList{ + Rules: policy.InterfaceRulesList{ policy.RulesForInterface{ Interface: types.ManifestRefWithOptRevision{ Path: interfacePath, @@ -276,12 +276,12 @@ func TestPolicyEnforcedClient_mergePolicies(t *testing.T) { }, RequiredTypeInstances: []policy.RequiredTypeInstanceToInject{ { - RequiredTypeInstanceReference: policy.RequiredTypeInstanceReference{ + TypeInstanceReference: policy.TypeInstanceReference{ ID: "1314-142-123-111", Description: ptr.String("Sample TI"), - }, - TypeRef: &types.ManifestRef{ - Path: "cap.type.gcp.auth.service-account", + TypeRef: &types.TypeRef{ + Path: "cap.type.gcp.auth.service-account", + }, }, }, }, @@ -306,7 +306,7 @@ func TestPolicyEnforcedClient_mergePolicies(t *testing.T) { }, global: policy.Policy{ Interface: policy.InterfacePolicy{ - Rules: policy.RulesList{ + Rules: policy.InterfaceRulesList{ policy.RulesForInterface{ Interface: types.ManifestRefWithOptRevision{ Path: interfacePath, @@ -330,12 +330,12 @@ func TestPolicyEnforcedClient_mergePolicies(t *testing.T) { }, RequiredTypeInstances: []policy.RequiredTypeInstanceToInject{ { - RequiredTypeInstanceReference: policy.RequiredTypeInstanceReference{ + TypeInstanceReference: policy.TypeInstanceReference{ ID: "1314-142-123-222", Description: ptr.String("Sample TI"), - }, - TypeRef: &types.ManifestRef{ - Path: "cap.type.gcp.auth.service-account", + TypeRef: &types.TypeRef{ + Path: "cap.type.gcp.auth.service-account", + }, }, }, }, @@ -365,7 +365,7 @@ func TestPolicyEnforcedClient_mergePolicies(t *testing.T) { }, expected: policy.Policy{ Interface: policy.InterfacePolicy{ - Rules: policy.RulesList{ + Rules: policy.InterfaceRulesList{ policy.RulesForInterface{ Interface: types.ManifestRefWithOptRevision{ Path: interfacePath, @@ -390,12 +390,12 @@ func TestPolicyEnforcedClient_mergePolicies(t *testing.T) { }, RequiredTypeInstances: []policy.RequiredTypeInstanceToInject{ { - RequiredTypeInstanceReference: policy.RequiredTypeInstanceReference{ + TypeInstanceReference: policy.TypeInstanceReference{ ID: "1314-142-123-111", Description: ptr.String("Sample TI"), - }, - TypeRef: &types.ManifestRef{ - Path: "cap.type.gcp.auth.service-account", + TypeRef: &types.TypeRef{ + Path: "cap.type.gcp.auth.service-account", + }, }, }, }, @@ -429,7 +429,7 @@ func TestPolicyEnforcedClient_mergePolicies(t *testing.T) { name: "action first then global for different interfaces - only rules", action: policy.ActionPolicy{ Interface: policy.InterfacePolicy{ - Rules: policy.RulesList{ + Rules: policy.InterfaceRulesList{ policy.RulesForInterface{ Interface: types.ManifestRefWithOptRevision{ Path: interfacePath, @@ -447,7 +447,7 @@ func TestPolicyEnforcedClient_mergePolicies(t *testing.T) { }, global: policy.Policy{ Interface: policy.InterfacePolicy{ - Rules: policy.RulesList{ + Rules: policy.InterfaceRulesList{ policy.RulesForInterface{ Interface: types.ManifestRefWithOptRevision{ Path: secondInterfacePath, @@ -465,7 +465,7 @@ func TestPolicyEnforcedClient_mergePolicies(t *testing.T) { }, expected: policy.Policy{ Interface: policy.InterfacePolicy{ - Rules: policy.RulesList{ + Rules: policy.InterfaceRulesList{ policy.RulesForInterface{ Interface: types.ManifestRefWithOptRevision{ Path: interfacePath, @@ -499,7 +499,7 @@ func TestPolicyEnforcedClient_mergePolicies(t *testing.T) { name: "merge type instances and additional input", action: policy.ActionPolicy{ Interface: policy.InterfacePolicy{ - Rules: policy.RulesList{ + Rules: policy.InterfaceRulesList{ policy.RulesForInterface{ Interface: types.ManifestRefWithOptRevision{ Path: interfacePath, @@ -522,12 +522,12 @@ func TestPolicyEnforcedClient_mergePolicies(t *testing.T) { }, RequiredTypeInstances: []policy.RequiredTypeInstanceToInject{ { - RequiredTypeInstanceReference: policy.RequiredTypeInstanceReference{ + TypeInstanceReference: policy.TypeInstanceReference{ ID: "1314-142-123-111", Description: ptr.String("Sample TI"), - }, - TypeRef: &types.ManifestRef{ - Path: "cap.type.gcp.auth.service-account", + TypeRef: &types.TypeRef{ + Path: "cap.type.gcp.auth.service-account", + }, }, }, }, @@ -552,7 +552,7 @@ func TestPolicyEnforcedClient_mergePolicies(t *testing.T) { }, global: policy.Policy{ Interface: policy.InterfacePolicy{ - Rules: policy.RulesList{ + Rules: policy.InterfaceRulesList{ policy.RulesForInterface{ Interface: types.ManifestRefWithOptRevision{ Path: interfacePath, @@ -565,12 +565,12 @@ func TestPolicyEnforcedClient_mergePolicies(t *testing.T) { Inject: &policy.InjectData{ RequiredTypeInstances: []policy.RequiredTypeInstanceToInject{ { - RequiredTypeInstanceReference: policy.RequiredTypeInstanceReference{ + TypeInstanceReference: policy.TypeInstanceReference{ ID: "123-321-123", Description: ptr.String("Sample TI"), - }, - TypeRef: &types.ManifestRef{ - Path: "cap.type.x", + TypeRef: &types.TypeRef{ + Path: "cap.type.x", + }, }, }, }, @@ -595,7 +595,7 @@ func TestPolicyEnforcedClient_mergePolicies(t *testing.T) { }, expected: policy.Policy{ Interface: policy.InterfacePolicy{ - Rules: policy.RulesList{ + Rules: policy.InterfaceRulesList{ policy.RulesForInterface{ Interface: types.ManifestRefWithOptRevision{ Path: interfacePath, @@ -608,21 +608,21 @@ func TestPolicyEnforcedClient_mergePolicies(t *testing.T) { Inject: &policy.InjectData{ RequiredTypeInstances: []policy.RequiredTypeInstanceToInject{ { - RequiredTypeInstanceReference: policy.RequiredTypeInstanceReference{ + TypeInstanceReference: policy.TypeInstanceReference{ ID: "123-321-123", Description: ptr.String("Sample TI"), - }, - TypeRef: &types.ManifestRef{ - Path: "cap.type.x", + TypeRef: &types.TypeRef{ + Path: "cap.type.x", + }, }, }, { - RequiredTypeInstanceReference: policy.RequiredTypeInstanceReference{ + TypeInstanceReference: policy.TypeInstanceReference{ ID: "1314-142-123-111", Description: ptr.String("Sample TI"), - }, - TypeRef: &types.ManifestRef{ - Path: "cap.type.gcp.auth.service-account", + TypeRef: &types.TypeRef{ + Path: "cap.type.gcp.auth.service-account", + }, }, }, }, @@ -683,6 +683,384 @@ func TestPolicyEnforcedClient_mergePolicies(t *testing.T) { } } +func TestPolicyEnforcedClient_mergeTypeInstancePolicies(t *testing.T) { + tests := []struct { + name string + global policy.Policy + action policy.ActionPolicy + expected policy.Policy + order policy.MergeOrder + }{ + { + name: "only global policy", + global: policy.Policy{ + TypeInstance: policy.TypeInstancePolicy{ + Rules: []policy.RulesForTypeInstance{ + { + TypeRef: types.ManifestRefWithOptRevision{ + Path: "cap.type.aws.auth.credentials", + Revision: ptr.String("0.1.0"), + }, + Backend: policy.TypeInstanceBackend{ + TypeInstanceReference: policy.TypeInstanceReference{ + ID: "00fd161c-01bd-47a6-9872-47490e11f996", + Description: ptr.String("Vault TI"), + }, + }, + }, + { + TypeRef: types.ManifestRefWithOptRevision{ + Path: "cap.type.aws.*", + }, + Backend: policy.TypeInstanceBackend{ + TypeInstanceReference: policy.TypeInstanceReference{ + ID: "31bb8355-10d7-49ce-a739-4554d8a40b63", + }, + }, + }, + { + TypeRef: types.ManifestRefWithOptRevision{ + Path: "cap.*", + }, + Backend: policy.TypeInstanceBackend{ + TypeInstanceReference: policy.TypeInstanceReference{ + ID: "a36ed738-dfe7-45ec-acd1-8e44e8db893b", + Description: ptr.String("Default Capact PostgreSQL backend"), + }, + }, + }, + }, + }, + }, + action: policy.ActionPolicy{}, + expected: policy.Policy{ + TypeInstance: policy.TypeInstancePolicy{ + Rules: []policy.RulesForTypeInstance{ + { + TypeRef: types.ManifestRefWithOptRevision{ + Path: "cap.type.aws.auth.credentials", + Revision: ptr.String("0.1.0"), + }, + Backend: policy.TypeInstanceBackend{ + TypeInstanceReference: policy.TypeInstanceReference{ + ID: "00fd161c-01bd-47a6-9872-47490e11f996", + Description: ptr.String("Vault TI"), + }, + }, + }, + { + TypeRef: types.ManifestRefWithOptRevision{ + Path: "cap.type.aws.*", + }, + Backend: policy.TypeInstanceBackend{ + TypeInstanceReference: policy.TypeInstanceReference{ + ID: "31bb8355-10d7-49ce-a739-4554d8a40b63", + }, + }, + }, + { + TypeRef: types.ManifestRefWithOptRevision{ + Path: "cap.*", + }, + Backend: policy.TypeInstanceBackend{ + TypeInstanceReference: policy.TypeInstanceReference{ + ID: "a36ed738-dfe7-45ec-acd1-8e44e8db893b", + Description: ptr.String("Default Capact PostgreSQL backend"), + }, + }, + }, + }, + }, + }, + order: policy.MergeOrder{policy.Action, policy.Global}, + }, + { + name: "only action policy", + action: policy.ActionPolicy{ + TypeInstance: policy.TypeInstancePolicy{ + Rules: []policy.RulesForTypeInstance{ + { + TypeRef: types.ManifestRefWithOptRevision{ + Path: "cap.type.aws.auth.credentials", + Revision: ptr.String("0.1.0"), + }, + Backend: policy.TypeInstanceBackend{ + TypeInstanceReference: policy.TypeInstanceReference{ + ID: "00fd161c-01bd-47a6-9872-47490e11f996", + Description: ptr.String("Vault TI"), + }, + }, + }, + { + TypeRef: types.ManifestRefWithOptRevision{ + Path: "cap.type.aws.*", + }, + Backend: policy.TypeInstanceBackend{ + TypeInstanceReference: policy.TypeInstanceReference{ + ID: "31bb8355-10d7-49ce-a739-4554d8a40b63", + }, + }, + }, + { + TypeRef: types.ManifestRefWithOptRevision{ + Path: "cap.*", + }, + Backend: policy.TypeInstanceBackend{ + TypeInstanceReference: policy.TypeInstanceReference{ + ID: "a36ed738-dfe7-45ec-acd1-8e44e8db893b", + Description: ptr.String("Default Capact PostgreSQL backend"), + }, + }, + }, + }, + }, + }, + global: policy.Policy{}, + expected: policy.Policy{ + TypeInstance: policy.TypeInstancePolicy{ + Rules: []policy.RulesForTypeInstance{ + { + TypeRef: types.ManifestRefWithOptRevision{ + Path: "cap.type.aws.auth.credentials", + Revision: ptr.String("0.1.0"), + }, + Backend: policy.TypeInstanceBackend{ + TypeInstanceReference: policy.TypeInstanceReference{ + ID: "00fd161c-01bd-47a6-9872-47490e11f996", + Description: ptr.String("Vault TI"), + }, + }, + }, + { + TypeRef: types.ManifestRefWithOptRevision{ + Path: "cap.type.aws.*", + }, + Backend: policy.TypeInstanceBackend{ + TypeInstanceReference: policy.TypeInstanceReference{ + ID: "31bb8355-10d7-49ce-a739-4554d8a40b63", + }, + }, + }, + { + TypeRef: types.ManifestRefWithOptRevision{ + Path: "cap.*", + }, + Backend: policy.TypeInstanceBackend{ + TypeInstanceReference: policy.TypeInstanceReference{ + ID: "a36ed738-dfe7-45ec-acd1-8e44e8db893b", + Description: ptr.String("Default Capact PostgreSQL backend"), + }, + }, + }, + }, + }, + }, + order: policy.MergeOrder{policy.Action, policy.Global}, + }, + { + name: "action first then global for the same Types", + action: policy.ActionPolicy{ + TypeInstance: policy.TypeInstancePolicy{ + Rules: []policy.RulesForTypeInstance{ + { + TypeRef: types.ManifestRefWithOptRevision{ + Path: "cap.type.aws.auth.credentials", + Revision: ptr.String("0.1.0"), + }, + Backend: policy.TypeInstanceBackend{ + TypeInstanceReference: policy.TypeInstanceReference{ + ID: "00fd161c-01bd-47a6-9872-47490e11f996", + Description: ptr.String("Vault TI"), + }, + }, + }, + { + TypeRef: types.ManifestRefWithOptRevision{ + Path: "cap.type.aws.*", + }, + Backend: policy.TypeInstanceBackend{ + TypeInstanceReference: policy.TypeInstanceReference{ + ID: "31bb8355-10d7-49ce-a739-4554d8a40b63", + }, + }, + }, + { + TypeRef: types.ManifestRefWithOptRevision{ + Path: "cap.*", + }, + Backend: policy.TypeInstanceBackend{ + TypeInstanceReference: policy.TypeInstanceReference{ + ID: "a36ed738-dfe7-45ec-acd1-8e44e8db893b", + Description: ptr.String("Default Capact PostgreSQL backend"), + }, + }, + }, + }, + }, + }, + global: policy.Policy{ + TypeInstance: policy.TypeInstancePolicy{ + Rules: []policy.RulesForTypeInstance{ + { + TypeRef: types.ManifestRefWithOptRevision{ + Path: "cap.type.aws.auth.credentials", + Revision: ptr.String("0.1.0"), + }, + Backend: policy.TypeInstanceBackend{ + TypeInstanceReference: policy.TypeInstanceReference{ + ID: "1234-1234-1234-1234-1234", + Description: ptr.String("Vault TI"), + }, + }, + }, + }, + }, + }, + expected: policy.Policy{ + TypeInstance: policy.TypeInstancePolicy{ + Rules: []policy.RulesForTypeInstance{ + { + TypeRef: types.ManifestRefWithOptRevision{ + Path: "cap.type.aws.auth.credentials", + Revision: ptr.String("0.1.0"), + }, + Backend: policy.TypeInstanceBackend{ + TypeInstanceReference: policy.TypeInstanceReference{ + ID: "1234-1234-1234-1234-1234", + Description: ptr.String("Vault TI"), + }, + }, + }, + { + TypeRef: types.ManifestRefWithOptRevision{ + Path: "cap.type.aws.*", + }, + Backend: policy.TypeInstanceBackend{ + TypeInstanceReference: policy.TypeInstanceReference{ + ID: "31bb8355-10d7-49ce-a739-4554d8a40b63", + }, + }, + }, + { + TypeRef: types.ManifestRefWithOptRevision{ + Path: "cap.*", + }, + Backend: policy.TypeInstanceBackend{ + TypeInstanceReference: policy.TypeInstanceReference{ + ID: "a36ed738-dfe7-45ec-acd1-8e44e8db893b", + Description: ptr.String("Default Capact PostgreSQL backend"), + }, + }, + }, + }, + }, + }, + order: policy.MergeOrder{policy.Action, policy.Global}, + }, + { + name: "action first then global for different Types", + action: policy.ActionPolicy{ + TypeInstance: policy.TypeInstancePolicy{ + Rules: []policy.RulesForTypeInstance{ + { + TypeRef: types.ManifestRefWithOptRevision{ + Path: "cap.type.aws.auth.credentials", + Revision: ptr.String("0.1.0"), + }, + Backend: policy.TypeInstanceBackend{ + TypeInstanceReference: policy.TypeInstanceReference{ + ID: "00fd161c-01bd-47a6-9872-47490e11f996", + Description: ptr.String("Vault TI"), + }, + }, + }, + { + TypeRef: types.ManifestRefWithOptRevision{ + Path: "cap.*", + }, + Backend: policy.TypeInstanceBackend{ + TypeInstanceReference: policy.TypeInstanceReference{ + ID: "a36ed738-dfe7-45ec-acd1-8e44e8db893b", + Description: ptr.String("Default Capact PostgreSQL backend"), + }, + }, + }, + }, + }, + }, + global: policy.Policy{ + TypeInstance: policy.TypeInstancePolicy{ + Rules: []policy.RulesForTypeInstance{ + { + TypeRef: types.ManifestRefWithOptRevision{ + Path: "cap.type.aws.*", + }, + Backend: policy.TypeInstanceBackend{ + TypeInstanceReference: policy.TypeInstanceReference{ + ID: "31bb8355-10d7-49ce-a739-4554d8a40b63", + }, + }, + }, + }, + }, + }, + expected: policy.Policy{ + TypeInstance: policy.TypeInstancePolicy{ + Rules: []policy.RulesForTypeInstance{ + { + TypeRef: types.ManifestRefWithOptRevision{ + Path: "cap.type.aws.auth.credentials", + Revision: ptr.String("0.1.0"), + }, + Backend: policy.TypeInstanceBackend{ + TypeInstanceReference: policy.TypeInstanceReference{ + ID: "00fd161c-01bd-47a6-9872-47490e11f996", + Description: ptr.String("Vault TI"), + }, + }, + }, + { + TypeRef: types.ManifestRefWithOptRevision{ + Path: "cap.*", + }, + Backend: policy.TypeInstanceBackend{ + TypeInstanceReference: policy.TypeInstanceReference{ + ID: "a36ed738-dfe7-45ec-acd1-8e44e8db893b", + Description: ptr.String("Default Capact PostgreSQL backend"), + }, + }, + }, + { + TypeRef: types.ManifestRefWithOptRevision{ + Path: "cap.type.aws.*", + }, + Backend: policy.TypeInstanceBackend{ + TypeInstanceReference: policy.TypeInstanceReference{ + ID: "31bb8355-10d7-49ce-a739-4554d8a40b63", + }, + }, + }, + }, + }, + }, + order: policy.MergeOrder{policy.Action, policy.Global}, + }, + } + for _, test := range tests { + tt := test + t.Run(tt.name, func(t *testing.T) { + // given + cli := client.NewPolicyEnforcedClient(nil, nil) + cli.SetPolicyOrder(tt.order) + cli.SetGlobalPolicy(tt.global) + cli.SetActionPolicy(tt.action) + + // expect + assert.Equal(t, tt.expected, cli.Policy()) + }) + } +} + func TestNestedWorkflowPolicy(t *testing.T) { w1 := workflowPolicyWithAdditionalInput(map[string]interface{}{"a": 1}) w2 := workflowPolicyWithAdditionalInput(map[string]interface{}{"a": 2, "b": 3}) diff --git a/pkg/hub/client/public/facade/types/additional_refs.go b/pkg/hub/client/public/facade/types/additional_refs.go new file mode 100644 index 000000000..95959f120 --- /dev/null +++ b/pkg/hub/client/public/facade/types/additional_refs.go @@ -0,0 +1,72 @@ +package types + +import ( + "context" + + "capact.io/capact/internal/ptr" + "capact.io/capact/internal/regexutil" + gqlpublicapi "capact.io/capact/pkg/hub/api/graphql/public" + "capact.io/capact/pkg/hub/client/public" + "capact.io/capact/pkg/sdk/apis/0.0.1/types" + + "github.com/pkg/errors" +) + +// listAdditionalRefsFields defines preset for response fields required for collection Type's spec.additionalRefs +const listAdditionalRefsFields = public.TypeRevisionRootFields | public.TypeRevisionSpecAdditionalRefsField + +// ListAdditionalRefsClient defines external Hub calls used by ListAdditionalRefs. +type ListAdditionalRefsClient interface { + ListTypes(ctx context.Context, opts ...public.TypeOption) ([]*gqlpublicapi.Type, error) +} + +// ListAdditionalRefsOutput holds Type's spec.additionalRefs entry indexed by TypeRef key. +type ListAdditionalRefsOutput map[types.TypeRef][]string + +// ListAdditionalRefs knows how to fetch Type's spec.additionalRefs entries for all revisions for all given Types in a bit efficient way: +// - uses OR to get all Types in a single call +// - requests only required fields to prevent over-fetching +// - uses map type to give O(1) access to returned responses. +func ListAdditionalRefs(ctx context.Context, cli ListAdditionalRefsClient, reqTypes []types.TypeRef) (ListAdditionalRefsOutput, error) { + filter := regexutil.OrStringSlice(mapToPaths(reqTypes)) + + opts := []public.TypeOption{ + public.WithTypeRevisions(listAdditionalRefsFields), + public.WithTypeFilter(gqlpublicapi.TypeFilter{ + PathPattern: ptr.String(filter), + }), + } + + res, err := cli.ListTypes(ctx, opts...) + if err != nil { + return ListAdditionalRefsOutput{}, errors.Wrap(err, "while fetching Types' additionalRefs for all revisions") + } + + out := ListAdditionalRefsOutput{} + for _, item := range res { + if item == nil { + continue + } + for _, rev := range item.Revisions { + if rev.Spec == nil { + continue + } + out[types.TypeRef{ + Path: item.Path, + Revision: rev.Revision, + }] = rev.Spec.AdditionalRefs + } + } + + return out, nil +} + +func mapToPaths(in []types.TypeRef) []string { + var paths []string + + for _, expType := range in { + paths = append(paths, expType.Path) + } + + return paths +} diff --git a/pkg/sdk/apis/0.0.1/types/types.extend.go b/pkg/sdk/apis/0.0.1/types/types.extend.go index 0947f85b9..7f7c197ff 100644 --- a/pkg/sdk/apis/0.0.1/types/types.extend.go +++ b/pkg/sdk/apis/0.0.1/types/types.extend.go @@ -1,10 +1,17 @@ // Package types holds manually added types. package types -import "fmt" +import ( + "fmt" + "strings" +) -// OCFPathPrefix defines path prefix that all OCF manifest must have. -const OCFPathPrefix = "cap." +const ( + // OCFPathPrefix defines path prefix that all OCF manifest must have. + OCFPathPrefix = "cap." + // HubBackendParentNodeName define parent path for the core hub storage. + HubBackendParentNodeName = "cap.core.type.hub.storage" +) // InterfaceRef holds the full path and revision to the Interface type InterfaceRef ManifestRefWithOptRevision @@ -84,3 +91,14 @@ type ManifestMetadata struct { OCFVersion OCFVersion `yaml:"ocfVersion"` Kind ManifestKind `yaml:"kind"` } + +// TrimLastNodeFromOCFPath removes the last node name from the given OCF path. +// For example, for `cap.core.type.examples.name` returns `cap.core.type.examples`. +func TrimLastNodeFromOCFPath(in string) string { + idx := strings.LastIndex(in, ".") + if idx == -1 { + return in + } + + return in[:idx] +} diff --git a/pkg/sdk/apis/0.0.1/types/types.extend_test.go b/pkg/sdk/apis/0.0.1/types/types.extend_test.go new file mode 100644 index 000000000..7ddaae9bc --- /dev/null +++ b/pkg/sdk/apis/0.0.1/types/types.extend_test.go @@ -0,0 +1,33 @@ +package types + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestTrimLastNodeFromOCFPath(t *testing.T) { + tests := map[string]struct { + givenPath string + expPath string + }{ + "Should remove last node name": { + givenPath: "cap.core.type.examples.name", + expPath: "cap.core.type.examples", + }, + "Should trim trailing separator": { + givenPath: "cap.", + expPath: "cap", + }, + "Should return given path if no separator detected": { + givenPath: "cap", + expPath: "cap", + }, + } + for tn, tc := range tests { + t.Run(tn, func(t *testing.T) { + gotPath := TrimLastNodeFromOCFPath(tc.givenPath) + assert.Equal(t, tc.expPath, gotPath) + }) + } +} diff --git a/pkg/sdk/renderer/argo/dedicated_renderer.go b/pkg/sdk/renderer/argo/dedicated_renderer.go index d89669ec0..ae98f6c12 100644 --- a/pkg/sdk/renderer/argo/dedicated_renderer.go +++ b/pkg/sdk/renderer/argo/dedicated_renderer.go @@ -10,14 +10,15 @@ import ( "capact.io/capact/internal/ctxutil" "capact.io/capact/internal/k8s-engine/graphql/domain/action" - "capact.io/capact/internal/ptr" + "capact.io/capact/pkg/engine/k8s/policy" hubpublicapi "capact.io/capact/pkg/hub/api/graphql/public" "capact.io/capact/pkg/sdk/apis/0.0.1/types" "github.com/Knetic/govaluate" wfv1 "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1" "github.com/pkg/errors" + "go.uber.org/zap" apiv1 "k8s.io/api/core/v1" ) @@ -45,6 +46,7 @@ type dedicatedRenderer struct { typeInstancesToOutput *OutputTypeInstances typeInstancesToUpdate UpdateTypeInstances registeredOutputTypeInstanceNames []*string + log *zap.Logger } // InputArtifact is an Argo artifact with a reference to a Capact TypeInstance. @@ -54,8 +56,9 @@ type InputArtifact struct { typeInstanceReference *string } -func newDedicatedRenderer(maxDepth int, policyEnforcedCli PolicyEnforcedHubClient, typeInstanceHandler *TypeInstanceHandler, opts ...RendererOption) *dedicatedRenderer { +func newDedicatedRenderer(log *zap.Logger, maxDepth int, policyEnforcedCli PolicyEnforcedHubClient, typeInstanceHandler *TypeInstanceHandler, opts ...RendererOption) *dedicatedRenderer { r := &dedicatedRenderer{ + log: log, maxDepth: maxDepth, policyEnforcedCli: policyEnforcedCli, typeInstanceHandler: typeInstanceHandler, @@ -167,12 +170,20 @@ func (r *dedicatedRenderer) GetRootTemplates() []*Template { return r.processedTemplates } +// RootImplementation represents a root implementation against which steps are rendered. +// It's used to access the imports and requires sections used by steps. +// - The `capact-action` is selected based on `spec.imports[*].alias`. +// - The `capact-outputTypeInstances[*].backend` is selected based on `spec.requires[].alias`. +type RootImplementation struct { + Revision hubpublicapi.ImplementationRevision + Rule policy.Rule +} + // TODO Refactor it. It's too long // 1. Split it to smaller functions and leave only high level steps here // 2. Do not use global state, calling it multiple times seems not to work //nolint:gocyclo // This legacy function is complex but the team too busy to simplify it -func (r *dedicatedRenderer) RenderTemplateSteps(ctx context.Context, workflow *Workflow, importsCollection []*hubpublicapi.ImplementationImport, - typeInstances []types.InputTypeInstanceRef, prefix string) (map[string]*string, error) { +func (r *dedicatedRenderer) RenderTemplateSteps(ctx context.Context, workflow *Workflow, rootimpl RootImplementation, typeInstances []types.InputTypeInstanceRef, prefix string) (map[string]*string, error) { r.currentIteration++ if ctxutil.ShouldExit(ctx) { @@ -183,7 +194,15 @@ func (r *dedicatedRenderer) RenderTemplateSteps(ctx context.Context, workflow *W return nil, NewMaxDepthError(r.maxDepth) } - outputTypeInstances := map[string]*string{} + var ( + outputTypeInstances = map[string]*string{} + // importsCollection describes collection from the Implementation manifest for which we + // render steps which may uses `capact-action`. Because of the `alias` logic + // between `Implementation.spec.imports` and `Implementation.spec.action.args.workflow.templates[*].steps.capact-action` + // we need it. + // If we are in context of `capact-action` the new Implementation is selected which satisfy a given import Interface. + importsCollection = rootimpl.Revision.Spec.Imports + ) for _, tpl := range workflow.Templates { // 0. Aggregate processed templates @@ -318,7 +337,7 @@ func (r *dedicatedRenderer) RenderTemplateSteps(ctx context.Context, workflow *W r.InjectAdditionalInput(step, additionalParameters) for k, v := range newArtifactMappings { - artifactMappings[k] = v + artifactMappings[k] = v.Name } step.Template = importedWorkflow.Entrypoint @@ -329,13 +348,17 @@ func (r *dedicatedRenderer) RenderTemplateSteps(ctx context.Context, workflow *W // 3.9 Add TypeInstances to the upload graph inputArtifacts := r.tplInputArguments[step.Template] - if err := r.addOutputTypeInstancesToGraph(step, workflowPrefix, iface, &implementation, inputArtifacts); err != nil { + typeInstancesBackends, err := r.policyEnforcedCli.ListTypeInstancesBackendsBasedOnPolicy(ctx, rootimpl.Rule, rootimpl.Revision) + if err != nil { + return nil, errors.Wrap(err, "while resolving TypeInstance Backend based on Policy") + } + if err := r.addOutputTypeInstancesToGraph(step, workflowPrefix, iface, &implementation, inputArtifacts, typeInstancesBackends, newArtifactMappings); err != nil { return nil, errors.Wrap(err, "while adding TypeInstances to graph") } // 3.10 Render imported Workflow templates and add them to root templates // TODO(advanced-rendering): currently not supported. - actionOutputTypeInstances, err := r.RenderTemplateSteps(ctx, importedWorkflow, implementation.Spec.Imports, nil, workflowPrefix) + actionOutputTypeInstances, err := r.RenderTemplateSteps(ctx, importedWorkflow, RootImplementation{Revision: implementation, Rule: rule}, nil, workflowPrefix) if err != nil { return nil, err } @@ -395,7 +418,12 @@ func (r *dedicatedRenderer) ResolveRunnerInterface(impl hubpublicapi.Implementat return fullRef.Path, nil } -func (r *dedicatedRenderer) UnmarshalWorkflowFromImplementation(prefix string, implementation *hubpublicapi.ImplementationRevision) (*Workflow, map[string]string, error) { +type artefactNameWithBackend struct { + Name string + Backend *string +} + +func (r *dedicatedRenderer) UnmarshalWorkflowFromImplementation(prefix string, implementation *hubpublicapi.ImplementationRevision) (*Workflow, map[string]artefactNameWithBackend, error) { workflow, err := r.createWorkflow(implementation) if err != nil { return nil, nil, errors.Wrap(err, "while unmarshaling Argo Workflow from OCF Implementation") @@ -403,7 +431,7 @@ func (r *dedicatedRenderer) UnmarshalWorkflowFromImplementation(prefix string, i if workflow == nil || workflow.WorkflowSpec == nil || workflow.Entrypoint == "" { return nil, nil, errors.New("workflow and its entrypoint cannot be empty") } - artifactsNameMapping := map[string]string{} + artifactsNameMapping := map[string]artefactNameWithBackend{} for i := range workflow.Templates { tmpl := workflow.Templates[i] @@ -420,7 +448,9 @@ func (r *dedicatedRenderer) UnmarshalWorkflowFromImplementation(prefix string, i } newName := addPrefix(prefix, artifact.GlobalName) - artifactsNameMapping[artifact.GlobalName] = newName + artifactsNameMapping[artifact.GlobalName] = artefactNameWithBackend{ + Name: newName, + } artifact.GlobalName = newName } } @@ -436,17 +466,24 @@ func (r *dedicatedRenderer) UnmarshalWorkflowFromImplementation(prefix string, i step.Template = addPrefix(prefix, step.Template) } - typeInstances := make([]TypeInstanceDefinition, 0, len(step.CapactTypeInstanceOutputs)+len(step.CapactTypeInstanceUpdates)) + typeInstances := make([]CapactTypeInstanceOutputs, 0, len(step.CapactTypeInstanceOutputs)+len(step.CapactTypeInstanceUpdates)) typeInstances = append(typeInstances, step.CapactTypeInstanceOutputs...) - typeInstances = append(typeInstances, step.CapactTypeInstanceUpdates...) + for _, item := range step.CapactTypeInstanceUpdates { + typeInstances = append(typeInstances, CapactTypeInstanceOutputs{ + TypeInstanceDefinition: item, + }) + } for _, ti := range typeInstances { - tiStep, template, artifactMappings := r.getOutputTypeInstanceTemplate(step, ti, prefix) + tiStep, template, artifactMappings := r.getOutputTypeInstanceTemplate(step, ti.TypeInstanceDefinition, prefix) workflow.Templates = append(workflow.Templates, &template) tmpl.Steps = append(tmpl.Steps, ParallelSteps{&tiStep}) for k, v := range artifactMappings { - artifactsNameMapping[k] = v + artifactsNameMapping[k] = artefactNameWithBackend{ + Name: v, + Backend: ti.Backend, + } } } } @@ -927,17 +964,22 @@ func (r *dedicatedRenderer) registerTemplateInputArguments(step *WorkflowStep, a r.tplInputArguments[step.Template] = inputArtifacts } -func (r *dedicatedRenderer) addOutputTypeInstancesToGraph(step *WorkflowStep, prefix string, iface *hubpublicapi.InterfaceRevision, impl *hubpublicapi.ImplementationRevision, inputArtifacts []InputArtifact) error { +func (r *dedicatedRenderer) addOutputTypeInstancesToGraph(step *WorkflowStep, prefix string, iface *hubpublicapi.InterfaceRevision, impl *hubpublicapi.ImplementationRevision, inputArtifacts []InputArtifact, backends policy.TypeInstanceBackendCollection, mappings map[string]artefactNameWithBackend) error { artifactNamesMap := map[string]*string{} for _, artifact := range inputArtifacts { artifactNamesMap[artifact.artifact.Name] = artifact.typeInstanceReference } for _, item := range impl.Spec.OutputTypeInstanceRelations { - name := item.TypeInstanceName + var ( + name = item.TypeInstanceName + stepOutputBackendAlias = "" + ) + if step != nil { // we have to track the renaming based on capact-outputTypeInstances and prefix it if output := findOutputTypeInstance(step, item.TypeInstanceName); output != nil { + stepOutputBackendAlias = ptr.StringPtrToString(output.Backend) name = addPrefix(prefix, output.From) r.tryReplaceTypeInstanceName(output.Name, name) } else { @@ -955,16 +997,40 @@ func (r *dedicatedRenderer) addOutputTypeInstancesToGraph(step *WorkflowStep, pr artifactName := r.addTypeInstanceName(name) artifactNamesMap[item.TypeInstanceName] = artifactName + // select backend + upperLayerStepOutputBackendAlias := ptr.StringPtrToString(mappings[name].Backend) + backendAlias, err := r.selectBackendAlias(upperLayerStepOutputBackendAlias, stepOutputBackendAlias) + if err != nil { + return errors.Wrapf(err, "while resolving backend alias for %s", step.Name) + } + + backend, err := r.selectBackend(backendAlias, typeRef, backends) + if err != nil { + return errors.Wrapf(err, "while resolving backend ID for %s", step.Name) + } + + log := r.log.With(zap.String("artifactName", *artifactName)) + log.Debug("Selected TypeInstance Backend", zap.Any("backend", backend), zap.Any("backends", backends.GetAll())) + + // add output r.typeInstancesToOutput.typeInstances = append(r.typeInstancesToOutput.typeInstances, OutputTypeInstance{ ArtifactName: artifactName, + Backend: backend, TypeInstance: types.OutputTypeInstance{ - TypeRef: &types.TypeRef{ - Path: typeRef.Path, - Revision: typeRef.Revision, - }, + TypeRef: &typeRef, }, }) + // setup uses + + // with backend + // TODO(storage): isn't it better to move it to Local Hub? IMO YES. + r.typeInstancesToOutput.relations = append(r.typeInstancesToOutput.relations, OutputTypeInstanceRelation{ + From: artifactName, + To: ptr.String(backend.ID), + }) + + // with other artifacts for _, uses := range item.Uses { usesArtifactName, ok := artifactNamesMap[uses] if !ok { @@ -981,6 +1047,33 @@ func (r *dedicatedRenderer) addOutputTypeInstancesToGraph(step *WorkflowStep, pr return nil } +func (*dedicatedRenderer) selectBackendAlias(upperStep, resolvedStep string) (string, error) { + if upperStep != "" && resolvedStep != "" { + return "", errors.Errorf("cannot override backend on capact-outputTypeInstances") + } + if upperStep != "" { + return upperStep, nil + } + return resolvedStep, nil +} + +func (*dedicatedRenderer) selectBackend(alias string, typeRef types.TypeRef, backends policy.TypeInstanceBackendCollection) (policy.TypeInstanceBackend, error) { + if alias == "" { // alias not set, get the cluster defaults + return backends.GetByTypeRef(typeRef), nil + } + + // when alias is specified, required TypeInstance needs to be injected + backend, found := backends.GetByAlias(alias) + if !found { + return policy.TypeInstanceBackend{}, fmt.Errorf("cannot find backend storage for specified %s alias", alias) + } + if !backend.ExtendsHubStorage { + return policy.TypeInstanceBackend{}, fmt.Errorf("TypeInstance with %q alias is not a Hub storage", alias) + } + + return backend, nil +} + func (r *dedicatedRenderer) registerUpdatedTypeInstances(step *WorkflowStep, availableTypeInstances map[argoArtifactRef]*string, prefix string) error { for _, update := range step.CapactTypeInstanceUpdates { typeInstance, ok := availableTypeInstances[argoArtifactRef{ diff --git a/pkg/sdk/renderer/argo/dedicated_renderer_test.go b/pkg/sdk/renderer/argo/dedicated_renderer_test.go index cc0278687..636a5fddb 100644 --- a/pkg/sdk/renderer/argo/dedicated_renderer_test.go +++ b/pkg/sdk/renderer/argo/dedicated_renderer_test.go @@ -3,9 +3,11 @@ package argo import ( "testing" + "capact.io/capact/internal/logger" hubclient "capact.io/capact/pkg/hub/client" "capact.io/capact/pkg/hub/client/fake" policyvalidation "capact.io/capact/pkg/sdk/validation/policy" + "github.com/stretchr/testify/require" ) @@ -22,7 +24,7 @@ func createFakeDedicatedRendererObject(t *testing.T) *dedicatedRenderer { opts := []RendererOption{} maxDepth := 20 - return newDedicatedRenderer(maxDepth, policyEnforcedClient, typeInstanceHandler, opts...) + return newDedicatedRenderer(logger.Noop(), maxDepth, policyEnforcedClient, typeInstanceHandler, opts...) } func TestCapactWhenContainDashes(t *testing.T) { diff --git a/pkg/sdk/renderer/argo/fixtures_test.go b/pkg/sdk/renderer/argo/fixtures_test.go index 2fe1f1261..5b7f62c99 100644 --- a/pkg/sdk/renderer/argo/fixtures_test.go +++ b/pkg/sdk/renderer/argo/fixtures_test.go @@ -9,7 +9,7 @@ import ( func fixGCPGlobalPolicy() policy.Policy { return policy.Policy{ Interface: policy.InterfacePolicy{ - Rules: policy.RulesList{ + Rules: policy.InterfaceRulesList{ { Interface: types.ManifestRefWithOptRevision{ Path: "cap.interface.database.postgresql.install", @@ -33,7 +33,7 @@ func fixGCPGlobalPolicy() policy.Policy { Inject: &policy.InjectData{ RequiredTypeInstances: []policy.RequiredTypeInstanceToInject{ { - RequiredTypeInstanceReference: policy.RequiredTypeInstanceReference{ + TypeInstanceReference: policy.TypeInstanceReference{ ID: "c268d3f5-8834-434b-bea2-b677793611c5", Description: ptr.String("GCP SA"), }, @@ -66,7 +66,7 @@ func fixGCPGlobalPolicy() policy.Policy { func fixAWSGlobalPolicy(additionalParameters ...policy.AdditionalParametersToInject) policy.Policy { return policy.Policy{ Interface: policy.InterfacePolicy{ - Rules: policy.RulesList{ + Rules: policy.InterfaceRulesList{ { Interface: types.ManifestRefWithOptRevision{ Path: "cap.interface.database.postgresql.install", @@ -90,7 +90,7 @@ func fixAWSGlobalPolicy(additionalParameters ...policy.AdditionalParametersToInj Inject: &policy.InjectData{ RequiredTypeInstances: []policy.RequiredTypeInstanceToInject{ { - RequiredTypeInstanceReference: policy.RequiredTypeInstanceReference{ + TypeInstanceReference: policy.TypeInstanceReference{ ID: "517cf827-233c-4bf1-8fc9-48534424dd58", Description: ptr.String("AWS Credentials"), }, @@ -119,7 +119,7 @@ func fixAWSGlobalPolicy(additionalParameters ...policy.AdditionalParametersToInj func fixGlobalPolicyForFallback() policy.Policy { return policy.Policy{ Interface: policy.InterfacePolicy{ - Rules: policy.RulesList{ + Rules: policy.InterfaceRulesList{ { Interface: types.ManifestRefWithOptRevision{ Path: "cap.interface.database.postgresql.install", @@ -138,7 +138,7 @@ func fixGlobalPolicyForFallback() policy.Policy { Inject: &policy.InjectData{ RequiredTypeInstances: []policy.RequiredTypeInstanceToInject{ { - RequiredTypeInstanceReference: policy.RequiredTypeInstanceReference{ + TypeInstanceReference: policy.TypeInstanceReference{ ID: "517cf827-233c-4bf1-8fc9-48534424dd58", Description: ptr.String("AWS Credentials"), }, @@ -187,7 +187,7 @@ func fixGlobalPolicyForFallback() policy.Policy { func fixTerraformPolicy() policy.Policy { return policy.Policy{ Interface: policy.InterfacePolicy{ - Rules: policy.RulesList{ + Rules: policy.InterfaceRulesList{ { Interface: types.ManifestRefWithOptRevision{ Path: "cap.interface.database.postgresql.install", @@ -201,7 +201,7 @@ func fixTerraformPolicy() policy.Policy { Inject: &policy.InjectData{ RequiredTypeInstances: []policy.RequiredTypeInstanceToInject{ { - RequiredTypeInstanceReference: policy.RequiredTypeInstanceReference{ + TypeInstanceReference: policy.TypeInstanceReference{ ID: "c268d3f5-8834-434b-bea2-b677793611c5", Description: ptr.String("GCP SA"), }, @@ -229,7 +229,7 @@ func fixTerraformPolicy() policy.Policy { func fixAWSRDSPolicy() policy.Policy { return policy.Policy{ Interface: policy.InterfacePolicy{ - Rules: policy.RulesList{ + Rules: policy.InterfaceRulesList{ { Interface: types.ManifestRefWithOptRevision{ Path: "cap.interface.database.postgresql.install", @@ -243,7 +243,7 @@ func fixAWSRDSPolicy() policy.Policy { Inject: &policy.InjectData{ RequiredTypeInstances: []policy.RequiredTypeInstanceToInject{ { - RequiredTypeInstanceReference: policy.RequiredTypeInstanceReference{ + TypeInstanceReference: policy.TypeInstanceReference{ ID: "517cf827-233c-4bf1-8fc9-48534424dd58", Description: ptr.String("AWS SA"), }, @@ -279,7 +279,7 @@ func fixAWSRDSPolicy() policy.Policy { func fixExistingDBPolicy() policy.Policy { return policy.Policy{ Interface: policy.InterfacePolicy{ - Rules: policy.RulesList{ + Rules: policy.InterfaceRulesList{ { Interface: types.ManifestRefWithOptRevision{ Path: "cap.interface.productivity.mattermost.install", diff --git a/pkg/sdk/renderer/argo/helpers.go b/pkg/sdk/renderer/argo/helpers.go index 8298f8ee0..503880236 100644 --- a/pkg/sdk/renderer/argo/helpers.go +++ b/pkg/sdk/renderer/argo/helpers.go @@ -34,9 +34,9 @@ func GetEntrypointWorkflowIndex(w *Workflow) (int, error) { return 0, NewEntrypointWorkflowIndexNotFoundError(w.Entrypoint) } -func findTypeInstanceTypeRef(typeInstanceName string, impl *hubpublicgraphql.ImplementationRevision, iface *hubpublicgraphql.InterfaceRevision) (*hubpublicgraphql.TypeReference, error) { +func findTypeInstanceTypeRef(typeInstanceName string, impl *hubpublicgraphql.ImplementationRevision, iface *hubpublicgraphql.InterfaceRevision) (types.TypeRef, error) { if iface == nil { - return nil, NewTypeReferenceNotFoundError(typeInstanceName) + return types.TypeRef{}, NewTypeReferenceNotFoundError(typeInstanceName) } var toSearch []*hubpublicgraphql.OutputTypeInstance @@ -51,15 +51,15 @@ func findTypeInstanceTypeRef(typeInstanceName string, impl *hubpublicgraphql.Imp for i := range toSearch { ti := toSearch[i] - if ti.Name == typeInstanceName { - return ti.TypeRef, nil + if ti.Name == typeInstanceName && ti.TypeRef != nil { + return types.TypeRef(*ti.TypeRef), nil } } - return nil, NewTypeReferenceNotFoundError(typeInstanceName) + return types.TypeRef{}, NewTypeReferenceNotFoundError(typeInstanceName) } -func findOutputTypeInstance(step *WorkflowStep, typeInstanceName string) *TypeInstanceDefinition { +func findOutputTypeInstance(step *WorkflowStep, typeInstanceName string) *CapactTypeInstanceOutputs { for _, output := range step.CapactTypeInstanceOutputs { if output.From == typeInstanceName { return &output diff --git a/pkg/sdk/renderer/argo/renderer.go b/pkg/sdk/renderer/argo/renderer.go index 831728fdd..9484586d3 100644 --- a/pkg/sdk/renderer/argo/renderer.go +++ b/pkg/sdk/renderer/argo/renderer.go @@ -5,6 +5,8 @@ import ( "encoding/json" "time" + "go.uber.org/zap" + "capact.io/capact/pkg/engine/k8s/policy" hubpublicapi "capact.io/capact/pkg/hub/api/graphql/public" hubclient "capact.io/capact/pkg/hub/client" @@ -27,6 +29,7 @@ type PolicyEnforcedHubClient interface { ListRequiredTypeInstancesToInjectBasedOnPolicy(policyRule policy.Rule, implRev hubpublicapi.ImplementationRevision) ([]types.InputTypeInstanceRef, error) ListAdditionalTypeInstancesToInjectBasedOnPolicy(policyRule policy.Rule, implRev hubpublicapi.ImplementationRevision) ([]types.InputTypeInstanceRef, error) ListAdditionalInputToInjectBasedOnPolicy(ctx context.Context, policyRule policy.Rule, implRev hubpublicapi.ImplementationRevision) (types.ParametersCollection, error) + ListTypeInstancesBackendsBasedOnPolicy(ctx context.Context, policyRule policy.Rule, implRev hubpublicapi.ImplementationRevision) (policy.TypeInstanceBackendCollection, error) SetGlobalPolicy(policy policy.Policy) SetActionPolicy(policy policy.ActionPolicy) PushWorkflowStepPolicy(policy policy.WorkflowPolicy) error @@ -48,16 +51,18 @@ type Renderer struct { typeInstanceHandler *TypeInstanceHandler wfValidator workflowValidator hubClient hubclient.HubClient + log *zap.Logger } // NewRenderer returns a new Renderer instance. -func NewRenderer(cfg renderer.Config, hubClient hubclient.HubClient, typeInstanceHandler *TypeInstanceHandler, validator workflowValidator) *Renderer { +func NewRenderer(log *zap.Logger, cfg renderer.Config, hubClient hubclient.HubClient, typeInstanceHandler *TypeInstanceHandler, validator workflowValidator) *Renderer { r := &Renderer{ typeInstanceHandler: typeInstanceHandler, maxDepth: cfg.MaxDepth, renderTimeout: cfg.RenderTimeout, hubClient: hubClient, wfValidator: validator, + log: log, } return r @@ -73,7 +78,7 @@ func (r *Renderer) Render(ctx context.Context, input *RenderInput) (*RenderOutpu policyEnforcedClient := hubclient.NewPolicyEnforcedClient(r.hubClient, r.wfValidator.PolicyValidator()) // 0. Populate render options - dedicatedRenderer := newDedicatedRenderer(r.maxDepth, policyEnforcedClient, r.typeInstanceHandler, input.Options...) + dedicatedRenderer := newDedicatedRenderer(r.log, r.maxDepth, policyEnforcedClient, r.typeInstanceHandler, input.Options...) ctxWithTimeout, cancel := context.WithTimeout(ctx, r.renderTimeout) defer cancel() @@ -110,7 +115,7 @@ func (r *Renderer) Render(ctx context.Context, input *RenderInput) (*RenderOutpu } // 3. Extract workflow from the root Implementation - rootWorkflow, _, err := dedicatedRenderer.UnmarshalWorkflowFromImplementation("", &implementation) + rootWorkflow, newArtifactMappings, err := dedicatedRenderer.UnmarshalWorkflowFromImplementation("", &implementation) if err != nil { return nil, errors.Wrap(err, "while creating root workflow") } @@ -179,12 +184,22 @@ func (r *Renderer) Render(ctx context.Context, input *RenderInput) (*RenderOutpu availableArtifacts := dedicatedRenderer.tplInputArguments[dedicatedRenderer.entrypointStep.Template] // 9. Register output TypeInstances - if err := dedicatedRenderer.addOutputTypeInstancesToGraph(nil, "", iface, &implementation, availableArtifacts); err != nil { + typeInstancesBackends, err := policyEnforcedClient.ListTypeInstancesBackendsBasedOnPolicy(ctx, rule, implementation) + if err != nil { + return nil, errors.Wrap(err, "while resolving TypeInstance backend based on Policy") + } + + r.log.Debug("AAaaaaaaAaaaaaaAaaaaaaAaaaaaaAaaaaaaAaaaaaaAaaaaaaAaaaaaaAaaaaaaAaaaaaaAaaaaaaAaaaaaaAaaaaaa") + if err := dedicatedRenderer.addOutputTypeInstancesToGraph(nil, "", iface, &implementation, availableArtifacts, typeInstancesBackends, newArtifactMappings); err != nil { return nil, errors.Wrap(err, "while noting output artifacts") } + r.log.Debug("AAaaaaaaAaaaaaaAaaaaaaAaaaaaaAaaaaaaAaaaaaaAaaaaaaAaaaaaaAaaaaaaAaaaaaaAaaaaaaAaaaaaaAaaaaaa") // 10. Render rootWorkflow templates - _, err = dedicatedRenderer.RenderTemplateSteps(ctxWithTimeout, rootWorkflow, implementation.Spec.Imports, dedicatedRenderer.inputTypeInstances, "") + _, err = dedicatedRenderer.RenderTemplateSteps(ctxWithTimeout, rootWorkflow, RootImplementation{ + Revision: implementation, + Rule: rule, + }, dedicatedRenderer.inputTypeInstances, "") if err != nil { return nil, err } diff --git a/pkg/sdk/renderer/argo/renderer_test.go b/pkg/sdk/renderer/argo/renderer_test.go index 7ec1a71f7..2bc5983a5 100644 --- a/pkg/sdk/renderer/argo/renderer_test.go +++ b/pkg/sdk/renderer/argo/renderer_test.go @@ -7,13 +7,13 @@ import ( "testing" "time" - actionvalidation "capact.io/capact/pkg/sdk/validation/interfaceio" - policyvalidation "capact.io/capact/pkg/sdk/validation/policy" - + "capact.io/capact/internal/logger" "capact.io/capact/pkg/engine/k8s/policy" "capact.io/capact/pkg/hub/client/fake" "capact.io/capact/pkg/sdk/apis/0.0.1/types" "capact.io/capact/pkg/sdk/renderer" + actionvalidation "capact.io/capact/pkg/sdk/validation/interfaceio" + policyvalidation "capact.io/capact/pkg/sdk/validation/policy" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -44,8 +44,8 @@ func TestRenderHappyPath(t *testing.T) { interfaceIOValidator := actionvalidation.NewValidator(fakeCli) policyIOValidator := policyvalidation.NewValidator(fakeCli) wfValidator := renderer.NewWorkflowInputValidator(interfaceIOValidator, policyIOValidator) - - argoRenderer := NewRenderer(renderer.Config{ + l, _ := logger.New(logger.Config{DevMode: true}) + argoRenderer := NewRenderer(l, renderer.Config{ RenderTimeout: time.Second, MaxDepth: 20, }, fakeCli, typeInstanceHandler, wfValidator) @@ -301,7 +301,7 @@ func TestRenderHappyPathWithCustomPolicies(t *testing.T) { policyIOValidator := policyvalidation.NewValidator(fakeCli) wfValidator := renderer.NewWorkflowInputValidator(interfaceIOValidator, policyIOValidator) - argoRenderer := NewRenderer(renderer.Config{ + argoRenderer := NewRenderer(logger.Noop(), renderer.Config{ RenderTimeout: time.Hour, MaxDepth: 50, }, fakeCli, typeInstanceHandler, wfValidator) @@ -348,7 +348,7 @@ func TestRendererMaxDepth(t *testing.T) { policyIOValidator := policyvalidation.NewValidator(fakeCli) wfValidator := renderer.NewWorkflowInputValidator(interfaceIOValidator, policyIOValidator) - argoRenderer := NewRenderer(renderer.Config{ + argoRenderer := NewRenderer(logger.Noop(), renderer.Config{ RenderTimeout: time.Second, MaxDepth: 3, }, fakeCli, typeInstanceHandler, wfValidator) @@ -387,7 +387,7 @@ func TestRendererDenyAllPolicy(t *testing.T) { policyIOValidator := policyvalidation.NewValidator(fakeCli) wfValidator := renderer.NewWorkflowInputValidator(interfaceIOValidator, policyIOValidator) - argoRenderer := NewRenderer(renderer.Config{ + argoRenderer := NewRenderer(logger.Noop(), renderer.Config{ RenderTimeout: time.Second, MaxDepth: 3, }, fakeCli, typeInstanceHandler, wfValidator) diff --git a/pkg/sdk/renderer/argo/typeinstance_handler.go b/pkg/sdk/renderer/argo/typeinstance_handler.go index 43644d8ad..92ea1194b 100644 --- a/pkg/sdk/renderer/argo/typeinstance_handler.go +++ b/pkg/sdk/renderer/argo/typeinstance_handler.go @@ -5,11 +5,12 @@ import ( "path" "strings" - "github.com/google/uuid" - + "capact.io/capact/pkg/engine/k8s/policy" graphqllocal "capact.io/capact/pkg/hub/api/graphql/local" "capact.io/capact/pkg/sdk/apis/0.0.1/types" + wfv1 "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1" + "github.com/google/uuid" apiv1 "k8s.io/api/core/v1" "sigs.k8s.io/yaml" ) @@ -104,6 +105,7 @@ type OutputTypeInstanceRelation struct { type OutputTypeInstance struct { ArtifactName *string TypeInstance types.OutputTypeInstance + Backend policy.TypeInstanceBackend } // OutputTypeInstances holds information about the output TypeInstances @@ -143,6 +145,10 @@ func (r *TypeInstanceHandler) AddUploadTypeInstancesStep(rootWorkflow *Workflow, Revision: ti.TypeInstance.TypeRef.Revision, }, Attributes: []*graphqllocal.AttributeReferenceInput{}, + Backend: &graphqllocal.TypeInstanceBackendInput{ + // TODO: remove this and instead + ID: ti.Backend.ID, + }, }) artifacts = append(artifacts, wfv1.Artifact{ @@ -157,6 +163,7 @@ func (r *TypeInstanceHandler) AddUploadTypeInstancesStep(rootWorkflow *Workflow, } for _, relation := range output.relations { + // TODO(https://github.com/capactio/capact/issues/604): add relations to used Backend TypeInstance. payload.UsesRelations = append(payload.UsesRelations, &graphqllocal.TypeInstanceUsesRelationInput{ From: *relation.From, To: *relation.To, diff --git a/pkg/sdk/renderer/argo/types.go b/pkg/sdk/renderer/argo/types.go index 0b970d467..21858d9e1 100644 --- a/pkg/sdk/renderer/argo/types.go +++ b/pkg/sdk/renderer/argo/types.go @@ -29,16 +29,22 @@ type ParallelSteps []*WorkflowStep // It extends the Argo WorkflowStep and adds Capact specific properties. type WorkflowStep struct { *wfv1.WorkflowStep - CapactWhen *string `json:"capact-when,omitempty"` - CapactAction *string `json:"capact-action,omitempty"` - CapactPolicy *policy.WorkflowPolicy `json:"capact-policy,omitempty"` - CapactTypeInstanceOutputs []TypeInstanceDefinition `json:"capact-outputTypeInstances,omitempty"` - CapactTypeInstanceUpdates []TypeInstanceDefinition `json:"capact-updateTypeInstances,omitempty"` + CapactWhen *string `json:"capact-when,omitempty"` + CapactAction *string `json:"capact-action,omitempty"` + CapactPolicy *policy.WorkflowPolicy `json:"capact-policy,omitempty"` + CapactTypeInstanceOutputs []CapactTypeInstanceOutputs `json:"capact-outputTypeInstances,omitempty"` + CapactTypeInstanceUpdates []TypeInstanceDefinition `json:"capact-updateTypeInstances,omitempty"` // internal fields typeInstanceOutputs map[string]*string } +// CapactTypeInstanceOutputs holds data defined for `capact-outputTypeInstances` field on step level. +type CapactTypeInstanceOutputs struct { + TypeInstanceDefinition `json:",inline"` + Backend *string `json:"backend,omitempty"` +} + // TypeInstanceDefinition represents a TypeInstance, // which is created in a step. type TypeInstanceDefinition struct { diff --git a/pkg/sdk/validation/manifest/json_remote_implementation.go b/pkg/sdk/validation/manifest/json_remote_implementation.go index 998d7bdb9..b7b923f50 100644 --- a/pkg/sdk/validation/manifest/json_remote_implementation.go +++ b/pkg/sdk/validation/manifest/json_remote_implementation.go @@ -6,10 +6,9 @@ import ( "fmt" "strings" - "capact.io/capact/internal/ptr" - "capact.io/capact/internal/regexutil" gqlpublicapi "capact.io/capact/pkg/hub/api/graphql/public" "capact.io/capact/pkg/hub/client/public" + typesutil "capact.io/capact/pkg/hub/client/public/facade/types" "capact.io/capact/pkg/sdk/apis/0.0.1/types" wfv1 "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1" @@ -20,14 +19,10 @@ import ( "github.com/pkg/errors" ) -const ( - typeListQueryFields = public.TypeRevisionRootFields | public.TypeRevisionSpecAdditionalRefsField -) - // ParentNodesAssociation represents relations between parent node and associated other types. // - key holds the parent node path // - value holds list of associated Types -type ParentNodesAssociation map[string][]types.ManifestRef +type ParentNodesAssociation map[string][]types.TypeRef // RemoteImplementationValidator is a validator for Implementation manifest, which calls Hub in order to do validation checks. type RemoteImplementationValidator struct { @@ -254,7 +249,7 @@ func (v *RemoteImplementationValidator) resolveRequiresPath(parentPrefix string, allReqItems = append(allReqItems, reqItem.AnyOf...) for _, requiresSubItem := range allReqItems { - ref := types.ManifestRef{ + ref := types.TypeRef{ Path: strings.Join([]string{parentPrefix, requiresSubItem.Name}, "."), // default assumption Revision: requiresSubItem.Revision, } @@ -286,30 +281,12 @@ func (v *RemoteImplementationValidator) checkParentNodesAssociation(ctx context. var validationErrs []error for parentNode, expTypesRefs := range relations { - typesPath, expAttachedTypes := v.mapToPathAndPathRevIndex(expTypesRefs) - - filter := regexutil.OrStringSlice(typesPath) - res, err := v.hub.ListTypes(ctx, public.WithTypeRevisions(typeListQueryFields), public.WithTypeFilter(gqlpublicapi.TypeFilter{ - PathPattern: ptr.String(filter), - })) + gotAttachedTypes, err := typesutil.ListAdditionalRefs(ctx, v.hub, expTypesRefs) if err != nil { return ValidationResult{}, errors.Wrap(err, "while fetching Types based on parent node") } - gotAttachedTypes := map[string][]string{} - for _, item := range res { - if item == nil { - continue - } - for _, rev := range item.Revisions { - if rev.Spec == nil { - continue - } - gotAttachedTypes[v.key(item.Path, rev.Revision)] = rev.Spec.AdditionalRefs - } - } - - missingEntries := v.detectMissingChildren(gotAttachedTypes, expAttachedTypes, parentNode) + missingEntries := v.detectMissingChildren(gotAttachedTypes, expTypesRefs, parentNode) if len(missingEntries) == 0 { continue } @@ -324,21 +301,7 @@ func (v *RemoteImplementationValidator) checkParentNodesAssociation(ctx context. return ValidationResult{Errors: validationErrs}, nil } -func (v *RemoteImplementationValidator) mapToPathAndPathRevIndex(in []types.ManifestRef) ([]string, []string) { - var ( - paths []string - pathsRevIdx []string - ) - - for _, expType := range in { - paths = append(paths, expType.Path) - pathsRevIdx = append(pathsRevIdx, v.key(expType.Path, expType.Revision)) - } - - return paths, pathsRevIdx -} - -func (v *RemoteImplementationValidator) detectMissingChildren(gotAttachedTypes map[string][]string, expAttachedTypes []string, expParent string) []string { +func (v *RemoteImplementationValidator) detectMissingChildren(gotAttachedTypes typesutil.ListAdditionalRefsOutput, expAttachedTypes []types.TypeRef, expParent string) []string { var missingChildren []string for _, exp := range expAttachedTypes { @@ -352,7 +315,7 @@ func (v *RemoteImplementationValidator) detectMissingChildren(gotAttachedTypes m continue } - missingChildren = append(missingChildren, fmt.Sprintf("%q", exp)) + missingChildren = append(missingChildren, fmt.Sprintf(`"%s:%s"`, exp.Path, exp.Revision)) } return missingChildren @@ -366,7 +329,3 @@ func (v *RemoteImplementationValidator) stringSliceContains(slice []string, elem } return false } - -func (v *RemoteImplementationValidator) key(a, b string) string { - return fmt.Sprintf("%s:%s", a, b) -} diff --git a/pkg/sdk/validation/policy/fixtures_test.go b/pkg/sdk/validation/policy/fixtures_test.go index f34f2de7c..3e0d952a5 100644 --- a/pkg/sdk/validation/policy/fixtures_test.go +++ b/pkg/sdk/validation/policy/fixtures_test.go @@ -9,7 +9,7 @@ import ( func fixPolicyWithoutTypeRef() policy.Policy { return policy.Policy{ Interface: policy.InterfacePolicy{ - Rules: policy.RulesList{ + Rules: policy.InterfaceRulesList{ { Interface: types.ManifestRefWithOptRevision{ Path: "cap.*", @@ -20,12 +20,12 @@ func fixPolicyWithoutTypeRef() policy.Policy { Inject: &policy.InjectData{ RequiredTypeInstances: []policy.RequiredTypeInstanceToInject{ { - RequiredTypeInstanceReference: policy.RequiredTypeInstanceReference{ + TypeInstanceReference: policy.TypeInstanceReference{ ID: "id", }, }, { - RequiredTypeInstanceReference: policy.RequiredTypeInstanceReference{ + TypeInstanceReference: policy.TypeInstanceReference{ ID: "id2", Description: ptr.String("ID 2"), }, @@ -45,13 +45,108 @@ func fixPolicyWithoutTypeRef() policy.Policy { }, }, }, + TypeInstance: policy.TypeInstancePolicy{ + Rules: []policy.RulesForTypeInstance{ + { + TypeRef: types.ManifestRefWithOptRevision{ + Path: "cap.type.aws.auth.credentials", + Revision: ptr.String("0.1.0"), + }, + Backend: policy.TypeInstanceBackend{ + TypeInstanceReference: policy.TypeInstanceReference{ + ID: "00fd161c-01bd-47a6-9872-47490e11f996", + Description: ptr.String("Vault TI"), + }, + }, + }, + { + TypeRef: types.ManifestRefWithOptRevision{ + Path: "cap.type.aws.*", + Revision: ptr.String("0.1.0"), + }, + Backend: policy.TypeInstanceBackend{ + TypeInstanceReference: policy.TypeInstanceReference{ + ID: "31bb8355-10d7-49ce-a739-4554d8a40b63", + }, + }, + }, + { + TypeRef: types.ManifestRefWithOptRevision{ + Path: "cap.*", + Revision: ptr.String("0.1.0"), + }, + Backend: policy.TypeInstanceBackend{ + TypeInstanceReference: policy.TypeInstanceReference{ + ID: "a36ed738-dfe7-45ec-acd1-8e44e8db893b", + Description: ptr.String("Default Capact PostgreSQL backend"), + }, + }, + }, + }, + }, + } +} + +func fixPolicyWithWrongBackendForTypeRef() policy.Policy { + return policy.Policy{ + TypeInstance: policy.TypeInstancePolicy{ + Rules: []policy.RulesForTypeInstance{ + { + TypeRef: types.ManifestRefWithOptRevision{ + Path: "cap.type.aws.auth.credentials", + Revision: ptr.String("0.1.0"), + }, + Backend: policy.TypeInstanceBackend{ + TypeInstanceReference: policy.TypeInstanceReference{ + TypeRef: &types.TypeRef{ + Path: "cap.type.hashicorp.vault.storage", + Revision: "0.1.0", + }, + ID: "00fd161c-01bd-47a6-9872-47490e11f996", + Description: ptr.String("Vault TI"), + }, + }, + }, + { + TypeRef: types.ManifestRefWithOptRevision{ + Path: "cap.type.aws.*", + Revision: ptr.String("0.1.0"), + }, + Backend: policy.TypeInstanceBackend{ + TypeInstanceReference: policy.TypeInstanceReference{ + TypeRef: &types.TypeRef{ + Path: "cap.type.aws.auth.secret-manager", + Revision: "0.1.0", + }, + ID: "31bb8355-10d7-49ce-a739-4554d8a40b63", + }, + }, + }, + { + TypeRef: types.ManifestRefWithOptRevision{ + Path: "cap.*", + Revision: ptr.String("0.1.0"), + }, + Backend: policy.TypeInstanceBackend{ + TypeInstanceReference: policy.TypeInstanceReference{ + TypeRef: &types.TypeRef{ + Path: "cap.core.type.hub.storage.postresql", + Revision: "0.1.0", + }, + ID: "a36ed738-dfe7-45ec-acd1-8e44e8db893b", + Description: ptr.String("Default Capact PostgreSQL backend"), + }, + }, + }, + }, + }, } } func fixPolicyWithTypeRef() policy.Policy { return policy.Policy{ Interface: policy.InterfacePolicy{ - Rules: policy.RulesList{ + Rules: policy.InterfaceRulesList{ { Interface: types.ManifestRefWithOptRevision{ Path: "cap.*", @@ -62,22 +157,22 @@ func fixPolicyWithTypeRef() policy.Policy { Inject: &policy.InjectData{ RequiredTypeInstances: []policy.RequiredTypeInstanceToInject{ { - RequiredTypeInstanceReference: policy.RequiredTypeInstanceReference{ + TypeInstanceReference: policy.TypeInstanceReference{ ID: "id", - }, - TypeRef: &types.ManifestRef{ - Path: "cap.type.sample", - Revision: "0.1.0", + TypeRef: &types.TypeRef{ + Path: "cap.type.sample", + Revision: "0.1.0", + }, }, }, { - RequiredTypeInstanceReference: policy.RequiredTypeInstanceReference{ + TypeInstanceReference: policy.TypeInstanceReference{ ID: "id2", Description: ptr.String("ID 2"), - }, - TypeRef: &types.ManifestRef{ - Path: "cap.type.sample2", - Revision: "0.2.0", + TypeRef: &types.TypeRef{ + Path: "cap.type.sample2", + Revision: "0.2.0", + }, }, }, }, diff --git a/pkg/sdk/validation/policy/policy.go b/pkg/sdk/validation/policy/policy.go index 0dfd65e54..caa79168b 100644 --- a/pkg/sdk/validation/policy/policy.go +++ b/pkg/sdk/validation/policy/policy.go @@ -49,7 +49,7 @@ func (v *Validator) LoadAdditionalInputParametersSchemas(ctx context.Context, im } // IsTypeRefInjectableAndEqualToImplReq returns boolean value if a given Type reference matches the one in Implementation requirement item and can be injected. -func (v *Validator) IsTypeRefInjectableAndEqualToImplReq(typeRef *types.ManifestRef, reqItem *gqlpublicapi.ImplementationRequirementItem) bool { +func (v *Validator) IsTypeRefInjectableAndEqualToImplReq(typeRef *types.TypeRef, reqItem *gqlpublicapi.ImplementationRequirementItem) bool { // check requirement item valid if reqItem == nil || reqItem.TypeRef == nil || reqItem.Alias == nil { return false @@ -95,8 +95,31 @@ func (v *Validator) ValidateAdditionalInputParameters(ctx context.Context, param // ValidateTypeInstancesMetadata validates that every TypeInstance has metadata resolved. func (v *Validator) ValidateTypeInstancesMetadata(in policy.Policy) validation.Result { + resultBldr := validation.NewResultBuilder("Metadata for") + + unresolvedIDs := map[string]struct{}{} unresolvedTypeInstances := metadata.TypeInstanceIDsWithUnresolvedMetadataForPolicy(in) - return v.validationResultForTIMetadata(unresolvedTypeInstances) + for _, ti := range unresolvedTypeInstances { + resultBldr.ReportIssue(string(ti.Kind), "missing Type reference for %s", ti.String(false)) + unresolvedIDs[ti.ID] = struct{}{} + } + + for _, rule := range in.TypeInstance.Rules { + if _, unresolved := unresolvedIDs[rule.Backend.ID]; unresolved { + continue // Type was not resolved, so `ExtendsHubStorage` has zero value + } + + if rule.Backend.ExtendsHubStorage { + continue + } + ref := metadata.TypeInstanceMetadata{ + ID: rule.Backend.ID, + Description: rule.Backend.Description, + } + resultBldr.ReportIssue("BackendTypeInstance", "Type reference %s is not a Hub storage", ref.String(false)) + } + + return resultBldr.Result() } // ValidateTypeInstancesMetadataForRule validates whether the TypeInstance injection metadata are resolved. diff --git a/pkg/sdk/validation/policy/policy_test.go b/pkg/sdk/validation/policy/policy_test.go index d63917524..9729bab30 100644 --- a/pkg/sdk/validation/policy/policy_test.go +++ b/pkg/sdk/validation/policy/policy_test.go @@ -34,18 +34,34 @@ func TestValidator_ValidateTypeInstancesMetadata(t *testing.T) { Input: fixPolicyWithTypeRef(), }, { - Name: "Invalid", + Name: "Policy Without TypeRefs", Input: fixPolicyWithoutTypeRef(), ExpectedErrMessage: ptr.String( heredoc.Docf(` - Metadata for "AdditionalTypeInstance": * missing Type reference for ID: "id3", name: "id-3" + - Metadata for "BackendTypeInstance": + * missing Type reference for ID: "00fd161c-01bd-47a6-9872-47490e11f996", description: "Vault TI" + * missing Type reference for ID: "31bb8355-10d7-49ce-a739-4554d8a40b63" + * missing Type reference for ID: "a36ed738-dfe7-45ec-acd1-8e44e8db893b", description: "Default Capact PostgreSQL backend" - Metadata for "RequiredTypeInstance": * missing Type reference for ID: "id" * missing Type reference for ID: "id2", description: "ID 2"`, ), ), }, + { + Name: "Policy with wrong backends for Types", + Input: fixPolicyWithWrongBackendForTypeRef(), + ExpectedErrMessage: ptr.String( + heredoc.Docf(` + - Metadata for "BackendTypeInstance": + * Type reference ID: "00fd161c-01bd-47a6-9872-47490e11f996", description: "Vault TI" is not a Hub storage + * Type reference ID: "31bb8355-10d7-49ce-a739-4554d8a40b63" is not a Hub storage + * Type reference ID: "a36ed738-dfe7-45ec-acd1-8e44e8db893b", description: "Default Capact PostgreSQL backend" is not a Hub storage`, + ), + ), + }, } for _, testCase := range tests { @@ -73,7 +89,7 @@ func TestValidator_IsTypeRefInjectableAndEqualToImplReq(t *testing.T) { validator := policyvalidation.NewValidator(nil) tests := []struct { Name string - TypeRef *types.ManifestRef + TypeRef *types.TypeRef ReqItem *gqlpublicapi.ImplementationRequirementItem ExpectedResult bool }{ @@ -91,7 +107,7 @@ func TestValidator_IsTypeRefInjectableAndEqualToImplReq(t *testing.T) { }, { Name: "Empty ReqItem", - TypeRef: &types.ManifestRef{ + TypeRef: &types.TypeRef{ Path: "path", Revision: "revision", }, @@ -100,7 +116,7 @@ func TestValidator_IsTypeRefInjectableAndEqualToImplReq(t *testing.T) { }, { Name: "Different path", - TypeRef: &types.ManifestRef{ + TypeRef: &types.TypeRef{ Path: "path1", Revision: "1.0.0", }, @@ -115,7 +131,7 @@ func TestValidator_IsTypeRefInjectableAndEqualToImplReq(t *testing.T) { }, { Name: "Different revision", - TypeRef: &types.ManifestRef{ + TypeRef: &types.TypeRef{ Path: "path", Revision: "1.0.0", }, @@ -130,7 +146,7 @@ func TestValidator_IsTypeRefInjectableAndEqualToImplReq(t *testing.T) { }, { Name: "Equal but empty alias", - TypeRef: &types.ManifestRef{ + TypeRef: &types.TypeRef{ Path: "path", Revision: "revision", }, @@ -144,7 +160,7 @@ func TestValidator_IsTypeRefInjectableAndEqualToImplReq(t *testing.T) { }, { Name: "Equal", - TypeRef: &types.ManifestRef{ + TypeRef: &types.TypeRef{ Path: "path", Revision: "revision", }, From 98c11a6ed6bbf37fd03c0b0665825b24b9450458 Mon Sep 17 00:00:00 2001 From: Mateusz Szostok Date: Tue, 8 Feb 2022 12:41:55 +0100 Subject: [PATCH 02/15] Remove setting default storage and relations in renderer --- pkg/engine/api/graphql/schema.graphql | 2 +- pkg/engine/api/graphql/schema_gen.go | 2 +- pkg/engine/k8s/policy/type_instance.go | 19 ++++++------------- pkg/engine/k8s/policy/type_instance_test.go | 18 +++++++++++++++++- pkg/hub/client/policy_enforced_client.go | 11 ----------- pkg/sdk/renderer/argo/dedicated_renderer.go | 16 ++++------------ pkg/sdk/renderer/argo/typeinstance_handler.go | 1 - 7 files changed, 29 insertions(+), 40 deletions(-) diff --git a/pkg/engine/api/graphql/schema.graphql b/pkg/engine/api/graphql/schema.graphql index 3885a00db..8852e3d9a 100644 --- a/pkg/engine/api/graphql/schema.graphql +++ b/pkg/engine/api/graphql/schema.graphql @@ -304,7 +304,7 @@ enum ActionStatusPhase { input PolicyInput { interface: InterfacePolicyInput - typeInstance: TypeInstancePolicyInput + typeInstance: TypeInstancePolicyInput } # TypeInstance Policy Input diff --git a/pkg/engine/api/graphql/schema_gen.go b/pkg/engine/api/graphql/schema_gen.go index 5490da85c..8f9d2441d 100644 --- a/pkg/engine/api/graphql/schema_gen.go +++ b/pkg/engine/api/graphql/schema_gen.go @@ -1150,7 +1150,7 @@ enum ActionStatusPhase { input PolicyInput { interface: InterfacePolicyInput - typeInstance: TypeInstancePolicyInput + typeInstance: TypeInstancePolicyInput } # TypeInstance Policy Input diff --git a/pkg/engine/k8s/policy/type_instance.go b/pkg/engine/k8s/policy/type_instance.go index 68452757a..9ebbd72f4 100644 --- a/pkg/engine/k8s/policy/type_instance.go +++ b/pkg/engine/k8s/policy/type_instance.go @@ -31,15 +31,8 @@ type TypeInstanceBackend struct { // TypeInstanceBackendCollection knows which Backend should be used for a given TypeInstance based on the TypeRef. type TypeInstanceBackendCollection struct { - byTypeRef map[string]TypeInstanceBackend - byAlias map[string]TypeInstanceBackend - defaultBackend TypeInstanceBackend -} - -// SetDefault sets the default storage backend. -// TODO(storage): probably not needed anymore. -func (t *TypeInstanceBackendCollection) SetDefault(backend TypeInstanceBackend) { - t.defaultBackend = backend + byTypeRef map[string]TypeInstanceBackend + byAlias map[string]TypeInstanceBackend } // SetByTypeRef associates a given TypeRef with a given storage backend instance. @@ -64,14 +57,14 @@ func (t *TypeInstanceBackendCollection) SetByTypeRef(ref types.ManifestRefWithOp // - cap.* // // If both methods fail, default backend is returned. -func (t TypeInstanceBackendCollection) GetByTypeRef(typeRef types.TypeRef) TypeInstanceBackend { +func (t TypeInstanceBackendCollection) GetByTypeRef(typeRef types.TypeRef) (TypeInstanceBackend, bool) { // 1. Try the explicit TypeRef backend, found := t.byTypeRef[t.key(types.ManifestRefWithOptRevision{ Path: typeRef.Path, Revision: ptr.String(typeRef.Revision), })] if found { - return backend + return backend, true } // 2. Try to find matching pattern for a given TypeRef. @@ -93,13 +86,13 @@ func (t TypeInstanceBackendCollection) GetByTypeRef(typeRef types.TypeRef) TypeI for _, pattern := range keyPatterns { backend, found := t.byTypeRef[pattern] if found { - return backend + return backend, true } } iterations++ } - return t.defaultBackend + return TypeInstanceBackend{}, false } // SetByAlias associates a given alias with a given storage backend instance. diff --git a/pkg/engine/k8s/policy/type_instance_test.go b/pkg/engine/k8s/policy/type_instance_test.go index cd1ab198d..7040d287f 100644 --- a/pkg/engine/k8s/policy/type_instance_test.go +++ b/pkg/engine/k8s/policy/type_instance_test.go @@ -25,12 +25,14 @@ func TestTypeInstanceBackendCollection_GetByTypeRef(t *testing.T) { tests := map[string]struct { givenTypeRef types.TypeRef expBackend policy.TypeInstanceBackend + expFound bool }{ "Should match exact type ref": { givenTypeRef: types.TypeRef{ Path: "cap.type.capactio.examples.message", Revision: "0.1.0", }, + expFound: true, expBackend: fixTypeInstanceBackend("ID1"), }, "Should match pattern cap.type.capactio.examples.*": { @@ -38,6 +40,7 @@ func TestTypeInstanceBackendCollection_GetByTypeRef(t *testing.T) { Path: "cap.type.capactio.examples.other-that-message", Revision: "0.1.0", }, + expFound: true, expBackend: fixTypeInstanceBackend("ID2"), }, "Should match pattern cap.type.capactio.examples.*:0.2.0": { @@ -45,6 +48,7 @@ func TestTypeInstanceBackendCollection_GetByTypeRef(t *testing.T) { Path: "cap.type.capactio.examples.other-that-message", Revision: "0.2.0", }, + expFound: true, expBackend: fixTypeInstanceBackend("ID3"), }, "Should match generic cap.* with revision": { @@ -52,6 +56,7 @@ func TestTypeInstanceBackendCollection_GetByTypeRef(t *testing.T) { Path: "cap.type.aws.examples", Revision: "0.1.0", }, + expFound: true, expBackend: fixTypeInstanceBackend("ID5"), }, "Should match generic cap.* with different revision": { @@ -59,13 +64,24 @@ func TestTypeInstanceBackendCollection_GetByTypeRef(t *testing.T) { Path: "cap.type.aws.examples", Revision: "0.2.0", }, + expFound: true, expBackend: fixTypeInstanceBackend("ID4"), }, + + "Should not found backend for unknown path": { + givenTypeRef: types.TypeRef{ + Path: "some.not.registered.type.ref", + Revision: "0.2.0", + }, + expFound: false, + }, } for tn, tc := range tests { t.Run(tn, func(t *testing.T) { - gotBackend := data.GetByTypeRef(tc.givenTypeRef) + gotBackend, gotFound := data.GetByTypeRef(tc.givenTypeRef) + assert.Equal(t, tc.expBackend, gotBackend) + assert.Equal(t, tc.expFound, gotFound) }) } } diff --git a/pkg/hub/client/policy_enforced_client.go b/pkg/hub/client/policy_enforced_client.go index d37858613..39ffe9f48 100644 --- a/pkg/hub/client/policy_enforced_client.go +++ b/pkg/hub/client/policy_enforced_client.go @@ -5,7 +5,6 @@ import ( "fmt" "sync" - "capact.io/capact/internal/ptr" "capact.io/capact/pkg/engine/k8s/policy" "capact.io/capact/pkg/engine/k8s/policy/metadata" hublocalgraphql "capact.io/capact/pkg/hub/api/graphql/local" @@ -109,16 +108,6 @@ func (e *PolicyEnforcedClient) ListImplementationRevisionForInterface(ctx contex func (e *PolicyEnforcedClient) ListTypeInstancesBackendsBasedOnPolicy(_ context.Context, rule policy.Rule, implRev hubpublicgraphql.ImplementationRevision) (policy.TypeInstanceBackendCollection, error) { out := policy.TypeInstanceBackendCollection{} - // TODO(storage): set default Hub storage - // TODO(storage): but should we really set the default? In this case it's always required on type instance creation. - // if not, it can be empty and Local Hub will save it in own core storage. - out.SetDefault(policy.TypeInstanceBackend{ - TypeInstanceReference: policy.TypeInstanceReference{ - ID: "123123123123", - Description: ptr.String("Default Capact Hub storage"), - }, - }) - // 1. Global Defaults based on TypeRefs for _, rule := range e.mergedPolicy.TypeInstance.Rules { out.SetByTypeRef(rule.TypeRef, rule.Backend) diff --git a/pkg/sdk/renderer/argo/dedicated_renderer.go b/pkg/sdk/renderer/argo/dedicated_renderer.go index ae98f6c12..29bd2f79d 100644 --- a/pkg/sdk/renderer/argo/dedicated_renderer.go +++ b/pkg/sdk/renderer/argo/dedicated_renderer.go @@ -1022,15 +1022,6 @@ func (r *dedicatedRenderer) addOutputTypeInstancesToGraph(step *WorkflowStep, pr }) // setup uses - - // with backend - // TODO(storage): isn't it better to move it to Local Hub? IMO YES. - r.typeInstancesToOutput.relations = append(r.typeInstancesToOutput.relations, OutputTypeInstanceRelation{ - From: artifactName, - To: ptr.String(backend.ID), - }) - - // with other artifacts for _, uses := range item.Uses { usesArtifactName, ok := artifactNamesMap[uses] if !ok { @@ -1058,11 +1049,12 @@ func (*dedicatedRenderer) selectBackendAlias(upperStep, resolvedStep string) (st } func (*dedicatedRenderer) selectBackend(alias string, typeRef types.TypeRef, backends policy.TypeInstanceBackendCollection) (policy.TypeInstanceBackend, error) { - if alias == "" { // alias not set, get the cluster defaults - return backends.GetByTypeRef(typeRef), nil + if alias == "" { // alias not set, get the Policy default based on TypeRef + backend, _ := backends.GetByTypeRef(typeRef) + return backend, nil } - // when alias is specified, required TypeInstance needs to be injected + // when alias is specified, required TypeInstance needs to be injected and be of Hub storage type backend, found := backends.GetByAlias(alias) if !found { return policy.TypeInstanceBackend{}, fmt.Errorf("cannot find backend storage for specified %s alias", alias) diff --git a/pkg/sdk/renderer/argo/typeinstance_handler.go b/pkg/sdk/renderer/argo/typeinstance_handler.go index 92ea1194b..0dd47f4d1 100644 --- a/pkg/sdk/renderer/argo/typeinstance_handler.go +++ b/pkg/sdk/renderer/argo/typeinstance_handler.go @@ -146,7 +146,6 @@ func (r *TypeInstanceHandler) AddUploadTypeInstancesStep(rootWorkflow *Workflow, }, Attributes: []*graphqllocal.AttributeReferenceInput{}, Backend: &graphqllocal.TypeInstanceBackendInput{ - // TODO: remove this and instead ID: ti.Backend.ID, }, }) From 3879e09cb30351f2d281ff1a0458f4c995b53093 Mon Sep 17 00:00:00 2001 From: Mateusz Szostok Date: Tue, 8 Feb 2022 15:35:52 +0100 Subject: [PATCH 03/15] Fix indend in graphql file --- hub-js/graphql/local/schema.graphql | 667 ++++++++++++++-------------- 1 file changed, 333 insertions(+), 334 deletions(-) diff --git a/hub-js/graphql/local/schema.graphql b/hub-js/graphql/local/schema.graphql index c033fe731..f63698b93 100644 --- a/hub-js/graphql/local/schema.graphql +++ b/hub-js/graphql/local/schema.graphql @@ -2,10 +2,10 @@ # To make it work for other graphql client we need to add them to the schema manually, based on: # https://github.com/neo4j-graphql/neo4j-graphql-js/blob/master/src/augment/directives.js directive @relation( - name: String - direction: String - from: String - to: String + name: String + direction: String + from: String + to: String ) on FIELD_DEFINITION | OBJECT directive @cypher(statement: String) on FIELD_DEFINITION @@ -33,69 +33,68 @@ LockOwner defines owner name who locked a given TypeInstance scalar LockOwnerID type TypeInstance { - id: ID! @id - - lockedBy: LockOwnerID - - """ - Common properties for all TypeInstances which cannot be changed - """ - typeRef: TypeInstanceTypeReference! - @relation(name: "OF_TYPE", direction: "OUT") - uses: [TypeInstance!]! @relation(name: "USES", direction: "OUT") - usedBy: [TypeInstance!]! @relation(name: "USES", direction: "IN") - - latestResourceVersion: TypeInstanceResourceVersion - @cypher( - statement: "MATCH (this)-[:CONTAINS]->(tir:TypeInstanceResourceVersion) RETURN tir ORDER BY tir.resourceVersion DESC LIMIT 1" - ) - firstResourceVersion: TypeInstanceResourceVersion - @cypher( - statement: "MATCH (this)-[:CONTAINS]->(tir:TypeInstanceResourceVersion) RETURN tir ORDER BY tir.resourceVersion ASC LIMIT 1" - ) - previousResourceVersion: TypeInstanceResourceVersion - @cypher( - statement: "MATCH (this)-[:CONTAINS]->(tir:TypeInstanceResourceVersion) RETURN tir ORDER BY tir.resourceVersion DESC SKIP 1 LIMIT 1" - ) - resourceVersion(resourceVersion: Int!): TypeInstanceResourceVersion - @cypher( - statement: "MATCH (this)-[:CONTAINS]->(tir:TypeInstanceResourceVersion {resourceVersion: $resourceVersion}) RETURN tir" - ) - resourceVersions: [TypeInstanceResourceVersion!]! - @relation(name: "CONTAINS", direction: "OUT") + id: ID! @id + + lockedBy: LockOwnerID + + """ + Common properties for all TypeInstances which cannot be changed + """ + typeRef: TypeInstanceTypeReference! + @relation(name: "OF_TYPE", direction: "OUT") + uses: [TypeInstance!]! @relation(name: "USES", direction: "OUT") + usedBy: [TypeInstance!]! @relation(name: "USES", direction: "IN") + + latestResourceVersion: TypeInstanceResourceVersion + @cypher( + statement: "MATCH (this)-[:CONTAINS]->(tir:TypeInstanceResourceVersion) RETURN tir ORDER BY tir.resourceVersion DESC LIMIT 1" + ) + firstResourceVersion: TypeInstanceResourceVersion + @cypher( + statement: "MATCH (this)-[:CONTAINS]->(tir:TypeInstanceResourceVersion) RETURN tir ORDER BY tir.resourceVersion ASC LIMIT 1" + ) + previousResourceVersion: TypeInstanceResourceVersion + @cypher( + statement: "MATCH (this)-[:CONTAINS]->(tir:TypeInstanceResourceVersion) RETURN tir ORDER BY tir.resourceVersion DESC SKIP 1 LIMIT 1" + ) + resourceVersion(resourceVersion: Int!): TypeInstanceResourceVersion + @cypher( + statement: "MATCH (this)-[:CONTAINS]->(tir:TypeInstanceResourceVersion {resourceVersion: $resourceVersion}) RETURN tir" + ) + resourceVersions: [TypeInstanceResourceVersion!]! + @relation(name: "CONTAINS", direction: "OUT") } type TypeInstanceResourceVersion { - resourceVersion: Int! @index - createdBy: String - - metadata: TypeInstanceResourceVersionMetadata! - @relation(name: "DESCRIBED_BY", direction: "OUT") - spec: TypeInstanceResourceVersionSpec! - @relation(name: "SPECIFIED_BY", direction: "OUT") - - # TODO: can be changed to TypeInstance, and a proper relation set - backend: TypeInstanceBackend! + resourceVersion: Int! @index + createdBy: String + + metadata: TypeInstanceResourceVersionMetadata! + @relation(name: "DESCRIBED_BY", direction: "OUT") + spec: TypeInstanceResourceVersionSpec! + @relation(name: "SPECIFIED_BY", direction: "OUT") + # TODO: can be changed to TypeInstance, and a proper relation set + backend: TypeInstanceBackend! } type TypeInstanceResourceVersionMetadata { - attributes: [AttributeReference!] - @relation(name: "CHARACTERIZED_BY", direction: "OUT") + attributes: [AttributeReference!] + @relation(name: "CHARACTERIZED_BY", direction: "OUT") } type TypeInstanceResourceVersionSpec { - value: Any! - @cypher( - statement: """ - RETURN apoc.convert.fromJsonMap(this.value) - """ - ) - - """ - CURRENTLY NOT IMPLEMENTED - """ - instrumentation: TypeInstanceInstrumentation - @relation(name: "INSTRUMENTED_WITH", direction: "OUT") + value: Any! + @cypher( + statement: """ + RETURN apoc.convert.fromJsonMap(this.value) + """ + ) + + """ + CURRENTLY NOT IMPLEMENTED + """ + instrumentation: TypeInstanceInstrumentation + @relation(name: "INSTRUMENTED_WITH", direction: "OUT") } type TypeInstanceBackend { @@ -103,385 +102,385 @@ type TypeInstanceBackend { } type TypeInstanceTypeReference { - path: NodePath! - revision: Version! + path: NodePath! + revision: Version! } input AttributeReferenceInput { - path: NodePath! - revision: Version! + path: NodePath! + revision: Version! } type AttributeReference { - path: NodePath! - revision: Version! + path: NodePath! + revision: Version! } """ CURRENTLY NOT IMPLEMENTED """ type TypeInstanceInstrumentation { - metrics: TypeInstanceInstrumentationMetrics - @relation(name: "MEASURED_BY", direction: "OUT") - health: TypeInstanceInstrumentationHealth - @relation(name: "INDICATED_BY", direction: "OUT") + metrics: TypeInstanceInstrumentationMetrics + @relation(name: "MEASURED_BY", direction: "OUT") + health: TypeInstanceInstrumentationHealth + @relation(name: "INDICATED_BY", direction: "OUT") } """ CURRENTLY NOT IMPLEMENTED """ type TypeInstanceInstrumentationMetrics { - endpoint: String - regex: String # optional regex for scraping metrics - dashboards: [TypeInstanceInstrumentationMetricsDashboard!]! - @relation(name: "ON", direction: "OUT") + endpoint: String + regex: String # optional regex for scraping metrics + dashboards: [TypeInstanceInstrumentationMetricsDashboard!]! + @relation(name: "ON", direction: "OUT") } """ CURRENTLY NOT IMPLEMENTED """ type TypeInstanceInstrumentationMetricsDashboard { - url: String! + url: String! } """ CURRENTLY NOT IMPLEMENTED """ type TypeInstanceInstrumentationHealth { - url: String - method: HTTPRequestMethod - - # resolver, which does a HTTP call on a given URL - # and expects status code greater than or equal to 200 - # and less than 400 - # TODO implement TypeInstance health check, for resolution of this field - status: TypeInstanceInstrumentationHealthStatus + url: String + method: HTTPRequestMethod + + # resolver, which does a HTTP call on a given URL + # and expects status code greater than or equal to 200 + # and less than 400 + # TODO implement TypeInstance health check, for resolution of this field + status: TypeInstanceInstrumentationHealthStatus } """ CURRENTLY NOT IMPLEMENTED """ enum TypeInstanceInstrumentationHealthStatus { - UNKNOWN - READY - FAILING + UNKNOWN + READY + FAILING } enum HTTPRequestMethod { - GET - POST + GET + POST } input AttributeFilterInput { - path: NodePath! - rule: FilterRule = INCLUDE + path: NodePath! + rule: FilterRule = INCLUDE - """ - If not provided, any revision of the Attribute applies to this filter - """ - revision: Version + """ + If not provided, any revision of the Attribute applies to this filter + """ + revision: Version } enum FilterRule { - INCLUDE - EXCLUDE + INCLUDE + EXCLUDE } input TypeInstanceFilter { - attributes: [AttributeFilterInput] - typeRef: TypeRefFilterInput - createdBy: String + attributes: [AttributeFilterInput] + typeRef: TypeRefFilterInput + createdBy: String } input TypeRefFilterInput { - path: NodePath! + path: NodePath! - """ - If not provided, it returns TypeInstances for all revisions of given Type - """ - revision: Version + """ + If not provided, it returns TypeInstances for all revisions of given Type + """ + revision: Version } input TypeInstanceTypeReferenceInput { - path: NodePath! - revision: Version! + path: NodePath! + revision: Version! } input TypeInstanceBackendInput { - id: String! + id: String! } input CreateTypeInstanceInput { - """ - Used to define the relationships, between the created TypeInstances - """ - alias: String - - createdBy: String - typeRef: TypeInstanceTypeReferenceInput! - attributes: [AttributeReferenceInput!] - """ - If not provided, TypeInstance value is stored as static value in Local Hub core storage. - """ - backend: TypeInstanceBackendInput - value: Any + """ + Used to define the relationships, between the created TypeInstances + """ + alias: String + + createdBy: String + typeRef: TypeInstanceTypeReferenceInput! + attributes: [AttributeReferenceInput!] + value: Any + """ + If not provided, TypeInstance value is stored as static value in Local Hub core storage. + """ + backend: TypeInstanceBackendInput } input TypeInstanceUsesRelationInput { - """ - Can be existing TypeInstance ID or alias of a TypeInstance from typeInstances list - """ - from: String! - - """ - Can be existing TypeInstance ID or alias of a TypeInstance from typeInstances list - """ - to: String! + """ + Can be existing TypeInstance ID or alias of a TypeInstance from typeInstances list + """ + from: String! + + """ + Can be existing TypeInstance ID or alias of a TypeInstance from typeInstances list + """ + to: String! } input CreateTypeInstancesInput { - typeInstances: [CreateTypeInstanceInput!]! - usesRelations: [TypeInstanceUsesRelationInput!]! + typeInstances: [CreateTypeInstanceInput!]! + usesRelations: [TypeInstanceUsesRelationInput!]! } type CreateTypeInstanceOutput { - id: ID! - alias: String! + id: ID! + alias: String! } """ At least one property needs to be specified. """ input UpdateTypeInstanceInput { - """ - The attributes property is optional. If not provided, previous value is used. - """ - attributes: [AttributeReferenceInput!] - - """ - The value property is optional. If not provided, previous value is used. - """ - value: Any + """ + The attributes property is optional. If not provided, previous value is used. + """ + attributes: [AttributeReferenceInput!] + + """ + The value property is optional. If not provided, previous value is used. + """ + value: Any } input UpdateTypeInstancesInput { - """ - Allows you to update TypeInstances which are locked by a given ownerID. If not provided, - you can update only those TypeInstances which are not locked. - """ - ownerID: LockOwnerID - createdBy: String - - id: ID! - typeInstance: UpdateTypeInstanceInput! + """ + Allows you to update TypeInstances which are locked by a given ownerID. If not provided, + you can update only those TypeInstances which are not locked. + """ + ownerID: LockOwnerID + createdBy: String + + id: ID! + typeInstance: UpdateTypeInstanceInput! } input LockTypeInstancesInput { - ids: [ID!]! - ownerID: LockOwnerID! + ids: [ID!]! + ownerID: LockOwnerID! } input UnlockTypeInstancesInput { - ids: [ID!]! - ownerID: LockOwnerID! + ids: [ID!]! + ownerID: LockOwnerID! } type Query { - typeInstances(filter: TypeInstanceFilter = {}): [TypeInstance!]! - @cypher( - statement: """ - WITH [x IN $filter.attributes WHERE x.rule = "EXCLUDE" | x ] AS excluded, - [x IN $filter.attributes WHERE x.rule = "INCLUDE" | x ] AS included - - CALL { - WITH excluded - UNWIND excluded AS f - MATCH (ex:AttributeReference {path: f.path}) - WHERE (f.revision IS NULL) OR (ex.revision = f.revision) - RETURN collect(ex) as excludedAttributes - } - - MATCH (tir:TypeInstanceResourceVersion)-[:DESCRIBED_BY]->(meta:TypeInstanceResourceVersionMetadata) - OPTIONAL MATCH (meta)-[:CHARACTERIZED_BY]->(attr:AttributeReference) - MATCH (ti:TypeInstance)-[:OF_TYPE]->(typeRef:TypeInstanceTypeReference) - MATCH (ti:TypeInstance)-[:CONTAINS]->(tir) - WHERE - $filter = {} OR - ( - ( - $filter.typeRef IS NULL - OR - ( + typeInstances(filter: TypeInstanceFilter = {}): [TypeInstance!]! + @cypher( + statement: """ + WITH [x IN $filter.attributes WHERE x.rule = "EXCLUDE" | x ] AS excluded, + [x IN $filter.attributes WHERE x.rule = "INCLUDE" | x ] AS included + + CALL { + WITH excluded + UNWIND excluded AS f + MATCH (ex:AttributeReference {path: f.path}) + WHERE (f.revision IS NULL) OR (ex.revision = f.revision) + RETURN collect(ex) as excludedAttributes + } + + MATCH (tir:TypeInstanceResourceVersion)-[:DESCRIBED_BY]->(meta:TypeInstanceResourceVersionMetadata) + OPTIONAL MATCH (meta)-[:CHARACTERIZED_BY]->(attr:AttributeReference) + MATCH (ti:TypeInstance)-[:OF_TYPE]->(typeRef:TypeInstanceTypeReference) + MATCH (ti:TypeInstance)-[:CONTAINS]->(tir) + WHERE + $filter = {} OR + ( + ( + $filter.typeRef IS NULL + OR + ( ($filter.typeRef.revision IS NULL AND typeRef.path = $filter.typeRef.path) OR (typeRef.path = $filter.typeRef.path AND typeRef.revision = $filter.typeRef.revision) - ) - ) - AND - ($filter.createdBy IS NULL OR tir.createdBy = $filter.createdBy) - AND - ( - $filter.attributes IS NULL - OR - ( + ) + ) + AND + ($filter.createdBy IS NULL OR tir.createdBy = $filter.createdBy) + AND + ( + $filter.attributes IS NULL + OR + ( all(inc IN included WHERE - (tir)-[:DESCRIBED_BY]->(meta:TypeInstanceResourceVersionMetadata)-[:CHARACTERIZED_BY]->(attr:AttributeReference {path: inc.path}) - AND - (inc.revision IS NULL OR attr.revision = inc.revision) + (tir)-[:DESCRIBED_BY]->(meta:TypeInstanceResourceVersionMetadata)-[:CHARACTERIZED_BY]->(attr:AttributeReference {path: inc.path}) + AND + (inc.revision IS NULL OR attr.revision = inc.revision) ) AND none(exc IN excludedAttributes WHERE (tir)-[:DESCRIBED_BY]->(meta:TypeInstanceResourceVersionMetadata)-[:CHARACTERIZED_BY]->(exc)) - ) - ) - ) - - RETURN DISTINCT ti - """ - ) - - typeInstance(id: ID!): TypeInstance - @cypher( - statement: """ - MATCH (this:TypeInstance {id: $id}) - RETURN this - """ + ) ) + ) + + RETURN DISTINCT ti + """ + ) + + typeInstance(id: ID!): TypeInstance + @cypher( + statement: """ + MATCH (this:TypeInstance {id: $id}) + RETURN this + """ + ) } type Mutation { - createTypeInstances( - in: CreateTypeInstancesInput! - ): [CreateTypeInstanceOutput!]! - - # TODO extend input with TypeInstanceInstrumentation - createTypeInstance(in: CreateTypeInstanceInput!): TypeInstance! - @cypher( - statement: """ - WITH apoc.convert.toJson($in.value) as value - MERGE (typeRef:TypeInstanceTypeReference {path: $in.typeRef.path, revision: $in.typeRef.revision}) - - CREATE (ti:TypeInstance {id: apoc.create.uuid()}) - CREATE (ti)-[:OF_TYPE]->(typeRef) - - CREATE (tir: TypeInstanceResourceVersion {resourceVersion: 1, createdBy: $in.createdBy}) - CREATE (ti)-[:CONTAINS]->(tir) - - CREATE (tir)-[:DESCRIBED_BY]->(metadata: TypeInstanceResourceVersionMetadata) - CREATE (tir)-[:SPECIFIED_BY]->(spec: TypeInstanceResourceVersionSpec {value: value}) - - FOREACH (attr in $in.attributes | + createTypeInstances( + in: CreateTypeInstancesInput! + ): [CreateTypeInstanceOutput!]! + + # TODO extend input with TypeInstanceInstrumentation + createTypeInstance(in: CreateTypeInstanceInput!): TypeInstance! + @cypher( + statement: """ + WITH apoc.convert.toJson($in.value) as value + MERGE (typeRef:TypeInstanceTypeReference {path: $in.typeRef.path, revision: $in.typeRef.revision}) + + CREATE (ti:TypeInstance {id: apoc.create.uuid()}) + CREATE (ti)-[:OF_TYPE]->(typeRef) + + CREATE (tir: TypeInstanceResourceVersion {resourceVersion: 1, createdBy: $in.createdBy}) + CREATE (ti)-[:CONTAINS]->(tir) + + CREATE (tir)-[:DESCRIBED_BY]->(metadata: TypeInstanceResourceVersionMetadata) + CREATE (tir)-[:SPECIFIED_BY]->(spec: TypeInstanceResourceVersionSpec {value: value}) + + FOREACH (attr in $in.attributes | + MERGE (attrRef: AttributeReference {path: attr.path, revision: attr.revision}) + CREATE (metadata)-[:CHARACTERIZED_BY]->(attrRef) + ) + + RETURN ti + """ + ) + + updateTypeInstances(in: [UpdateTypeInstancesInput]!): [TypeInstance!]! + @cypher( + statement: """ + CALL { + UNWIND $in AS item + RETURN collect(item.id) as allInputIDs + } + + // Check if all TypeInstances were found + WITH * + CALL { + WITH allInputIDs + MATCH (ti:TypeInstance) + WHERE ti.id IN allInputIDs + WITH collect(ti.id) as foundIDs + RETURN foundIDs + } + CALL apoc.util.validate(size(foundIDs) < size(allInputIDs), apoc.convert.toJson({code: 404, ids: foundIDs}), null) + + // Check if given TypeInstances are not already locked by others + WITH * + CALL { + WITH * + UNWIND $in AS item + MATCH (tic:TypeInstance {id: item.id}) + WHERE tic.lockedBy IS NOT NULL AND (item.ownerID IS NULL OR tic.lockedBy <> item.ownerID) + WITH collect(tic.id) as lockedIDs + RETURN lockedIDs + } + CALL apoc.util.validate(size(lockedIDs) > 0, apoc.convert.toJson({code: 409, ids: lockedIDs}), null) + + UNWIND $in as item + MATCH (ti: TypeInstance {id: item.id}) + CALL { + WITH ti + MATCH (ti)-[:CONTAINS]->(latestRevision:TypeInstanceResourceVersion) + RETURN latestRevision + ORDER BY latestRevision.resourceVersion DESC LIMIT 1 + } + + CREATE (tir: TypeInstanceResourceVersion {resourceVersion: latestRevision.resourceVersion + 1, createdBy: item.createdBy}) + CREATE (ti)-[:CONTAINS]->(tir) + + // Handle the `spec.value` property + CREATE (spec: TypeInstanceResourceVersionSpec) + CREATE (tir)-[:SPECIFIED_BY]->(spec) + + WITH ti, tir, spec, latestRevision, item + CALL apoc.do.when( + item.typeInstance.value IS NOT NULL, + ' + SET spec.value = apoc.convert.toJson(item.typeInstance.value) RETURN spec + ', + ' + MATCH (latestRevision)-[:SPECIFIED_BY]->(latestSpec: TypeInstanceResourceVersionSpec) + SET spec.value = latestSpec.value RETURN spec + ', + {spec:spec, latestRevision: latestRevision, item: item}) YIELD value + + // Handle the `metadata.attributes` property + CREATE (metadata: TypeInstanceResourceVersionMetadata) + CREATE (tir)-[:DESCRIBED_BY]->(metadata) + + WITH ti, tir, latestRevision, metadata, item + CALL apoc.do.when( + item.typeInstance.attributes IS NOT NULL, + ' + FOREACH (attr in item.typeInstance.attributes | MERGE (attrRef: AttributeReference {path: attr.path, revision: attr.revision}) CREATE (metadata)-[:CHARACTERIZED_BY]->(attrRef) - ) - - RETURN ti - """ - ) - - updateTypeInstances(in: [UpdateTypeInstancesInput]!): [TypeInstance!]! - @cypher( - statement: """ - CALL { - UNWIND $in AS item - RETURN collect(item.id) as allInputIDs - } - - // Check if all TypeInstances were found - WITH * - CALL { - WITH allInputIDs - MATCH (ti:TypeInstance) - WHERE ti.id IN allInputIDs - WITH collect(ti.id) as foundIDs - RETURN foundIDs - } - CALL apoc.util.validate(size(foundIDs) < size(allInputIDs), apoc.convert.toJson({code: 404, ids: foundIDs}), null) - - // Check if given TypeInstances are not already locked by others - WITH * - CALL { - WITH * - UNWIND $in AS item - MATCH (tic:TypeInstance {id: item.id}) - WHERE tic.lockedBy IS NOT NULL AND (item.ownerID IS NULL OR tic.lockedBy <> item.ownerID) - WITH collect(tic.id) as lockedIDs - RETURN lockedIDs - } - CALL apoc.util.validate(size(lockedIDs) > 0, apoc.convert.toJson({code: 409, ids: lockedIDs}), null) - - UNWIND $in as item - MATCH (ti: TypeInstance {id: item.id}) - CALL { - WITH ti - MATCH (ti)-[:CONTAINS]->(latestRevision:TypeInstanceResourceVersion) - RETURN latestRevision - ORDER BY latestRevision.resourceVersion DESC LIMIT 1 - } - - CREATE (tir: TypeInstanceResourceVersion {resourceVersion: latestRevision.resourceVersion + 1, createdBy: item.createdBy}) - CREATE (ti)-[:CONTAINS]->(tir) - - // Handle the `spec.value` property - CREATE (spec: TypeInstanceResourceVersionSpec) - CREATE (tir)-[:SPECIFIED_BY]->(spec) - - WITH ti, tir, spec, latestRevision, item - CALL apoc.do.when( - item.typeInstance.value IS NOT NULL, - ' - SET spec.value = apoc.convert.toJson(item.typeInstance.value) RETURN spec - ', - ' - MATCH (latestRevision)-[:SPECIFIED_BY]->(latestSpec: TypeInstanceResourceVersionSpec) - SET spec.value = latestSpec.value RETURN spec - ', - {spec:spec, latestRevision: latestRevision, item: item}) YIELD value - - // Handle the `metadata.attributes` property - CREATE (metadata: TypeInstanceResourceVersionMetadata) - CREATE (tir)-[:DESCRIBED_BY]->(metadata) - - WITH ti, tir, latestRevision, metadata, item - CALL apoc.do.when( - item.typeInstance.attributes IS NOT NULL, - ' - FOREACH (attr in item.typeInstance.attributes | - MERGE (attrRef: AttributeReference {path: attr.path, revision: attr.revision}) - CREATE (metadata)-[:CHARACTERIZED_BY]->(attrRef) - ) - - RETURN metadata - ', - ' - OPTIONAL MATCH (latestRevision)-[:DESCRIBED_BY]->(TypeInstanceResourceVersionMetadata)-[:CHARACTERIZED_BY]->(latestAttrRef: AttributeReference) - WHERE latestAttrRef IS NOT NULL - WITH *, COLLECT(latestAttrRef) AS latestAttrRefs - FOREACH (attr in latestAttrRefs | + ) + + RETURN metadata + ', + ' + OPTIONAL MATCH (latestRevision)-[:DESCRIBED_BY]->(TypeInstanceResourceVersionMetadata)-[:CHARACTERIZED_BY]->(latestAttrRef: AttributeReference) + WHERE latestAttrRef IS NOT NULL + WITH *, COLLECT(latestAttrRef) AS latestAttrRefs + FOREACH (attr in latestAttrRefs | CREATE (metadata)-[:CHARACTERIZED_BY]->(attr) - ) - - RETURN metadata - ', - {metadata: metadata, latestRevision: latestRevision, item: item} - ) YIELD value - - RETURN ti - """ - ) - - deleteTypeInstance(id: ID!, ownerID: LockOwnerID): ID! - - """ - Mark given TypeInstances as locked by a given owner. - If at least one TypeInstance is already locked with different OwnerID, an error is returned. - """ - lockTypeInstances(in: LockTypeInstancesInput!): [ID!]! - - """ - Remove lock from given TypeInstances. - If at least one TypeInstance was not locked by a given owner, an error is returned. - """ - unlockTypeInstances(in: UnlockTypeInstancesInput!): [ID!]! + ) + + RETURN metadata + ', + {metadata: metadata, latestRevision: latestRevision, item: item} + ) YIELD value + + RETURN ti + """ + ) + + deleteTypeInstance(id: ID!, ownerID: LockOwnerID): ID! + + """ + Mark given TypeInstances as locked by a given owner. + If at least one TypeInstance is already locked with different OwnerID, an error is returned. + """ + lockTypeInstances(in: LockTypeInstancesInput!): [ID!]! + + """ + Remove lock from given TypeInstances. + If at least one TypeInstance was not locked by a given owner, an error is returned. + """ + unlockTypeInstances(in: UnlockTypeInstancesInput!): [ID!]! } # TODO: Prepare directive for user authorization in https://github.com/capactio/capact/issues/508 From 90c39c9c9fdccf6787f4269d7bb1587a0c779706 Mon Sep 17 00:00:00 2001 From: Mateusz Szostok Date: Fri, 11 Feb 2022 12:18:31 +0100 Subject: [PATCH 04/15] FINALLYYYY! Add e2e test coverage --- .editorconfig | 4 +- .../crds/core.capact.io_actions.yaml | 13 + docs/proposal/20211207-delegated-storage.md | 56 +- hack/lib/utilities.sh | 7 +- hub-js/graphql/local/examples.graphql | 4 + hub-js/graphql/local/schema.graphql | 36 +- hub-js/src/index.ts | 32 +- hub-js/src/schema/local.ts | 82 +- .../k8s-engine/controller/action_service.go | 13 +- .../graphql/domain/action/converter.go | 2 + .../graphql/domain/policy/converter.go | 2 +- .../graphql/domain/policy/fixtures_test.go | 12 +- pkg/engine/api/graphql/models_gen.go | 20 +- pkg/engine/api/graphql/schema.graphql | 50 +- pkg/engine/api/graphql/schema_gen.go | 281 +++++- pkg/engine/client/fields.go | 4 + pkg/engine/k8s/api/v1alpha1/action_types.go | 9 + .../k8s/api/v1alpha1/zz_generated.deepcopy.go | 16 + .../k8s/policy/metadata/metadata_resolver.go | 1 + pkg/hub/api/graphql/local/models_gen.go | 7 +- pkg/hub/api/graphql/local/schema_gen.go | 855 ++++++++++-------- pkg/hub/client/local/fields.go | 17 +- pkg/hub/client/local/options.go | 4 +- pkg/hub/client/policy_enforced_client.go | 1 + pkg/sdk/renderer/argo/renderer.go | 4 - pkg/sdk/renderer/argo/typeinstance_handler.go | 12 +- test/e2e/action_test.go | 286 ++++-- test/e2e/e2e_suite_test.go | 4 +- 28 files changed, 1217 insertions(+), 617 deletions(-) diff --git a/.editorconfig b/.editorconfig index c461e151f..ea98e12f3 100755 --- a/.editorconfig +++ b/.editorconfig @@ -4,8 +4,8 @@ insert_final_newline = true [{*.yml, *.yaml}] indent_size = 2 -[*.graphql] -indent_size = 4 +[{*.graphql, *.js, *.ts}] +indent_size = 2 indent_style = space [Makefile] diff --git a/deploy/kubernetes/crds/core.capact.io_actions.yaml b/deploy/kubernetes/crds/core.capact.io_actions.yaml index 80e89c619..3711a293d 100644 --- a/deploy/kubernetes/crds/core.capact.io_actions.yaml +++ b/deploy/kubernetes/crds/core.capact.io_actions.yaml @@ -272,6 +272,18 @@ spec: description: OutputTypeInstanceDetails describes the output TypeInstance. properties: + backend: + description: Backend contains information in which backend + this TypeInstance is stored. + properties: + abstract: + type: boolean + id: + type: string + required: + - abstract + - id + type: object id: description: ID is a unique identifier of the TypeInstance. type: string @@ -291,6 +303,7 @@ spec: - path type: object required: + - backend - id - typeReference type: object diff --git a/docs/proposal/20211207-delegated-storage.md b/docs/proposal/20211207-delegated-storage.md index 65f1f7061..bedb2b5ed 100644 --- a/docs/proposal/20211207-delegated-storage.md +++ b/docs/proposal/20211207-delegated-storage.md @@ -66,7 +66,7 @@ The main goal is to support the following use-cases: Other backends: - AWS Secrets Manager - Google Key Management - + - Store and retrieve dynamic data - Examples: - Kubernetes cluster (e.g. Flux's Helm releases or Kubernetes Secrets) @@ -126,7 +126,7 @@ Also, the additional, nice-to-have goals are: path: cap.type.helm.storage spec: additionalRefs: - - "cap.core.type.hub.storage" + - "cap.core.type.hub.storage" jsonSchema: value: |- # JSON schema with: { @@ -186,7 +186,7 @@ Also, the additional, nice-to-have goals are: - Register a storage backend by creating such TypeInstance. Regardless the option, at the end there is one TypeInstance produced: - + ```yaml id: 3ef2e4ac-9070-4093-a3ce-142139fd4a16 typeRef: @@ -243,7 +243,7 @@ Also, the additional, nice-to-have goals are: # no url as it is known for Hub (Hub is already connected with PostgreSQL) acceptValue: true contextSchema: null - backend: + backend: abstract: true # Special keyword which specifies the built-in storage option which stores already all other metadata. ``` @@ -253,7 +253,7 @@ Also, the additional, nice-to-have goals are: The property indicates that Hub queries the PostgreSQL database directly. - - It is the default backend for static TypeInstance values. To learn more, read the [Configuring storage backends](#configuring-storage-backends) paragraph. + - It is the default backend for static TypeInstance values. To learn more, read the [Configuring storage backends](#configuring-storage-backends) paragraph. ## Workflow syntax - Create @@ -270,7 +270,7 @@ Also, the additional, nice-to-have goals are: ``` - This workflow cannot be run unless there is a `helm-release` storage backend installed (where `helm-release` is only workflow alias). - - If there are no specific storage backend requirements set, the default backend will be used. To learn more, read the [Configuring default storage backends](#configuring-default-storage-backends) paragraph. + - If there are no specific storage backend requirements set, the default backend will be used. To learn more, read the [Configuring default storage backends](#configuring-default-storage-backends) paragraph. 1. Content Developer outputs one of the following Argo workflow artifacts: @@ -297,7 +297,7 @@ Also, the additional, nice-to-have goals are: However, the `context` are backend-specific properties, which means Content Developer need to explicitly specify the backend as described later. 1. To save a specific value with additional parameters: - + For example, for an implementation of Kubernetes secrets storage backend, which actually creates and updates these secrets during TypeInstance creation: ```yaml @@ -312,8 +312,8 @@ Also, the additional, nice-to-have goals are: The storage backend has to have `contextSchema` specified, as well as the `acceptValue` property set to `true`. In that way, someday we will be able to extend such approach with additional properties: - - ```yaml + + ```yaml instrumentation: # someday - if we want to unify the approach health: endpoint: foo.bar/healthz @@ -329,7 +329,7 @@ Also, the additional, nice-to-have goals are: capact-outputTypeInstances: - name: mattermost-config from: additional - # no backend definition -> use default storage backend + # no backend definition -> stored in built-in Local Hub storage (done by Local Hub) # option 2 - specific backend (defined in `Implementation.spec.requires` property) capact-outputTypeInstances: @@ -381,7 +381,7 @@ Also, the additional, nice-to-have goals are: value: { backend: { id: "3ef2e4ac-9070-4093-a3ce-142139fd4a16", - context: { + context: { ## TODO: support that! currently only ID. name: "release-name", namespace: "release-namespace", } @@ -389,7 +389,7 @@ Also, the additional, nice-to-have goals are: } } ], - usesRelations: [ + usesRelations: [ ## TODO: this relation is created by Local Hub. It always need to be done as the **Uninstalling storage backends** depends on that. { from: "helm-release", to: "3ef2e4ac-9070-4093-a3ce-142139fd4a16" @@ -407,7 +407,7 @@ Also, the additional, nice-to-have goals are: 1. Hub resolves details about the service (TypeInstance details) 1. If the TypeInstance value is provided: - + 1. Hub checks whether the storage backend accepts TypeInstance value (`acceptValue` property). If not, and the value has been provided, it returns error. 1. Hub validates the TypeInstance value against Type JSON Schema. @@ -448,8 +448,8 @@ Also, the additional, nice-to-have goals are: backend: context: # additional parameters that might be modified via the service handling `onCreate` hook name: release-name - namespace: release-namespace - backend: + namespace: release-namespace + backend: ## TODO(storage-: It was done like that currently, Based on that this id: 3ef2e4ac-9070-4093-a3ce-142139fd4a16 # helm-release backend - resolved UUID based on the injected TypeInstance ``` @@ -509,7 +509,7 @@ Capact Local Hub calls proper storage backend service while accessing the TypeIn message OnUpdateRequest { string typeinstance_id = 1; - + OnUpdateData old_data = 2; OnUpdateData new_data = 3; } @@ -568,7 +568,7 @@ Capact Local Hub calls proper storage backend service while accessing the TypeIn rpc GetLockedBy(GetLockedByRequest) returns (GetLockedByResponse); rpc OnLock(OnLockUnlockRequest) returns (OnLockUnlockResponse); rpc OnUnlock(OnLockUnlockRequest) returns (OnLockUnlockResponse); - } + } ``` @@ -656,7 +656,7 @@ The storage backends configuration consists of two different parts: default back interface: rules: - interface: - path: cap.interface.database.postgresql.install + path: cap.interface.database.postgresql.install oneOf: - implementationConstraints: # constraints to select Bitnami PostgreSQL installation, for example: @@ -664,7 +664,7 @@ The storage backends configuration consists of two different parts: default back inject: requiredTypeInstances: - id: b4cf15d2-79b1-45ee-9729-6b83289ecabc # Different TypeInstance of `cap.type.helm.storage` Type - it will be used instead of the one from `interface.rules.default.inject` - description: "Helm Release storage" + description: "Helm Release storage" default: # properties applied to all rules above inject: @@ -777,7 +777,7 @@ query ListTypeInstances { } value # url + contextSchema } - + } } } @@ -935,7 +935,7 @@ To avoid implementing a special storage backend service every time we have such } }, "additionalProperties": true - } + } } }, "acceptValue": { # specifies if a given storage backend (app) accepts TypeInstance value while creating/updating TypeInstance, or just context. @@ -1014,7 +1014,7 @@ To avoid implementing a special storage backend service every time we have such }, "additionalProperties": true } - backend: + backend: id: a36ed738-dfe7-45ec-acd1-8e44e8db893b # PostgreSQL backend ``` @@ -1067,7 +1067,7 @@ To avoid implementing a special storage backend service every time we have such mattermost: relatedTypeInstanceAlias: mattermost-config # kept for better readability typeInstanceID: b895d2d4-d0e0-4f7c-9666-4c3d197d1795 # resolved ID based on `relatedTypeInstanceAlias`. It will be used for further template rendering - backend: + backend: id: abd48b8c-99bd-40a7-99c0-047bd69f1db8 # capact-gotemplate backend - resolved UUID ``` @@ -1107,7 +1107,7 @@ Unfortunately, that won't be possible anymore, and instead we should get all the As described in proposal, every storage backend Type should follow the convention of having JSON schema with `uri` and `contextSchema` properties. That could be possible if we implement an ability to define validating JSON schema for Type nodes (e.g. `cap.core.type.hub.storage`), and use such schemas to validate Types attached to these nodes (via `spec.additionalRefs` property). For example, the `cap.core.hub.storage` node could have JSON Schema defined, which validates Type values (JSON schema) attached to such node. In the end, that would be JSON schema validating another JSON schema. - + **Reason:** It is possible, but it's complex and brings too little benefits for now to implement it. 1. Adding optional `TypeInstance.metadata.name` or `alias`, which is unique across all TypeInstances and immutable regardless resourceVersion. It would allow easier referencing storage backends in the `TypeInstance.spec.backend` field: @@ -1152,7 +1152,7 @@ Unfortunately, that won't be possible anymore, and instead we should get all the - name: helm-release from: helm-release # values backend: vault - context: "{{steps.foo.output.artifacts.foo}}" + context: "{{steps.foo.output.artifacts.foo}}" # option 3 - register something which already exist as external TypeInstance - based on context capact-outputTypeInstances: @@ -1166,8 +1166,8 @@ Unfortunately, that won't be possible anymore, and instead we should get all the ### Storage backend service implementation 1. Using Actions as a way to do CRUD operations (separate Interface/Implementation per Create/Update/Get/Delete operation) - - **Reason:** While the idea may seem exciting, that would be really time consuming and ineffective. We are too far from the point at where we can think about such solution. + + **Reason:** While the idea may seem exciting, that would be really time consuming and ineffective. We are too far from the point at where we can think about such solution. ### Configuring default storage backends @@ -1236,7 +1236,7 @@ Once approved, we need to address the following list of items: 1. Update Policy - Add new properties - Handle common TypeInstance injections -1. Update documentation +1. Update documentation - Policy - Content Development guide - Type features diff --git a/hack/lib/utilities.sh b/hack/lib/utilities.sh index f5b0b3720..80b3d125f 100644 --- a/hack/lib/utilities.sh +++ b/hack/lib/utilities.sh @@ -220,7 +220,8 @@ capact::install() { export ENABLE_ADDING_TRUSTED_CERT=${ENABLE_ADDING_TRUSTED_CERT:-"true"} export HUB_MANIFESTS_SOURCE_REPO_REF=${HUB_MANIFESTS_SOURCE_REPO_REF:-${CAPACT_HUB_MANIFESTS_SOURCE_REPO_REF}} export HUB_MANIFESTS_SOURCE_REPO_URL=${HUB_MANIFESTS_SOURCE_REPO_URL:-${CAPACT_HUB_MANIFESTS_SOURCE_REPO_URL}} - export COMPONENTS="neo4j,ingress-nginx,argo,cert-manager,capact" + export COMPONENTS=${COMPONENTS:-"neo4j,ingress-nginx,argo,cert-manager,capact"} + export BUILD_IMAGES_LIST=${BUILD_IMAGES_LIST:-"argo-actions,argo-runner,e2e-test,gateway,hub-js,k8s-engine,populator"} export INGRESS_CONTROLLER_OVERRIDES=${INGRESS_CONTROLLER_OVERRIDES:=""} export CAPACT_OVERRIDES=${CAPACT_OVERRIDES:=""} export CAPACT_INSTALL_ADDITIONAL_OPTS="" @@ -255,7 +256,7 @@ capact::install() { fi if [[ "${BUILD_IMAGES:-"true"}" == "false" ]]; then - BUILD_IMAGES_FLAG=--build-image="" + BUILD_IMAGES_LIST="" fi if [ -n "${CAPACT_HELM_REPO:-}" ]; then @@ -277,7 +278,7 @@ capact::install() { --update-hosts-file="${ENABLE_HOSTS_UPDATE}" \ --update-trusted-certs="${ENABLE_ADDING_TRUSTED_CERT}" \ --install-component="${COMPONENTS}" \ - ${BUILD_IMAGES_FLAG:-} \ + --build-image="${BUILD_IMAGES_LIST}" \ --version="${CAPACT_VERSION}" \ ${CAPACT_INSTALL_ADDITIONAL_OPTS} } diff --git a/hub-js/graphql/local/examples.graphql b/hub-js/graphql/local/examples.graphql index a9a31f958..ae3431383 100644 --- a/hub-js/graphql/local/examples.graphql +++ b/hub-js/graphql/local/examples.graphql @@ -167,6 +167,10 @@ fragment TypeInstance on TypeInstance { revision } lockedBy + backend { + id + abstract + } latestResourceVersion { ...TypeInstanceResourceVersion diff --git a/hub-js/graphql/local/schema.graphql b/hub-js/graphql/local/schema.graphql index f63698b93..404643001 100644 --- a/hub-js/graphql/local/schema.graphql +++ b/hub-js/graphql/local/schema.graphql @@ -44,6 +44,7 @@ type TypeInstance { @relation(name: "OF_TYPE", direction: "OUT") uses: [TypeInstance!]! @relation(name: "USES", direction: "OUT") usedBy: [TypeInstance!]! @relation(name: "USES", direction: "IN") + backend: TypeInstanceBackendReference! @relation(name: "STORED_IN", direction: "OUT") latestResourceVersion: TypeInstanceResourceVersion @cypher( @@ -73,8 +74,6 @@ type TypeInstanceResourceVersion { @relation(name: "DESCRIBED_BY", direction: "OUT") spec: TypeInstanceResourceVersionSpec! @relation(name: "SPECIFIED_BY", direction: "OUT") - # TODO: can be changed to TypeInstance, and a proper relation set - backend: TypeInstanceBackend! } type TypeInstanceResourceVersionMetadata { @@ -97,8 +96,9 @@ type TypeInstanceResourceVersionSpec { @relation(name: "INSTRUMENTED_WITH", direction: "OUT") } -type TypeInstanceBackend { - id: String! +type TypeInstanceBackendReference { + id: String! + abstract: Boolean! } type TypeInstanceTypeReference { @@ -356,17 +356,37 @@ type Mutation { createTypeInstance(in: CreateTypeInstanceInput!): TypeInstance! @cypher( statement: """ - WITH apoc.convert.toJson($in.value) as value - MERGE (typeRef:TypeInstanceTypeReference {path: $in.typeRef.path, revision: $in.typeRef.revision}) - CREATE (ti:TypeInstance {id: apoc.create.uuid()}) + + // Backend + WITH * + CALL apoc.do.when( + $in.backend.id IS NOT NULL, + 'RETURN $in.backend.id as id', + ' + // TODO(storage): this should be resolved by Local Hub server during the insertion, not in cypher. + MATCH (backend:TypeInstance)-[:OF_TYPE]->(typeRef {path: "cap.core.type.hub.storage.neo4j"}) + RETURN backend.id as id + ', + {in: $in} + ) YIELD value as backend + MATCH (backendTI:TypeInstance {id: backend.id})-[:STORED_IN]->(backendTIRef) + CREATE (ti)-[:USES]->(backendTI) + // TODO(storage): It should be taken from the uses relation but we don't have access to the TypeRef.additionalRefs to check + // if a given type is a backend or not. Maybe we will introduce a dedicated property to distinguish them from others. + MERGE (storageRef:TypeInstanceBackendReference {abstract: backendTIRef.abstract, id: backendTIRef.id}) + CREATE (ti)-[:STORED_IN]->(storageRef) + + // TypeRef + MERGE (typeRef:TypeInstanceTypeReference {path: $in.typeRef.path, revision: $in.typeRef.revision}) CREATE (ti)-[:OF_TYPE]->(typeRef) + // Revision CREATE (tir: TypeInstanceResourceVersion {resourceVersion: 1, createdBy: $in.createdBy}) CREATE (ti)-[:CONTAINS]->(tir) CREATE (tir)-[:DESCRIBED_BY]->(metadata: TypeInstanceResourceVersionMetadata) - CREATE (tir)-[:SPECIFIED_BY]->(spec: TypeInstanceResourceVersionSpec {value: value}) + CREATE (tir)-[:SPECIFIED_BY]->(spec: TypeInstanceResourceVersionSpec {value: apoc.convert.toJson($in.value)}) FOREACH (attr in $in.attributes | MERGE (attrRef: AttributeReference {path: attr.path, revision: attr.revision}) diff --git a/hub-js/src/index.ts b/hub-js/src/index.ts index e232db2ce..1d2fc5565 100644 --- a/hub-js/src/index.ts +++ b/hub-js/src/index.ts @@ -1,20 +1,21 @@ -import { ApolloServer } from "apollo-server-express"; +import {ApolloServer} from "apollo-server-express"; import * as express from "express"; -import neo4j, { Driver } from "neo4j-driver"; +import neo4j, {Driver} from "neo4j-driver"; import { createTerminus, HealthCheck, HealthCheckError, } from "@godaddy/terminus"; import * as http from "http"; -import { GraphQLSchema } from "graphql"; +import {GraphQLSchema} from "graphql"; -import { assertSchemaOnDatabase, getSchemaForMode } from "./schema"; -import { config } from "./config"; -import { logger } from "./logger"; +import {assertSchemaOnDatabase, getSchemaForMode, HubMode} from "./schema"; +import {config} from "./config"; +import {logger} from "./logger"; +import {ensureCoreStorageTypeInstance} from "./schema/local"; async function main() { - logger.info("Using Neo4j database", { endpoint: config.neo4j.endpoint }); + logger.info("Using Neo4j database", {endpoint: config.neo4j.endpoint}); const driver = neo4j.driver( config.neo4j.endpoint, @@ -37,9 +38,14 @@ async function main() { }; const server = await setupHttpServer(schema, driver, healthCheck); - const { bindPort, bindAddress } = config.graphql; + const {bindPort, bindAddress} = config.graphql; - logger.info("Starting Hub", { mode: config.hubMode }); + logger.info("Starting Hub", {mode: config.hubMode}); + + if (config.hubMode === HubMode.Local) { + await ensureCoreStorageTypeInstance({driver}, `http://${bindAddress}:${bindPort}/graphql`) + logger.info("Successfully registered TypeInstance for core backend storage"); + } server.listen(bindPort, bindAddress, () => { logger.info("GraphQL API is listening", { @@ -54,16 +60,16 @@ async function setupHttpServer( healthCheck: HealthCheck ): Promise { const app = express(); - app.use(express.json({ limit: config.express.bodySizeLimit })); + app.use(express.json({limit: config.express.bodySizeLimit})); - const apolloServer = new ApolloServer({ schema, context: { driver } }); + const apolloServer = new ApolloServer({schema, context: {driver}}); await apolloServer.start(); - apolloServer.applyMiddleware({ app }); + apolloServer.applyMiddleware({app}); const server = http.createServer(app); createTerminus(server, { - healthChecks: { "/healthz": healthCheck }, + healthChecks: {"/healthz": healthCheck}, }); return server; diff --git a/hub-js/src/schema/local.ts b/hub-js/src/schema/local.ts index 8159a1d27..be3acb996 100644 --- a/hub-js/src/schema/local.ts +++ b/hub-js/src/schema/local.ts @@ -73,14 +73,35 @@ export const schema = makeAugmentedSchema({ // create TypeInstances const createTypeInstanceResult = await tx.run( `UNWIND $typeInstances AS typeInstance - MERGE (typeRef:TypeInstanceTypeReference {path: typeInstance.typeRef.path, revision: typeInstance.typeRef.revision}) - CREATE (ti:TypeInstance {id: apoc.create.uuid()}) + + // Backend + WITH * + CALL apoc.do.when( + typeInstance.backend.id IS NOT NULL, + 'RETURN $in.backend.id as id', + ' + // TODO(storage): this should be resolved by Local Hub server during the insertion, not in cypher. + MATCH (backend:TypeInstance)-[:OF_TYPE]->(typeRef {path: "cap.core.type.hub.storage.neo4j", revision: "0.1.0"}) + RETURN backend.id as id + ', + {in: typeInstance} + ) YIELD value as backend + MATCH (backendTI:TypeInstance {id: backend.id})-[:STORED_IN]->(backendTIRef) + CREATE (ti)-[:USES]->(backendTI) + // TODO(storage): It should be taken from the uses relation but we don't have access to the TypeRef.additionalRefs to check + // if a given type is a backend or not. Maybe we will introduce a dedicated property to distinguish them from others. + MERGE (storageRef:TypeInstanceBackendReference {abstract: backendTIRef.abstract, id: backendTI.id}) + CREATE (ti)-[:STORED_IN]->(storageRef) + + // TypeRef + MERGE (typeRef:TypeInstanceTypeReference {path: typeInstance.typeRef.path, revision: typeInstance.typeRef.revision}) CREATE (ti)-[:OF_TYPE]->(typeRef) - + + // Revision CREATE (tir: TypeInstanceResourceVersion {resourceVersion: 1, createdBy: typeInstance.createdBy}) CREATE (ti)-[:CONTAINS]->(tir) - + CREATE (tir)-[:DESCRIBED_BY]->(metadata: TypeInstanceResourceVersionMetadata) CREATE (tir)-[:SPECIFIED_BY]->(spec: TypeInstanceResourceVersionSpec {value: apoc.convert.toJson(typeInstance.value)}) @@ -206,7 +227,7 @@ export const schema = makeAugmentedSchema({ await tx.run( ` OPTIONAL MATCH (ti:TypeInstance {id: $id}) - + // Check if a given TypeInstance was found CALL apoc.util.validate(ti IS NULL, apoc.convert.toJson({code: 404}), null) @@ -226,9 +247,9 @@ export const schema = makeAugmentedSchema({ MATCH (metadata:TypeInstanceResourceVersionMetadata)<-[:DESCRIBED_BY]-(tirs) MATCH (tirs)-[:SPECIFIED_BY]->(spec: TypeInstanceResourceVersionSpec) OPTIONAL MATCH (metadata)-[:CHARACTERIZED_BY]->(attrRef: AttributeReference) - + DETACH DELETE ti, metadata, spec, tirs - + WITH typeRef CALL { MATCH (typeRef) @@ -236,7 +257,7 @@ export const schema = makeAugmentedSchema({ DELETE (typeRef) RETURN 'remove typeRef' } - + WITH * CALL { MATCH (attrRef) @@ -244,7 +265,7 @@ export const schema = makeAugmentedSchema({ DELETE (attrRef) RETURN 'remove attr' } - + RETURN $id`, { id: args.id, ownerID: args.ownerID || null } ); @@ -346,9 +367,9 @@ async function switchLocking( ) { const instanceLockedByOthers = await tx.run( `MATCH (ti:TypeInstance) - WHERE ti.id IN $in.ids + WHERE ti.id IN $in.ids WITH collect(ti) as allIDs - + // Check if all TypeInstances were found CALL apoc.when( size(allIDs) < size($in.ids), @@ -356,7 +377,7 @@ async function switchLocking( 'RETURN false as notFoundErr', {in: $in, allIDs: allIDs} ) YIELD value as checkIDs - + // Check if given TypeInstances are not already locked by others CALL { MATCH (ti:TypeInstance) @@ -364,7 +385,7 @@ async function switchLocking( WITH collect(ti) as lockedIDs RETURN lockedIDs } - + // Execute lock only if all TypeInstance were found and none of them are already locked by another owner WITH * CALL apoc.do.when( @@ -377,7 +398,7 @@ async function switchLocking( ', {in: $in, checkIDs: checkIDs, lockedIDs: lockedIDs} ) YIELD value as lockingProcess - + RETURN allIDs, lockedIDs, lockingProcess`, { in: args.in } ); @@ -453,3 +474,36 @@ function tryToExtractCustomError( return null; } + +export async function ensureCoreStorageTypeInstance(context: ContextWithDriver, uri: string) { + const neo4jSession = context.driver.session(); + const value = { + uri: uri + } + try { + await neo4jSession.writeTransaction( + async (tx: Transaction) => { + await tx.run(` + MERGE (ti:TypeInstance {id: "318b99bd-9b26-4bc1-8259-0a7ff5dae61c"}) + MERGE (typeRef:TypeInstanceTypeReference {path: "cap.core.type.hub.storage.neo4j", revision: "0.1.0"}) + MERGE (backend:TypeInstanceBackendReference {abstract: true, id: ti.id, description: "Built-in Hub storage"}) + MERGE (tir: TypeInstanceResourceVersion {resourceVersion: 1, createdBy: "core"}) + MERGE (spec: TypeInstanceResourceVersionSpec {value: apoc.convert.toJson($value)}) + + MERGE (ti)-[:OF_TYPE]->(typeRef) + MERGE (ti)-[:STORED_IN]->(backend) + MERGE (ti)-[:CONTAINS]->(tir) + MERGE (tir)-[:DESCRIBED_BY]->(metadata:TypeInstanceResourceVersionMetadata) + MERGE (tir)-[:SPECIFIED_BY]->(spec) + + RETURN ti + `, {value}); + } + ); + } catch (e) { + const err = e as Error; + throw new Error(`while ensuring TypeInstance for core backend storage: ${err.message}`); + } finally { + await neo4jSession.close(); + } +} diff --git a/internal/k8s-engine/controller/action_service.go b/internal/k8s-engine/controller/action_service.go index 6d2aa36e9..f3de4589d 100644 --- a/internal/k8s-engine/controller/action_service.go +++ b/internal/k8s-engine/controller/action_service.go @@ -43,6 +43,8 @@ const ( k8sJobRunnerInputDataMountPath = "/mnt" k8sJobRunnerVolumeName = "input-volume" k8sJobActiveDeadlinePadding = 10 * time.Second + + listTypeInstanceFields = local.TypeInstanceRootFields | local.TypeInstanceTypeRefFields | local.TypeInstanceBackendFields ) type ( @@ -591,20 +593,25 @@ func (a *ActionService) GetTypeInstancesFromAction(ctx context.Context, action * typeInstances, err := a.typeInstanceGetter.ListTypeInstances(ctx, &gqllocalapi.TypeInstanceFilter{ CreatedBy: &ownerID, - }, local.WithFields(local.TypeInstanceRootFields|local.TypeInstanceTypeRefFields)) + }, local.WithFields(listTypeInstanceFields)) if err != nil { return nil, errors.Wrap(err, "while listing TypeInstances") } var res []v1alpha1.OutputTypeInstanceDetails for _, ti := range typeInstances { - res = append(res, v1alpha1.OutputTypeInstanceDetails{ + out := v1alpha1.OutputTypeInstanceDetails{ ID: ti.ID, TypeRef: &v1alpha1.ManifestReference{ Path: v1alpha1.NodePath(ti.TypeRef.Path), Revision: &ti.TypeRef.Revision, }, - }) + } + if ti.Backend != nil { + out.Backend = v1alpha1.TypeInstanceBackend(*ti.Backend) + } + + res = append(res, out) } return res, nil diff --git a/internal/k8s-engine/graphql/domain/action/converter.go b/internal/k8s-engine/graphql/domain/action/converter.go index be8074e23..2e19003aa 100644 --- a/internal/k8s-engine/graphql/domain/action/converter.go +++ b/internal/k8s-engine/graphql/domain/action/converter.go @@ -314,9 +314,11 @@ func (c *Converter) actionOutputToGraphQL(in *v1alpha1.ActionOutput) *graphql.Ac var gqlTypeInstances []*graphql.OutputTypeInstanceDetails for _, item := range *in.TypeInstances { + gqlBackend := graphql.TypeInstanceBackendDetails(item.Backend) gqlTypeInstances = append(gqlTypeInstances, &graphql.OutputTypeInstanceDetails{ ID: item.ID, TypeRef: c.manifestRefToGraphQL(item.TypeRef), + Backend: &gqlBackend, }) } diff --git a/internal/k8s-engine/graphql/domain/policy/converter.go b/internal/k8s-engine/graphql/domain/policy/converter.go index 8732b1066..3bc3c4566 100644 --- a/internal/k8s-engine/graphql/domain/policy/converter.go +++ b/internal/k8s-engine/graphql/domain/policy/converter.go @@ -95,7 +95,7 @@ func (c *Converter) typeInstanceToGraphQL(in policy.TypeInstancePolicy) *graphql gqlRules = append(gqlRules, &graphql.RulesForTypeInstance{ TypeRef: &ref, - Backend: &graphql.TypeInstanceBackend{ + Backend: &graphql.TypeInstanceBackendRule{ ID: rule.Backend.ID, Description: rule.Backend.Description, }, diff --git a/internal/k8s-engine/graphql/domain/policy/fixtures_test.go b/internal/k8s-engine/graphql/domain/policy/fixtures_test.go index 9fc66a59c..86de2dbb3 100644 --- a/internal/k8s-engine/graphql/domain/policy/fixtures_test.go +++ b/internal/k8s-engine/graphql/domain/policy/fixtures_test.go @@ -80,7 +80,7 @@ func fixGQLInput() graphql.PolicyInput { Path: "cap.type.aws.auth.credentials", Revision: ptr.String("0.1.0"), }, - Backend: &graphql.TypeInstanceBackendInput{ + Backend: &graphql.TypeInstanceBackendRuleInput{ ID: "00fd161c-01bd-47a6-9872-47490e11f996", Description: ptr.String("Vault TI"), }, @@ -89,7 +89,7 @@ func fixGQLInput() graphql.PolicyInput { TypeRef: &graphql.ManifestReferenceInput{ Path: "cap.type.aws.*", }, - Backend: &graphql.TypeInstanceBackendInput{ + Backend: &graphql.TypeInstanceBackendRuleInput{ ID: "31bb8355-10d7-49ce-a739-4554d8a40b63", }, }, @@ -97,7 +97,7 @@ func fixGQLInput() graphql.PolicyInput { TypeRef: &graphql.ManifestReferenceInput{ Path: "cap.*", }, - Backend: &graphql.TypeInstanceBackendInput{ + Backend: &graphql.TypeInstanceBackendRuleInput{ ID: "a36ed738-dfe7-45ec-acd1-8e44e8db893b", Description: ptr.String("Default Capact PostgreSQL backend"), }, @@ -180,7 +180,7 @@ func fixGQL() graphql.Policy { Path: "cap.type.aws.auth.credentials", Revision: ptr.String("0.1.0"), }, - Backend: &graphql.TypeInstanceBackend{ + Backend: &graphql.TypeInstanceBackendRule{ ID: "00fd161c-01bd-47a6-9872-47490e11f996", Description: ptr.String("Vault TI"), }, @@ -189,7 +189,7 @@ func fixGQL() graphql.Policy { TypeRef: &graphql.ManifestReferenceWithOptionalRevision{ Path: "cap.type.aws.*", }, - Backend: &graphql.TypeInstanceBackend{ + Backend: &graphql.TypeInstanceBackendRule{ ID: "31bb8355-10d7-49ce-a739-4554d8a40b63", }, }, @@ -197,7 +197,7 @@ func fixGQL() graphql.Policy { TypeRef: &graphql.ManifestReferenceWithOptionalRevision{ Path: "cap.*", }, - Backend: &graphql.TypeInstanceBackend{ + Backend: &graphql.TypeInstanceBackendRule{ ID: "a36ed738-dfe7-45ec-acd1-8e44e8db893b", Description: ptr.String("Default Capact PostgreSQL backend"), }, diff --git a/pkg/engine/api/graphql/models_gen.go b/pkg/engine/api/graphql/models_gen.go index a87db8d73..b4457fe95 100644 --- a/pkg/engine/api/graphql/models_gen.go +++ b/pkg/engine/api/graphql/models_gen.go @@ -159,8 +159,9 @@ type ManifestReferenceInput struct { // Describes output TypeInstance of an Action type OutputTypeInstanceDetails struct { - ID string `json:"id"` - TypeRef *ManifestReference `json:"typeRef"` + ID string `json:"id"` + TypeRef *ManifestReference `json:"typeRef"` + Backend *TypeInstanceBackendDetails `json:"backend"` } type Policy struct { @@ -210,12 +211,12 @@ type RulesForInterfaceInput struct { type RulesForTypeInstance struct { TypeRef *ManifestReferenceWithOptionalRevision `json:"typeRef"` - Backend *TypeInstanceBackend `json:"backend"` + Backend *TypeInstanceBackendRule `json:"backend"` } type RulesForTypeInstanceInput struct { - TypeRef *ManifestReferenceInput `json:"typeRef"` - Backend *TypeInstanceBackendInput `json:"backend"` + TypeRef *ManifestReferenceInput `json:"typeRef"` + Backend *TypeInstanceBackendRuleInput `json:"backend"` } // Additional Action status from the Runner @@ -224,12 +225,17 @@ type RunnerStatus struct { Status interface{} `json:"status"` } -type TypeInstanceBackend struct { +type TypeInstanceBackendDetails struct { + ID string `json:"id"` + Abstract bool `json:"abstract"` +} + +type TypeInstanceBackendRule struct { ID string `json:"id"` Description *string `json:"description"` } -type TypeInstanceBackendInput struct { +type TypeInstanceBackendRuleInput struct { ID string `json:"id"` Description *string `json:"description"` } diff --git a/pkg/engine/api/graphql/schema.graphql b/pkg/engine/api/graphql/schema.graphql index 8852e3d9a..32e89efd3 100644 --- a/pkg/engine/api/graphql/schema.graphql +++ b/pkg/engine/api/graphql/schema.graphql @@ -257,6 +257,12 @@ Describes output TypeInstance of an Action type OutputTypeInstanceDetails { id: ID! typeRef: ManifestReference! + backend: TypeInstanceBackendDetails! +} + +type TypeInstanceBackendDetails { + id: String! + abstract: Boolean! } """ @@ -303,33 +309,33 @@ enum ActionStatusPhase { } input PolicyInput { - interface: InterfacePolicyInput - typeInstance: TypeInstancePolicyInput + interface: InterfacePolicyInput + typeInstance: TypeInstancePolicyInput } # TypeInstance Policy Input input TypeInstancePolicyInput { - rules: [RulesForTypeInstanceInput!]! + rules: [RulesForTypeInstanceInput!]! } input RulesForTypeInstanceInput { - typeRef: ManifestReferenceInput! - backend: TypeInstanceBackendInput! + typeRef: ManifestReferenceInput! + backend: TypeInstanceBackendRuleInput! } -input TypeInstanceBackendInput { - id: ID! - description: String +input TypeInstanceBackendRuleInput { + id: ID! + description: String } # Interface Policy Input input InterfacePolicyInput { - rules: [RulesForInterfaceInput!]! + rules: [RulesForInterfaceInput!]! } input RulesForInterfaceInput { interface: ManifestReferenceInput! - oneOf: [PolicyRuleInput!]! + oneOf: [PolicyRuleInput!]! } input PolicyRuleInput { @@ -344,8 +350,8 @@ input PolicyRuleInjectDataInput { } input AdditionalParameterInput { - name: String! - value: Any! + name: String! + value: Any! } input PolicyRuleImplementationConstraintsInput { @@ -372,27 +378,27 @@ type Policy { # TypeInstance Policy type TypeInstancePolicy { - rules: [RulesForTypeInstance!]! + rules: [RulesForTypeInstance!]! } type RulesForTypeInstance { - typeRef: ManifestReferenceWithOptionalRevision! - backend: TypeInstanceBackend! + typeRef: ManifestReferenceWithOptionalRevision! + backend: TypeInstanceBackendRule! } -type TypeInstanceBackend { - id: ID! - description: String +type TypeInstanceBackendRule { + id: ID! + description: String } # Interface Policy type InterfacePolicy { - rules: [RulesForInterface!]! + rules: [RulesForInterface!]! } type RulesForInterface { interface: ManifestReferenceWithOptionalRevision! - oneOf: [PolicyRule!]! + oneOf: [PolicyRule!]! } type PolicyRule { @@ -412,8 +418,8 @@ type AdditionalTypeInstanceReference { } type AdditionalParameter { - name: String! - value: Any! + name: String! + value: Any! } type PolicyRuleImplementationConstraints { diff --git a/pkg/engine/api/graphql/schema_gen.go b/pkg/engine/api/graphql/schema_gen.go index 8f9d2441d..808b1d07a 100644 --- a/pkg/engine/api/graphql/schema_gen.go +++ b/pkg/engine/api/graphql/schema_gen.go @@ -127,6 +127,7 @@ type ComplexityRoot struct { } OutputTypeInstanceDetails struct { + Backend func(childComplexity int) int ID func(childComplexity int) int TypeRef func(childComplexity int) int } @@ -178,7 +179,12 @@ type ComplexityRoot struct { Status func(childComplexity int) int } - TypeInstanceBackend struct { + TypeInstanceBackendDetails struct { + Abstract func(childComplexity int) int + ID func(childComplexity int) int + } + + TypeInstanceBackendRule struct { Description func(childComplexity int) int ID func(childComplexity int) int } @@ -574,6 +580,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.UpdatePolicy(childComplexity, args["in"].(PolicyInput)), true + case "OutputTypeInstanceDetails.backend": + if e.complexity.OutputTypeInstanceDetails.Backend == nil { + break + } + + return e.complexity.OutputTypeInstanceDetails.Backend(childComplexity), true + case "OutputTypeInstanceDetails.id": if e.complexity.OutputTypeInstanceDetails.ID == nil { break @@ -738,19 +751,33 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.RunnerStatus.Status(childComplexity), true - case "TypeInstanceBackend.description": - if e.complexity.TypeInstanceBackend.Description == nil { + case "TypeInstanceBackendDetails.abstract": + if e.complexity.TypeInstanceBackendDetails.Abstract == nil { + break + } + + return e.complexity.TypeInstanceBackendDetails.Abstract(childComplexity), true + + case "TypeInstanceBackendDetails.id": + if e.complexity.TypeInstanceBackendDetails.ID == nil { + break + } + + return e.complexity.TypeInstanceBackendDetails.ID(childComplexity), true + + case "TypeInstanceBackendRule.description": + if e.complexity.TypeInstanceBackendRule.Description == nil { break } - return e.complexity.TypeInstanceBackend.Description(childComplexity), true + return e.complexity.TypeInstanceBackendRule.Description(childComplexity), true - case "TypeInstanceBackend.id": - if e.complexity.TypeInstanceBackend.ID == nil { + case "TypeInstanceBackendRule.id": + if e.complexity.TypeInstanceBackendRule.ID == nil { break } - return e.complexity.TypeInstanceBackend.ID(childComplexity), true + return e.complexity.TypeInstanceBackendRule.ID(childComplexity), true case "TypeInstancePolicy.rules": if e.complexity.TypeInstancePolicy.Rules == nil { @@ -1103,6 +1130,12 @@ Describes output TypeInstance of an Action type OutputTypeInstanceDetails { id: ID! typeRef: ManifestReference! + backend: TypeInstanceBackendDetails! +} + +type TypeInstanceBackendDetails { + id: String! + abstract: Boolean! } """ @@ -1149,33 +1182,33 @@ enum ActionStatusPhase { } input PolicyInput { - interface: InterfacePolicyInput - typeInstance: TypeInstancePolicyInput + interface: InterfacePolicyInput + typeInstance: TypeInstancePolicyInput } # TypeInstance Policy Input input TypeInstancePolicyInput { - rules: [RulesForTypeInstanceInput!]! + rules: [RulesForTypeInstanceInput!]! } input RulesForTypeInstanceInput { - typeRef: ManifestReferenceInput! - backend: TypeInstanceBackendInput! + typeRef: ManifestReferenceInput! + backend: TypeInstanceBackendRuleInput! } -input TypeInstanceBackendInput { - id: ID! - description: String +input TypeInstanceBackendRuleInput { + id: ID! + description: String } # Interface Policy Input input InterfacePolicyInput { - rules: [RulesForInterfaceInput!]! + rules: [RulesForInterfaceInput!]! } input RulesForInterfaceInput { interface: ManifestReferenceInput! - oneOf: [PolicyRuleInput!]! + oneOf: [PolicyRuleInput!]! } input PolicyRuleInput { @@ -1190,8 +1223,8 @@ input PolicyRuleInjectDataInput { } input AdditionalParameterInput { - name: String! - value: Any! + name: String! + value: Any! } input PolicyRuleImplementationConstraintsInput { @@ -1218,27 +1251,27 @@ type Policy { # TypeInstance Policy type TypeInstancePolicy { - rules: [RulesForTypeInstance!]! + rules: [RulesForTypeInstance!]! } type RulesForTypeInstance { - typeRef: ManifestReferenceWithOptionalRevision! - backend: TypeInstanceBackend! + typeRef: ManifestReferenceWithOptionalRevision! + backend: TypeInstanceBackendRule! } -type TypeInstanceBackend { - id: ID! - description: String +type TypeInstanceBackendRule { + id: ID! + description: String } # Interface Policy type InterfacePolicy { - rules: [RulesForInterface!]! + rules: [RulesForInterface!]! } type RulesForInterface { interface: ManifestReferenceWithOptionalRevision! - oneOf: [PolicyRule!]! + oneOf: [PolicyRule!]! } type PolicyRule { @@ -1258,8 +1291,8 @@ type AdditionalTypeInstanceReference { } type AdditionalParameter { - name: String! - value: Any! + name: String! + value: Any! } type PolicyRuleImplementationConstraints { @@ -3169,6 +3202,41 @@ func (ec *executionContext) _OutputTypeInstanceDetails_typeRef(ctx context.Conte return ec.marshalNManifestReference2ᚖcapactᚗioᚋcapactᚋpkgᚋengineᚋapiᚋgraphqlᚐManifestReference(ctx, field.Selections, res) } +func (ec *executionContext) _OutputTypeInstanceDetails_backend(ctx context.Context, field graphql.CollectedField, obj *OutputTypeInstanceDetails) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "OutputTypeInstanceDetails", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Backend, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*TypeInstanceBackendDetails) + fc.Result = res + return ec.marshalNTypeInstanceBackendDetails2ᚖcapactᚗioᚋcapactᚋpkgᚋengineᚋapiᚋgraphqlᚐTypeInstanceBackendDetails(ctx, field.Selections, res) +} + func (ec *executionContext) _Policy_interface(ctx context.Context, field graphql.CollectedField, obj *Policy) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -3878,9 +3946,9 @@ func (ec *executionContext) _RulesForTypeInstance_backend(ctx context.Context, f } return graphql.Null } - res := resTmp.(*TypeInstanceBackend) + res := resTmp.(*TypeInstanceBackendRule) fc.Result = res - return ec.marshalNTypeInstanceBackend2ᚖcapactᚗioᚋcapactᚋpkgᚋengineᚋapiᚋgraphqlᚐTypeInstanceBackend(ctx, field.Selections, res) + return ec.marshalNTypeInstanceBackendRule2ᚖcapactᚗioᚋcapactᚋpkgᚋengineᚋapiᚋgraphqlᚐTypeInstanceBackendRule(ctx, field.Selections, res) } func (ec *executionContext) _RunnerStatus_status(ctx context.Context, field graphql.CollectedField, obj *RunnerStatus) (ret graphql.Marshaler) { @@ -3915,7 +3983,7 @@ func (ec *executionContext) _RunnerStatus_status(ctx context.Context, field grap return ec.marshalOAny2interface(ctx, field.Selections, res) } -func (ec *executionContext) _TypeInstanceBackend_id(ctx context.Context, field graphql.CollectedField, obj *TypeInstanceBackend) (ret graphql.Marshaler) { +func (ec *executionContext) _TypeInstanceBackendDetails_id(ctx context.Context, field graphql.CollectedField, obj *TypeInstanceBackendDetails) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) @@ -3923,7 +3991,77 @@ func (ec *executionContext) _TypeInstanceBackend_id(ctx context.Context, field g } }() fc := &graphql.FieldContext{ - Object: "TypeInstanceBackend", + Object: "TypeInstanceBackendDetails", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.ID, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) _TypeInstanceBackendDetails_abstract(ctx context.Context, field graphql.CollectedField, obj *TypeInstanceBackendDetails) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "TypeInstanceBackendDetails", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Abstract, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(bool) + fc.Result = res + return ec.marshalNBoolean2bool(ctx, field.Selections, res) +} + +func (ec *executionContext) _TypeInstanceBackendRule_id(ctx context.Context, field graphql.CollectedField, obj *TypeInstanceBackendRule) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "TypeInstanceBackendRule", Field: field, Args: nil, IsMethod: false, @@ -3950,7 +4088,7 @@ func (ec *executionContext) _TypeInstanceBackend_id(ctx context.Context, field g return ec.marshalNID2string(ctx, field.Selections, res) } -func (ec *executionContext) _TypeInstanceBackend_description(ctx context.Context, field graphql.CollectedField, obj *TypeInstanceBackend) (ret graphql.Marshaler) { +func (ec *executionContext) _TypeInstanceBackendRule_description(ctx context.Context, field graphql.CollectedField, obj *TypeInstanceBackendRule) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) @@ -3958,7 +4096,7 @@ func (ec *executionContext) _TypeInstanceBackend_description(ctx context.Context } }() fc := &graphql.FieldContext{ - Object: "TypeInstanceBackend", + Object: "TypeInstanceBackendRule", Field: field, Args: nil, IsMethod: false, @@ -5692,7 +5830,7 @@ func (ec *executionContext) unmarshalInputRulesForTypeInstanceInput(ctx context. var err error ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("backend")) - it.Backend, err = ec.unmarshalNTypeInstanceBackendInput2ᚖcapactᚗioᚋcapactᚋpkgᚋengineᚋapiᚋgraphqlᚐTypeInstanceBackendInput(ctx, v) + it.Backend, err = ec.unmarshalNTypeInstanceBackendRuleInput2ᚖcapactᚗioᚋcapactᚋpkgᚋengineᚋapiᚋgraphqlᚐTypeInstanceBackendRuleInput(ctx, v) if err != nil { return it, err } @@ -5702,8 +5840,8 @@ func (ec *executionContext) unmarshalInputRulesForTypeInstanceInput(ctx context. return it, nil } -func (ec *executionContext) unmarshalInputTypeInstanceBackendInput(ctx context.Context, obj interface{}) (TypeInstanceBackendInput, error) { - var it TypeInstanceBackendInput +func (ec *executionContext) unmarshalInputTypeInstanceBackendRuleInput(ctx context.Context, obj interface{}) (TypeInstanceBackendRuleInput, error) { + var it TypeInstanceBackendRuleInput var asMap = obj.(map[string]interface{}) for k, v := range asMap { @@ -6252,6 +6390,11 @@ func (ec *executionContext) _OutputTypeInstanceDetails(ctx context.Context, sel if out.Values[i] == graphql.Null { invalids++ } + case "backend": + out.Values[i] = ec._OutputTypeInstanceDetails_backend(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -6557,24 +6700,56 @@ func (ec *executionContext) _RunnerStatus(ctx context.Context, sel ast.Selection return out } -var typeInstanceBackendImplementors = []string{"TypeInstanceBackend"} +var typeInstanceBackendDetailsImplementors = []string{"TypeInstanceBackendDetails"} -func (ec *executionContext) _TypeInstanceBackend(ctx context.Context, sel ast.SelectionSet, obj *TypeInstanceBackend) graphql.Marshaler { - fields := graphql.CollectFields(ec.OperationContext, sel, typeInstanceBackendImplementors) +func (ec *executionContext) _TypeInstanceBackendDetails(ctx context.Context, sel ast.SelectionSet, obj *TypeInstanceBackendDetails) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, typeInstanceBackendDetailsImplementors) out := graphql.NewFieldSet(fields) var invalids uint32 for i, field := range fields { switch field.Name { case "__typename": - out.Values[i] = graphql.MarshalString("TypeInstanceBackend") + out.Values[i] = graphql.MarshalString("TypeInstanceBackendDetails") case "id": - out.Values[i] = ec._TypeInstanceBackend_id(ctx, field, obj) + out.Values[i] = ec._TypeInstanceBackendDetails_id(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + case "abstract": + out.Values[i] = ec._TypeInstanceBackendDetails_abstract(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch() + if invalids > 0 { + return graphql.Null + } + return out +} + +var typeInstanceBackendRuleImplementors = []string{"TypeInstanceBackendRule"} + +func (ec *executionContext) _TypeInstanceBackendRule(ctx context.Context, sel ast.SelectionSet, obj *TypeInstanceBackendRule) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, typeInstanceBackendRuleImplementors) + + out := graphql.NewFieldSet(fields) + var invalids uint32 + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("TypeInstanceBackendRule") + case "id": + out.Values[i] = ec._TypeInstanceBackendRule_id(ctx, field, obj) if out.Values[i] == graphql.Null { invalids++ } case "description": - out.Values[i] = ec._TypeInstanceBackend_description(ctx, field, obj) + out.Values[i] = ec._TypeInstanceBackendRule_description(ctx, field, obj) default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -7538,18 +7713,28 @@ func (ec *executionContext) marshalNTimestamp2capactᚗioᚋcapactᚋpkgᚋengin return v } -func (ec *executionContext) marshalNTypeInstanceBackend2ᚖcapactᚗioᚋcapactᚋpkgᚋengineᚋapiᚋgraphqlᚐTypeInstanceBackend(ctx context.Context, sel ast.SelectionSet, v *TypeInstanceBackend) graphql.Marshaler { +func (ec *executionContext) marshalNTypeInstanceBackendDetails2ᚖcapactᚗioᚋcapactᚋpkgᚋengineᚋapiᚋgraphqlᚐTypeInstanceBackendDetails(ctx context.Context, sel ast.SelectionSet, v *TypeInstanceBackendDetails) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + return ec._TypeInstanceBackendDetails(ctx, sel, v) +} + +func (ec *executionContext) marshalNTypeInstanceBackendRule2ᚖcapactᚗioᚋcapactᚋpkgᚋengineᚋapiᚋgraphqlᚐTypeInstanceBackendRule(ctx context.Context, sel ast.SelectionSet, v *TypeInstanceBackendRule) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { ec.Errorf(ctx, "must not be null") } return graphql.Null } - return ec._TypeInstanceBackend(ctx, sel, v) + return ec._TypeInstanceBackendRule(ctx, sel, v) } -func (ec *executionContext) unmarshalNTypeInstanceBackendInput2ᚖcapactᚗioᚋcapactᚋpkgᚋengineᚋapiᚋgraphqlᚐTypeInstanceBackendInput(ctx context.Context, v interface{}) (*TypeInstanceBackendInput, error) { - res, err := ec.unmarshalInputTypeInstanceBackendInput(ctx, v) +func (ec *executionContext) unmarshalNTypeInstanceBackendRuleInput2ᚖcapactᚗioᚋcapactᚋpkgᚋengineᚋapiᚋgraphqlᚐTypeInstanceBackendRuleInput(ctx context.Context, v interface{}) (*TypeInstanceBackendRuleInput, error) { + res, err := ec.unmarshalInputTypeInstanceBackendRuleInput(ctx, v) return &res, graphql.ErrorOnPath(ctx, err) } diff --git a/pkg/engine/client/fields.go b/pkg/engine/client/fields.go index 14597f4b9..d5523cde4 100644 --- a/pkg/engine/client/fields.go +++ b/pkg/engine/client/fields.go @@ -18,6 +18,10 @@ var actionFields = fmt.Sprintf(` output { typeInstances { id + backend { + id + abstract + } typeRef { path revision diff --git a/pkg/engine/k8s/api/v1alpha1/action_types.go b/pkg/engine/k8s/api/v1alpha1/action_types.go index 0b86db04d..3deb3248b 100644 --- a/pkg/engine/k8s/api/v1alpha1/action_types.go +++ b/pkg/engine/k8s/api/v1alpha1/action_types.go @@ -388,6 +388,15 @@ type OutputTypeInstanceDetails struct { // TypeRef contains data needed to resolve Type manifest. TypeRef *ManifestReference `json:"typeReference"` + + // Backend contains information in which backend this TypeInstance is stored. + Backend TypeInstanceBackend `json:"backend"` +} + +// TypeInstanceBackend holds information about TypeInstance backend. +type TypeInstanceBackend struct { + ID string `json:"id"` + Abstract bool `json:"abstract"` } // InputTypeInstanceToProvide describes optional input TypeInstance for advanced rendering mode iteration. diff --git a/pkg/engine/k8s/api/v1alpha1/zz_generated.deepcopy.go b/pkg/engine/k8s/api/v1alpha1/zz_generated.deepcopy.go index c83d930ce..9e7ee9b03 100644 --- a/pkg/engine/k8s/api/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/engine/k8s/api/v1alpha1/zz_generated.deepcopy.go @@ -361,6 +361,7 @@ func (in *OutputTypeInstanceDetails) DeepCopyInto(out *OutputTypeInstanceDetails *out = new(ManifestReference) (*in).DeepCopyInto(*out) } + out.Backend = in.Backend } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OutputTypeInstanceDetails. @@ -502,3 +503,18 @@ func (in *RunnerStatus) DeepCopy() *RunnerStatus { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TypeInstanceBackend) DeepCopyInto(out *TypeInstanceBackend) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TypeInstanceBackend. +func (in *TypeInstanceBackend) DeepCopy() *TypeInstanceBackend { + if in == nil { + return nil + } + out := new(TypeInstanceBackend) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/engine/k8s/policy/metadata/metadata_resolver.go b/pkg/engine/k8s/policy/metadata/metadata_resolver.go index 851d3eaee..d661a6991 100644 --- a/pkg/engine/k8s/policy/metadata/metadata_resolver.go +++ b/pkg/engine/k8s/policy/metadata/metadata_resolver.go @@ -159,6 +159,7 @@ func (r *Resolver) setTypeRefsForAdditionalTypeInstances(policy *policy.Policy, } } +// TODO(storage) set also rule.TypeRef.Revision.. func (r *Resolver) setTypeRefsForBackendTypeInstances(policy *policy.Policy, typeRefs map[string]TypeRefWithAdditionalRefs) { for ruleIdx, rule := range policy.TypeInstance.Rules { typeRef, exists := typeRefs[rule.Backend.ID] diff --git a/pkg/hub/api/graphql/local/models_gen.go b/pkg/hub/api/graphql/local/models_gen.go index ee96578d6..a72c29a1e 100644 --- a/pkg/hub/api/graphql/local/models_gen.go +++ b/pkg/hub/api/graphql/local/models_gen.go @@ -31,9 +31,9 @@ type CreateTypeInstanceInput struct { CreatedBy *string `json:"createdBy"` TypeRef *TypeInstanceTypeReferenceInput `json:"typeRef"` Attributes []*AttributeReferenceInput `json:"attributes"` + Value interface{} `json:"value"` // If not provided, TypeInstance value is stored as static value in Local Hub core storage. Backend *TypeInstanceBackendInput `json:"backend"` - Value interface{} `json:"value"` } type CreateTypeInstanceOutput struct { @@ -58,6 +58,7 @@ type TypeInstance struct { TypeRef *TypeInstanceTypeReference `json:"typeRef"` Uses []*TypeInstance `json:"uses"` UsedBy []*TypeInstance `json:"usedBy"` + Backend *TypeInstanceBackend `json:"backend"` LatestResourceVersion *TypeInstanceResourceVersion `json:"latestResourceVersion"` FirstResourceVersion *TypeInstanceResourceVersion `json:"firstResourceVersion"` PreviousResourceVersion *TypeInstanceResourceVersion `json:"previousResourceVersion"` @@ -66,7 +67,8 @@ type TypeInstance struct { } type TypeInstanceBackend struct { - ID string `json:"id"` + ID string `json:"id"` + Abstract bool `json:"abstract"` } type TypeInstanceBackendInput struct { @@ -109,7 +111,6 @@ type TypeInstanceResourceVersion struct { CreatedBy *string `json:"createdBy"` Metadata *TypeInstanceResourceVersionMetadata `json:"metadata"` Spec *TypeInstanceResourceVersionSpec `json:"spec"` - Backend *TypeInstanceBackend `json:"backend"` } type TypeInstanceResourceVersionMetadata struct { diff --git a/pkg/hub/api/graphql/local/schema_gen.go b/pkg/hub/api/graphql/local/schema_gen.go index cdd9a1ef0..c581912e2 100644 --- a/pkg/hub/api/graphql/local/schema_gen.go +++ b/pkg/hub/api/graphql/local/schema_gen.go @@ -72,6 +72,7 @@ type ComplexityRoot struct { } TypeInstance struct { + Backend func(childComplexity int) int FirstResourceVersion func(childComplexity int) int ID func(childComplexity int) int LatestResourceVersion func(childComplexity int) int @@ -85,7 +86,8 @@ type ComplexityRoot struct { } TypeInstanceBackend struct { - ID func(childComplexity int) int + Abstract func(childComplexity int) int + ID func(childComplexity int) int } TypeInstanceInstrumentation struct { @@ -110,7 +112,6 @@ type ComplexityRoot struct { } TypeInstanceResourceVersion struct { - Backend func(childComplexity int) int CreatedBy func(childComplexity int) int Metadata func(childComplexity int) int ResourceVersion func(childComplexity int) int @@ -284,6 +285,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Query.TypeInstances(childComplexity, args["filter"].(*TypeInstanceFilter)), true + case "TypeInstance.backend": + if e.complexity.TypeInstance.Backend == nil { + break + } + + return e.complexity.TypeInstance.Backend(childComplexity), true + case "TypeInstance.firstResourceVersion": if e.complexity.TypeInstance.FirstResourceVersion == nil { break @@ -359,6 +367,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.TypeInstance.Uses(childComplexity), true + case "TypeInstanceBackend.abstract": + if e.complexity.TypeInstanceBackend.Abstract == nil { + break + } + + return e.complexity.TypeInstanceBackend.Abstract(childComplexity), true + case "TypeInstanceBackend.id": if e.complexity.TypeInstanceBackend.ID == nil { break @@ -429,13 +444,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.TypeInstanceInstrumentationMetricsDashboard.URL(childComplexity), true - case "TypeInstanceResourceVersion.backend": - if e.complexity.TypeInstanceResourceVersion.Backend == nil { - break - } - - return e.complexity.TypeInstanceResourceVersion.Backend(childComplexity), true - case "TypeInstanceResourceVersion.createdBy": if e.complexity.TypeInstanceResourceVersion.CreatedBy == nil { break @@ -567,10 +575,10 @@ var sources = []*ast.Source{ # To make it work for other graphql client we need to add them to the schema manually, based on: # https://github.com/neo4j-graphql/neo4j-graphql-js/blob/master/src/augment/directives.js directive @relation( - name: String - direction: String - from: String - to: String + name: String + direction: String + from: String + to: String ) on FIELD_DEFINITION | OBJECT directive @cypher(statement: String) on FIELD_DEFINITION @@ -598,455 +606,474 @@ LockOwner defines owner name who locked a given TypeInstance scalar LockOwnerID type TypeInstance { - id: ID! @id - - lockedBy: LockOwnerID - - """ - Common properties for all TypeInstances which cannot be changed - """ - typeRef: TypeInstanceTypeReference! - @relation(name: "OF_TYPE", direction: "OUT") - uses: [TypeInstance!]! @relation(name: "USES", direction: "OUT") - usedBy: [TypeInstance!]! @relation(name: "USES", direction: "IN") - - latestResourceVersion: TypeInstanceResourceVersion - @cypher( - statement: "MATCH (this)-[:CONTAINS]->(tir:TypeInstanceResourceVersion) RETURN tir ORDER BY tir.resourceVersion DESC LIMIT 1" - ) - firstResourceVersion: TypeInstanceResourceVersion - @cypher( - statement: "MATCH (this)-[:CONTAINS]->(tir:TypeInstanceResourceVersion) RETURN tir ORDER BY tir.resourceVersion ASC LIMIT 1" - ) - previousResourceVersion: TypeInstanceResourceVersion - @cypher( - statement: "MATCH (this)-[:CONTAINS]->(tir:TypeInstanceResourceVersion) RETURN tir ORDER BY tir.resourceVersion DESC SKIP 1 LIMIT 1" - ) - resourceVersion(resourceVersion: Int!): TypeInstanceResourceVersion - @cypher( - statement: "MATCH (this)-[:CONTAINS]->(tir:TypeInstanceResourceVersion {resourceVersion: $resourceVersion}) RETURN tir" - ) - resourceVersions: [TypeInstanceResourceVersion!]! - @relation(name: "CONTAINS", direction: "OUT") + id: ID! @id + + lockedBy: LockOwnerID + + """ + Common properties for all TypeInstances which cannot be changed + """ + typeRef: TypeInstanceTypeReference! + @relation(name: "OF_TYPE", direction: "OUT") + uses: [TypeInstance!]! @relation(name: "USES", direction: "OUT") + usedBy: [TypeInstance!]! @relation(name: "USES", direction: "IN") + backend: TypeInstanceBackend! @relation(name: "STORED_IN", direction: "OUT") + + latestResourceVersion: TypeInstanceResourceVersion + @cypher( + statement: "MATCH (this)-[:CONTAINS]->(tir:TypeInstanceResourceVersion) RETURN tir ORDER BY tir.resourceVersion DESC LIMIT 1" + ) + firstResourceVersion: TypeInstanceResourceVersion + @cypher( + statement: "MATCH (this)-[:CONTAINS]->(tir:TypeInstanceResourceVersion) RETURN tir ORDER BY tir.resourceVersion ASC LIMIT 1" + ) + previousResourceVersion: TypeInstanceResourceVersion + @cypher( + statement: "MATCH (this)-[:CONTAINS]->(tir:TypeInstanceResourceVersion) RETURN tir ORDER BY tir.resourceVersion DESC SKIP 1 LIMIT 1" + ) + resourceVersion(resourceVersion: Int!): TypeInstanceResourceVersion + @cypher( + statement: "MATCH (this)-[:CONTAINS]->(tir:TypeInstanceResourceVersion {resourceVersion: $resourceVersion}) RETURN tir" + ) + resourceVersions: [TypeInstanceResourceVersion!]! + @relation(name: "CONTAINS", direction: "OUT") } type TypeInstanceResourceVersion { - resourceVersion: Int! @index - createdBy: String + resourceVersion: Int! @index + createdBy: String - metadata: TypeInstanceResourceVersionMetadata! - @relation(name: "DESCRIBED_BY", direction: "OUT") - spec: TypeInstanceResourceVersionSpec! - @relation(name: "SPECIFIED_BY", direction: "OUT") - - # TODO: can be changed to TypeInstance, and a proper relation set - backend: TypeInstanceBackend! + metadata: TypeInstanceResourceVersionMetadata! + @relation(name: "DESCRIBED_BY", direction: "OUT") + spec: TypeInstanceResourceVersionSpec! + @relation(name: "SPECIFIED_BY", direction: "OUT") } type TypeInstanceResourceVersionMetadata { - attributes: [AttributeReference!] - @relation(name: "CHARACTERIZED_BY", direction: "OUT") + attributes: [AttributeReference!] + @relation(name: "CHARACTERIZED_BY", direction: "OUT") } type TypeInstanceResourceVersionSpec { - value: Any! - @cypher( - statement: """ - RETURN apoc.convert.fromJsonMap(this.value) - """ - ) + value: Any! + @cypher( + statement: """ + RETURN apoc.convert.fromJsonMap(this.value) + """ + ) - """ - CURRENTLY NOT IMPLEMENTED - """ - instrumentation: TypeInstanceInstrumentation - @relation(name: "INSTRUMENTED_WITH", direction: "OUT") + """ + CURRENTLY NOT IMPLEMENTED + """ + instrumentation: TypeInstanceInstrumentation + @relation(name: "INSTRUMENTED_WITH", direction: "OUT") } type TypeInstanceBackend { - id: String! + id: String! + abstract: Boolean! } type TypeInstanceTypeReference { - path: NodePath! - revision: Version! + path: NodePath! + revision: Version! } input AttributeReferenceInput { - path: NodePath! - revision: Version! + path: NodePath! + revision: Version! } type AttributeReference { - path: NodePath! - revision: Version! + path: NodePath! + revision: Version! } """ CURRENTLY NOT IMPLEMENTED """ type TypeInstanceInstrumentation { - metrics: TypeInstanceInstrumentationMetrics - @relation(name: "MEASURED_BY", direction: "OUT") - health: TypeInstanceInstrumentationHealth - @relation(name: "INDICATED_BY", direction: "OUT") + metrics: TypeInstanceInstrumentationMetrics + @relation(name: "MEASURED_BY", direction: "OUT") + health: TypeInstanceInstrumentationHealth + @relation(name: "INDICATED_BY", direction: "OUT") } """ CURRENTLY NOT IMPLEMENTED """ type TypeInstanceInstrumentationMetrics { - endpoint: String - regex: String # optional regex for scraping metrics - dashboards: [TypeInstanceInstrumentationMetricsDashboard!]! - @relation(name: "ON", direction: "OUT") + endpoint: String + regex: String # optional regex for scraping metrics + dashboards: [TypeInstanceInstrumentationMetricsDashboard!]! + @relation(name: "ON", direction: "OUT") } """ CURRENTLY NOT IMPLEMENTED """ type TypeInstanceInstrumentationMetricsDashboard { - url: String! + url: String! } """ CURRENTLY NOT IMPLEMENTED """ type TypeInstanceInstrumentationHealth { - url: String - method: HTTPRequestMethod + url: String + method: HTTPRequestMethod - # resolver, which does a HTTP call on a given URL - # and expects status code greater than or equal to 200 - # and less than 400 - # TODO implement TypeInstance health check, for resolution of this field - status: TypeInstanceInstrumentationHealthStatus + # resolver, which does a HTTP call on a given URL + # and expects status code greater than or equal to 200 + # and less than 400 + # TODO implement TypeInstance health check, for resolution of this field + status: TypeInstanceInstrumentationHealthStatus } """ CURRENTLY NOT IMPLEMENTED """ enum TypeInstanceInstrumentationHealthStatus { - UNKNOWN - READY - FAILING + UNKNOWN + READY + FAILING } enum HTTPRequestMethod { - GET - POST + GET + POST } input AttributeFilterInput { - path: NodePath! - rule: FilterRule = INCLUDE + path: NodePath! + rule: FilterRule = INCLUDE - """ - If not provided, any revision of the Attribute applies to this filter - """ - revision: Version + """ + If not provided, any revision of the Attribute applies to this filter + """ + revision: Version } enum FilterRule { - INCLUDE - EXCLUDE + INCLUDE + EXCLUDE } input TypeInstanceFilter { - attributes: [AttributeFilterInput] - typeRef: TypeRefFilterInput - createdBy: String + attributes: [AttributeFilterInput] + typeRef: TypeRefFilterInput + createdBy: String } input TypeRefFilterInput { - path: NodePath! + path: NodePath! - """ - If not provided, it returns TypeInstances for all revisions of given Type - """ - revision: Version + """ + If not provided, it returns TypeInstances for all revisions of given Type + """ + revision: Version } input TypeInstanceTypeReferenceInput { - path: NodePath! - revision: Version! + path: NodePath! + revision: Version! } input TypeInstanceBackendInput { - id: String! + id: String! } input CreateTypeInstanceInput { - """ - Used to define the relationships, between the created TypeInstances - """ - alias: String + """ + Used to define the relationships, between the created TypeInstances + """ + alias: String - createdBy: String - typeRef: TypeInstanceTypeReferenceInput! - attributes: [AttributeReferenceInput!] - """ - If not provided, TypeInstance value is stored as static value in Local Hub core storage. - """ - backend: TypeInstanceBackendInput - value: Any + createdBy: String + typeRef: TypeInstanceTypeReferenceInput! + attributes: [AttributeReferenceInput!] + value: Any + """ + If not provided, TypeInstance value is stored as static value in Local Hub core storage. + """ + backend: TypeInstanceBackendInput } input TypeInstanceUsesRelationInput { - """ - Can be existing TypeInstance ID or alias of a TypeInstance from typeInstances list - """ - from: String! + """ + Can be existing TypeInstance ID or alias of a TypeInstance from typeInstances list + """ + from: String! - """ - Can be existing TypeInstance ID or alias of a TypeInstance from typeInstances list - """ - to: String! + """ + Can be existing TypeInstance ID or alias of a TypeInstance from typeInstances list + """ + to: String! } input CreateTypeInstancesInput { - typeInstances: [CreateTypeInstanceInput!]! - usesRelations: [TypeInstanceUsesRelationInput!]! + typeInstances: [CreateTypeInstanceInput!]! + usesRelations: [TypeInstanceUsesRelationInput!]! } type CreateTypeInstanceOutput { - id: ID! - alias: String! + id: ID! + alias: String! } """ At least one property needs to be specified. """ input UpdateTypeInstanceInput { - """ - The attributes property is optional. If not provided, previous value is used. - """ - attributes: [AttributeReferenceInput!] + """ + The attributes property is optional. If not provided, previous value is used. + """ + attributes: [AttributeReferenceInput!] - """ - The value property is optional. If not provided, previous value is used. - """ - value: Any + """ + The value property is optional. If not provided, previous value is used. + """ + value: Any } input UpdateTypeInstancesInput { - """ - Allows you to update TypeInstances which are locked by a given ownerID. If not provided, - you can update only those TypeInstances which are not locked. - """ - ownerID: LockOwnerID - createdBy: String + """ + Allows you to update TypeInstances which are locked by a given ownerID. If not provided, + you can update only those TypeInstances which are not locked. + """ + ownerID: LockOwnerID + createdBy: String - id: ID! - typeInstance: UpdateTypeInstanceInput! + id: ID! + typeInstance: UpdateTypeInstanceInput! } input LockTypeInstancesInput { - ids: [ID!]! - ownerID: LockOwnerID! + ids: [ID!]! + ownerID: LockOwnerID! } input UnlockTypeInstancesInput { - ids: [ID!]! - ownerID: LockOwnerID! + ids: [ID!]! + ownerID: LockOwnerID! } type Query { - typeInstances(filter: TypeInstanceFilter = {}): [TypeInstance!]! - @cypher( - statement: """ - WITH [x IN $filter.attributes WHERE x.rule = "EXCLUDE" | x ] AS excluded, - [x IN $filter.attributes WHERE x.rule = "INCLUDE" | x ] AS included - - CALL { - WITH excluded - UNWIND excluded AS f - MATCH (ex:AttributeReference {path: f.path}) - WHERE (f.revision IS NULL) OR (ex.revision = f.revision) - RETURN collect(ex) as excludedAttributes - } - - MATCH (tir:TypeInstanceResourceVersion)-[:DESCRIBED_BY]->(meta:TypeInstanceResourceVersionMetadata) - OPTIONAL MATCH (meta)-[:CHARACTERIZED_BY]->(attr:AttributeReference) - MATCH (ti:TypeInstance)-[:OF_TYPE]->(typeRef:TypeInstanceTypeReference) - MATCH (ti:TypeInstance)-[:CONTAINS]->(tir) - WHERE - $filter = {} OR - ( - ( - $filter.typeRef IS NULL - OR - ( + typeInstances(filter: TypeInstanceFilter = {}): [TypeInstance!]! + @cypher( + statement: """ + WITH [x IN $filter.attributes WHERE x.rule = "EXCLUDE" | x ] AS excluded, + [x IN $filter.attributes WHERE x.rule = "INCLUDE" | x ] AS included + + CALL { + WITH excluded + UNWIND excluded AS f + MATCH (ex:AttributeReference {path: f.path}) + WHERE (f.revision IS NULL) OR (ex.revision = f.revision) + RETURN collect(ex) as excludedAttributes + } + + MATCH (tir:TypeInstanceResourceVersion)-[:DESCRIBED_BY]->(meta:TypeInstanceResourceVersionMetadata) + OPTIONAL MATCH (meta)-[:CHARACTERIZED_BY]->(attr:AttributeReference) + MATCH (ti:TypeInstance)-[:OF_TYPE]->(typeRef:TypeInstanceTypeReference) + MATCH (ti:TypeInstance)-[:CONTAINS]->(tir) + WHERE + $filter = {} OR + ( + ( + $filter.typeRef IS NULL + OR + ( ($filter.typeRef.revision IS NULL AND typeRef.path = $filter.typeRef.path) OR (typeRef.path = $filter.typeRef.path AND typeRef.revision = $filter.typeRef.revision) - ) - ) - AND - ($filter.createdBy IS NULL OR tir.createdBy = $filter.createdBy) - AND - ( - $filter.attributes IS NULL - OR - ( + ) + ) + AND + ($filter.createdBy IS NULL OR tir.createdBy = $filter.createdBy) + AND + ( + $filter.attributes IS NULL + OR + ( all(inc IN included WHERE - (tir)-[:DESCRIBED_BY]->(meta:TypeInstanceResourceVersionMetadata)-[:CHARACTERIZED_BY]->(attr:AttributeReference {path: inc.path}) - AND - (inc.revision IS NULL OR attr.revision = inc.revision) + (tir)-[:DESCRIBED_BY]->(meta:TypeInstanceResourceVersionMetadata)-[:CHARACTERIZED_BY]->(attr:AttributeReference {path: inc.path}) + AND + (inc.revision IS NULL OR attr.revision = inc.revision) ) AND none(exc IN excludedAttributes WHERE (tir)-[:DESCRIBED_BY]->(meta:TypeInstanceResourceVersionMetadata)-[:CHARACTERIZED_BY]->(exc)) - ) - ) - ) - - RETURN DISTINCT ti - """ + ) ) + ) - typeInstance(id: ID!): TypeInstance - @cypher( - statement: """ - MATCH (this:TypeInstance {id: $id}) - RETURN this - """ - ) + RETURN DISTINCT ti + """ + ) + + typeInstance(id: ID!): TypeInstance + @cypher( + statement: """ + MATCH (this:TypeInstance {id: $id}) + RETURN this + """ + ) } type Mutation { - createTypeInstances( - in: CreateTypeInstancesInput! - ): [CreateTypeInstanceOutput!]! - - # TODO extend input with TypeInstanceInstrumentation - createTypeInstance(in: CreateTypeInstanceInput!): TypeInstance! - @cypher( - statement: """ - WITH apoc.convert.toJson($in.value) as value - MERGE (typeRef:TypeInstanceTypeReference {path: $in.typeRef.path, revision: $in.typeRef.revision}) - - CREATE (ti:TypeInstance {id: apoc.create.uuid()}) - CREATE (ti)-[:OF_TYPE]->(typeRef) - - CREATE (tir: TypeInstanceResourceVersion {resourceVersion: 1, createdBy: $in.createdBy}) - CREATE (ti)-[:CONTAINS]->(tir) - - CREATE (tir)-[:DESCRIBED_BY]->(metadata: TypeInstanceResourceVersionMetadata) - CREATE (tir)-[:SPECIFIED_BY]->(spec: TypeInstanceResourceVersionSpec {value: value}) - - FOREACH (attr in $in.attributes | - MERGE (attrRef: AttributeReference {path: attr.path, revision: attr.revision}) - CREATE (metadata)-[:CHARACTERIZED_BY]->(attrRef) - ) - - RETURN ti - """ - ) - - updateTypeInstances(in: [UpdateTypeInstancesInput]!): [TypeInstance!]! - @cypher( - statement: """ - CALL { - UNWIND $in AS item - RETURN collect(item.id) as allInputIDs - } - - // Check if all TypeInstances were found - WITH * - CALL { - WITH allInputIDs - MATCH (ti:TypeInstance) - WHERE ti.id IN allInputIDs - WITH collect(ti.id) as foundIDs - RETURN foundIDs - } - CALL apoc.util.validate(size(foundIDs) < size(allInputIDs), apoc.convert.toJson({code: 404, ids: foundIDs}), null) - - // Check if given TypeInstances are not already locked by others - WITH * - CALL { - WITH * - UNWIND $in AS item - MATCH (tic:TypeInstance {id: item.id}) - WHERE tic.lockedBy IS NOT NULL AND (item.ownerID IS NULL OR tic.lockedBy <> item.ownerID) - WITH collect(tic.id) as lockedIDs - RETURN lockedIDs - } - CALL apoc.util.validate(size(lockedIDs) > 0, apoc.convert.toJson({code: 409, ids: lockedIDs}), null) - - UNWIND $in as item - MATCH (ti: TypeInstance {id: item.id}) - CALL { - WITH ti - MATCH (ti)-[:CONTAINS]->(latestRevision:TypeInstanceResourceVersion) - RETURN latestRevision - ORDER BY latestRevision.resourceVersion DESC LIMIT 1 - } - - CREATE (tir: TypeInstanceResourceVersion {resourceVersion: latestRevision.resourceVersion + 1, createdBy: item.createdBy}) - CREATE (ti)-[:CONTAINS]->(tir) - - // Handle the ` + "`" + `spec.value` + "`" + ` property - CREATE (spec: TypeInstanceResourceVersionSpec) - CREATE (tir)-[:SPECIFIED_BY]->(spec) - - WITH ti, tir, spec, latestRevision, item - CALL apoc.do.when( - item.typeInstance.value IS NOT NULL, - ' - SET spec.value = apoc.convert.toJson(item.typeInstance.value) RETURN spec - ', - ' - MATCH (latestRevision)-[:SPECIFIED_BY]->(latestSpec: TypeInstanceResourceVersionSpec) - SET spec.value = latestSpec.value RETURN spec - ', - {spec:spec, latestRevision: latestRevision, item: item}) YIELD value - - // Handle the ` + "`" + `metadata.attributes` + "`" + ` property - CREATE (metadata: TypeInstanceResourceVersionMetadata) - CREATE (tir)-[:DESCRIBED_BY]->(metadata) - - WITH ti, tir, latestRevision, metadata, item - CALL apoc.do.when( - item.typeInstance.attributes IS NOT NULL, - ' - FOREACH (attr in item.typeInstance.attributes | + createTypeInstances( + in: CreateTypeInstancesInput! + ): [CreateTypeInstanceOutput!]! + + # TODO extend input with TypeInstanceInstrumentation + createTypeInstance(in: CreateTypeInstanceInput!): TypeInstance! + @cypher( + statement: """ + CREATE (ti:TypeInstance {id: apoc.create.uuid()}) + + // Backend + WITH * + CALL apoc.do.when( + $in.backend.id IS NOT NULL, + 'RETURN $in.backend.id as id', + ' + // TODO(storage): this should be resolved by Local Hub server during the insertion, not in cypher. + MATCH (backend:TypeInstance)-[:OF_TYPE]->(typeRef {path: "cap.core.type.hub.storage.neo4j"}) + RETURN backend.id as id + ', + {in: $in} + ) YIELD value as backend + MATCH (toTi:TypeInstance {id: backend.id}) + CREATE (ti)-[:USES]->(toTi) + // TODO(storage): It should be taken from the uses relation but we don't have access to the TypeRef.additionalRefs to check + // if a given type is a backend or not. Maybe we will introduce a dedicated property to distinguish them from others. + MERGE (tiBackend:TypeInstanceBackend {abstract: false, id: backend.id}) + CREATE (ti)-[:STORED_IN]->(tiBackend) + + // TypeRef + MERGE (typeRef:TypeInstanceTypeReference {path: $in.typeRef.path, revision: $in.typeRef.revision}) + CREATE (ti)-[:OF_TYPE]->(typeRef) + + // Revision + CREATE (tir: TypeInstanceResourceVersion {resourceVersion: 1, createdBy: $in.createdBy}) + CREATE (ti)-[:CONTAINS]->(tir) + + CREATE (tir)-[:DESCRIBED_BY]->(metadata: TypeInstanceResourceVersionMetadata) + CREATE (tir)-[:SPECIFIED_BY]->(spec: TypeInstanceResourceVersionSpec {value: apoc.convert.toJson($in.value)}) + + FOREACH (attr in $in.attributes | + MERGE (attrRef: AttributeReference {path: attr.path, revision: attr.revision}) + CREATE (metadata)-[:CHARACTERIZED_BY]->(attrRef) + ) + + RETURN ti + """ + ) + + updateTypeInstances(in: [UpdateTypeInstancesInput]!): [TypeInstance!]! + @cypher( + statement: """ + CALL { + UNWIND $in AS item + RETURN collect(item.id) as allInputIDs + } + + // Check if all TypeInstances were found + WITH * + CALL { + WITH allInputIDs + MATCH (ti:TypeInstance) + WHERE ti.id IN allInputIDs + WITH collect(ti.id) as foundIDs + RETURN foundIDs + } + CALL apoc.util.validate(size(foundIDs) < size(allInputIDs), apoc.convert.toJson({code: 404, ids: foundIDs}), null) + + // Check if given TypeInstances are not already locked by others + WITH * + CALL { + WITH * + UNWIND $in AS item + MATCH (tic:TypeInstance {id: item.id}) + WHERE tic.lockedBy IS NOT NULL AND (item.ownerID IS NULL OR tic.lockedBy <> item.ownerID) + WITH collect(tic.id) as lockedIDs + RETURN lockedIDs + } + CALL apoc.util.validate(size(lockedIDs) > 0, apoc.convert.toJson({code: 409, ids: lockedIDs}), null) + + UNWIND $in as item + MATCH (ti: TypeInstance {id: item.id}) + CALL { + WITH ti + MATCH (ti)-[:CONTAINS]->(latestRevision:TypeInstanceResourceVersion) + RETURN latestRevision + ORDER BY latestRevision.resourceVersion DESC LIMIT 1 + } + + CREATE (tir: TypeInstanceResourceVersion {resourceVersion: latestRevision.resourceVersion + 1, createdBy: item.createdBy}) + CREATE (ti)-[:CONTAINS]->(tir) + + // Handle the ` + "`" + `spec.value` + "`" + ` property + CREATE (spec: TypeInstanceResourceVersionSpec) + CREATE (tir)-[:SPECIFIED_BY]->(spec) + + WITH ti, tir, spec, latestRevision, item + CALL apoc.do.when( + item.typeInstance.value IS NOT NULL, + ' + SET spec.value = apoc.convert.toJson(item.typeInstance.value) RETURN spec + ', + ' + MATCH (latestRevision)-[:SPECIFIED_BY]->(latestSpec: TypeInstanceResourceVersionSpec) + SET spec.value = latestSpec.value RETURN spec + ', + {spec:spec, latestRevision: latestRevision, item: item}) YIELD value + + // Handle the ` + "`" + `metadata.attributes` + "`" + ` property + CREATE (metadata: TypeInstanceResourceVersionMetadata) + CREATE (tir)-[:DESCRIBED_BY]->(metadata) + + WITH ti, tir, latestRevision, metadata, item + CALL apoc.do.when( + item.typeInstance.attributes IS NOT NULL, + ' + FOREACH (attr in item.typeInstance.attributes | MERGE (attrRef: AttributeReference {path: attr.path, revision: attr.revision}) CREATE (metadata)-[:CHARACTERIZED_BY]->(attrRef) - ) - - RETURN metadata - ', - ' - OPTIONAL MATCH (latestRevision)-[:DESCRIBED_BY]->(TypeInstanceResourceVersionMetadata)-[:CHARACTERIZED_BY]->(latestAttrRef: AttributeReference) - WHERE latestAttrRef IS NOT NULL - WITH *, COLLECT(latestAttrRef) AS latestAttrRefs - FOREACH (attr in latestAttrRefs | + ) + + RETURN metadata + ', + ' + OPTIONAL MATCH (latestRevision)-[:DESCRIBED_BY]->(TypeInstanceResourceVersionMetadata)-[:CHARACTERIZED_BY]->(latestAttrRef: AttributeReference) + WHERE latestAttrRef IS NOT NULL + WITH *, COLLECT(latestAttrRef) AS latestAttrRefs + FOREACH (attr in latestAttrRefs | CREATE (metadata)-[:CHARACTERIZED_BY]->(attr) - ) + ) - RETURN metadata - ', - {metadata: metadata, latestRevision: latestRevision, item: item} - ) YIELD value + RETURN metadata + ', + {metadata: metadata, latestRevision: latestRevision, item: item} + ) YIELD value - RETURN ti - """ - ) + RETURN ti + """ + ) - deleteTypeInstance(id: ID!, ownerID: LockOwnerID): ID! + deleteTypeInstance(id: ID!, ownerID: LockOwnerID): ID! - """ - Mark given TypeInstances as locked by a given owner. - If at least one TypeInstance is already locked with different OwnerID, an error is returned. - """ - lockTypeInstances(in: LockTypeInstancesInput!): [ID!]! + """ + Mark given TypeInstances as locked by a given owner. + If at least one TypeInstance is already locked with different OwnerID, an error is returned. + """ + lockTypeInstances(in: LockTypeInstancesInput!): [ID!]! - """ - Remove lock from given TypeInstances. - If at least one TypeInstance was not locked by a given owner, an error is returned. - """ - unlockTypeInstances(in: UnlockTypeInstancesInput!): [ID!]! + """ + Remove lock from given TypeInstances. + If at least one TypeInstance was not locked by a given owner, an error is returned. + """ + unlockTypeInstances(in: UnlockTypeInstancesInput!): [ID!]! } # TODO: Prepare directive for user authorization in https://github.com/capactio/capact/issues/508 @@ -1523,7 +1550,7 @@ func (ec *executionContext) _Mutation_createTypeInstance(ctx context.Context, fi return ec.resolvers.Mutation().CreateTypeInstance(rctx, args["in"].(CreateTypeInstanceInput)) } directive1 := func(ctx context.Context) (interface{}, error) { - statement, err := ec.unmarshalOString2ᚖstring(ctx, "WITH apoc.convert.toJson($in.value) as value\nMERGE (typeRef:TypeInstanceTypeReference {path: $in.typeRef.path, revision: $in.typeRef.revision})\n\nCREATE (ti:TypeInstance {id: apoc.create.uuid()})\nCREATE (ti)-[:OF_TYPE]->(typeRef)\n\nCREATE (tir: TypeInstanceResourceVersion {resourceVersion: 1, createdBy: $in.createdBy})\nCREATE (ti)-[:CONTAINS]->(tir)\n\nCREATE (tir)-[:DESCRIBED_BY]->(metadata: TypeInstanceResourceVersionMetadata)\nCREATE (tir)-[:SPECIFIED_BY]->(spec: TypeInstanceResourceVersionSpec {value: value})\n\nFOREACH (attr in $in.attributes |\nMERGE (attrRef: AttributeReference {path: attr.path, revision: attr.revision})\nCREATE (metadata)-[:CHARACTERIZED_BY]->(attrRef)\n)\n\nRETURN ti") + statement, err := ec.unmarshalOString2ᚖstring(ctx, "CREATE (ti:TypeInstance {id: apoc.create.uuid()})\n\n// Backend\nWITH *\nCALL apoc.do.when(\n $in.backend.id IS NOT NULL,\n 'RETURN $in.backend.id as id',\n '\n // TODO(storage): this should be resolved by Local Hub server during the insertion, not in cypher.\n MATCH (backend:TypeInstance)-[:OF_TYPE]->(typeRef {path: \"cap.core.type.hub.storage.neo4j\"})\n RETURN backend.id as id\n ',\n {in: $in}\n) YIELD value as backend\nMATCH (toTi:TypeInstance {id: backend.id})\nCREATE (ti)-[:USES]->(toTi)\n// TODO(storage): It should be taken from the uses relation but we don't have access to the TypeRef.additionalRefs to check\n// if a given type is a backend or not. Maybe we will introduce a dedicated property to distinguish them from others.\nMERGE (tiBackend:TypeInstanceBackend {abstract: false, id: backend.id})\nCREATE (ti)-[:STORED_IN]->(tiBackend)\n\n// TypeRef\nMERGE (typeRef:TypeInstanceTypeReference {path: $in.typeRef.path, revision: $in.typeRef.revision})\nCREATE (ti)-[:OF_TYPE]->(typeRef)\n\n// Revision\nCREATE (tir: TypeInstanceResourceVersion {resourceVersion: 1, createdBy: $in.createdBy})\nCREATE (ti)-[:CONTAINS]->(tir)\n\nCREATE (tir)-[:DESCRIBED_BY]->(metadata: TypeInstanceResourceVersionMetadata)\nCREATE (tir)-[:SPECIFIED_BY]->(spec: TypeInstanceResourceVersionSpec {value: apoc.convert.toJson($in.value)})\n\nFOREACH (attr in $in.attributes |\n MERGE (attrRef: AttributeReference {path: attr.path, revision: attr.revision})\n CREATE (metadata)-[:CHARACTERIZED_BY]->(attrRef)\n)\n\nRETURN ti") if err != nil { return nil, err } @@ -1589,7 +1616,7 @@ func (ec *executionContext) _Mutation_updateTypeInstances(ctx context.Context, f return ec.resolvers.Mutation().UpdateTypeInstances(rctx, args["in"].([]*UpdateTypeInstancesInput)) } directive1 := func(ctx context.Context) (interface{}, error) { - statement, err := ec.unmarshalOString2ᚖstring(ctx, "CALL {\nUNWIND $in AS item\nRETURN collect(item.id) as allInputIDs\n}\n\n// Check if all TypeInstances were found\nWITH *\nCALL {\nWITH allInputIDs\nMATCH (ti:TypeInstance)\nWHERE ti.id IN allInputIDs\nWITH collect(ti.id) as foundIDs\nRETURN foundIDs\n}\nCALL apoc.util.validate(size(foundIDs) < size(allInputIDs), apoc.convert.toJson({code: 404, ids: foundIDs}), null)\n\n// Check if given TypeInstances are not already locked by others\nWITH *\nCALL {\nWITH *\nUNWIND $in AS item\nMATCH (tic:TypeInstance {id: item.id})\nWHERE tic.lockedBy IS NOT NULL AND (item.ownerID IS NULL OR tic.lockedBy <> item.ownerID)\nWITH collect(tic.id) as lockedIDs\nRETURN lockedIDs\n}\nCALL apoc.util.validate(size(lockedIDs) > 0, apoc.convert.toJson({code: 409, ids: lockedIDs}), null)\n\nUNWIND $in as item\nMATCH (ti: TypeInstance {id: item.id})\nCALL {\nWITH ti\nMATCH (ti)-[:CONTAINS]->(latestRevision:TypeInstanceResourceVersion)\nRETURN latestRevision\nORDER BY latestRevision.resourceVersion DESC LIMIT 1\n}\n\nCREATE (tir: TypeInstanceResourceVersion {resourceVersion: latestRevision.resourceVersion + 1, createdBy: item.createdBy})\nCREATE (ti)-[:CONTAINS]->(tir)\n\n// Handle the `spec.value` property\nCREATE (spec: TypeInstanceResourceVersionSpec)\nCREATE (tir)-[:SPECIFIED_BY]->(spec)\n\nWITH ti, tir, spec, latestRevision, item\nCALL apoc.do.when(\nitem.typeInstance.value IS NOT NULL,\n'\nSET spec.value = apoc.convert.toJson(item.typeInstance.value) RETURN spec\n',\n'\nMATCH (latestRevision)-[:SPECIFIED_BY]->(latestSpec: TypeInstanceResourceVersionSpec)\nSET spec.value = latestSpec.value RETURN spec\n',\n{spec:spec, latestRevision: latestRevision, item: item}) YIELD value\n\n// Handle the `metadata.attributes` property\nCREATE (metadata: TypeInstanceResourceVersionMetadata)\nCREATE (tir)-[:DESCRIBED_BY]->(metadata)\n\nWITH ti, tir, latestRevision, metadata, item\nCALL apoc.do.when(\nitem.typeInstance.attributes IS NOT NULL,\n'\nFOREACH (attr in item.typeInstance.attributes |\nMERGE (attrRef: AttributeReference {path: attr.path, revision: attr.revision})\nCREATE (metadata)-[:CHARACTERIZED_BY]->(attrRef)\n)\n\nRETURN metadata\n',\n'\nOPTIONAL MATCH (latestRevision)-[:DESCRIBED_BY]->(TypeInstanceResourceVersionMetadata)-[:CHARACTERIZED_BY]->(latestAttrRef: AttributeReference)\nWHERE latestAttrRef IS NOT NULL\nWITH *, COLLECT(latestAttrRef) AS latestAttrRefs\nFOREACH (attr in latestAttrRefs |\nCREATE (metadata)-[:CHARACTERIZED_BY]->(attr)\n)\n\nRETURN metadata\n',\n{metadata: metadata, latestRevision: latestRevision, item: item}\n) YIELD value\n\nRETURN ti") + statement, err := ec.unmarshalOString2ᚖstring(ctx, "CALL {\n UNWIND $in AS item\n RETURN collect(item.id) as allInputIDs\n}\n\n// Check if all TypeInstances were found\nWITH *\nCALL {\n WITH allInputIDs\n MATCH (ti:TypeInstance)\n WHERE ti.id IN allInputIDs\n WITH collect(ti.id) as foundIDs\n RETURN foundIDs\n}\nCALL apoc.util.validate(size(foundIDs) < size(allInputIDs), apoc.convert.toJson({code: 404, ids: foundIDs}), null)\n\n// Check if given TypeInstances are not already locked by others\nWITH *\nCALL {\n WITH *\n UNWIND $in AS item\n MATCH (tic:TypeInstance {id: item.id})\n WHERE tic.lockedBy IS NOT NULL AND (item.ownerID IS NULL OR tic.lockedBy <> item.ownerID)\n WITH collect(tic.id) as lockedIDs\n RETURN lockedIDs\n}\nCALL apoc.util.validate(size(lockedIDs) > 0, apoc.convert.toJson({code: 409, ids: lockedIDs}), null)\n\nUNWIND $in as item\nMATCH (ti: TypeInstance {id: item.id})\nCALL {\n WITH ti\n MATCH (ti)-[:CONTAINS]->(latestRevision:TypeInstanceResourceVersion)\n RETURN latestRevision\n ORDER BY latestRevision.resourceVersion DESC LIMIT 1\n}\n\nCREATE (tir: TypeInstanceResourceVersion {resourceVersion: latestRevision.resourceVersion + 1, createdBy: item.createdBy})\nCREATE (ti)-[:CONTAINS]->(tir)\n\n// Handle the `spec.value` property\nCREATE (spec: TypeInstanceResourceVersionSpec)\nCREATE (tir)-[:SPECIFIED_BY]->(spec)\n\nWITH ti, tir, spec, latestRevision, item\nCALL apoc.do.when(\n item.typeInstance.value IS NOT NULL,\n '\n SET spec.value = apoc.convert.toJson(item.typeInstance.value) RETURN spec\n ',\n '\n MATCH (latestRevision)-[:SPECIFIED_BY]->(latestSpec: TypeInstanceResourceVersionSpec)\n SET spec.value = latestSpec.value RETURN spec\n ',\n {spec:spec, latestRevision: latestRevision, item: item}) YIELD value\n\n// Handle the `metadata.attributes` property\nCREATE (metadata: TypeInstanceResourceVersionMetadata)\nCREATE (tir)-[:DESCRIBED_BY]->(metadata)\n\nWITH ti, tir, latestRevision, metadata, item\nCALL apoc.do.when(\n item.typeInstance.attributes IS NOT NULL,\n '\n FOREACH (attr in item.typeInstance.attributes |\n MERGE (attrRef: AttributeReference {path: attr.path, revision: attr.revision})\n CREATE (metadata)-[:CHARACTERIZED_BY]->(attrRef)\n )\n\n RETURN metadata\n ',\n '\n OPTIONAL MATCH (latestRevision)-[:DESCRIBED_BY]->(TypeInstanceResourceVersionMetadata)-[:CHARACTERIZED_BY]->(latestAttrRef: AttributeReference)\n WHERE latestAttrRef IS NOT NULL\n WITH *, COLLECT(latestAttrRef) AS latestAttrRefs\n FOREACH (attr in latestAttrRefs |\n CREATE (metadata)-[:CHARACTERIZED_BY]->(attr)\n )\n\n RETURN metadata\n ',\n {metadata: metadata, latestRevision: latestRevision, item: item}\n) YIELD value\n\nRETURN ti") if err != nil { return nil, err } @@ -1781,7 +1808,7 @@ func (ec *executionContext) _Query_typeInstances(ctx context.Context, field grap return ec.resolvers.Query().TypeInstances(rctx, args["filter"].(*TypeInstanceFilter)) } directive1 := func(ctx context.Context) (interface{}, error) { - statement, err := ec.unmarshalOString2ᚖstring(ctx, "WITH [x IN $filter.attributes WHERE x.rule = \"EXCLUDE\" | x ] AS excluded,\n[x IN $filter.attributes WHERE x.rule = \"INCLUDE\" | x ] AS included\n\nCALL {\nWITH excluded\nUNWIND excluded AS f\nMATCH (ex:AttributeReference {path: f.path})\nWHERE (f.revision IS NULL) OR (ex.revision = f.revision)\nRETURN collect(ex) as excludedAttributes\n}\n\nMATCH (tir:TypeInstanceResourceVersion)-[:DESCRIBED_BY]->(meta:TypeInstanceResourceVersionMetadata)\nOPTIONAL MATCH (meta)-[:CHARACTERIZED_BY]->(attr:AttributeReference)\nMATCH (ti:TypeInstance)-[:OF_TYPE]->(typeRef:TypeInstanceTypeReference)\nMATCH (ti:TypeInstance)-[:CONTAINS]->(tir)\nWHERE\n$filter = {} OR\n(\n(\n$filter.typeRef IS NULL\nOR\n(\n($filter.typeRef.revision IS NULL AND typeRef.path = $filter.typeRef.path)\nOR\n(typeRef.path = $filter.typeRef.path AND typeRef.revision = $filter.typeRef.revision)\n)\n)\nAND\n($filter.createdBy IS NULL OR tir.createdBy = $filter.createdBy)\nAND\n(\n$filter.attributes IS NULL\nOR\n(\nall(inc IN included WHERE\n(tir)-[:DESCRIBED_BY]->(meta:TypeInstanceResourceVersionMetadata)-[:CHARACTERIZED_BY]->(attr:AttributeReference {path: inc.path})\nAND\n(inc.revision IS NULL OR attr.revision = inc.revision)\n)\nAND\nnone(exc IN excludedAttributes WHERE (tir)-[:DESCRIBED_BY]->(meta:TypeInstanceResourceVersionMetadata)-[:CHARACTERIZED_BY]->(exc))\n)\n)\n)\n\nRETURN DISTINCT ti") + statement, err := ec.unmarshalOString2ᚖstring(ctx, "WITH [x IN $filter.attributes WHERE x.rule = \"EXCLUDE\" | x ] AS excluded,\n [x IN $filter.attributes WHERE x.rule = \"INCLUDE\" | x ] AS included\n\nCALL {\n WITH excluded\n UNWIND excluded AS f\n MATCH (ex:AttributeReference {path: f.path})\n WHERE (f.revision IS NULL) OR (ex.revision = f.revision)\n RETURN collect(ex) as excludedAttributes\n}\n\nMATCH (tir:TypeInstanceResourceVersion)-[:DESCRIBED_BY]->(meta:TypeInstanceResourceVersionMetadata)\nOPTIONAL MATCH (meta)-[:CHARACTERIZED_BY]->(attr:AttributeReference)\nMATCH (ti:TypeInstance)-[:OF_TYPE]->(typeRef:TypeInstanceTypeReference)\nMATCH (ti:TypeInstance)-[:CONTAINS]->(tir)\nWHERE\n$filter = {} OR\n(\n (\n $filter.typeRef IS NULL\n OR\n (\n ($filter.typeRef.revision IS NULL AND typeRef.path = $filter.typeRef.path)\n OR\n (typeRef.path = $filter.typeRef.path AND typeRef.revision = $filter.typeRef.revision)\n )\n )\n AND\n ($filter.createdBy IS NULL OR tir.createdBy = $filter.createdBy)\n AND\n (\n \t$filter.attributes IS NULL\n OR\n (\n all(inc IN included WHERE\n (tir)-[:DESCRIBED_BY]->(meta:TypeInstanceResourceVersionMetadata)-[:CHARACTERIZED_BY]->(attr:AttributeReference {path: inc.path})\n AND\n (inc.revision IS NULL OR attr.revision = inc.revision)\n )\n AND\n none(exc IN excludedAttributes WHERE (tir)-[:DESCRIBED_BY]->(meta:TypeInstanceResourceVersionMetadata)-[:CHARACTERIZED_BY]->(exc))\n )\n )\n)\n\nRETURN DISTINCT ti") if err != nil { return nil, err } @@ -2228,6 +2255,69 @@ func (ec *executionContext) _TypeInstance_usedBy(ctx context.Context, field grap return ec.marshalNTypeInstance2ᚕᚖcapactᚗioᚋcapactᚋpkgᚋhubᚋapiᚋgraphqlᚋlocalᚐTypeInstanceᚄ(ctx, field.Selections, res) } +func (ec *executionContext) _TypeInstance_backend(ctx context.Context, field graphql.CollectedField, obj *TypeInstance) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "TypeInstance", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Backend, nil + } + directive1 := func(ctx context.Context) (interface{}, error) { + name, err := ec.unmarshalOString2ᚖstring(ctx, "STORED_IN") + if err != nil { + return nil, err + } + direction, err := ec.unmarshalOString2ᚖstring(ctx, "OUT") + if err != nil { + return nil, err + } + if ec.directives.Relation == nil { + return nil, errors.New("directive relation is not implemented") + } + return ec.directives.Relation(ctx, obj, directive0, name, direction, nil, nil) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, graphql.ErrorOnPath(ctx, err) + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.(*TypeInstanceBackend); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *capact.io/capact/pkg/hub/api/graphql/local.TypeInstanceBackend`, tmp) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*TypeInstanceBackend) + fc.Result = res + return ec.marshalNTypeInstanceBackend2ᚖcapactᚗioᚋcapactᚋpkgᚋhubᚋapiᚋgraphqlᚋlocalᚐTypeInstanceBackend(ctx, field.Selections, res) +} + func (ec *executionContext) _TypeInstance_latestResourceVersion(ctx context.Context, field graphql.CollectedField, obj *TypeInstance) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -2557,6 +2647,41 @@ func (ec *executionContext) _TypeInstanceBackend_id(ctx context.Context, field g return ec.marshalNString2string(ctx, field.Selections, res) } +func (ec *executionContext) _TypeInstanceBackend_abstract(ctx context.Context, field graphql.CollectedField, obj *TypeInstanceBackend) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "TypeInstanceBackend", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Abstract, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(bool) + fc.Result = res + return ec.marshalNBoolean2bool(ctx, field.Selections, res) +} + func (ec *executionContext) _TypeInstanceInstrumentation_metrics(ctx context.Context, field graphql.CollectedField, obj *TypeInstanceInstrumentation) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -3148,41 +3273,6 @@ func (ec *executionContext) _TypeInstanceResourceVersion_spec(ctx context.Contex return ec.marshalNTypeInstanceResourceVersionSpec2ᚖcapactᚗioᚋcapactᚋpkgᚋhubᚋapiᚋgraphqlᚋlocalᚐTypeInstanceResourceVersionSpec(ctx, field.Selections, res) } -func (ec *executionContext) _TypeInstanceResourceVersion_backend(ctx context.Context, field graphql.CollectedField, obj *TypeInstanceResourceVersion) (ret graphql.Marshaler) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - fc := &graphql.FieldContext{ - Object: "TypeInstanceResourceVersion", - Field: field, - Args: nil, - IsMethod: false, - IsResolver: false, - } - - ctx = graphql.WithFieldContext(ctx, fc) - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return obj.Backend, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(*TypeInstanceBackend) - fc.Result = res - return ec.marshalNTypeInstanceBackend2ᚖcapactᚗioᚋcapactᚋpkgᚋhubᚋapiᚋgraphqlᚋlocalᚐTypeInstanceBackend(ctx, field.Selections, res) -} - func (ec *executionContext) _TypeInstanceResourceVersionMetadata_attributes(ctx context.Context, field graphql.CollectedField, obj *TypeInstanceResourceVersionMetadata) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -4625,19 +4715,19 @@ func (ec *executionContext) unmarshalInputCreateTypeInstanceInput(ctx context.Co if err != nil { return it, err } - case "backend": + case "value": var err error - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("backend")) - it.Backend, err = ec.unmarshalOTypeInstanceBackendInput2ᚖcapactᚗioᚋcapactᚋpkgᚋhubᚋapiᚋgraphqlᚋlocalᚐTypeInstanceBackendInput(ctx, v) + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("value")) + it.Value, err = ec.unmarshalOAny2interface(ctx, v) if err != nil { return it, err } - case "value": + case "backend": var err error - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("value")) - it.Value, err = ec.unmarshalOAny2interface(ctx, v) + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("backend")) + it.Backend, err = ec.unmarshalOTypeInstanceBackendInput2ᚖcapactᚗioᚋcapactᚋpkgᚋhubᚋapiᚋgraphqlᚋlocalᚐTypeInstanceBackendInput(ctx, v) if err != nil { return it, err } @@ -5159,6 +5249,11 @@ func (ec *executionContext) _TypeInstance(ctx context.Context, sel ast.Selection if out.Values[i] == graphql.Null { invalids++ } + case "backend": + out.Values[i] = ec._TypeInstance_backend(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } case "latestResourceVersion": out.Values[i] = ec._TypeInstance_latestResourceVersion(ctx, field, obj) case "firstResourceVersion": @@ -5199,6 +5294,11 @@ func (ec *executionContext) _TypeInstanceBackend(ctx context.Context, sel ast.Se if out.Values[i] == graphql.Null { invalids++ } + case "abstract": + out.Values[i] = ec._TypeInstanceBackend_abstract(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -5350,11 +5450,6 @@ func (ec *executionContext) _TypeInstanceResourceVersion(ctx context.Context, se if out.Values[i] == graphql.Null { invalids++ } - case "backend": - out.Values[i] = ec._TypeInstanceResourceVersion_backend(ctx, field, obj) - if out.Values[i] == graphql.Null { - invalids++ - } default: panic("unknown field " + strconv.Quote(field.Name)) } diff --git a/pkg/hub/client/local/fields.go b/pkg/hub/client/local/fields.go index 6c4d3d939..b63c4dfd7 100644 --- a/pkg/hub/client/local/fields.go +++ b/pkg/hub/client/local/fields.go @@ -7,6 +7,7 @@ import ( var typeInstancesFieldsRegistry = map[TypeInstancesQueryFields]string{ TypeInstanceRootFields: rootFields, TypeInstanceTypeRefFields: typeRefFields, + TypeInstanceBackendFields: backendFields, TypeInstanceUsesIDField: usesIDField, TypeInstanceUsedByIDField: usedByIDField, TypeInstanceLatestResourceVersionField: latestResourceVersionField, @@ -27,6 +28,12 @@ var ( revision }` + backendFields = ` + backend { + id + abstract + }` + usedByIDField = ` usedBy { id @@ -57,6 +64,8 @@ var ( %s + %s + latestResourceVersion { %s } @@ -64,18 +73,18 @@ var ( firstResourceVersion { %s } - + previousResourceVersion { %s } - + resourceVersions { %s } - + resourceVersion(resourceVersion: 1) { %s - }`, rootFields, typeRefFields, + }`, rootFields, typeRefFields, backendFields, typeInstanceResourceVersion, typeInstanceResourceVersion, typeInstanceResourceVersion, typeInstanceResourceVersion, typeInstanceResourceVersion) ) diff --git a/pkg/hub/client/local/options.go b/pkg/hub/client/local/options.go index 33b6f39cd..358520473 100644 --- a/pkg/hub/client/local/options.go +++ b/pkg/hub/client/local/options.go @@ -4,7 +4,7 @@ import ( "strings" ) -// TypeInstancesQueryFields allows to configure which fields should be returned for TypeInstance query. +// TypeInstancesQueryFields allows configuring which fields should be returned for TypeInstance query. type TypeInstancesQueryFields uint64 const ( @@ -12,6 +12,8 @@ const ( TypeInstanceRootFields TypeInstancesQueryFields = 1 << iota // TypeInstanceTypeRefFields returns TypeInstance's TypeRef fields. TypeInstanceTypeRefFields + // TypeInstanceBackendFields returns TypeInstance's Backend fields. + TypeInstanceBackendFields // TypeInstanceUsedByIDField returns IDs for UsedBy field. TypeInstanceUsedByIDField // TypeInstanceUsesIDField returns IDs for Uses field. diff --git a/pkg/hub/client/policy_enforced_client.go b/pkg/hub/client/policy_enforced_client.go index 39ffe9f48..a703226fa 100644 --- a/pkg/hub/client/policy_enforced_client.go +++ b/pkg/hub/client/policy_enforced_client.go @@ -110,6 +110,7 @@ func (e *PolicyEnforcedClient) ListTypeInstancesBackendsBasedOnPolicy(_ context. // 1. Global Defaults based on TypeRefs for _, rule := range e.mergedPolicy.TypeInstance.Rules { + // ensure that rule.TypeRef.Revision is resolved! out.SetByTypeRef(rule.TypeRef, rule.Backend) } diff --git a/pkg/sdk/renderer/argo/renderer.go b/pkg/sdk/renderer/argo/renderer.go index 9484586d3..43fd5c27e 100644 --- a/pkg/sdk/renderer/argo/renderer.go +++ b/pkg/sdk/renderer/argo/renderer.go @@ -17,8 +17,6 @@ import ( ) const ( - // UserInputName is exported so we can use that as a reference in `capact act create` validation process. - UserInputName = "input-parameters" runnerContext = "runner-context" ) @@ -189,11 +187,9 @@ func (r *Renderer) Render(ctx context.Context, input *RenderInput) (*RenderOutpu return nil, errors.Wrap(err, "while resolving TypeInstance backend based on Policy") } - r.log.Debug("AAaaaaaaAaaaaaaAaaaaaaAaaaaaaAaaaaaaAaaaaaaAaaaaaaAaaaaaaAaaaaaaAaaaaaaAaaaaaaAaaaaaaAaaaaaa") if err := dedicatedRenderer.addOutputTypeInstancesToGraph(nil, "", iface, &implementation, availableArtifacts, typeInstancesBackends, newArtifactMappings); err != nil { return nil, errors.Wrap(err, "while noting output artifacts") } - r.log.Debug("AAaaaaaaAaaaaaaAaaaaaaAaaaaaaAaaaaaaAaaaaaaAaaaaaaAaaaaaaAaaaaaaAaaaaaaAaaaaaaAaaaaaaAaaaaaa") // 10. Render rootWorkflow templates _, err = dedicatedRenderer.RenderTemplateSteps(ctxWithTimeout, rootWorkflow, RootImplementation{ diff --git a/pkg/sdk/renderer/argo/typeinstance_handler.go b/pkg/sdk/renderer/argo/typeinstance_handler.go index 0dd47f4d1..f7627408a 100644 --- a/pkg/sdk/renderer/argo/typeinstance_handler.go +++ b/pkg/sdk/renderer/argo/typeinstance_handler.go @@ -137,7 +137,7 @@ func (r *TypeInstanceHandler) AddUploadTypeInstancesStep(rootWorkflow *Workflow, } for _, ti := range output.typeInstances { - payload.TypeInstances = append(payload.TypeInstances, &graphqllocal.CreateTypeInstanceInput{ + gqlTI := &graphqllocal.CreateTypeInstanceInput{ Alias: ti.ArtifactName, CreatedBy: &ownerID, TypeRef: &graphqllocal.TypeInstanceTypeReferenceInput{ @@ -145,10 +145,13 @@ func (r *TypeInstanceHandler) AddUploadTypeInstancesStep(rootWorkflow *Workflow, Revision: ti.TypeInstance.TypeRef.Revision, }, Attributes: []*graphqllocal.AttributeReferenceInput{}, - Backend: &graphqllocal.TypeInstanceBackendInput{ + } + if ti.Backend.ID != "" { + gqlTI.Backend = &graphqllocal.TypeInstanceBackendInput{ ID: ti.Backend.ID, - }, - }) + } + } + payload.TypeInstances = append(payload.TypeInstances, gqlTI) artifacts = append(artifacts, wfv1.Artifact{ Name: *ti.ArtifactName, @@ -162,7 +165,6 @@ func (r *TypeInstanceHandler) AddUploadTypeInstancesStep(rootWorkflow *Workflow, } for _, relation := range output.relations { - // TODO(https://github.com/capactio/capact/issues/604): add relations to used Backend TypeInstance. payload.UsesRelations = append(payload.UsesRelations, &graphqllocal.TypeInstanceUsesRelationInput{ From: *relation.From, To: *relation.To, diff --git a/test/e2e/action_test.go b/test/e2e/action_test.go index c3f403528..a649b15dc 100644 --- a/test/e2e/action_test.go +++ b/test/e2e/action_test.go @@ -4,6 +4,7 @@ package e2e import ( + "capact.io/capact/internal/cli/heredoc" "context" "encoding/json" "fmt" @@ -47,18 +48,57 @@ var _ = Describe("Action", func() { actionName = getActionName() // Ensure Test Policy - updateGlobalPolicy(ctx, engineClient, nil) + setGlobalTestPolicy(ctx, engineClient) }) AfterEach(func() { // cleanup Action engineClient.DeleteAction(ctx, actionName) - engineClient.DeleteAction(ctx, failingActionName) + //engineClient.DeleteAction(ctx, failingActionName) }) + const actionPath = "cap.interface.capactio.capact.validation.action.passing" Context("Action execution", func() { - It("should pick proper Implementation and inject TypeInstance based on cluster policy", func() { + It("should pick proper Implementation B", func() { actionPath := "cap.interface.capactio.capact.validation.action.passing" + testValue := "Implementation B" + + By("1. Preparing input Type Instances") + By("1.1 Creating TypeInstance which will be downloaded") + download := getTypeInstanceInputForDownload(testValue) + downloadTI, downloadTICleanup := createTypeInstance(ctx, hubClient, download) + defer downloadTICleanup() + By("1.2 Create TypeInstance which is required for Implementation B to be picked based on Policy") + typeInstanceValue := getTypeInstanceInputForPolicy() + typeInstance, tiCleanupFn := createTypeInstance(ctx, hubClient, typeInstanceValue) + defer tiCleanupFn() + injectedTypeInstanceID := typeInstance.ID + + inputData := &enginegraphql.ActionInputData{ + TypeInstances: []*enginegraphql.InputTypeInstanceData{ + {Name: "testInput", ID: downloadTI.ID}, + }, + } + builtinStorage := getBuiltinStorageTypeInstance(ctx, hubClient) + + By("2. Modifying Policy to make Implementation B picked for next run...") + globalPolicyRequiredTypeInstances := enginegraphql.RequiredTypeInstanceReferenceInput{ + ID: injectedTypeInstanceID, + Description: ptr.String("Test TypeInstance"), + } + setGlobalTestPolicy(ctx, engineClient, prependInjectRuleForPassingActionInterface(globalPolicyRequiredTypeInstances)) + + By("3. Expecting Implementation B is picked based on test policy...") + action := createActionAndWaitForReadyToRunPhase(ctx, engineClient, actionName, actionPath, inputData) + assertActionRenderedWorkflowContains(action, "echo 'Implementation B'") + runActionAndWaitForSucceeded(ctx, engineClient, actionName) + + By("4. Check Uploaded TypeInstances") + assertUploadedTypeInstance(ctx, hubClient, testValue, builtinStorage.Backend) + assertOutputTypeInstancesInActionStatus(ctx, engineClient, action.Name, HaveLen(1)) + }) + + It("should pick Implementation A with default Policy", func() { testValue := "Implementation A" By("1. Preparing input Type Instances") @@ -73,22 +113,31 @@ var _ = Describe("Action", func() { updateTI, updateTICleanup := createTypeInstance(ctx, hubClient, update) defer updateTICleanup() - typeInstances := []*enginegraphql.InputTypeInstanceData{ - {Name: "testUpdate", ID: updateTI.ID}, - {Name: "testInput", ID: downloadTI.ID}, - } + By("1.3 Creating TypeInstance that describes Helm storage") + helmStorage := getTypeInstanceHelmStorage() + helmStorageTI, helmStorageTICleanup := createTypeInstance(ctx, hubClient, helmStorage) + defer helmStorageTICleanup() inputData := &enginegraphql.ActionInputData{ - TypeInstances: typeInstances, + TypeInstances: []*enginegraphql.InputTypeInstanceData{ + {Name: "testUpdate", ID: updateTI.ID}, + {Name: "testInput", ID: downloadTI.ID}, + }, } - By("1.3 Create TypeInstance which is required for Implementation B to be picked based on Policy") - typeInstanceValue := getTypeInstanceInputForPolicy() - typeInstance, tiCleanupFn := createTypeInstance(ctx, hubClient, typeInstanceValue) - defer tiCleanupFn() - injectedTypeInstanceID := typeInstance.ID - - By("2. Expecting Implementation A is picked based on test policy and requirements met...") + builtinStorage := getBuiltinStorageTypeInstance(ctx, hubClient) + expUpdatedTI := &enginegraphql.OutputTypeInstanceDetails{ + ID: updateTI.ID, + TypeRef: &enginegraphql.ManifestReference{ + Path: updateTI.TypeRef.Path, + Revision: updateTI.TypeRef.Revision, + }, + Backend: &enginegraphql.TypeInstanceBackendDetails{ + ID: builtinStorage.ID, + Abstract: builtinStorage.Backend.Abstract, + }, + } + By("2. Expecting Implementation A is picked and builtin storage is used...") action := createActionAndWaitForReadyToRunPhase(ctx, engineClient, actionName, actionPath, inputData) assertActionRenderedWorkflowContains(action, "echo 'Implementation A'") @@ -96,17 +145,8 @@ var _ = Describe("Action", func() { By("3. Check TypeInstances") By("3.1 Check uploaded TypeInstances") - assertUploadedTypeInstance(ctx, hubClient, testValue) - - assertOutputTypeInstancesInActionStatus(ctx, engineClient, action.Name, And(ContainElement( - &enginegraphql.OutputTypeInstanceDetails{ - ID: updateTI.ID, - TypeRef: &enginegraphql.ManifestReference{ - Path: updateTI.TypeRef.Path, - Revision: updateTI.TypeRef.Revision, - }, - }, - ), HaveLen(2))) + assertUploadedTypeInstance(ctx, hubClient, testValue, &hublocalgraphql.TypeInstanceBackend{ID: builtinStorage.ID, Abstract: true}) + assertOutputTypeInstancesInActionStatus(ctx, engineClient, action.Name, And(ContainElement(expUpdatedTI), HaveLen(2))) By("3.2 Check updated TypeInstances") updateTI, err := hubClient.FindTypeInstance(ctx, updateTI.ID) @@ -123,23 +163,20 @@ var _ = Describe("Action", func() { By("3.4 Waiting for Action deleted") waitForActionDeleted(ctx, engineClient, actionName) - By("4. Modifying Policy to make Implementation B picked for next run...") - globalPolicyRequiredTypeInstances := enginegraphql.RequiredTypeInstanceReferenceInput{ - ID: injectedTypeInstanceID, - Description: ptr.String("Test TypeInstance"), - } - updateGlobalPolicy(ctx, engineClient, &globalPolicyRequiredTypeInstances) + By("4. Modifying Policy to change backend storage for uploaded TypeInstance via TypeRef...") + setGlobalTestPolicy(ctx, engineClient, withHelmBackendForUploadTypeRef(helmStorageTI.ID)) - By("5. Expecting Implementation B is picked based on test policy...") + By("5. Expecting Implementation A is picked and the Helm storage is used for uploaded TypeInstance...") action = createActionAndWaitForReadyToRunPhase(ctx, engineClient, actionName, actionPath, inputData) - assertActionRenderedWorkflowContains(action, "echo 'Implementation B'") + assertActionRenderedWorkflowContains(action, "echo 'Implementation A'") runActionAndWaitForSucceeded(ctx, engineClient, actionName) By("6. Check Uploaded TypeInstances") - assertUploadedTypeInstance(ctx, hubClient, testValue) - - By("7. Check output TypeInstances in Action status") - assertOutputTypeInstancesInActionStatus(ctx, engineClient, action.Name, HaveLen(1)) + assertUploadedTypeInstance(ctx, hubClient, testValue, &hublocalgraphql.TypeInstanceBackend{ID: helmStorageTI.ID, Abstract: false}) + assertOutputTypeInstancesInActionStatus(ctx, engineClient, action.Name, And( + HaveLen(2), + ContainElement(expUpdatedTI), + )) }) It("should have failed status after a failed workflow", func() { @@ -319,6 +356,40 @@ func getTypeInstanceInputForUpdate() *hublocalgraphql.CreateTypeInstanceInput { } } +func getTypeInstanceHelmStorage() *hublocalgraphql.CreateTypeInstanceInput { + return &hublocalgraphql.CreateTypeInstanceInput{ + TypeRef: &hublocalgraphql.TypeInstanceTypeReferenceInput{ + Path: "cap.type.helm.storage", + Revision: "0.1.0", + }, + Attributes: []*hublocalgraphql.AttributeReferenceInput{}, + Value: map[string]interface{}{ + "url": "helm-release.default:50051", + "acceptValue": true, + "contextSchema": heredoc.Doc(` + { + "$id": "#/properties/contextSchema", + "type": "object", + "required": [ + "name", + "namespace" + ], + "properties": { + "name": { + "$id": "#/properties/contextSchema/properties/name", + "type": "string" + }, + "namespace": { + "$id": "#/properties/contextSchema/properties/namespace", + "type": "string" + } + }, + "additionalProperties": false + }`), + }, + } +} + func createActionAndWaitForReadyToRunPhase(ctx context.Context, engineClient *engine.Client, actionName, actionPath string, input *enginegraphql.ActionInputData) *enginegraphql.Action { _, err := engineClient.CreateAction(ctx, &enginegraphql.ActionDetailsInput{ Name: actionName, @@ -389,18 +460,70 @@ func createTypeInstance(ctx context.Context, hubClient *hubclient.Client, in *hu return createdTypeInstance, cleanupFn } -func updateGlobalPolicy(ctx context.Context, client *engine.Client, reqTypeInstance *enginegraphql.RequiredTypeInstanceReferenceInput) { - var reqInput []*enginegraphql.RequiredTypeInstanceReferenceInput - // nils element are not handled by GraphQL - if reqTypeInstance != nil { - reqInput = append(reqInput, reqTypeInstance) +type policyOption func(*enginegraphql.PolicyInput) + +func withHelmBackendForUploadTypeRef(backendID string) policyOption { + return func(policy *enginegraphql.PolicyInput) { + policy.TypeInstance = &enginegraphql.TypeInstancePolicyInput{ + Rules: []*enginegraphql.RulesForTypeInstanceInput{ + { + TypeRef: &enginegraphql.ManifestReferenceInput{ + Path: "cap.type.capactio.capact.validation.upload", + Revision: ptr.String("0.1.0"), + }, + Backend: &enginegraphql.TypeInstanceBackendRuleInput{ + ID: backendID, + Description: ptr.String("Default Hub backend storage via TypeRef"), + }, + }, + }, + } + } +} + +func prependInjectRuleForPassingActionInterface(in enginegraphql.RequiredTypeInstanceReferenceInput) policyOption { + reqInput := []*enginegraphql.RequiredTypeInstanceReferenceInput{ + &in, + } + manifestRef := func(path string) []*enginegraphql.ManifestReferenceInput { + return []*enginegraphql.ManifestReferenceInput{ + { + Path: path, + }, + } } - p := PolicyInputTestFixture(reqInput) + return func(policy *enginegraphql.PolicyInput) { + for idx, rule := range policy.Interface.Rules { + if rule.Interface.Path != actionPassingPath { + continue + } + policy.Interface.Rules[idx].OneOf = append([]*enginegraphql.PolicyRuleInput{ + { + ImplementationConstraints: &enginegraphql.PolicyRuleImplementationConstraintsInput{ + Requires: manifestRef("cap.type.capactio.capact.validation.single-key"), + Attributes: manifestRef("cap.attribute.capactio.capact.validation.policy.most-preferred"), + }, + Inject: &enginegraphql.PolicyRuleInjectDataInput{ + RequiredTypeInstances: reqInput, + }, + }, + }, policy.Interface.Rules[idx].OneOf...) + } + } +} + +func setGlobalTestPolicy(ctx context.Context, client *engine.Client, opts ...policyOption) { + p := fixGQLTestPolicyInput() + + for _, opt := range opts { + opt(p) + } + _, err := client.UpdatePolicy(ctx, p) Expect(err).ToNot(HaveOccurred()) } -func assertUploadedTypeInstance(ctx context.Context, hubClient *hubclient.Client, testValue string) { +func assertUploadedTypeInstance(ctx context.Context, hubClient *hubclient.Client, expValue string, expBackend *hublocalgraphql.TypeInstanceBackend) { uploaded, err := hubClient.ListTypeInstances(ctx, &hublocalgraphql.TypeInstanceFilter{ TypeRef: &hublocalgraphql.TypeRefFilterInput{ Path: "cap.type.capactio.capact.validation.upload", @@ -410,13 +533,61 @@ func assertUploadedTypeInstance(ctx context.Context, hubClient *hubclient.Client Expect(err).ToNot(HaveOccurred()) Expect(len(uploaded)).Should(BeNumerically(">", 0)) - ti, err := getTypeInstanceWithValue(uploaded, testValue) + ti, err := getTypeInstanceWithValue(uploaded, expValue) Expect(err).ToNot(HaveOccurred()) + Expect(ti.Backend).Should(Equal(expBackend)) + err = hubClient.DeleteTypeInstance(ctx, ti.ID) Expect(err).ToNot(HaveOccurred()) } +func getBuiltinStorageTypeInstance(ctx context.Context, hubClient *hubclient.Client) hublocalgraphql.TypeInstance { + coreStorage, err := hubClient.ListTypeInstances(ctx, &hublocalgraphql.TypeInstanceFilter{ + TypeRef: &hublocalgraphql.TypeRefFilterInput{ + Path: "cap.core.type.hub.storage.neo4j", + Revision: ptr.String("0.1.0"), + }, + }) + Expect(err).ToNot(HaveOccurred()) + Expect(coreStorage).Should(HaveLen(1)) + + return coreStorage[0] +} + +func matchOutputTypeInstanceOnTypeRef(expected *enginegraphql.OutputTypeInstanceDetails) types.GomegaMatcher { + return &outputTypeInstanceOnTypeRefMatcher{ + expected: expected, + } +} + +type outputTypeInstanceOnTypeRefMatcher struct { + expected *enginegraphql.OutputTypeInstanceDetails +} + +func (matcher *outputTypeInstanceOnTypeRefMatcher) Match(actual interface{}) (success bool, err error) { + response, ok := actual.([]*enginegraphql.OutputTypeInstanceDetails) + if !ok { + return false, fmt.Errorf("MatchOutputTypeInstanceOnTypeRef matcher expects an enginegraphql.OutputTypeInstanceDetails") + } + + for idx := range response { + response[idx].ID = "" + } + // ignore IDs + matcher.expected.ID = "" + + return ContainElement(matcher.expected).Match(response) +} + +func (matcher *outputTypeInstanceOnTypeRefMatcher) FailureMessage(actual interface{}) (message string) { + return fmt.Sprintf("Expected\n\t%#v\nto contain element matching\n\t%#v", actual, matcher.expected) +} + +func (matcher *outputTypeInstanceOnTypeRefMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return fmt.Sprintf("Expected\n\t%#v\nnot to contain element matching\n\t%#v", actual, matcher.expected) +} + func assertOutputTypeInstancesInActionStatus(ctx context.Context, engineClient *engine.Client, actionName string, match types.GomegaMatcher, ) { @@ -461,12 +632,10 @@ func mapToInputParameters(params map[string]interface{}) (*enginegraphql.JSON, e return &res, nil } -func PolicyInputTestFixture(reqInput []*enginegraphql.RequiredTypeInstanceReferenceInput) *enginegraphql.PolicyInput { - manifestRef := func(path string) []*enginegraphql.ManifestReferenceInput { - return []*enginegraphql.ManifestReferenceInput{ - { - Path: path, - }, +func fixGQLTestPolicyInput() *enginegraphql.PolicyInput { + manifestRef := func(path string) *enginegraphql.ManifestReferenceInput { + return &enginegraphql.ManifestReferenceInput{ + Path: path, } } @@ -474,17 +643,8 @@ func PolicyInputTestFixture(reqInput []*enginegraphql.RequiredTypeInstanceRefere Interface: &enginegraphql.InterfacePolicyInput{ Rules: []*enginegraphql.RulesForInterfaceInput{ { - Interface: manifestRef(actionPassingPath)[0], + Interface: manifestRef(actionPassingPath), OneOf: []*enginegraphql.PolicyRuleInput{ - { - ImplementationConstraints: &enginegraphql.PolicyRuleImplementationConstraintsInput{ - Requires: manifestRef("cap.type.capactio.capact.validation.single-key"), - Attributes: manifestRef("cap.attribute.capactio.capact.validation.policy.most-preferred"), - }, - Inject: &enginegraphql.PolicyRuleInjectDataInput{ - RequiredTypeInstances: reqInput, - }, - }, { ImplementationConstraints: &enginegraphql.PolicyRuleImplementationConstraintsInput{ Path: ptr.String("cap.implementation.capactio.capact.validation.action.passing-a"), @@ -494,7 +654,7 @@ func PolicyInputTestFixture(reqInput []*enginegraphql.RequiredTypeInstanceRefere }, // allow all others { - Interface: manifestRef("cap.*")[0], + Interface: manifestRef("cap.*"), OneOf: []*enginegraphql.PolicyRuleInput{ { ImplementationConstraints: &enginegraphql.PolicyRuleImplementationConstraintsInput{}, diff --git a/test/e2e/e2e_suite_test.go b/test/e2e/e2e_suite_test.go index 5c78d3036..45fe3e941 100644 --- a/test/e2e/e2e_suite_test.go +++ b/test/e2e/e2e_suite_test.go @@ -59,8 +59,8 @@ var _ = BeforeSuite(func() { Expect(err).ToNot(HaveOccurred()) originalGlobalPolicy = input - waitTillServiceEndpointsAreReady() - waitTillDataIsPopulated() + //waitTillServiceEndpointsAreReady() + //waitTillDataIsPopulated() }) var _ = AfterSuite(func() { From 31d4f96ebe2fda43ebc23870bb22f8e012cb20e8 Mon Sep 17 00:00:00 2001 From: Mateusz Szostok Date: Fri, 11 Feb 2022 13:33:05 +0100 Subject: [PATCH 05/15] Fix abstract propagation, remove custom matcher, add more assertions --- hub-js/graphql/local/schema.graphql | 12 +- hub-js/src/schema/local.ts | 12 +- pkg/hub/api/graphql/local/models_gen.go | 12 +- pkg/hub/api/graphql/local/schema_gen.go | 68 ++++---- test/e2e/action_test.go | 214 ++++++++++++------------ 5 files changed, 162 insertions(+), 156 deletions(-) diff --git a/hub-js/graphql/local/schema.graphql b/hub-js/graphql/local/schema.graphql index 404643001..241e1b481 100644 --- a/hub-js/graphql/local/schema.graphql +++ b/hub-js/graphql/local/schema.graphql @@ -362,19 +362,23 @@ type Mutation { WITH * CALL apoc.do.when( $in.backend.id IS NOT NULL, - 'RETURN $in.backend.id as id', + ' + WITH false as abstract + RETURN $in.backend.id as id, abstract + ', ' // TODO(storage): this should be resolved by Local Hub server during the insertion, not in cypher. + WITH true as abstract MATCH (backend:TypeInstance)-[:OF_TYPE]->(typeRef {path: "cap.core.type.hub.storage.neo4j"}) - RETURN backend.id as id + RETURN backend.id as id, abstract ', {in: $in} ) YIELD value as backend - MATCH (backendTI:TypeInstance {id: backend.id})-[:STORED_IN]->(backendTIRef) + MATCH (backendTI:TypeInstance {id: backend.id}) CREATE (ti)-[:USES]->(backendTI) // TODO(storage): It should be taken from the uses relation but we don't have access to the TypeRef.additionalRefs to check // if a given type is a backend or not. Maybe we will introduce a dedicated property to distinguish them from others. - MERGE (storageRef:TypeInstanceBackendReference {abstract: backendTIRef.abstract, id: backendTIRef.id}) + MERGE (storageRef:TypeInstanceBackendReference {abstract: backend.abstract, id: backendTI.id}) CREATE (ti)-[:STORED_IN]->(storageRef) // TypeRef diff --git a/hub-js/src/schema/local.ts b/hub-js/src/schema/local.ts index be3acb996..22575c06c 100644 --- a/hub-js/src/schema/local.ts +++ b/hub-js/src/schema/local.ts @@ -79,19 +79,23 @@ export const schema = makeAugmentedSchema({ WITH * CALL apoc.do.when( typeInstance.backend.id IS NOT NULL, - 'RETURN $in.backend.id as id', + ' + WITH false as abstract + RETURN $in.backend.id as id, abstract + ', ' // TODO(storage): this should be resolved by Local Hub server during the insertion, not in cypher. + WITH true as abstract MATCH (backend:TypeInstance)-[:OF_TYPE]->(typeRef {path: "cap.core.type.hub.storage.neo4j", revision: "0.1.0"}) - RETURN backend.id as id + RETURN backend.id as id, abstract ', {in: typeInstance} ) YIELD value as backend - MATCH (backendTI:TypeInstance {id: backend.id})-[:STORED_IN]->(backendTIRef) + MATCH (backendTI:TypeInstance {id: backend.id}) CREATE (ti)-[:USES]->(backendTI) // TODO(storage): It should be taken from the uses relation but we don't have access to the TypeRef.additionalRefs to check // if a given type is a backend or not. Maybe we will introduce a dedicated property to distinguish them from others. - MERGE (storageRef:TypeInstanceBackendReference {abstract: backendTIRef.abstract, id: backendTI.id}) + MERGE (storageRef:TypeInstanceBackendReference {abstract: backend.abstract, id: backendTI.id}) CREATE (ti)-[:STORED_IN]->(storageRef) // TypeRef diff --git a/pkg/hub/api/graphql/local/models_gen.go b/pkg/hub/api/graphql/local/models_gen.go index a72c29a1e..bf9d71857 100644 --- a/pkg/hub/api/graphql/local/models_gen.go +++ b/pkg/hub/api/graphql/local/models_gen.go @@ -58,7 +58,7 @@ type TypeInstance struct { TypeRef *TypeInstanceTypeReference `json:"typeRef"` Uses []*TypeInstance `json:"uses"` UsedBy []*TypeInstance `json:"usedBy"` - Backend *TypeInstanceBackend `json:"backend"` + Backend *TypeInstanceBackendReference `json:"backend"` LatestResourceVersion *TypeInstanceResourceVersion `json:"latestResourceVersion"` FirstResourceVersion *TypeInstanceResourceVersion `json:"firstResourceVersion"` PreviousResourceVersion *TypeInstanceResourceVersion `json:"previousResourceVersion"` @@ -66,15 +66,15 @@ type TypeInstance struct { ResourceVersions []*TypeInstanceResourceVersion `json:"resourceVersions"` } -type TypeInstanceBackend struct { - ID string `json:"id"` - Abstract bool `json:"abstract"` -} - type TypeInstanceBackendInput struct { ID string `json:"id"` } +type TypeInstanceBackendReference struct { + ID string `json:"id"` + Abstract bool `json:"abstract"` +} + type TypeInstanceFilter struct { Attributes []*AttributeFilterInput `json:"attributes"` TypeRef *TypeRefFilterInput `json:"typeRef"` diff --git a/pkg/hub/api/graphql/local/schema_gen.go b/pkg/hub/api/graphql/local/schema_gen.go index c581912e2..93402c8f8 100644 --- a/pkg/hub/api/graphql/local/schema_gen.go +++ b/pkg/hub/api/graphql/local/schema_gen.go @@ -85,7 +85,7 @@ type ComplexityRoot struct { Uses func(childComplexity int) int } - TypeInstanceBackend struct { + TypeInstanceBackendReference struct { Abstract func(childComplexity int) int ID func(childComplexity int) int } @@ -367,19 +367,19 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.TypeInstance.Uses(childComplexity), true - case "TypeInstanceBackend.abstract": - if e.complexity.TypeInstanceBackend.Abstract == nil { + case "TypeInstanceBackendReference.abstract": + if e.complexity.TypeInstanceBackendReference.Abstract == nil { break } - return e.complexity.TypeInstanceBackend.Abstract(childComplexity), true + return e.complexity.TypeInstanceBackendReference.Abstract(childComplexity), true - case "TypeInstanceBackend.id": - if e.complexity.TypeInstanceBackend.ID == nil { + case "TypeInstanceBackendReference.id": + if e.complexity.TypeInstanceBackendReference.ID == nil { break } - return e.complexity.TypeInstanceBackend.ID(childComplexity), true + return e.complexity.TypeInstanceBackendReference.ID(childComplexity), true case "TypeInstanceInstrumentation.health": if e.complexity.TypeInstanceInstrumentation.Health == nil { @@ -617,7 +617,7 @@ type TypeInstance { @relation(name: "OF_TYPE", direction: "OUT") uses: [TypeInstance!]! @relation(name: "USES", direction: "OUT") usedBy: [TypeInstance!]! @relation(name: "USES", direction: "IN") - backend: TypeInstanceBackend! @relation(name: "STORED_IN", direction: "OUT") + backend: TypeInstanceBackendReference! @relation(name: "STORED_IN", direction: "OUT") latestResourceVersion: TypeInstanceResourceVersion @cypher( @@ -669,7 +669,7 @@ type TypeInstanceResourceVersionSpec { @relation(name: "INSTRUMENTED_WITH", direction: "OUT") } -type TypeInstanceBackend { +type TypeInstanceBackendReference { id: String! abstract: Boolean! } @@ -935,20 +935,24 @@ type Mutation { WITH * CALL apoc.do.when( $in.backend.id IS NOT NULL, - 'RETURN $in.backend.id as id', + ' + WITH false as abstract + RETURN $in.backend.id as id, abstract + ', ' // TODO(storage): this should be resolved by Local Hub server during the insertion, not in cypher. + WITH true as abstract MATCH (backend:TypeInstance)-[:OF_TYPE]->(typeRef {path: "cap.core.type.hub.storage.neo4j"}) - RETURN backend.id as id + RETURN backend.id as id, abstract ', {in: $in} ) YIELD value as backend - MATCH (toTi:TypeInstance {id: backend.id}) - CREATE (ti)-[:USES]->(toTi) + MATCH (backendTI:TypeInstance {id: backend.id}) + CREATE (ti)-[:USES]->(backendTI) // TODO(storage): It should be taken from the uses relation but we don't have access to the TypeRef.additionalRefs to check // if a given type is a backend or not. Maybe we will introduce a dedicated property to distinguish them from others. - MERGE (tiBackend:TypeInstanceBackend {abstract: false, id: backend.id}) - CREATE (ti)-[:STORED_IN]->(tiBackend) + MERGE (storageRef:TypeInstanceBackendReference {abstract: backend.abstract, id: backendTI.id}) + CREATE (ti)-[:STORED_IN]->(storageRef) // TypeRef MERGE (typeRef:TypeInstanceTypeReference {path: $in.typeRef.path, revision: $in.typeRef.revision}) @@ -1550,7 +1554,7 @@ func (ec *executionContext) _Mutation_createTypeInstance(ctx context.Context, fi return ec.resolvers.Mutation().CreateTypeInstance(rctx, args["in"].(CreateTypeInstanceInput)) } directive1 := func(ctx context.Context) (interface{}, error) { - statement, err := ec.unmarshalOString2ᚖstring(ctx, "CREATE (ti:TypeInstance {id: apoc.create.uuid()})\n\n// Backend\nWITH *\nCALL apoc.do.when(\n $in.backend.id IS NOT NULL,\n 'RETURN $in.backend.id as id',\n '\n // TODO(storage): this should be resolved by Local Hub server during the insertion, not in cypher.\n MATCH (backend:TypeInstance)-[:OF_TYPE]->(typeRef {path: \"cap.core.type.hub.storage.neo4j\"})\n RETURN backend.id as id\n ',\n {in: $in}\n) YIELD value as backend\nMATCH (toTi:TypeInstance {id: backend.id})\nCREATE (ti)-[:USES]->(toTi)\n// TODO(storage): It should be taken from the uses relation but we don't have access to the TypeRef.additionalRefs to check\n// if a given type is a backend or not. Maybe we will introduce a dedicated property to distinguish them from others.\nMERGE (tiBackend:TypeInstanceBackend {abstract: false, id: backend.id})\nCREATE (ti)-[:STORED_IN]->(tiBackend)\n\n// TypeRef\nMERGE (typeRef:TypeInstanceTypeReference {path: $in.typeRef.path, revision: $in.typeRef.revision})\nCREATE (ti)-[:OF_TYPE]->(typeRef)\n\n// Revision\nCREATE (tir: TypeInstanceResourceVersion {resourceVersion: 1, createdBy: $in.createdBy})\nCREATE (ti)-[:CONTAINS]->(tir)\n\nCREATE (tir)-[:DESCRIBED_BY]->(metadata: TypeInstanceResourceVersionMetadata)\nCREATE (tir)-[:SPECIFIED_BY]->(spec: TypeInstanceResourceVersionSpec {value: apoc.convert.toJson($in.value)})\n\nFOREACH (attr in $in.attributes |\n MERGE (attrRef: AttributeReference {path: attr.path, revision: attr.revision})\n CREATE (metadata)-[:CHARACTERIZED_BY]->(attrRef)\n)\n\nRETURN ti") + statement, err := ec.unmarshalOString2ᚖstring(ctx, "CREATE (ti:TypeInstance {id: apoc.create.uuid()})\n\n// Backend\nWITH *\nCALL apoc.do.when(\n $in.backend.id IS NOT NULL,\n '\n WITH false as abstract\n RETURN $in.backend.id as id, abstract\n ',\n '\n // TODO(storage): this should be resolved by Local Hub server during the insertion, not in cypher.\n WITH true as abstract\n MATCH (backend:TypeInstance)-[:OF_TYPE]->(typeRef {path: \"cap.core.type.hub.storage.neo4j\"})\n RETURN backend.id as id, abstract\n ',\n {in: $in}\n) YIELD value as backend\nMATCH (backendTI:TypeInstance {id: backend.id})\nCREATE (ti)-[:USES]->(backendTI)\n// TODO(storage): It should be taken from the uses relation but we don't have access to the TypeRef.additionalRefs to check\n// if a given type is a backend or not. Maybe we will introduce a dedicated property to distinguish them from others.\nMERGE (storageRef:TypeInstanceBackendReference {abstract: backend.abstract, id: backendTI.id})\nCREATE (ti)-[:STORED_IN]->(storageRef)\n\n// TypeRef\nMERGE (typeRef:TypeInstanceTypeReference {path: $in.typeRef.path, revision: $in.typeRef.revision})\nCREATE (ti)-[:OF_TYPE]->(typeRef)\n\n// Revision\nCREATE (tir: TypeInstanceResourceVersion {resourceVersion: 1, createdBy: $in.createdBy})\nCREATE (ti)-[:CONTAINS]->(tir)\n\nCREATE (tir)-[:DESCRIBED_BY]->(metadata: TypeInstanceResourceVersionMetadata)\nCREATE (tir)-[:SPECIFIED_BY]->(spec: TypeInstanceResourceVersionSpec {value: apoc.convert.toJson($in.value)})\n\nFOREACH (attr in $in.attributes |\n MERGE (attrRef: AttributeReference {path: attr.path, revision: attr.revision})\n CREATE (metadata)-[:CHARACTERIZED_BY]->(attrRef)\n)\n\nRETURN ti") if err != nil { return nil, err } @@ -2298,10 +2302,10 @@ func (ec *executionContext) _TypeInstance_backend(ctx context.Context, field gra if tmp == nil { return nil, nil } - if data, ok := tmp.(*TypeInstanceBackend); ok { + if data, ok := tmp.(*TypeInstanceBackendReference); ok { return data, nil } - return nil, fmt.Errorf(`unexpected type %T from directive, should be *capact.io/capact/pkg/hub/api/graphql/local.TypeInstanceBackend`, tmp) + return nil, fmt.Errorf(`unexpected type %T from directive, should be *capact.io/capact/pkg/hub/api/graphql/local.TypeInstanceBackendReference`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -2313,9 +2317,9 @@ func (ec *executionContext) _TypeInstance_backend(ctx context.Context, field gra } return graphql.Null } - res := resTmp.(*TypeInstanceBackend) + res := resTmp.(*TypeInstanceBackendReference) fc.Result = res - return ec.marshalNTypeInstanceBackend2ᚖcapactᚗioᚋcapactᚋpkgᚋhubᚋapiᚋgraphqlᚋlocalᚐTypeInstanceBackend(ctx, field.Selections, res) + return ec.marshalNTypeInstanceBackendReference2ᚖcapactᚗioᚋcapactᚋpkgᚋhubᚋapiᚋgraphqlᚋlocalᚐTypeInstanceBackendReference(ctx, field.Selections, res) } func (ec *executionContext) _TypeInstance_latestResourceVersion(ctx context.Context, field graphql.CollectedField, obj *TypeInstance) (ret graphql.Marshaler) { @@ -2612,7 +2616,7 @@ func (ec *executionContext) _TypeInstance_resourceVersions(ctx context.Context, return ec.marshalNTypeInstanceResourceVersion2ᚕᚖcapactᚗioᚋcapactᚋpkgᚋhubᚋapiᚋgraphqlᚋlocalᚐTypeInstanceResourceVersionᚄ(ctx, field.Selections, res) } -func (ec *executionContext) _TypeInstanceBackend_id(ctx context.Context, field graphql.CollectedField, obj *TypeInstanceBackend) (ret graphql.Marshaler) { +func (ec *executionContext) _TypeInstanceBackendReference_id(ctx context.Context, field graphql.CollectedField, obj *TypeInstanceBackendReference) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) @@ -2620,7 +2624,7 @@ func (ec *executionContext) _TypeInstanceBackend_id(ctx context.Context, field g } }() fc := &graphql.FieldContext{ - Object: "TypeInstanceBackend", + Object: "TypeInstanceBackendReference", Field: field, Args: nil, IsMethod: false, @@ -2647,7 +2651,7 @@ func (ec *executionContext) _TypeInstanceBackend_id(ctx context.Context, field g return ec.marshalNString2string(ctx, field.Selections, res) } -func (ec *executionContext) _TypeInstanceBackend_abstract(ctx context.Context, field graphql.CollectedField, obj *TypeInstanceBackend) (ret graphql.Marshaler) { +func (ec *executionContext) _TypeInstanceBackendReference_abstract(ctx context.Context, field graphql.CollectedField, obj *TypeInstanceBackendReference) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) @@ -2655,7 +2659,7 @@ func (ec *executionContext) _TypeInstanceBackend_abstract(ctx context.Context, f } }() fc := &graphql.FieldContext{ - Object: "TypeInstanceBackend", + Object: "TypeInstanceBackendReference", Field: field, Args: nil, IsMethod: false, @@ -5278,24 +5282,24 @@ func (ec *executionContext) _TypeInstance(ctx context.Context, sel ast.Selection return out } -var typeInstanceBackendImplementors = []string{"TypeInstanceBackend"} +var typeInstanceBackendReferenceImplementors = []string{"TypeInstanceBackendReference"} -func (ec *executionContext) _TypeInstanceBackend(ctx context.Context, sel ast.SelectionSet, obj *TypeInstanceBackend) graphql.Marshaler { - fields := graphql.CollectFields(ec.OperationContext, sel, typeInstanceBackendImplementors) +func (ec *executionContext) _TypeInstanceBackendReference(ctx context.Context, sel ast.SelectionSet, obj *TypeInstanceBackendReference) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, typeInstanceBackendReferenceImplementors) out := graphql.NewFieldSet(fields) var invalids uint32 for i, field := range fields { switch field.Name { case "__typename": - out.Values[i] = graphql.MarshalString("TypeInstanceBackend") + out.Values[i] = graphql.MarshalString("TypeInstanceBackendReference") case "id": - out.Values[i] = ec._TypeInstanceBackend_id(ctx, field, obj) + out.Values[i] = ec._TypeInstanceBackendReference_id(ctx, field, obj) if out.Values[i] == graphql.Null { invalids++ } case "abstract": - out.Values[i] = ec._TypeInstanceBackend_abstract(ctx, field, obj) + out.Values[i] = ec._TypeInstanceBackendReference_abstract(ctx, field, obj) if out.Values[i] == graphql.Null { invalids++ } @@ -6086,14 +6090,14 @@ func (ec *executionContext) marshalNTypeInstance2ᚖcapactᚗioᚋcapactᚋpkg return ec._TypeInstance(ctx, sel, v) } -func (ec *executionContext) marshalNTypeInstanceBackend2ᚖcapactᚗioᚋcapactᚋpkgᚋhubᚋapiᚋgraphqlᚋlocalᚐTypeInstanceBackend(ctx context.Context, sel ast.SelectionSet, v *TypeInstanceBackend) graphql.Marshaler { +func (ec *executionContext) marshalNTypeInstanceBackendReference2ᚖcapactᚗioᚋcapactᚋpkgᚋhubᚋapiᚋgraphqlᚋlocalᚐTypeInstanceBackendReference(ctx context.Context, sel ast.SelectionSet, v *TypeInstanceBackendReference) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { ec.Errorf(ctx, "must not be null") } return graphql.Null } - return ec._TypeInstanceBackend(ctx, sel, v) + return ec._TypeInstanceBackendReference(ctx, sel, v) } func (ec *executionContext) marshalNTypeInstanceInstrumentationMetricsDashboard2ᚕᚖcapactᚗioᚋcapactᚋpkgᚋhubᚋapiᚋgraphqlᚋlocalᚐTypeInstanceInstrumentationMetricsDashboardᚄ(ctx context.Context, sel ast.SelectionSet, v []*TypeInstanceInstrumentationMetricsDashboard) graphql.Marshaler { diff --git a/test/e2e/action_test.go b/test/e2e/action_test.go index a649b15dc..ea0ecdb15 100644 --- a/test/e2e/action_test.go +++ b/test/e2e/action_test.go @@ -47,64 +47,26 @@ var _ = Describe("Action", func() { hubClient = getHubGraphQLClient() actionName = getActionName() - // Ensure Test Policy + // Ensure default Test Policy setGlobalTestPolicy(ctx, engineClient) }) AfterEach(func() { // cleanup Action engineClient.DeleteAction(ctx, actionName) - //engineClient.DeleteAction(ctx, failingActionName) + engineClient.DeleteAction(ctx, failingActionName) }) const actionPath = "cap.interface.capactio.capact.validation.action.passing" Context("Action execution", func() { - It("should pick proper Implementation B", func() { - actionPath := "cap.interface.capactio.capact.validation.action.passing" - testValue := "Implementation B" - - By("1. Preparing input Type Instances") - By("1.1 Creating TypeInstance which will be downloaded") - download := getTypeInstanceInputForDownload(testValue) - downloadTI, downloadTICleanup := createTypeInstance(ctx, hubClient, download) - defer downloadTICleanup() - By("1.2 Create TypeInstance which is required for Implementation B to be picked based on Policy") - typeInstanceValue := getTypeInstanceInputForPolicy() - typeInstance, tiCleanupFn := createTypeInstance(ctx, hubClient, typeInstanceValue) - defer tiCleanupFn() - injectedTypeInstanceID := typeInstance.ID - - inputData := &enginegraphql.ActionInputData{ - TypeInstances: []*enginegraphql.InputTypeInstanceData{ - {Name: "testInput", ID: downloadTI.ID}, - }, - } - builtinStorage := getBuiltinStorageTypeInstance(ctx, hubClient) - - By("2. Modifying Policy to make Implementation B picked for next run...") - globalPolicyRequiredTypeInstances := enginegraphql.RequiredTypeInstanceReferenceInput{ - ID: injectedTypeInstanceID, - Description: ptr.String("Test TypeInstance"), - } - setGlobalTestPolicy(ctx, engineClient, prependInjectRuleForPassingActionInterface(globalPolicyRequiredTypeInstances)) - By("3. Expecting Implementation B is picked based on test policy...") - action := createActionAndWaitForReadyToRunPhase(ctx, engineClient, actionName, actionPath, inputData) - assertActionRenderedWorkflowContains(action, "echo 'Implementation B'") - runActionAndWaitForSucceeded(ctx, engineClient, actionName) - - By("4. Check Uploaded TypeInstances") - assertUploadedTypeInstance(ctx, hubClient, testValue, builtinStorage.Backend) - assertOutputTypeInstancesInActionStatus(ctx, engineClient, action.Name, HaveLen(1)) - }) - - It("should pick Implementation A with default Policy", func() { - testValue := "Implementation A" + It("should pick Implementation A with empty inject Policy", func() { + implIndicatorValue := "Implementation A" By("1. Preparing input Type Instances") By("1.1 Creating TypeInstance which will be downloaded") - download := getTypeInstanceInputForDownload(testValue) + download := getTypeInstanceInputForDownload(implIndicatorValue) downloadTI, downloadTICleanup := createTypeInstance(ctx, hubClient, download) defer downloadTICleanup() @@ -120,8 +82,8 @@ var _ = Describe("Action", func() { inputData := &enginegraphql.ActionInputData{ TypeInstances: []*enginegraphql.InputTypeInstanceData{ - {Name: "testUpdate", ID: updateTI.ID}, {Name: "testInput", ID: downloadTI.ID}, + {Name: "testUpdate", ID: updateTI.ID}, }, } @@ -134,49 +96,107 @@ var _ = Describe("Action", func() { }, Backend: &enginegraphql.TypeInstanceBackendDetails{ ID: builtinStorage.ID, - Abstract: builtinStorage.Backend.Abstract, + Abstract: true, }, } By("2. Expecting Implementation A is picked and builtin storage is used...") action := createActionAndWaitForReadyToRunPhase(ctx, engineClient, actionName, actionPath, inputData) - assertActionRenderedWorkflowContains(action, "echo 'Implementation A'") + assertActionRenderedWorkflowContains(action, "echo '%s'", implIndicatorValue) runActionAndWaitForSucceeded(ctx, engineClient, actionName) - By("3. Check TypeInstances") By("3.1 Check uploaded TypeInstances") - assertUploadedTypeInstance(ctx, hubClient, testValue, &hublocalgraphql.TypeInstanceBackend{ID: builtinStorage.ID, Abstract: true}) - assertOutputTypeInstancesInActionStatus(ctx, engineClient, action.Name, And(ContainElement(expUpdatedTI), HaveLen(2))) + uploadedTI, cleanup := getUploadedTypeInstanceByValue(ctx, hubClient, implIndicatorValue) + defer cleanup() // We need to clean it up as it's not deleted when Action is deleted. + Expect(uploadedTI.Backend).Should(Equal(&hublocalgraphql.TypeInstanceBackendReference{ID: builtinStorage.ID, Abstract: true})) By("3.2 Check updated TypeInstances") - updateTI, err := hubClient.FindTypeInstance(ctx, updateTI.ID) - Expect(err).ToNot(HaveOccurred()) - Expect(updateTI).ToNot(BeNil()) + getTypeInstanceByIDAndValue(ctx, hubClient, updateTI.ID, implIndicatorValue) - _, err = getTypeInstanceWithValue([]hublocalgraphql.TypeInstance{*updateTI}, testValue) - Expect(err).ToNot(HaveOccurred()) + By("3.3 Check Action output TypeInstances") + assertOutputTypeInstancesInActionStatus(ctx, engineClient, action.Name, And(ContainElements(expUpdatedTI, uploadedTI), HaveLen(2))) - By("3.3 Deleting Action...") - err = engineClient.DeleteAction(ctx, actionName) + By("4. Deleting Action...") + err := engineClient.DeleteAction(ctx, actionName) Expect(err).ToNot(HaveOccurred()) - By("3.4 Waiting for Action deleted") + By("5. Waiting for Action deleted") waitForActionDeleted(ctx, engineClient, actionName) - By("4. Modifying Policy to change backend storage for uploaded TypeInstance via TypeRef...") + By("6. Modifying Policy to change backend storage for uploaded TypeInstance via TypeRef...") setGlobalTestPolicy(ctx, engineClient, withHelmBackendForUploadTypeRef(helmStorageTI.ID)) - By("5. Expecting Implementation A is picked and the Helm storage is used for uploaded TypeInstance...") + By("7. Expecting Implementation A is picked and the Helm storage is used for uploaded TypeInstance...") action = createActionAndWaitForReadyToRunPhase(ctx, engineClient, actionName, actionPath, inputData) - assertActionRenderedWorkflowContains(action, "echo 'Implementation A'") + assertActionRenderedWorkflowContains(action, "echo '%s'", implIndicatorValue) runActionAndWaitForSucceeded(ctx, engineClient, actionName) - By("6. Check Uploaded TypeInstances") - assertUploadedTypeInstance(ctx, hubClient, testValue, &hublocalgraphql.TypeInstanceBackend{ID: helmStorageTI.ID, Abstract: false}) - assertOutputTypeInstancesInActionStatus(ctx, engineClient, action.Name, And( - HaveLen(2), - ContainElement(expUpdatedTI), - )) + By("8.1 Check uploaded TypeInstances") + uploadedTI, cleanup = getUploadedTypeInstanceByValue(ctx, hubClient, implIndicatorValue) + defer cleanup() // We need to clean it up as it's not deleted when Action is deleted. + Expect(uploadedTI.Backend).Should(Equal(&hublocalgraphql.TypeInstanceBackendReference{ID: helmStorageTI.ID, Abstract: false})) + + By("8.2 Check Action output TypeInstances") + assertOutputTypeInstancesInActionStatus(ctx, engineClient, action.Name, And(ContainElements(expUpdatedTI, uploadedTI), HaveLen(2))) + + }) + + It("should pick proper Implementation B", func() { + implIndicatorValue := "Implementation B" + + By("1. Preparing input Type Instances") + By("1.1 Creating TypeInstance which will be downloaded") + download := getTypeInstanceInputForDownload(implIndicatorValue) + downloadTI, downloadTICleanup := createTypeInstance(ctx, hubClient, download) + defer downloadTICleanup() + + By("1.2 Creating TypeInstance which will be downloaded and updated") + update := getTypeInstanceInputForUpdate() + updateTI, updateTICleanup := createTypeInstance(ctx, hubClient, update) + defer updateTICleanup() + + By("1.3 Creating TypeInstance that describes Helm storage") + helmStorage := getTypeInstanceHelmStorage() + helmStorageTI, helmStorageTICleanup := createTypeInstance(ctx, hubClient, helmStorage) + defer helmStorageTICleanup() + + By("1.4 Create TypeInstance which is required for Implementation B to be picked based on Policy") + typeInstanceValue := getTypeInstanceInputForPolicy() + injectTypeInstance, tiCleanupFn := createTypeInstance(ctx, hubClient, typeInstanceValue) + defer tiCleanupFn() + + inputData := &enginegraphql.ActionInputData{ + TypeInstances: []*enginegraphql.InputTypeInstanceData{ + {Name: "testInput", ID: downloadTI.ID}, + {Name: "testUpdate", ID: updateTI.ID}, + }, + } + + By("2. Modifying Policy to pick Implementation B...") + globalPolicyRequiredTypeInstances := []*enginegraphql.RequiredTypeInstanceReferenceInput{ + { + ID: injectTypeInstance.ID, + Description: ptr.String("Test TypeInstance"), + }, + { + ID: helmStorageTI.ID, + Description: ptr.String("Helm backend TypeInstance"), + }, + } + setGlobalTestPolicy(ctx, engineClient, prependInjectRuleForPassingActionInterface(globalPolicyRequiredTypeInstances)) + + By("3. Expecting Implementation B is picked and injected Helm storage is used...") + action := createActionAndWaitForReadyToRunPhase(ctx, engineClient, actionName, actionPath, inputData) + assertActionRenderedWorkflowContains(action, "echo '%s'", implIndicatorValue) + runActionAndWaitForSucceeded(ctx, engineClient, actionName) + + By("4.1 Check uploaded TypeInstances") + uploadedTI, cleanup := getUploadedTypeInstanceByValue(ctx, hubClient, implIndicatorValue) + defer cleanup() // We need to clean it up as it's not deleted when Action is deleted. + Expect(uploadedTI.Backend).Should(Equal(&hublocalgraphql.TypeInstanceBackendReference{ID: helmStorageTI.ID, Abstract: false})) + + By("4.2 Check Action output TypeInstances") + assertOutputTypeInstancesInActionStatus(ctx, engineClient, action.Name, And(ContainElements(uploadedTI), HaveLen(2))) }) It("should have failed status after a failed workflow", func() { @@ -413,11 +433,11 @@ func createActionAndWaitForReadyToRunPhase(ctx context.Context, engineClient *en return action } -func assertActionRenderedWorkflowContains(action *enginegraphql.Action, stringToFind string) { +func assertActionRenderedWorkflowContains(action *enginegraphql.Action, toFindFormat string, toFindArgs ...interface{}) { jsonBytes, err := json.Marshal(action.RenderedAction) Expect(err).ToNot(HaveOccurred()) Expect( - strings.Contains(string(jsonBytes), stringToFind), + strings.Contains(string(jsonBytes), fmt.Sprintf(toFindFormat, toFindArgs...)), ).To(BeTrue()) } @@ -481,10 +501,7 @@ func withHelmBackendForUploadTypeRef(backendID string) policyOption { } } -func prependInjectRuleForPassingActionInterface(in enginegraphql.RequiredTypeInstanceReferenceInput) policyOption { - reqInput := []*enginegraphql.RequiredTypeInstanceReferenceInput{ - &in, - } +func prependInjectRuleForPassingActionInterface(reqInput []*enginegraphql.RequiredTypeInstanceReferenceInput) policyOption { manifestRef := func(path string) []*enginegraphql.ManifestReferenceInput { return []*enginegraphql.ManifestReferenceInput{ { @@ -523,7 +540,17 @@ func setGlobalTestPolicy(ctx context.Context, client *engine.Client, opts ...pol Expect(err).ToNot(HaveOccurred()) } -func assertUploadedTypeInstance(ctx context.Context, hubClient *hubclient.Client, expValue string, expBackend *hublocalgraphql.TypeInstanceBackend) { +func getTypeInstanceByIDAndValue(ctx context.Context, hubClient *hubclient.Client, id, expValue string) *hublocalgraphql.TypeInstance { + updateTI, err := hubClient.FindTypeInstance(ctx, id) + Expect(err).ToNot(HaveOccurred()) + Expect(updateTI).ToNot(BeNil()) + _, err = getTypeInstanceWithValue([]hublocalgraphql.TypeInstance{*updateTI}, expValue) + Expect(err).ToNot(HaveOccurred()) + + return updateTI +} + +func getUploadedTypeInstanceByValue(ctx context.Context, hubClient *hubclient.Client, expValue string) (*hublocalgraphql.TypeInstance, func()) { uploaded, err := hubClient.ListTypeInstances(ctx, &hublocalgraphql.TypeInstanceFilter{ TypeRef: &hublocalgraphql.TypeRefFilterInput{ Path: "cap.type.capactio.capact.validation.upload", @@ -536,10 +563,10 @@ func assertUploadedTypeInstance(ctx context.Context, hubClient *hubclient.Client ti, err := getTypeInstanceWithValue(uploaded, expValue) Expect(err).ToNot(HaveOccurred()) - Expect(ti.Backend).Should(Equal(expBackend)) - - err = hubClient.DeleteTypeInstance(ctx, ti.ID) - Expect(err).ToNot(HaveOccurred()) + return ti, func() { + err = hubClient.DeleteTypeInstance(ctx, ti.ID) + Expect(err).ToNot(HaveOccurred()) + } } func getBuiltinStorageTypeInstance(ctx context.Context, hubClient *hubclient.Client) hublocalgraphql.TypeInstance { @@ -555,39 +582,6 @@ func getBuiltinStorageTypeInstance(ctx context.Context, hubClient *hubclient.Cli return coreStorage[0] } -func matchOutputTypeInstanceOnTypeRef(expected *enginegraphql.OutputTypeInstanceDetails) types.GomegaMatcher { - return &outputTypeInstanceOnTypeRefMatcher{ - expected: expected, - } -} - -type outputTypeInstanceOnTypeRefMatcher struct { - expected *enginegraphql.OutputTypeInstanceDetails -} - -func (matcher *outputTypeInstanceOnTypeRefMatcher) Match(actual interface{}) (success bool, err error) { - response, ok := actual.([]*enginegraphql.OutputTypeInstanceDetails) - if !ok { - return false, fmt.Errorf("MatchOutputTypeInstanceOnTypeRef matcher expects an enginegraphql.OutputTypeInstanceDetails") - } - - for idx := range response { - response[idx].ID = "" - } - // ignore IDs - matcher.expected.ID = "" - - return ContainElement(matcher.expected).Match(response) -} - -func (matcher *outputTypeInstanceOnTypeRefMatcher) FailureMessage(actual interface{}) (message string) { - return fmt.Sprintf("Expected\n\t%#v\nto contain element matching\n\t%#v", actual, matcher.expected) -} - -func (matcher *outputTypeInstanceOnTypeRefMatcher) NegatedFailureMessage(actual interface{}) (message string) { - return fmt.Sprintf("Expected\n\t%#v\nnot to contain element matching\n\t%#v", actual, matcher.expected) -} - func assertOutputTypeInstancesInActionStatus(ctx context.Context, engineClient *engine.Client, actionName string, match types.GomegaMatcher, ) { From c9f0c7f170603b69e97ba21d9ca50e80c0299dcf Mon Sep 17 00:00:00 2001 From: Mateusz Szostok Date: Fri, 11 Feb 2022 13:39:30 +0100 Subject: [PATCH 06/15] Extract values to const --- docs/proposal/20211207-delegated-storage.md | 56 ++++++++++----------- hub-js/src/schema/local.ts | 32 ++++++------ test/e2e/action_test.go | 17 ++++--- test/e2e/e2e_suite_test.go | 4 +- 4 files changed, 56 insertions(+), 53 deletions(-) diff --git a/docs/proposal/20211207-delegated-storage.md b/docs/proposal/20211207-delegated-storage.md index bedb2b5ed..65f1f7061 100644 --- a/docs/proposal/20211207-delegated-storage.md +++ b/docs/proposal/20211207-delegated-storage.md @@ -66,7 +66,7 @@ The main goal is to support the following use-cases: Other backends: - AWS Secrets Manager - Google Key Management - + - Store and retrieve dynamic data - Examples: - Kubernetes cluster (e.g. Flux's Helm releases or Kubernetes Secrets) @@ -126,7 +126,7 @@ Also, the additional, nice-to-have goals are: path: cap.type.helm.storage spec: additionalRefs: - - "cap.core.type.hub.storage" + - "cap.core.type.hub.storage" jsonSchema: value: |- # JSON schema with: { @@ -186,7 +186,7 @@ Also, the additional, nice-to-have goals are: - Register a storage backend by creating such TypeInstance. Regardless the option, at the end there is one TypeInstance produced: - + ```yaml id: 3ef2e4ac-9070-4093-a3ce-142139fd4a16 typeRef: @@ -243,7 +243,7 @@ Also, the additional, nice-to-have goals are: # no url as it is known for Hub (Hub is already connected with PostgreSQL) acceptValue: true contextSchema: null - backend: + backend: abstract: true # Special keyword which specifies the built-in storage option which stores already all other metadata. ``` @@ -253,7 +253,7 @@ Also, the additional, nice-to-have goals are: The property indicates that Hub queries the PostgreSQL database directly. - - It is the default backend for static TypeInstance values. To learn more, read the [Configuring storage backends](#configuring-storage-backends) paragraph. + - It is the default backend for static TypeInstance values. To learn more, read the [Configuring storage backends](#configuring-storage-backends) paragraph. ## Workflow syntax - Create @@ -270,7 +270,7 @@ Also, the additional, nice-to-have goals are: ``` - This workflow cannot be run unless there is a `helm-release` storage backend installed (where `helm-release` is only workflow alias). - - If there are no specific storage backend requirements set, the default backend will be used. To learn more, read the [Configuring default storage backends](#configuring-default-storage-backends) paragraph. + - If there are no specific storage backend requirements set, the default backend will be used. To learn more, read the [Configuring default storage backends](#configuring-default-storage-backends) paragraph. 1. Content Developer outputs one of the following Argo workflow artifacts: @@ -297,7 +297,7 @@ Also, the additional, nice-to-have goals are: However, the `context` are backend-specific properties, which means Content Developer need to explicitly specify the backend as described later. 1. To save a specific value with additional parameters: - + For example, for an implementation of Kubernetes secrets storage backend, which actually creates and updates these secrets during TypeInstance creation: ```yaml @@ -312,8 +312,8 @@ Also, the additional, nice-to-have goals are: The storage backend has to have `contextSchema` specified, as well as the `acceptValue` property set to `true`. In that way, someday we will be able to extend such approach with additional properties: - - ```yaml + + ```yaml instrumentation: # someday - if we want to unify the approach health: endpoint: foo.bar/healthz @@ -329,7 +329,7 @@ Also, the additional, nice-to-have goals are: capact-outputTypeInstances: - name: mattermost-config from: additional - # no backend definition -> stored in built-in Local Hub storage (done by Local Hub) + # no backend definition -> use default storage backend # option 2 - specific backend (defined in `Implementation.spec.requires` property) capact-outputTypeInstances: @@ -381,7 +381,7 @@ Also, the additional, nice-to-have goals are: value: { backend: { id: "3ef2e4ac-9070-4093-a3ce-142139fd4a16", - context: { ## TODO: support that! currently only ID. + context: { name: "release-name", namespace: "release-namespace", } @@ -389,7 +389,7 @@ Also, the additional, nice-to-have goals are: } } ], - usesRelations: [ ## TODO: this relation is created by Local Hub. It always need to be done as the **Uninstalling storage backends** depends on that. + usesRelations: [ { from: "helm-release", to: "3ef2e4ac-9070-4093-a3ce-142139fd4a16" @@ -407,7 +407,7 @@ Also, the additional, nice-to-have goals are: 1. Hub resolves details about the service (TypeInstance details) 1. If the TypeInstance value is provided: - + 1. Hub checks whether the storage backend accepts TypeInstance value (`acceptValue` property). If not, and the value has been provided, it returns error. 1. Hub validates the TypeInstance value against Type JSON Schema. @@ -448,8 +448,8 @@ Also, the additional, nice-to-have goals are: backend: context: # additional parameters that might be modified via the service handling `onCreate` hook name: release-name - namespace: release-namespace - backend: ## TODO(storage-: It was done like that currently, Based on that this + namespace: release-namespace + backend: id: 3ef2e4ac-9070-4093-a3ce-142139fd4a16 # helm-release backend - resolved UUID based on the injected TypeInstance ``` @@ -509,7 +509,7 @@ Capact Local Hub calls proper storage backend service while accessing the TypeIn message OnUpdateRequest { string typeinstance_id = 1; - + OnUpdateData old_data = 2; OnUpdateData new_data = 3; } @@ -568,7 +568,7 @@ Capact Local Hub calls proper storage backend service while accessing the TypeIn rpc GetLockedBy(GetLockedByRequest) returns (GetLockedByResponse); rpc OnLock(OnLockUnlockRequest) returns (OnLockUnlockResponse); rpc OnUnlock(OnLockUnlockRequest) returns (OnLockUnlockResponse); - } + } ``` @@ -656,7 +656,7 @@ The storage backends configuration consists of two different parts: default back interface: rules: - interface: - path: cap.interface.database.postgresql.install + path: cap.interface.database.postgresql.install oneOf: - implementationConstraints: # constraints to select Bitnami PostgreSQL installation, for example: @@ -664,7 +664,7 @@ The storage backends configuration consists of two different parts: default back inject: requiredTypeInstances: - id: b4cf15d2-79b1-45ee-9729-6b83289ecabc # Different TypeInstance of `cap.type.helm.storage` Type - it will be used instead of the one from `interface.rules.default.inject` - description: "Helm Release storage" + description: "Helm Release storage" default: # properties applied to all rules above inject: @@ -777,7 +777,7 @@ query ListTypeInstances { } value # url + contextSchema } - + } } } @@ -935,7 +935,7 @@ To avoid implementing a special storage backend service every time we have such } }, "additionalProperties": true - } + } } }, "acceptValue": { # specifies if a given storage backend (app) accepts TypeInstance value while creating/updating TypeInstance, or just context. @@ -1014,7 +1014,7 @@ To avoid implementing a special storage backend service every time we have such }, "additionalProperties": true } - backend: + backend: id: a36ed738-dfe7-45ec-acd1-8e44e8db893b # PostgreSQL backend ``` @@ -1067,7 +1067,7 @@ To avoid implementing a special storage backend service every time we have such mattermost: relatedTypeInstanceAlias: mattermost-config # kept for better readability typeInstanceID: b895d2d4-d0e0-4f7c-9666-4c3d197d1795 # resolved ID based on `relatedTypeInstanceAlias`. It will be used for further template rendering - backend: + backend: id: abd48b8c-99bd-40a7-99c0-047bd69f1db8 # capact-gotemplate backend - resolved UUID ``` @@ -1107,7 +1107,7 @@ Unfortunately, that won't be possible anymore, and instead we should get all the As described in proposal, every storage backend Type should follow the convention of having JSON schema with `uri` and `contextSchema` properties. That could be possible if we implement an ability to define validating JSON schema for Type nodes (e.g. `cap.core.type.hub.storage`), and use such schemas to validate Types attached to these nodes (via `spec.additionalRefs` property). For example, the `cap.core.hub.storage` node could have JSON Schema defined, which validates Type values (JSON schema) attached to such node. In the end, that would be JSON schema validating another JSON schema. - + **Reason:** It is possible, but it's complex and brings too little benefits for now to implement it. 1. Adding optional `TypeInstance.metadata.name` or `alias`, which is unique across all TypeInstances and immutable regardless resourceVersion. It would allow easier referencing storage backends in the `TypeInstance.spec.backend` field: @@ -1152,7 +1152,7 @@ Unfortunately, that won't be possible anymore, and instead we should get all the - name: helm-release from: helm-release # values backend: vault - context: "{{steps.foo.output.artifacts.foo}}" + context: "{{steps.foo.output.artifacts.foo}}" # option 3 - register something which already exist as external TypeInstance - based on context capact-outputTypeInstances: @@ -1166,8 +1166,8 @@ Unfortunately, that won't be possible anymore, and instead we should get all the ### Storage backend service implementation 1. Using Actions as a way to do CRUD operations (separate Interface/Implementation per Create/Update/Get/Delete operation) - - **Reason:** While the idea may seem exciting, that would be really time consuming and ineffective. We are too far from the point at where we can think about such solution. + + **Reason:** While the idea may seem exciting, that would be really time consuming and ineffective. We are too far from the point at where we can think about such solution. ### Configuring default storage backends @@ -1236,7 +1236,7 @@ Once approved, we need to address the following list of items: 1. Update Policy - Add new properties - Handle common TypeInstance injections -1. Update documentation +1. Update documentation - Policy - Content Development guide - Type features diff --git a/hub-js/src/schema/local.ts b/hub-js/src/schema/local.ts index 22575c06c..e387b0746 100644 --- a/hub-js/src/schema/local.ts +++ b/hub-js/src/schema/local.ts @@ -480,14 +480,14 @@ function tryToExtractCustomError( } export async function ensureCoreStorageTypeInstance(context: ContextWithDriver, uri: string) { - const neo4jSession = context.driver.session(); - const value = { - uri: uri - } - try { - await neo4jSession.writeTransaction( - async (tx: Transaction) => { - await tx.run(` + const neo4jSession = context.driver.session(); + const value = { + uri: uri + } + try { + await neo4jSession.writeTransaction( + async (tx: Transaction) => { + await tx.run(` MERGE (ti:TypeInstance {id: "318b99bd-9b26-4bc1-8259-0a7ff5dae61c"}) MERGE (typeRef:TypeInstanceTypeReference {path: "cap.core.type.hub.storage.neo4j", revision: "0.1.0"}) MERGE (backend:TypeInstanceBackendReference {abstract: true, id: ti.id, description: "Built-in Hub storage"}) @@ -502,12 +502,12 @@ export async function ensureCoreStorageTypeInstance(context: ContextWithDriver, RETURN ti `, {value}); - } - ); - } catch (e) { - const err = e as Error; - throw new Error(`while ensuring TypeInstance for core backend storage: ${err.message}`); - } finally { - await neo4jSession.close(); - } + } + ); + } catch (e) { + const err = e as Error; + throw new Error(`while ensuring TypeInstance for core backend storage: ${err.message}`); + } finally { + await neo4jSession.close(); + } } diff --git a/test/e2e/action_test.go b/test/e2e/action_test.go index ea0ecdb15..ca9815e71 100644 --- a/test/e2e/action_test.go +++ b/test/e2e/action_test.go @@ -4,7 +4,6 @@ package e2e import ( - "capact.io/capact/internal/cli/heredoc" "context" "encoding/json" "fmt" @@ -13,6 +12,7 @@ import ( "strings" "time" + "capact.io/capact/internal/cli/heredoc" "capact.io/capact/internal/ptr" enginegraphql "capact.io/capact/pkg/engine/api/graphql" engine "capact.io/capact/pkg/engine/client" @@ -27,7 +27,10 @@ import ( ) const ( - actionPassingPath = "cap.interface.capactio.capact.validation.action.passing" + actionPassingPath = "cap.interface.capactio.capact.validation.action.passing" + uploadTypePath = "cap.type.capactio.capact.validation.upload" + builtinStorageTypePath = "cap.core.type.hub.storage.neo4j" + singleKeyTypePath = "cap.type.capactio.capact.validation.single-key" ) func getActionName() string { @@ -329,7 +332,7 @@ func getActionFunc(ctx context.Context, cl *engine.Client, name string) func() ( func getTypeInstanceInputForPolicy() *hublocalgraphql.CreateTypeInstanceInput { return &hublocalgraphql.CreateTypeInstanceInput{ TypeRef: &hublocalgraphql.TypeInstanceTypeReferenceInput{ - Path: "cap.type.capactio.capact.validation.single-key", + Path: singleKeyTypePath, Revision: "0.1.0", }, Attributes: []*hublocalgraphql.AttributeReferenceInput{ @@ -488,7 +491,7 @@ func withHelmBackendForUploadTypeRef(backendID string) policyOption { Rules: []*enginegraphql.RulesForTypeInstanceInput{ { TypeRef: &enginegraphql.ManifestReferenceInput{ - Path: "cap.type.capactio.capact.validation.upload", + Path: uploadTypePath, Revision: ptr.String("0.1.0"), }, Backend: &enginegraphql.TypeInstanceBackendRuleInput{ @@ -517,7 +520,7 @@ func prependInjectRuleForPassingActionInterface(reqInput []*enginegraphql.Requir policy.Interface.Rules[idx].OneOf = append([]*enginegraphql.PolicyRuleInput{ { ImplementationConstraints: &enginegraphql.PolicyRuleImplementationConstraintsInput{ - Requires: manifestRef("cap.type.capactio.capact.validation.single-key"), + Requires: manifestRef(singleKeyTypePath), Attributes: manifestRef("cap.attribute.capactio.capact.validation.policy.most-preferred"), }, Inject: &enginegraphql.PolicyRuleInjectDataInput{ @@ -553,7 +556,7 @@ func getTypeInstanceByIDAndValue(ctx context.Context, hubClient *hubclient.Clien func getUploadedTypeInstanceByValue(ctx context.Context, hubClient *hubclient.Client, expValue string) (*hublocalgraphql.TypeInstance, func()) { uploaded, err := hubClient.ListTypeInstances(ctx, &hublocalgraphql.TypeInstanceFilter{ TypeRef: &hublocalgraphql.TypeRefFilterInput{ - Path: "cap.type.capactio.capact.validation.upload", + Path: uploadTypePath, Revision: ptr.String("0.1.0"), }, }) @@ -572,7 +575,7 @@ func getUploadedTypeInstanceByValue(ctx context.Context, hubClient *hubclient.Cl func getBuiltinStorageTypeInstance(ctx context.Context, hubClient *hubclient.Client) hublocalgraphql.TypeInstance { coreStorage, err := hubClient.ListTypeInstances(ctx, &hublocalgraphql.TypeInstanceFilter{ TypeRef: &hublocalgraphql.TypeRefFilterInput{ - Path: "cap.core.type.hub.storage.neo4j", + Path: builtinStorageTypePath, Revision: ptr.String("0.1.0"), }, }) diff --git a/test/e2e/e2e_suite_test.go b/test/e2e/e2e_suite_test.go index 45fe3e941..5c78d3036 100644 --- a/test/e2e/e2e_suite_test.go +++ b/test/e2e/e2e_suite_test.go @@ -59,8 +59,8 @@ var _ = BeforeSuite(func() { Expect(err).ToNot(HaveOccurred()) originalGlobalPolicy = input - //waitTillServiceEndpointsAreReady() - //waitTillDataIsPopulated() + waitTillServiceEndpointsAreReady() + waitTillDataIsPopulated() }) var _ = AfterSuite(func() { From 70668565aa65c2dcc93c0d0df770e398bc60ba7f Mon Sep 17 00:00:00 2001 From: Mateusz Szostok Date: Fri, 11 Feb 2022 15:10:21 +0100 Subject: [PATCH 07/15] Self review, change hub-manifests repo --- hack/lib/const.sh | 4 +- hub-js/src/index.ts | 2 +- hub-js/src/schema/local.ts | 49 +++++++++--------- .../graphql/domain/action/fixtures_test.go | 50 ++++++++++++++++++- pkg/engine/k8s/policy/type_instance.go | 16 ++---- pkg/hub/client/policy_enforced_client.go | 1 - pkg/hub/client/public/facade/doc.go | 3 ++ pkg/sdk/renderer/argo/renderer_test.go | 3 +- ...rmost_workflow_with_user_input.golden.yaml | 6 +++ ...nput_and_without_TypeInstances.golden.yaml | 2 + ...k_installation_with_user_input.golden.yaml | 8 +++ ...tallation_with_GCP_SA_injected.golden.yaml | 2 + ...attermost_with_AWS_RDS_install.golden.yaml | 8 +++ ...tallation_with_GCP_SA_injected.golden.yaml | 6 +++ ..._with_CloudSQL_using_Terraform.golden.yaml | 7 +++ ..._with_existing_DB_installation.golden.yaml | 4 ++ ...additional_parameters_injected.golden.yaml | 4 ++ ...back_to_Bitnami_Implementation.golden.yaml | 2 + ...put_-_reference_by_ManifestRef.golden.yaml | 7 +++ ...nal_input_-_reference_by_alias.golden.yaml | 7 +++ 20 files changed, 148 insertions(+), 43 deletions(-) create mode 100644 pkg/hub/client/public/facade/doc.go diff --git a/hack/lib/const.sh b/hack/lib/const.sh index 00f670cfe..7e4246414 100644 --- a/hack/lib/const.sh +++ b/hack/lib/const.sh @@ -46,7 +46,7 @@ readonly CAPACT_USE_TEST_SETUP="false" # readonly CAPACT_INCREASE_RESOURCE_LIMITS="true" -readonly CAPACT_HUB_MANIFESTS_SOURCE_REPO_URL="github.com/capactio/hub-manifests" +readonly CAPACT_HUB_MANIFESTS_SOURCE_REPO_URL="github.com/mszostok/os-hub-manifests" # The git ref to checkout. It can point to a commit SHA, a branch name, or a tag. # If you want to use your forked version, remember to update CAPACT_HUB_MANIFESTS_SOURCE_REPO_URL respectively. -readonly CAPACT_HUB_MANIFESTS_SOURCE_REPO_REF="main" +readonly CAPACT_HUB_MANIFESTS_SOURCE_REPO_REF="policy-syntax/type-instances" diff --git a/hub-js/src/index.ts b/hub-js/src/index.ts index 1d2fc5565..3b57b73eb 100644 --- a/hub-js/src/index.ts +++ b/hub-js/src/index.ts @@ -43,7 +43,7 @@ async function main() { logger.info("Starting Hub", {mode: config.hubMode}); if (config.hubMode === HubMode.Local) { - await ensureCoreStorageTypeInstance({driver}, `http://${bindAddress}:${bindPort}/graphql`) + await ensureCoreStorageTypeInstance({driver}) logger.info("Successfully registered TypeInstance for core backend storage"); } diff --git a/hub-js/src/schema/local.ts b/hub-js/src/schema/local.ts index e387b0746..7b1415d8d 100644 --- a/hub-js/src/schema/local.ts +++ b/hub-js/src/schema/local.ts @@ -1,6 +1,6 @@ -import { readFileSync } from "fs"; -import { makeAugmentedSchema, neo4jgraphql } from "neo4j-graphql-js"; -import { Driver, Transaction } from "neo4j-driver"; +import {readFileSync} from "fs"; +import {makeAugmentedSchema, neo4jgraphql} from "neo4j-graphql-js"; +import {Driver, Transaction} from "neo4j-driver"; const typeDefs = readFileSync("./graphql/local/schema.graphql", "utf-8"); @@ -54,7 +54,7 @@ export const schema = makeAugmentedSchema({ args: CreateTypeInstancesArgs, context: ContextWithDriver ) => { - const { typeInstances, usesRelations } = args.in; + const {typeInstances, usesRelations} = args.in; const aliases = typeInstances .filter((x) => x.alias !== undefined) @@ -116,7 +116,7 @@ export const schema = makeAugmentedSchema({ RETURN ti.id as uuid, typeInstance.alias as alias `, - { typeInstances } + {typeInstances} ); if ( @@ -142,7 +142,7 @@ export const schema = makeAugmentedSchema({ {} ); const usesRelationsParams = usesRelations.map( - ({ from, to }: { from: string; to: string }) => ({ + ({from, to}: { from: string; to: string }) => ({ from: aliasMappings[from] || from, to: aliasMappings[to] || to, }) @@ -203,7 +203,7 @@ export const schema = makeAugmentedSchema({ ); break; case UpdateTypeInstanceErrorCode.NotFound: { - const ids = args.in.map(({ id }) => id); + const ids = args.in.map(({id}) => id); const notFoundIDs = ids.filter( (x) => !customErr.ids.includes(x) ); @@ -271,7 +271,7 @@ export const schema = makeAugmentedSchema({ } RETURN $id`, - { id: args.id, ownerID: args.ownerID || null } + {id: args.id, ownerID: args.ownerID || null} ); return args.id; } @@ -404,7 +404,7 @@ async function switchLocking( ) YIELD value as lockingProcess RETURN allIDs, lockedIDs, lockingProcess`, - { in: args.in } + {in: args.in} ); if (!instanceLockedByOthers.records.length) { @@ -479,28 +479,29 @@ function tryToExtractCustomError( return null; } -export async function ensureCoreStorageTypeInstance(context: ContextWithDriver, uri: string) { +export async function ensureCoreStorageTypeInstance(context: ContextWithDriver) { const neo4jSession = context.driver.session(); const value = { - uri: uri + acceptValue: false, + contextSchema: null, } try { await neo4jSession.writeTransaction( async (tx: Transaction) => { await tx.run(` - MERGE (ti:TypeInstance {id: "318b99bd-9b26-4bc1-8259-0a7ff5dae61c"}) - MERGE (typeRef:TypeInstanceTypeReference {path: "cap.core.type.hub.storage.neo4j", revision: "0.1.0"}) - MERGE (backend:TypeInstanceBackendReference {abstract: true, id: ti.id, description: "Built-in Hub storage"}) - MERGE (tir: TypeInstanceResourceVersion {resourceVersion: 1, createdBy: "core"}) - MERGE (spec: TypeInstanceResourceVersionSpec {value: apoc.convert.toJson($value)}) - - MERGE (ti)-[:OF_TYPE]->(typeRef) - MERGE (ti)-[:STORED_IN]->(backend) - MERGE (ti)-[:CONTAINS]->(tir) - MERGE (tir)-[:DESCRIBED_BY]->(metadata:TypeInstanceResourceVersionMetadata) - MERGE (tir)-[:SPECIFIED_BY]->(spec) - - RETURN ti + MERGE (ti:TypeInstance {id: "318b99bd-9b26-4bc1-8259-0a7ff5dae61c"}) + MERGE (typeRef:TypeInstanceTypeReference {path: "cap.core.type.hub.storage.neo4j", revision: "0.1.0"}) + MERGE (backend:TypeInstanceBackendReference {abstract: true, id: ti.id, description: "Built-in Hub storage"}) + MERGE (tir: TypeInstanceResourceVersion {resourceVersion: 1, createdBy: "core"}) + MERGE (spec: TypeInstanceResourceVersionSpec {value: apoc.convert.toJson($value)}) + + MERGE (ti)-[:OF_TYPE]->(typeRef) + MERGE (ti)-[:STORED_IN]->(backend) + MERGE (ti)-[:CONTAINS]->(tir) + MERGE (tir)-[:DESCRIBED_BY]->(metadata:TypeInstanceResourceVersionMetadata) + MERGE (tir)-[:SPECIFIED_BY]->(spec) + + RETURN ti `, {value}); } ); diff --git a/internal/k8s-engine/graphql/domain/action/fixtures_test.go b/internal/k8s-engine/graphql/domain/action/fixtures_test.go index 0003a199a..818eab78c 100644 --- a/internal/k8s-engine/graphql/domain/action/fixtures_test.go +++ b/internal/k8s-engine/graphql/domain/action/fixtures_test.go @@ -53,6 +53,10 @@ func fixGQLAction(t *testing.T, name string) graphql.Action { Path: "path1", Revision: "0.1.0", }, + Backend: &graphql.TypeInstanceBackendDetails{ + ID: "id11", + Abstract: true, + }, }, { ID: "id2", @@ -60,6 +64,11 @@ func fixGQLAction(t *testing.T, name string) graphql.Action { Path: "path2", Revision: "0.1.0", }, + + Backend: &graphql.TypeInstanceBackendDetails{ + ID: "id22", + Abstract: false, + }, }, }, }, @@ -167,6 +176,10 @@ func fixK8sAction(t *testing.T, name, namespace string) v1alpha1.Action { Path: "path1", Revision: ptr.String("0.1.0"), }, + Backend: v1alpha1.TypeInstanceBackend{ + ID: "id11", + Abstract: true, + }, }, { ID: "id2", @@ -174,6 +187,10 @@ func fixK8sAction(t *testing.T, name, namespace string) v1alpha1.Action { Path: "path2", Revision: ptr.String("0.1.0"), }, + Backend: v1alpha1.TypeInstanceBackend{ + ID: "id22", + Abstract: false, + }, }, }, }, @@ -338,6 +355,37 @@ func fixGQLInputActionPolicy() *graphql.PolicyInput { }, }, }, + TypeInstance: &graphql.TypeInstancePolicyInput{ + Rules: []*graphql.RulesForTypeInstanceInput{ + { + TypeRef: &graphql.ManifestReferenceInput{ + Path: "cap.type.aws.auth.credentials", + Revision: ptr.String("0.1.0"), + }, + Backend: &graphql.TypeInstanceBackendRuleInput{ + ID: "00fd161c-01bd-47a6-9872-47490e11f996", + Description: ptr.String("Vault TI"), + }, + }, + { + TypeRef: &graphql.ManifestReferenceInput{ + Path: "cap.type.aws.*", + }, + Backend: &graphql.TypeInstanceBackendRuleInput{ + ID: "31bb8355-10d7-49ce-a739-4554d8a40b63", + }, + }, + { + TypeRef: &graphql.ManifestReferenceInput{ + Path: "cap.*", + }, + Backend: &graphql.TypeInstanceBackendRuleInput{ + ID: "a36ed738-dfe7-45ec-acd1-8e44e8db893b", + Description: ptr.String("Default Capact PostgreSQL backend"), + }, + }, + }, + }, } } @@ -432,7 +480,7 @@ func fixModelInputSecret(name string, paramsEnabled, policyEnabled bool) *corev1 sec.StringData["parameter-input-parameters"] = `{"param":"one"}` } if policyEnabled { - sec.StringData["action-policy.json"] = `{"interface":{"rules":[{"interface":{"path":"cap.interface.dummy","revision":null},"oneOf":[{"implementationConstraints":{"requires":null,"attributes":null,"path":"cap.implementation.dummy"},"inject":{"requiredTypeInstances":[{"id":"policy-ti-id","description":"Sample description"}],"additionalParameters":[{"name":"additional-parameters","value":{"snapshot":true}}],"additionalTypeInstances":[{"name":"additional-ti","id":"additional-ti-id"}]}}]}]}}` + sec.StringData["action-policy.json"] = `{"interface":{"rules":[{"interface":{"path":"cap.interface.dummy","revision":null},"oneOf":[{"implementationConstraints":{"requires":null,"attributes":null,"path":"cap.implementation.dummy"},"inject":{"requiredTypeInstances":[{"id":"policy-ti-id","description":"Sample description"}],"additionalParameters":[{"name":"additional-parameters","value":{"snapshot":true}}],"additionalTypeInstances":[{"name":"additional-ti","id":"additional-ti-id"}]}}]}]},"typeInstance":{"rules":[{"typeRef":{"path":"cap.type.aws.auth.credentials","revision":"0.1.0"},"backend":{"id":"00fd161c-01bd-47a6-9872-47490e11f996","description":"Vault TI"}},{"typeRef":{"path":"cap.type.aws.*","revision":null},"backend":{"id":"31bb8355-10d7-49ce-a739-4554d8a40b63","description":null}},{"typeRef":{"path":"cap.*","revision":null},"backend":{"id":"a36ed738-dfe7-45ec-acd1-8e44e8db893b","description":"Default Capact PostgreSQL backend"}}]}}` } return sec diff --git a/pkg/engine/k8s/policy/type_instance.go b/pkg/engine/k8s/policy/type_instance.go index 9ebbd72f4..d081fe738 100644 --- a/pkg/engine/k8s/policy/type_instance.go +++ b/pkg/engine/k8s/policy/type_instance.go @@ -4,7 +4,6 @@ import ( "fmt" "capact.io/capact/internal/ptr" - "capact.io/capact/pkg/sdk/apis/0.0.1/types" ) @@ -40,7 +39,7 @@ func (t *TypeInstanceBackendCollection) SetByTypeRef(ref types.ManifestRefWithOp if t.byTypeRef == nil { t.byTypeRef = map[string]TypeInstanceBackend{} } - t.byTypeRef[t.key(ref)] = backend + t.byTypeRef[ref.String()] = backend } // GetByTypeRef returns storage backend for a given TypeRef. @@ -56,13 +55,13 @@ func (t *TypeInstanceBackendCollection) SetByTypeRef(ref types.ManifestRefWithOp // - cap.*:0.1.0 // - cap.* // -// If both methods fail, default backend is returned. func (t TypeInstanceBackendCollection) GetByTypeRef(typeRef types.TypeRef) (TypeInstanceBackend, bool) { // 1. Try the explicit TypeRef - backend, found := t.byTypeRef[t.key(types.ManifestRefWithOptRevision{ + key := types.ManifestRefWithOptRevision{ Path: typeRef.Path, Revision: ptr.String(typeRef.Revision), - })] + } + backend, found := t.byTypeRef[key.String()] if found { return backend, true } @@ -120,10 +119,3 @@ func (t *TypeInstanceBackendCollection) GetAll() map[string]TypeInstanceBackend } return out } - -func (t TypeInstanceBackendCollection) key(typeRef types.ManifestRefWithOptRevision) string { - if typeRef.Revision != nil && *typeRef.Revision != "" { - return fmt.Sprintf("%s:%s", typeRef.Path, *typeRef.Revision) - } - return typeRef.Path -} diff --git a/pkg/hub/client/policy_enforced_client.go b/pkg/hub/client/policy_enforced_client.go index a703226fa..39ffe9f48 100644 --- a/pkg/hub/client/policy_enforced_client.go +++ b/pkg/hub/client/policy_enforced_client.go @@ -110,7 +110,6 @@ func (e *PolicyEnforcedClient) ListTypeInstancesBackendsBasedOnPolicy(_ context. // 1. Global Defaults based on TypeRefs for _, rule := range e.mergedPolicy.TypeInstance.Rules { - // ensure that rule.TypeRef.Revision is resolved! out.SetByTypeRef(rule.TypeRef, rule.Backend) } diff --git a/pkg/hub/client/public/facade/doc.go b/pkg/hub/client/public/facade/doc.go new file mode 100644 index 000000000..4f0f045c8 --- /dev/null +++ b/pkg/hub/client/public/facade/doc.go @@ -0,0 +1,3 @@ +// Package facade groups dedicated function per kind. +// The purpose of it is to hide more complex underlying logic that can be reused across codebase. +package facade diff --git a/pkg/sdk/renderer/argo/renderer_test.go b/pkg/sdk/renderer/argo/renderer_test.go index 2bc5983a5..3e34de08c 100644 --- a/pkg/sdk/renderer/argo/renderer_test.go +++ b/pkg/sdk/renderer/argo/renderer_test.go @@ -44,8 +44,7 @@ func TestRenderHappyPath(t *testing.T) { interfaceIOValidator := actionvalidation.NewValidator(fakeCli) policyIOValidator := policyvalidation.NewValidator(fakeCli) wfValidator := renderer.NewWorkflowInputValidator(interfaceIOValidator, policyIOValidator) - l, _ := logger.New(logger.Config{DevMode: true}) - argoRenderer := NewRenderer(l, renderer.Config{ + argoRenderer := NewRenderer(logger.Noop(), renderer.Config{ RenderTimeout: time.Second, MaxDepth: 20, }, fakeCli, typeInstanceHandler, wfValidator) diff --git a/pkg/sdk/renderer/argo/testdata/TestRenderHappyPath/Mattermost_workflow_with_user_input.golden.yaml b/pkg/sdk/renderer/argo/testdata/TestRenderHappyPath/Mattermost_workflow_with_user_input.golden.yaml index b02d2fa35..d4a2cabcd 100644 --- a/pkg/sdk/renderer/argo/testdata/TestRenderHappyPath/Mattermost_workflow_with_user_input.golden.yaml +++ b/pkg/sdk/renderer/argo/testdata/TestRenderHappyPath/Mattermost_workflow_with_user_input.golden.yaml @@ -1117,6 +1117,7 @@ args: typeInstances: - alias: mattermost-config attributes: [] + backend: null createdBy: default/action typeRef: path: cap.type.productivity.mattermost.config @@ -1124,6 +1125,7 @@ args: value: null - alias: mattermost-install-install-db-postgresql attributes: [] + backend: null createdBy: default/action typeRef: path: cap.type.database.postgresql.config @@ -1131,6 +1133,7 @@ args: value: null - alias: mattermost-install-install-db-postgres-install-helm-install-helm-release attributes: [] + backend: null createdBy: default/action typeRef: path: cap.type.helm.chart.release @@ -1138,6 +1141,7 @@ args: value: null - alias: mattermost-install-create-user-user attributes: [] + backend: null createdBy: default/action typeRef: path: cap.type.database.postgresql.user @@ -1145,6 +1149,7 @@ args: value: null - alias: mattermost-install-create-db-database attributes: [] + backend: null createdBy: default/action typeRef: path: cap.type.database.postgresql.database @@ -1152,6 +1157,7 @@ args: value: null - alias: mattermost-install-helm-install-helm-release attributes: [] + backend: null createdBy: default/action typeRef: path: cap.type.helm.chart.release diff --git a/pkg/sdk/renderer/argo/testdata/TestRenderHappyPath/PostgreSQL_workflow_with_user_input_and_without_TypeInstances.golden.yaml b/pkg/sdk/renderer/argo/testdata/TestRenderHappyPath/PostgreSQL_workflow_with_user_input_and_without_TypeInstances.golden.yaml index 23cf8e558..32777a5c2 100644 --- a/pkg/sdk/renderer/argo/testdata/TestRenderHappyPath/PostgreSQL_workflow_with_user_input_and_without_TypeInstances.golden.yaml +++ b/pkg/sdk/renderer/argo/testdata/TestRenderHappyPath/PostgreSQL_workflow_with_user_input_and_without_TypeInstances.golden.yaml @@ -183,6 +183,7 @@ args: typeInstances: - alias: postgresql attributes: [] + backend: null createdBy: default/action typeRef: path: cap.type.database.postgresql.config @@ -190,6 +191,7 @@ args: value: null - alias: postgres-install-helm-install-helm-release attributes: [] + backend: null createdBy: default/action typeRef: path: cap.type.helm.chart.release diff --git a/pkg/sdk/renderer/argo/testdata/TestRenderHappyPath/Workflow_with_apps_stack_installation_with_user_input.golden.yaml b/pkg/sdk/renderer/argo/testdata/TestRenderHappyPath/Workflow_with_apps_stack_installation_with_user_input.golden.yaml index c0614e990..40446f4b1 100644 --- a/pkg/sdk/renderer/argo/testdata/TestRenderHappyPath/Workflow_with_apps_stack_installation_with_user_input.golden.yaml +++ b/pkg/sdk/renderer/argo/testdata/TestRenderHappyPath/Workflow_with_apps_stack_installation_with_user_input.golden.yaml @@ -1081,6 +1081,7 @@ args: typeInstances: - alias: stack-install-install-shared-db-postgresql attributes: [] + backend: null createdBy: default/action typeRef: path: cap.type.database.postgresql.config @@ -1088,6 +1089,7 @@ args: value: null - alias: stack-install-install-shared-db-postgres-install-helm-install-helm-release attributes: [] + backend: null createdBy: default/action typeRef: path: cap.type.helm.chart.release @@ -1095,6 +1097,7 @@ args: value: null - alias: app1-install-app1-install-app1-config attributes: [] + backend: null createdBy: default/action typeRef: path: cap.type.productivity.app1.config @@ -1102,6 +1105,7 @@ args: value: null - alias: app1-install-app1-install-app1-install-create-db-database attributes: [] + backend: null createdBy: default/action typeRef: path: cap.type.database.postgresql.database @@ -1109,6 +1113,7 @@ args: value: null - alias: app1-install-app1-install-app1-install-helm-install-helm-release attributes: [] + backend: null createdBy: default/action typeRef: path: cap.type.helm.chart.release @@ -1116,6 +1121,7 @@ args: value: null - alias: app2-install-app2-install-app2-config attributes: [] + backend: null createdBy: default/action typeRef: path: cap.type.productivity.app2.config @@ -1123,6 +1129,7 @@ args: value: null - alias: app2-install-app2-install-main-create-db-database attributes: [] + backend: null createdBy: default/action typeRef: path: cap.type.database.postgresql.database @@ -1130,6 +1137,7 @@ args: value: null - alias: app2-install-app2-install-main-helm-install-helm-release attributes: [] + backend: null createdBy: default/action typeRef: path: cap.type.helm.chart.release diff --git a/pkg/sdk/renderer/argo/testdata/TestRenderHappyPathWithCustomPolicies/CloudSQL_PostgreSQL_installation_with_GCP_SA_injected.golden.yaml b/pkg/sdk/renderer/argo/testdata/TestRenderHappyPathWithCustomPolicies/CloudSQL_PostgreSQL_installation_with_GCP_SA_injected.golden.yaml index 813c232af..dae24c648 100644 --- a/pkg/sdk/renderer/argo/testdata/TestRenderHappyPathWithCustomPolicies/CloudSQL_PostgreSQL_installation_with_GCP_SA_injected.golden.yaml +++ b/pkg/sdk/renderer/argo/testdata/TestRenderHappyPathWithCustomPolicies/CloudSQL_PostgreSQL_installation_with_GCP_SA_injected.golden.yaml @@ -197,6 +197,7 @@ args: typeInstances: - alias: postgresql attributes: [] + backend: null createdBy: owner typeRef: path: cap.type.database.postgresql.config @@ -204,6 +205,7 @@ args: value: null - alias: postgres-install-cloudsql-run-cloudsql-instance attributes: [] + backend: null createdBy: owner typeRef: path: cap.type.gcp.cloudsql.instance diff --git a/pkg/sdk/renderer/argo/testdata/TestRenderHappyPathWithCustomPolicies/Mattermost_with_AWS_RDS_install.golden.yaml b/pkg/sdk/renderer/argo/testdata/TestRenderHappyPathWithCustomPolicies/Mattermost_with_AWS_RDS_install.golden.yaml index 1472aff02..9a4352851 100644 --- a/pkg/sdk/renderer/argo/testdata/TestRenderHappyPathWithCustomPolicies/Mattermost_with_AWS_RDS_install.golden.yaml +++ b/pkg/sdk/renderer/argo/testdata/TestRenderHappyPathWithCustomPolicies/Mattermost_with_AWS_RDS_install.golden.yaml @@ -1429,6 +1429,7 @@ args: typeInstances: - alias: mattermost-config attributes: [] + backend: null createdBy: owner typeRef: path: cap.type.productivity.mattermost.config @@ -1436,6 +1437,7 @@ args: value: null - alias: mattermost-install-install-db-postgresql attributes: [] + backend: null createdBy: owner typeRef: path: cap.type.database.postgresql.config @@ -1443,6 +1445,7 @@ args: value: null - alias: mattermost-install-install-db-rds-instance attributes: [] + backend: null createdBy: owner typeRef: path: cap.type.aws.rds.instance @@ -1450,6 +1453,7 @@ args: value: null - alias: mattermost-install-install-db-rds-provision-terraform-apply-terraform-release attributes: [] + backend: null createdBy: owner typeRef: path: cap.type.terraform.release @@ -1457,6 +1461,7 @@ args: value: null - alias: mattermost-install-install-db-rds-provision-terraform-apply-tfstate attributes: [] + backend: null createdBy: owner typeRef: path: cap.core.type.generic.value @@ -1464,6 +1469,7 @@ args: value: null - alias: mattermost-install-create-user-user attributes: [] + backend: null createdBy: owner typeRef: path: cap.type.database.postgresql.user @@ -1471,6 +1477,7 @@ args: value: null - alias: mattermost-install-create-db-database attributes: [] + backend: null createdBy: owner typeRef: path: cap.type.database.postgresql.database @@ -1478,6 +1485,7 @@ args: value: null - alias: mattermost-install-helm-install-helm-release attributes: [] + backend: null createdBy: owner typeRef: path: cap.type.helm.chart.release diff --git a/pkg/sdk/renderer/argo/testdata/TestRenderHappyPathWithCustomPolicies/Mattermost_with_CloudSQL_PostgreSQL_installation_with_GCP_SA_injected.golden.yaml b/pkg/sdk/renderer/argo/testdata/TestRenderHappyPathWithCustomPolicies/Mattermost_with_CloudSQL_PostgreSQL_installation_with_GCP_SA_injected.golden.yaml index 5c81fd2df..2907f6dcc 100644 --- a/pkg/sdk/renderer/argo/testdata/TestRenderHappyPathWithCustomPolicies/Mattermost_with_CloudSQL_PostgreSQL_installation_with_GCP_SA_injected.golden.yaml +++ b/pkg/sdk/renderer/argo/testdata/TestRenderHappyPathWithCustomPolicies/Mattermost_with_CloudSQL_PostgreSQL_installation_with_GCP_SA_injected.golden.yaml @@ -1148,6 +1148,7 @@ args: typeInstances: - alias: mattermost-config attributes: [] + backend: null createdBy: owner typeRef: path: cap.type.productivity.mattermost.config @@ -1155,6 +1156,7 @@ args: value: null - alias: mattermost-install-install-db-postgresql attributes: [] + backend: null createdBy: owner typeRef: path: cap.type.database.postgresql.config @@ -1162,6 +1164,7 @@ args: value: null - alias: mattermost-install-install-db-postgres-install-cloudsql-run-cloudsql-instance attributes: [] + backend: null createdBy: owner typeRef: path: cap.type.gcp.cloudsql.instance @@ -1169,6 +1172,7 @@ args: value: null - alias: mattermost-install-create-user-user attributes: [] + backend: null createdBy: owner typeRef: path: cap.type.database.postgresql.user @@ -1176,6 +1180,7 @@ args: value: null - alias: mattermost-install-create-db-database attributes: [] + backend: null createdBy: owner typeRef: path: cap.type.database.postgresql.database @@ -1183,6 +1188,7 @@ args: value: null - alias: mattermost-install-helm-install-helm-release attributes: [] + backend: null createdBy: owner typeRef: path: cap.type.helm.chart.release diff --git a/pkg/sdk/renderer/argo/testdata/TestRenderHappyPathWithCustomPolicies/Mattermost_with_CloudSQL_using_Terraform.golden.yaml b/pkg/sdk/renderer/argo/testdata/TestRenderHappyPathWithCustomPolicies/Mattermost_with_CloudSQL_using_Terraform.golden.yaml index bdb2d3e97..b97715d18 100644 --- a/pkg/sdk/renderer/argo/testdata/TestRenderHappyPathWithCustomPolicies/Mattermost_with_CloudSQL_using_Terraform.golden.yaml +++ b/pkg/sdk/renderer/argo/testdata/TestRenderHappyPathWithCustomPolicies/Mattermost_with_CloudSQL_using_Terraform.golden.yaml @@ -1211,6 +1211,7 @@ args: typeInstances: - alias: mattermost-config attributes: [] + backend: null createdBy: owner typeRef: path: cap.type.productivity.mattermost.config @@ -1218,6 +1219,7 @@ args: value: null - alias: mattermost-install-install-db-postgresql attributes: [] + backend: null createdBy: owner typeRef: path: cap.type.database.postgresql.config @@ -1225,6 +1227,7 @@ args: value: null - alias: mattermost-install-install-db-postgres-install-terraform-apply-terraform-release attributes: [] + backend: null createdBy: owner typeRef: path: cap.type.terraform.release @@ -1232,6 +1235,7 @@ args: value: null - alias: mattermost-install-install-db-postgres-install-terraform-apply-tfstate attributes: [] + backend: null createdBy: owner typeRef: path: cap.core.type.generic.value @@ -1239,6 +1243,7 @@ args: value: null - alias: mattermost-install-create-user-user attributes: [] + backend: null createdBy: owner typeRef: path: cap.type.database.postgresql.user @@ -1246,6 +1251,7 @@ args: value: null - alias: mattermost-install-create-db-database attributes: [] + backend: null createdBy: owner typeRef: path: cap.type.database.postgresql.database @@ -1253,6 +1259,7 @@ args: value: null - alias: mattermost-install-helm-install-helm-release attributes: [] + backend: null createdBy: owner typeRef: path: cap.type.helm.chart.release diff --git a/pkg/sdk/renderer/argo/testdata/TestRenderHappyPathWithCustomPolicies/Mattermost_with_existing_DB_installation.golden.yaml b/pkg/sdk/renderer/argo/testdata/TestRenderHappyPathWithCustomPolicies/Mattermost_with_existing_DB_installation.golden.yaml index 7b4dba4b7..b8a07bdca 100644 --- a/pkg/sdk/renderer/argo/testdata/TestRenderHappyPathWithCustomPolicies/Mattermost_with_existing_DB_installation.golden.yaml +++ b/pkg/sdk/renderer/argo/testdata/TestRenderHappyPathWithCustomPolicies/Mattermost_with_existing_DB_installation.golden.yaml @@ -985,6 +985,7 @@ args: typeInstances: - alias: mattermost-config attributes: [] + backend: null createdBy: owner typeRef: path: cap.type.productivity.mattermost.config @@ -992,6 +993,7 @@ args: value: null - alias: mattermost-install-create-user-user attributes: [] + backend: null createdBy: owner typeRef: path: cap.type.database.postgresql.user @@ -999,6 +1001,7 @@ args: value: null - alias: mattermost-install-create-db-database attributes: [] + backend: null createdBy: owner typeRef: path: cap.type.database.postgresql.database @@ -1006,6 +1009,7 @@ args: value: null - alias: mattermost-install-helm-install-helm-release attributes: [] + backend: null createdBy: owner typeRef: path: cap.type.helm.chart.release diff --git a/pkg/sdk/renderer/argo/testdata/TestRenderHappyPathWithCustomPolicies/RDS_installation_with_AWS_SA_and_additional_parameters_injected.golden.yaml b/pkg/sdk/renderer/argo/testdata/TestRenderHappyPathWithCustomPolicies/RDS_installation_with_AWS_SA_and_additional_parameters_injected.golden.yaml index 2519164eb..d63517542 100644 --- a/pkg/sdk/renderer/argo/testdata/TestRenderHappyPathWithCustomPolicies/RDS_installation_with_AWS_SA_and_additional_parameters_injected.golden.yaml +++ b/pkg/sdk/renderer/argo/testdata/TestRenderHappyPathWithCustomPolicies/RDS_installation_with_AWS_SA_and_additional_parameters_injected.golden.yaml @@ -482,6 +482,7 @@ args: typeInstances: - alias: postgresql attributes: [] + backend: null createdBy: owner typeRef: path: cap.type.database.postgresql.config @@ -489,6 +490,7 @@ args: value: null - alias: rds-instance attributes: [] + backend: null createdBy: owner typeRef: path: cap.type.aws.rds.instance @@ -496,6 +498,7 @@ args: value: null - alias: rds-provision-terraform-apply-terraform-release attributes: [] + backend: null createdBy: owner typeRef: path: cap.type.terraform.release @@ -503,6 +506,7 @@ args: value: null - alias: rds-provision-terraform-apply-tfstate attributes: [] + backend: null createdBy: owner typeRef: path: cap.core.type.generic.value diff --git a/pkg/sdk/renderer/argo/testdata/TestRenderHappyPathWithCustomPolicies/Unmet_policy_constraints_-_fallback_to_Bitnami_Implementation.golden.yaml b/pkg/sdk/renderer/argo/testdata/TestRenderHappyPathWithCustomPolicies/Unmet_policy_constraints_-_fallback_to_Bitnami_Implementation.golden.yaml index 737351536..21493476a 100644 --- a/pkg/sdk/renderer/argo/testdata/TestRenderHappyPathWithCustomPolicies/Unmet_policy_constraints_-_fallback_to_Bitnami_Implementation.golden.yaml +++ b/pkg/sdk/renderer/argo/testdata/TestRenderHappyPathWithCustomPolicies/Unmet_policy_constraints_-_fallback_to_Bitnami_Implementation.golden.yaml @@ -183,6 +183,7 @@ args: typeInstances: - alias: postgresql attributes: [] + backend: null createdBy: owner typeRef: path: cap.type.database.postgresql.config @@ -190,6 +191,7 @@ args: value: null - alias: postgres-install-helm-install-helm-release attributes: [] + backend: null createdBy: owner typeRef: path: cap.type.helm.chart.release diff --git a/pkg/sdk/renderer/argo/testdata/TestRenderHappyPathWithCustomPolicies/Workflow_policy_injects_additional_input_-_reference_by_ManifestRef.golden.yaml b/pkg/sdk/renderer/argo/testdata/TestRenderHappyPathWithCustomPolicies/Workflow_policy_injects_additional_input_-_reference_by_ManifestRef.golden.yaml index e272701e6..075634e6a 100644 --- a/pkg/sdk/renderer/argo/testdata/TestRenderHappyPathWithCustomPolicies/Workflow_policy_injects_additional_input_-_reference_by_ManifestRef.golden.yaml +++ b/pkg/sdk/renderer/argo/testdata/TestRenderHappyPathWithCustomPolicies/Workflow_policy_injects_additional_input_-_reference_by_ManifestRef.golden.yaml @@ -869,6 +869,7 @@ args: typeInstances: - alias: app1-config attributes: [] + backend: null createdBy: owner typeRef: path: cap.type.productivity.app1.config @@ -876,6 +877,7 @@ args: value: null - alias: app1-install-install-db-postgresql attributes: [] + backend: null createdBy: owner typeRef: path: cap.type.database.postgresql.config @@ -883,6 +885,7 @@ args: value: null - alias: app1-install-install-db-rds-instance attributes: [] + backend: null createdBy: owner typeRef: path: cap.type.aws.rds.instance @@ -890,6 +893,7 @@ args: value: null - alias: app1-install-install-db-rds-provision-terraform-apply-terraform-release attributes: [] + backend: null createdBy: owner typeRef: path: cap.type.terraform.release @@ -897,6 +901,7 @@ args: value: null - alias: app1-install-install-db-rds-provision-terraform-apply-tfstate attributes: [] + backend: null createdBy: owner typeRef: path: cap.core.type.generic.value @@ -904,6 +909,7 @@ args: value: null - alias: app1-install-create-db-database attributes: [] + backend: null createdBy: owner typeRef: path: cap.type.database.postgresql.database @@ -911,6 +917,7 @@ args: value: null - alias: app1-install-helm-install-helm-release attributes: [] + backend: null createdBy: owner typeRef: path: cap.type.helm.chart.release diff --git a/pkg/sdk/renderer/argo/testdata/TestRenderHappyPathWithCustomPolicies/Workflow_policy_injects_additional_input_-_reference_by_alias.golden.yaml b/pkg/sdk/renderer/argo/testdata/TestRenderHappyPathWithCustomPolicies/Workflow_policy_injects_additional_input_-_reference_by_alias.golden.yaml index 974d81526..8e0730076 100644 --- a/pkg/sdk/renderer/argo/testdata/TestRenderHappyPathWithCustomPolicies/Workflow_policy_injects_additional_input_-_reference_by_alias.golden.yaml +++ b/pkg/sdk/renderer/argo/testdata/TestRenderHappyPathWithCustomPolicies/Workflow_policy_injects_additional_input_-_reference_by_alias.golden.yaml @@ -869,6 +869,7 @@ args: typeInstances: - alias: app2-config attributes: [] + backend: null createdBy: owner typeRef: path: cap.type.productivity.app2.config @@ -876,6 +877,7 @@ args: value: null - alias: main-install-db-postgresql attributes: [] + backend: null createdBy: owner typeRef: path: cap.type.database.postgresql.config @@ -883,6 +885,7 @@ args: value: null - alias: main-install-db-rds-instance attributes: [] + backend: null createdBy: owner typeRef: path: cap.type.aws.rds.instance @@ -890,6 +893,7 @@ args: value: null - alias: main-install-db-rds-provision-terraform-apply-terraform-release attributes: [] + backend: null createdBy: owner typeRef: path: cap.type.terraform.release @@ -897,6 +901,7 @@ args: value: null - alias: main-install-db-rds-provision-terraform-apply-tfstate attributes: [] + backend: null createdBy: owner typeRef: path: cap.core.type.generic.value @@ -904,6 +909,7 @@ args: value: null - alias: main-create-db-database attributes: [] + backend: null createdBy: owner typeRef: path: cap.type.database.postgresql.database @@ -911,6 +917,7 @@ args: value: null - alias: main-helm-install-helm-release attributes: [] + backend: null createdBy: owner typeRef: path: cap.type.helm.chart.release From 18fe3d3d482760f277a9c49775bf44cf5897897e Mon Sep 17 00:00:00 2001 From: Mateusz Szostok Date: Sat, 12 Feb 2022 23:59:59 +0100 Subject: [PATCH 08/15] Adjust integration tests --- test/e2e/action_test.go | 8 ++++++-- test/e2e/hub_test.go | 32 ++++++++++++++++++++++++-------- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/test/e2e/action_test.go b/test/e2e/action_test.go index ca9815e71..d24bab16c 100644 --- a/test/e2e/action_test.go +++ b/test/e2e/action_test.go @@ -63,9 +63,11 @@ var _ = Describe("Action", func() { Context("Action execution", func() { - It("should pick Implementation A with empty inject Policy", func() { + It("should pick Implementation A", func() { implIndicatorValue := "Implementation A" + // TODO: This can be extracted after switching to ginkgo v2 + // see: https://github.com/onsi/ginkgo/issues/70#issuecomment-924250145 By("1. Preparing input Type Instances") By("1.1 Creating TypeInstance which will be downloaded") @@ -144,9 +146,11 @@ var _ = Describe("Action", func() { }) - It("should pick proper Implementation B", func() { + It("should pick Implementation B", func() { implIndicatorValue := "Implementation B" + // TODO: This can be extracted after switching to ginkgo v2 + // see: https://github.com/onsi/ginkgo/issues/70#issuecomment-924250145 By("1. Preparing input Type Instances") By("1.1 Creating TypeInstance which will be downloaded") download := getTypeInstanceInputForDownload(implIndicatorValue) diff --git a/test/e2e/hub_test.go b/test/e2e/hub_test.go index 2dc439de8..194b16be1 100644 --- a/test/e2e/hub_test.go +++ b/test/e2e/hub_test.go @@ -246,6 +246,7 @@ var _ = Describe("GraphQL API", func() { Context("Local Hub", func() { It("should create, find and delete TypeInstance", func() { cli := getHubGraphQLClient() + builtinStorage := getBuiltinStorageTypeInstance(ctx, cli) // create TypeInstance createdTypeInstance, err := cli.CreateTypeInstance(ctx, &gqllocalapi.CreateTypeInstanceInput{ @@ -291,6 +292,10 @@ var _ = Describe("GraphQL API", func() { Path: "cap.type.capactio.capact.ti", Revision: "0.1.0", }, + Backend: &gqllocalapi.TypeInstanceBackendReference{ + ID: builtinStorage.ID, + Abstract: true, + }, Uses: []*gqllocalapi.TypeInstance{}, UsedBy: []*gqllocalapi.TypeInstance{}, LatestResourceVersion: rev, @@ -311,6 +316,7 @@ var _ = Describe("GraphQL API", func() { It("creates multiple TypeInstances with uses relations", func() { cli := getHubGraphQLClient() + builtinStorage := getBuiltinStorageTypeInstance(ctx, cli) createdTypeInstanceIDs, err := cli.CreateTypeInstances(ctx, createTypeInstancesInput()) @@ -325,11 +331,11 @@ var _ = Describe("GraphQL API", func() { childTiID := findCreatedTypeInstanceID("child", createdTypeInstanceIDs) Expect(childTiID).ToNot(BeNil()) - expectedChild := expectedChildTypeInstance(*childTiID) - expectedParent := expectedParentTypeInstance(*parentTiID) - expectedChild.UsedBy = []*gqllocalapi.TypeInstance{expectedParentTypeInstance(*parentTiID)} + expectedChild := expectedChildTypeInstance(*childTiID, builtinStorage.ID) + expectedParent := expectedParentTypeInstance(*parentTiID, builtinStorage.ID) + expectedChild.UsedBy = []*gqllocalapi.TypeInstance{expectedParentTypeInstance(*parentTiID, builtinStorage.ID)} expectedChild.Uses = []*gqllocalapi.TypeInstance{} - expectedParent.Uses = []*gqllocalapi.TypeInstance{expectedChildTypeInstance(*childTiID)} + expectedParent.Uses = []*gqllocalapi.TypeInstance{expectedChildTypeInstance(*childTiID, builtinStorage.ID)} expectedParent.UsedBy = []*gqllocalapi.TypeInstance{} assertTypeInstance(ctx, cli, *childTiID, expectedChild) @@ -731,7 +737,7 @@ func createTypeInstancesInput() *gqllocalapi.CreateTypeInstancesInput { } } -func expectedChildTypeInstance(ID string) *gqllocalapi.TypeInstance { +func expectedChildTypeInstance(tiID, backendID string) *gqllocalapi.TypeInstance { tiRev := &gqllocalapi.TypeInstanceResourceVersion{ ResourceVersion: 1, Metadata: &gqllocalapi.TypeInstanceResourceVersionMetadata{ @@ -750,11 +756,16 @@ func expectedChildTypeInstance(ID string) *gqllocalapi.TypeInstance { } return &gqllocalapi.TypeInstance{ - ID: ID, + ID: tiID, TypeRef: &gqllocalapi.TypeInstanceTypeReference{ Path: "com.child", Revision: "0.1.0", }, + + Backend: &gqllocalapi.TypeInstanceBackendReference{ + ID: backendID, + Abstract: true, + }, LatestResourceVersion: tiRev, FirstResourceVersion: tiRev, PreviousResourceVersion: nil, @@ -765,7 +776,7 @@ func expectedChildTypeInstance(ID string) *gqllocalapi.TypeInstance { } } -func expectedParentTypeInstance(ID string) *gqllocalapi.TypeInstance { +func expectedParentTypeInstance(tiID, backendID string) *gqllocalapi.TypeInstance { tiRev := &gqllocalapi.TypeInstanceResourceVersion{ ResourceVersion: 1, Metadata: &gqllocalapi.TypeInstanceResourceVersionMetadata{ @@ -784,11 +795,16 @@ func expectedParentTypeInstance(ID string) *gqllocalapi.TypeInstance { } return &gqllocalapi.TypeInstance{ - ID: ID, + ID: tiID, TypeRef: &gqllocalapi.TypeInstanceTypeReference{ Path: "com.parent", Revision: "0.1.0", }, + + Backend: &gqllocalapi.TypeInstanceBackendReference{ + ID: backendID, + Abstract: true, + }, LatestResourceVersion: tiRev, FirstResourceVersion: tiRev, PreviousResourceVersion: nil, From 777d7d63999f158df7375dc44c6dd83c8da385d2 Mon Sep 17 00:00:00 2001 From: Mateusz Szostok Date: Sun, 13 Feb 2022 16:06:22 +0100 Subject: [PATCH 09/15] Fix e2e tests --- test/e2e/action_test.go | 61 ++++++++++++++++++++++++----------------- test/e2e/hub_test.go | 6 ++-- 2 files changed, 39 insertions(+), 28 deletions(-) diff --git a/test/e2e/action_test.go b/test/e2e/action_test.go index d24bab16c..15a0b5b0d 100644 --- a/test/e2e/action_test.go +++ b/test/e2e/action_test.go @@ -18,6 +18,7 @@ import ( engine "capact.io/capact/pkg/engine/client" hublocalgraphql "capact.io/capact/pkg/hub/api/graphql/local" hubclient "capact.io/capact/pkg/hub/client" + "capact.io/capact/pkg/hub/client/local" . "github.com/onsi/ginkgo" . "github.com/onsi/ginkgo/extensions/table" @@ -93,17 +94,8 @@ var _ = Describe("Action", func() { } builtinStorage := getBuiltinStorageTypeInstance(ctx, hubClient) - expUpdatedTI := &enginegraphql.OutputTypeInstanceDetails{ - ID: updateTI.ID, - TypeRef: &enginegraphql.ManifestReference{ - Path: updateTI.TypeRef.Path, - Revision: updateTI.TypeRef.Revision, - }, - Backend: &enginegraphql.TypeInstanceBackendDetails{ - ID: builtinStorage.ID, - Abstract: true, - }, - } + expUpdatedTIOutput := mapToOutputTypeInstanceDetails(updateTI, builtinStorage.Backend) + By("2. Expecting Implementation A is picked and builtin storage is used...") action := createActionAndWaitForReadyToRunPhase(ctx, engineClient, actionName, actionPath, inputData) @@ -111,18 +103,20 @@ var _ = Describe("Action", func() { runActionAndWaitForSucceeded(ctx, engineClient, actionName) By("3.1 Check uploaded TypeInstances") - uploadedTI, cleanup := getUploadedTypeInstanceByValue(ctx, hubClient, implIndicatorValue) - defer cleanup() // We need to clean it up as it's not deleted when Action is deleted. - Expect(uploadedTI.Backend).Should(Equal(&hublocalgraphql.TypeInstanceBackendReference{ID: builtinStorage.ID, Abstract: true})) + expUploadTIBackend := &hublocalgraphql.TypeInstanceBackendReference{ID: builtinStorage.ID, Abstract: true} + uploadedTI, cleanupUploaded := getUploadedTypeInstanceByValue(ctx, hubClient, implIndicatorValue) + Expect(uploadedTI.Backend).Should(Equal(expUploadTIBackend)) By("3.2 Check updated TypeInstances") getTypeInstanceByIDAndValue(ctx, hubClient, updateTI.ID, implIndicatorValue) By("3.3 Check Action output TypeInstances") - assertOutputTypeInstancesInActionStatus(ctx, engineClient, action.Name, And(ContainElements(expUpdatedTI, uploadedTI), HaveLen(2))) + uploadedTIOutput := mapToOutputTypeInstanceDetails(uploadedTI, expUploadTIBackend) + assertOutputTypeInstancesInActionStatus(ctx, engineClient, action.Name, And(ContainElements(expUpdatedTIOutput, uploadedTIOutput), HaveLen(2))) By("4. Deleting Action...") err := engineClient.DeleteAction(ctx, actionName) + cleanupUploaded() // We need to clean it up as it's not deleted when Action is deleted. Expect(err).ToNot(HaveOccurred()) By("5. Waiting for Action deleted") @@ -137,13 +131,14 @@ var _ = Describe("Action", func() { runActionAndWaitForSucceeded(ctx, engineClient, actionName) By("8.1 Check uploaded TypeInstances") - uploadedTI, cleanup = getUploadedTypeInstanceByValue(ctx, hubClient, implIndicatorValue) - defer cleanup() // We need to clean it up as it's not deleted when Action is deleted. - Expect(uploadedTI.Backend).Should(Equal(&hublocalgraphql.TypeInstanceBackendReference{ID: helmStorageTI.ID, Abstract: false})) + expUploadTIBackend = &hublocalgraphql.TypeInstanceBackendReference{ID: helmStorageTI.ID, Abstract: false} + uploadedTI, cleanupUploaded = getUploadedTypeInstanceByValue(ctx, hubClient, implIndicatorValue) + defer cleanupUploaded() // We need to clean it up as it's not deleted when Action is deleted. + Expect(uploadedTI.Backend).Should(Equal(expUploadTIBackend)) By("8.2 Check Action output TypeInstances") - assertOutputTypeInstancesInActionStatus(ctx, engineClient, action.Name, And(ContainElements(expUpdatedTI, uploadedTI), HaveLen(2))) - + uploadedTIOutput = mapToOutputTypeInstanceDetails(uploadedTI, expUploadTIBackend) + assertOutputTypeInstancesInActionStatus(ctx, engineClient, action.Name, And(ContainElements(expUpdatedTIOutput, uploadedTIOutput), HaveLen(2))) }) It("should pick Implementation B", func() { @@ -198,12 +193,14 @@ var _ = Describe("Action", func() { runActionAndWaitForSucceeded(ctx, engineClient, actionName) By("4.1 Check uploaded TypeInstances") - uploadedTI, cleanup := getUploadedTypeInstanceByValue(ctx, hubClient, implIndicatorValue) - defer cleanup() // We need to clean it up as it's not deleted when Action is deleted. - Expect(uploadedTI.Backend).Should(Equal(&hublocalgraphql.TypeInstanceBackendReference{ID: helmStorageTI.ID, Abstract: false})) + expUploadTIBackend := &hublocalgraphql.TypeInstanceBackendReference{ID: helmStorageTI.ID, Abstract: false} + uploadedTI, cleanupUploaded := getUploadedTypeInstanceByValue(ctx, hubClient, implIndicatorValue) + defer cleanupUploaded() // We need to clean it up as it's not deleted when Action is deleted. + Expect(uploadedTI.Backend).Should(Equal(expUploadTIBackend)) By("4.2 Check Action output TypeInstances") - assertOutputTypeInstancesInActionStatus(ctx, engineClient, action.Name, And(ContainElements(uploadedTI), HaveLen(2))) + uploadedTIOutput := mapToOutputTypeInstanceDetails(uploadedTI, expUploadTIBackend) + assertOutputTypeInstancesInActionStatus(ctx, engineClient, action.Name, And(ContainElements(uploadedTIOutput), HaveLen(1))) }) It("should have failed status after a failed workflow", func() { @@ -309,6 +306,20 @@ var _ = Describe("Action", func() { }) }) +func mapToOutputTypeInstanceDetails(ti *hublocalgraphql.TypeInstance, backend *hublocalgraphql.TypeInstanceBackendReference) *enginegraphql.OutputTypeInstanceDetails { + return &enginegraphql.OutputTypeInstanceDetails{ + ID: ti.ID, + TypeRef: &enginegraphql.ManifestReference{ + Path: ti.TypeRef.Path, + Revision: ti.TypeRef.Revision, + }, + Backend: &enginegraphql.TypeInstanceBackendDetails{ + ID: backend.ID, + Abstract: backend.Abstract, + }, + } +} + func getActionStatusFunc(ctx context.Context, cl *engine.Client, name string) func() (enginegraphql.ActionStatusPhase, error) { return func() (enginegraphql.ActionStatusPhase, error) { action, err := cl.GetAction(ctx, name) @@ -582,7 +593,7 @@ func getBuiltinStorageTypeInstance(ctx context.Context, hubClient *hubclient.Cli Path: builtinStorageTypePath, Revision: ptr.String("0.1.0"), }, - }) + }, local.WithFields(local.TypeInstanceAllFields)) Expect(err).ToNot(HaveOccurred()) Expect(coreStorage).Should(HaveLen(1)) diff --git a/test/e2e/hub_test.go b/test/e2e/hub_test.go index 194b16be1..b3199c95a 100644 --- a/test/e2e/hub_test.go +++ b/test/e2e/hub_test.go @@ -296,7 +296,7 @@ var _ = Describe("GraphQL API", func() { ID: builtinStorage.ID, Abstract: true, }, - Uses: []*gqllocalapi.TypeInstance{}, + Uses: []*gqllocalapi.TypeInstance{&builtinStorage}, UsedBy: []*gqllocalapi.TypeInstance{}, LatestResourceVersion: rev, FirstResourceVersion: rev, @@ -334,8 +334,8 @@ var _ = Describe("GraphQL API", func() { expectedChild := expectedChildTypeInstance(*childTiID, builtinStorage.ID) expectedParent := expectedParentTypeInstance(*parentTiID, builtinStorage.ID) expectedChild.UsedBy = []*gqllocalapi.TypeInstance{expectedParentTypeInstance(*parentTiID, builtinStorage.ID)} - expectedChild.Uses = []*gqllocalapi.TypeInstance{} - expectedParent.Uses = []*gqllocalapi.TypeInstance{expectedChildTypeInstance(*childTiID, builtinStorage.ID)} + expectedChild.Uses = []*gqllocalapi.TypeInstance{&builtinStorage} + expectedParent.Uses = []*gqllocalapi.TypeInstance{expectedChildTypeInstance(*childTiID, builtinStorage.ID), &builtinStorage} expectedParent.UsedBy = []*gqllocalapi.TypeInstance{} assertTypeInstance(ctx, cli, *childTiID, expectedChild) From 5a4a9b95ff0aa3a26ebde746e7e5afe891523f15 Mon Sep 17 00:00:00 2001 From: Mateusz Szostok Date: Sun, 13 Feb 2022 16:19:45 +0100 Subject: [PATCH 10/15] Add seed to rand --- test/e2e/action_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/test/e2e/action_test.go b/test/e2e/action_test.go index 15a0b5b0d..14c477ee9 100644 --- a/test/e2e/action_test.go +++ b/test/e2e/action_test.go @@ -35,6 +35,7 @@ const ( ) func getActionName() string { + rand.Seed(time.Now().UTC().UnixNano()) return fmt.Sprintf("e2e-test-%d-%s", GinkgoParallelNode(), strconv.Itoa(rand.Intn(10000))) } From d8622804c0371d198a98cd71d89f84d551a766ce Mon Sep 17 00:00:00 2001 From: Mateusz Szostok Date: Sun, 13 Feb 2022 21:08:37 +0100 Subject: [PATCH 11/15] Fix bug in install scripts --- hack/lib/utilities.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hack/lib/utilities.sh b/hack/lib/utilities.sh index 80b3d125f..db95e5f4a 100644 --- a/hack/lib/utilities.sh +++ b/hack/lib/utilities.sh @@ -244,11 +244,11 @@ capact::install() { fi if [ -n "${HUB_MANIFESTS_SOURCE_REPO_REF:-}" ]; then - CAPACT_OVERRIDES+=",hub-public.populator.manifestsLocation.branch=${HUB_MANIFESTS_SOURCE_REPO_REF}" + CAPACT_OVERRIDES+=",hub-public.populator.manifestsLocations[0].branch=${HUB_MANIFESTS_SOURCE_REPO_REF}" fi if [ -n "${HUB_MANIFESTS_SOURCE_REPO_URL:-}" ]; then - CAPACT_OVERRIDES+=",hub-public.populator.manifestsLocation.repository=${HUB_MANIFESTS_SOURCE_REPO_URL}" + CAPACT_OVERRIDES+=",hub-public.populator.manifestsLocations[0].repository=${HUB_MANIFESTS_SOURCE_REPO_URL}" fi if [ -n "${DOCKER_REPOSITORY:-}" ]; then From 7c8a5e4b6421d0001a5004b0f898934172f39cb1 Mon Sep 17 00:00:00 2001 From: Mateusz Szostok Date: Sun, 13 Feb 2022 22:18:10 +0100 Subject: [PATCH 12/15] Use commit sha as branch with / is not supported --- hack/lib/const.sh | 2 +- test/e2e/hub_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/hack/lib/const.sh b/hack/lib/const.sh index 7e4246414..b91c68e0f 100644 --- a/hack/lib/const.sh +++ b/hack/lib/const.sh @@ -49,4 +49,4 @@ readonly CAPACT_INCREASE_RESOURCE_LIMITS="true" readonly CAPACT_HUB_MANIFESTS_SOURCE_REPO_URL="github.com/mszostok/os-hub-manifests" # The git ref to checkout. It can point to a commit SHA, a branch name, or a tag. # If you want to use your forked version, remember to update CAPACT_HUB_MANIFESTS_SOURCE_REPO_URL respectively. -readonly CAPACT_HUB_MANIFESTS_SOURCE_REPO_REF="policy-syntax/type-instances" +readonly CAPACT_HUB_MANIFESTS_SOURCE_REPO_REF="42fe61d2bee271c9829adc37c9fa64047fa81085" diff --git a/test/e2e/hub_test.go b/test/e2e/hub_test.go index b3199c95a..9b36348ed 100644 --- a/test/e2e/hub_test.go +++ b/test/e2e/hub_test.go @@ -335,7 +335,7 @@ var _ = Describe("GraphQL API", func() { expectedParent := expectedParentTypeInstance(*parentTiID, builtinStorage.ID) expectedChild.UsedBy = []*gqllocalapi.TypeInstance{expectedParentTypeInstance(*parentTiID, builtinStorage.ID)} expectedChild.Uses = []*gqllocalapi.TypeInstance{&builtinStorage} - expectedParent.Uses = []*gqllocalapi.TypeInstance{expectedChildTypeInstance(*childTiID, builtinStorage.ID), &builtinStorage} + expectedParent.Uses = []*gqllocalapi.TypeInstance{&builtinStorage, expectedChildTypeInstance(*childTiID, builtinStorage.ID)} expectedParent.UsedBy = []*gqllocalapi.TypeInstance{} assertTypeInstance(ctx, cli, *childTiID, expectedChild) From 86f0be8dbe65c3e69620e9a1343ea88e514171a6 Mon Sep 17 00:00:00 2001 From: Mateusz Szostok Date: Sun, 13 Feb 2022 23:41:18 +0100 Subject: [PATCH 13/15] Add more logging, fix panic --- pkg/sdk/renderer/argo/dedicated_renderer.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pkg/sdk/renderer/argo/dedicated_renderer.go b/pkg/sdk/renderer/argo/dedicated_renderer.go index 29bd2f79d..e296436c2 100644 --- a/pkg/sdk/renderer/argo/dedicated_renderer.go +++ b/pkg/sdk/renderer/argo/dedicated_renderer.go @@ -1004,13 +1004,15 @@ func (r *dedicatedRenderer) addOutputTypeInstancesToGraph(step *WorkflowStep, pr return errors.Wrapf(err, "while resolving backend alias for %s", step.Name) } + log := r.log.With(zap.String("artifactName", *artifactName)) + log.Debug("Available TypeInstance Backend", zap.Any("backends", backends.GetAll())) + backend, err := r.selectBackend(backendAlias, typeRef, backends) if err != nil { - return errors.Wrapf(err, "while resolving backend ID for %s", step.Name) + return errors.Wrapf(err, "while resolving backend ID for %s", name) } - log := r.log.With(zap.String("artifactName", *artifactName)) - log.Debug("Selected TypeInstance Backend", zap.Any("backend", backend), zap.Any("backends", backends.GetAll())) + log.Debug("Selected TypeInstance Backend", zap.Any("backend", backend)) // add output r.typeInstancesToOutput.typeInstances = append(r.typeInstancesToOutput.typeInstances, OutputTypeInstance{ From 020110a41ba1671fd4edfa85c4f43a38cac36170 Mon Sep 17 00:00:00 2001 From: Mateusz Szostok Date: Wed, 16 Feb 2022 17:59:07 +0100 Subject: [PATCH 14/15] Apply changes after review --- pkg/engine/api/graphql/schema.graphql | 4 +- pkg/engine/api/graphql/schema_gen.go | 4 +- .../k8s/policy/metadata/metadata_resolver.go | 4 +- pkg/hub/client/policy_enforced_client.go | 2 +- .../{facade/types => }/additional_refs.go | 13 +++-- pkg/hub/client/public/facade/doc.go | 3 -- pkg/sdk/renderer/argo/dedicated_renderer.go | 22 ++++---- .../manifest/json_remote_implementation.go | 5 +- pkg/sdk/validation/policy/policy.go | 50 ++++++++----------- test/e2e/action_test.go | 25 +++++----- 10 files changed, 58 insertions(+), 74 deletions(-) rename pkg/hub/client/public/{facade/types => }/additional_refs.go (82%) delete mode 100644 pkg/hub/client/public/facade/doc.go diff --git a/pkg/engine/api/graphql/schema.graphql b/pkg/engine/api/graphql/schema.graphql index 32e89efd3..d8c9c0330 100644 --- a/pkg/engine/api/graphql/schema.graphql +++ b/pkg/engine/api/graphql/schema.graphql @@ -335,7 +335,7 @@ input InterfacePolicyInput { input RulesForInterfaceInput { interface: ManifestReferenceInput! - oneOf: [PolicyRuleInput!]! + oneOf: [PolicyRuleInput!]! } input PolicyRuleInput { @@ -398,7 +398,7 @@ type InterfacePolicy { type RulesForInterface { interface: ManifestReferenceWithOptionalRevision! - oneOf: [PolicyRule!]! + oneOf: [PolicyRule!]! } type PolicyRule { diff --git a/pkg/engine/api/graphql/schema_gen.go b/pkg/engine/api/graphql/schema_gen.go index 808b1d07a..9b46e58ac 100644 --- a/pkg/engine/api/graphql/schema_gen.go +++ b/pkg/engine/api/graphql/schema_gen.go @@ -1208,7 +1208,7 @@ input InterfacePolicyInput { input RulesForInterfaceInput { interface: ManifestReferenceInput! - oneOf: [PolicyRuleInput!]! + oneOf: [PolicyRuleInput!]! } input PolicyRuleInput { @@ -1271,7 +1271,7 @@ type InterfacePolicy { type RulesForInterface { interface: ManifestReferenceWithOptionalRevision! - oneOf: [PolicyRule!]! + oneOf: [PolicyRule!]! } type PolicyRule { diff --git a/pkg/engine/k8s/policy/metadata/metadata_resolver.go b/pkg/engine/k8s/policy/metadata/metadata_resolver.go index d661a6991..9f41f5021 100644 --- a/pkg/engine/k8s/policy/metadata/metadata_resolver.go +++ b/pkg/engine/k8s/policy/metadata/metadata_resolver.go @@ -10,7 +10,6 @@ import ( hublocalgraphql "capact.io/capact/pkg/hub/api/graphql/local" hubpublicgraphql "capact.io/capact/pkg/hub/api/graphql/public" "capact.io/capact/pkg/hub/client/public" - typesutil "capact.io/capact/pkg/hub/client/public/facade/types" "capact.io/capact/pkg/sdk/apis/0.0.1/types" multierr "github.com/hashicorp/go-multierror" @@ -92,7 +91,7 @@ type TypeRefWithAdditionalRefs struct { func (r *Resolver) enrichWithParentNodes(ctx context.Context, refs map[string]hublocalgraphql.TypeInstanceTypeReference) (map[string]TypeRefWithAdditionalRefs, error) { typesPath := r.mapToTypeRefs(refs) - gotAttachedTypes, err := typesutil.ListAdditionalRefs(ctx, r.hubCli, typesPath) + gotAttachedTypes, err := public.ListAdditionalRefs(ctx, r.hubCli, typesPath) if err != nil { return nil, errors.Wrap(err, "while fetching Types") } @@ -159,7 +158,6 @@ func (r *Resolver) setTypeRefsForAdditionalTypeInstances(policy *policy.Policy, } } -// TODO(storage) set also rule.TypeRef.Revision.. func (r *Resolver) setTypeRefsForBackendTypeInstances(policy *policy.Policy, typeRefs map[string]TypeRefWithAdditionalRefs) { for ruleIdx, rule := range policy.TypeInstance.Rules { typeRef, exists := typeRefs[rule.Backend.ID] diff --git a/pkg/hub/client/policy_enforced_client.go b/pkg/hub/client/policy_enforced_client.go index 39ffe9f48..ea5fe79e3 100644 --- a/pkg/hub/client/policy_enforced_client.go +++ b/pkg/hub/client/policy_enforced_client.go @@ -113,7 +113,7 @@ func (e *PolicyEnforcedClient) ListTypeInstancesBackendsBasedOnPolicy(_ context. out.SetByTypeRef(rule.TypeRef, rule.Backend) } - // TODO(https://github.com/capactio/capact/issues/624): + // TODO(https://github.com/capactio/capact/issues/635): // 2. Global defaults based on required TypeInstance injection // e.mergedPolicy.Interface.Defaults diff --git a/pkg/hub/client/public/facade/types/additional_refs.go b/pkg/hub/client/public/additional_refs.go similarity index 82% rename from pkg/hub/client/public/facade/types/additional_refs.go rename to pkg/hub/client/public/additional_refs.go index 95959f120..12f0696d8 100644 --- a/pkg/hub/client/public/facade/types/additional_refs.go +++ b/pkg/hub/client/public/additional_refs.go @@ -1,4 +1,4 @@ -package types +package public import ( "context" @@ -6,18 +6,17 @@ import ( "capact.io/capact/internal/ptr" "capact.io/capact/internal/regexutil" gqlpublicapi "capact.io/capact/pkg/hub/api/graphql/public" - "capact.io/capact/pkg/hub/client/public" "capact.io/capact/pkg/sdk/apis/0.0.1/types" "github.com/pkg/errors" ) // listAdditionalRefsFields defines preset for response fields required for collection Type's spec.additionalRefs -const listAdditionalRefsFields = public.TypeRevisionRootFields | public.TypeRevisionSpecAdditionalRefsField +const listAdditionalRefsFields = TypeRevisionRootFields | TypeRevisionSpecAdditionalRefsField // ListAdditionalRefsClient defines external Hub calls used by ListAdditionalRefs. type ListAdditionalRefsClient interface { - ListTypes(ctx context.Context, opts ...public.TypeOption) ([]*gqlpublicapi.Type, error) + ListTypes(ctx context.Context, opts ...TypeOption) ([]*gqlpublicapi.Type, error) } // ListAdditionalRefsOutput holds Type's spec.additionalRefs entry indexed by TypeRef key. @@ -30,9 +29,9 @@ type ListAdditionalRefsOutput map[types.TypeRef][]string func ListAdditionalRefs(ctx context.Context, cli ListAdditionalRefsClient, reqTypes []types.TypeRef) (ListAdditionalRefsOutput, error) { filter := regexutil.OrStringSlice(mapToPaths(reqTypes)) - opts := []public.TypeOption{ - public.WithTypeRevisions(listAdditionalRefsFields), - public.WithTypeFilter(gqlpublicapi.TypeFilter{ + opts := []TypeOption{ + WithTypeRevisions(listAdditionalRefsFields), + WithTypeFilter(gqlpublicapi.TypeFilter{ PathPattern: ptr.String(filter), }), } diff --git a/pkg/hub/client/public/facade/doc.go b/pkg/hub/client/public/facade/doc.go deleted file mode 100644 index 4f0f045c8..000000000 --- a/pkg/hub/client/public/facade/doc.go +++ /dev/null @@ -1,3 +0,0 @@ -// Package facade groups dedicated function per kind. -// The purpose of it is to hide more complex underlying logic that can be reused across codebase. -package facade diff --git a/pkg/sdk/renderer/argo/dedicated_renderer.go b/pkg/sdk/renderer/argo/dedicated_renderer.go index e296436c2..0be34338f 100644 --- a/pkg/sdk/renderer/argo/dedicated_renderer.go +++ b/pkg/sdk/renderer/argo/dedicated_renderer.go @@ -1040,29 +1040,31 @@ func (r *dedicatedRenderer) addOutputTypeInstancesToGraph(step *WorkflowStep, pr return nil } -func (*dedicatedRenderer) selectBackendAlias(upperStep, resolvedStep string) (string, error) { +func (*dedicatedRenderer) selectBackendAlias(upperStep, resolvedStep string) (*string, error) { if upperStep != "" && resolvedStep != "" { - return "", errors.Errorf("cannot override backend on capact-outputTypeInstances") + return nil, errors.Errorf("cannot override backend on capact-outputTypeInstances") } - if upperStep != "" { - return upperStep, nil + for _, alias := range []string{upperStep, resolvedStep} { + if alias != "" { + return &alias, nil + } } - return resolvedStep, nil + return nil, nil } -func (*dedicatedRenderer) selectBackend(alias string, typeRef types.TypeRef, backends policy.TypeInstanceBackendCollection) (policy.TypeInstanceBackend, error) { - if alias == "" { // alias not set, get the Policy default based on TypeRef +func (*dedicatedRenderer) selectBackend(alias *string, typeRef types.TypeRef, backends policy.TypeInstanceBackendCollection) (policy.TypeInstanceBackend, error) { + if alias == nil { // alias not set, get the Policy default based on TypeRef backend, _ := backends.GetByTypeRef(typeRef) return backend, nil } // when alias is specified, required TypeInstance needs to be injected and be of Hub storage type - backend, found := backends.GetByAlias(alias) + backend, found := backends.GetByAlias(*alias) if !found { - return policy.TypeInstanceBackend{}, fmt.Errorf("cannot find backend storage for specified %s alias", alias) + return policy.TypeInstanceBackend{}, fmt.Errorf("cannot find backend storage for specified %q alias", *alias) } if !backend.ExtendsHubStorage { - return policy.TypeInstanceBackend{}, fmt.Errorf("TypeInstance with %q alias is not a Hub storage", alias) + return policy.TypeInstanceBackend{}, fmt.Errorf("TypeInstance with %q alias is not a Hub storage", *alias) } return backend, nil diff --git a/pkg/sdk/validation/manifest/json_remote_implementation.go b/pkg/sdk/validation/manifest/json_remote_implementation.go index b7b923f50..68832bf42 100644 --- a/pkg/sdk/validation/manifest/json_remote_implementation.go +++ b/pkg/sdk/validation/manifest/json_remote_implementation.go @@ -8,7 +8,6 @@ import ( gqlpublicapi "capact.io/capact/pkg/hub/api/graphql/public" "capact.io/capact/pkg/hub/client/public" - typesutil "capact.io/capact/pkg/hub/client/public/facade/types" "capact.io/capact/pkg/sdk/apis/0.0.1/types" wfv1 "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1" @@ -281,7 +280,7 @@ func (v *RemoteImplementationValidator) checkParentNodesAssociation(ctx context. var validationErrs []error for parentNode, expTypesRefs := range relations { - gotAttachedTypes, err := typesutil.ListAdditionalRefs(ctx, v.hub, expTypesRefs) + gotAttachedTypes, err := public.ListAdditionalRefs(ctx, v.hub, expTypesRefs) if err != nil { return ValidationResult{}, errors.Wrap(err, "while fetching Types based on parent node") } @@ -301,7 +300,7 @@ func (v *RemoteImplementationValidator) checkParentNodesAssociation(ctx context. return ValidationResult{Errors: validationErrs}, nil } -func (v *RemoteImplementationValidator) detectMissingChildren(gotAttachedTypes typesutil.ListAdditionalRefsOutput, expAttachedTypes []types.TypeRef, expParent string) []string { +func (v *RemoteImplementationValidator) detectMissingChildren(gotAttachedTypes public.ListAdditionalRefsOutput, expAttachedTypes []types.TypeRef, expParent string) []string { var missingChildren []string for _, exp := range expAttachedTypes { diff --git a/pkg/sdk/validation/policy/policy.go b/pkg/sdk/validation/policy/policy.go index caa79168b..7fbd83422 100644 --- a/pkg/sdk/validation/policy/policy.go +++ b/pkg/sdk/validation/policy/policy.go @@ -95,37 +95,14 @@ func (v *Validator) ValidateAdditionalInputParameters(ctx context.Context, param // ValidateTypeInstancesMetadata validates that every TypeInstance has metadata resolved. func (v *Validator) ValidateTypeInstancesMetadata(in policy.Policy) validation.Result { - resultBldr := validation.NewResultBuilder("Metadata for") - - unresolvedIDs := map[string]struct{}{} unresolvedTypeInstances := metadata.TypeInstanceIDsWithUnresolvedMetadataForPolicy(in) - for _, ti := range unresolvedTypeInstances { - resultBldr.ReportIssue(string(ti.Kind), "missing Type reference for %s", ti.String(false)) - unresolvedIDs[ti.ID] = struct{}{} - } - - for _, rule := range in.TypeInstance.Rules { - if _, unresolved := unresolvedIDs[rule.Backend.ID]; unresolved { - continue // Type was not resolved, so `ExtendsHubStorage` has zero value - } - - if rule.Backend.ExtendsHubStorage { - continue - } - ref := metadata.TypeInstanceMetadata{ - ID: rule.Backend.ID, - Description: rule.Backend.Description, - } - resultBldr.ReportIssue("BackendTypeInstance", "Type reference %s is not a Hub storage", ref.String(false)) - } - - return resultBldr.Result() + return v.validationResultForTIMetadata(unresolvedTypeInstances, in.TypeInstance.Rules) } // ValidateTypeInstancesMetadataForRule validates whether the TypeInstance injection metadata are resolved. func (v *Validator) ValidateTypeInstancesMetadataForRule(in policy.Rule) validation.Result { unresolvedTypeInstances := metadata.TypeInstanceIDsWithUnresolvedMetadataForRule(in) - return v.validationResultForTIMetadata(unresolvedTypeInstances) + return v.validationResultForTIMetadata(unresolvedTypeInstances, nil) } // AreTypeInstancesMetadataResolved returns whether every TypeInstance has metadata resolved. @@ -142,15 +119,28 @@ func (v *Validator) hasImplAdditionalInputParams(impl gqlpublicapi.Implementatio return true } -func (v *Validator) validationResultForTIMetadata(tis []metadata.TypeInstanceMetadata) validation.Result { - if len(tis) == 0 { - return validation.Result{} - } - +func (v *Validator) validationResultForTIMetadata(tis []metadata.TypeInstanceMetadata, rules []policy.RulesForTypeInstance) validation.Result { resultBldr := validation.NewResultBuilder("Metadata for") + unresolvedIDs := map[string]struct{}{} for _, ti := range tis { resultBldr.ReportIssue(string(ti.Kind), "missing Type reference for %s", ti.String(false)) + unresolvedIDs[ti.ID] = struct{}{} + } + + for _, rule := range rules { + if _, unresolved := unresolvedIDs[rule.Backend.ID]; unresolved { + continue // Type was not resolved, so `ExtendsHubStorage` has zero value + } + + if rule.Backend.ExtendsHubStorage { + continue + } + ref := metadata.TypeInstanceMetadata{ + ID: rule.Backend.ID, + Description: rule.Backend.Description, + } + resultBldr.ReportIssue("BackendTypeInstance", "Type reference %s is not a Hub storage", ref.String(false)) } return resultBldr.Result() diff --git a/test/e2e/action_test.go b/test/e2e/action_test.go index 14c477ee9..6e0b1f133 100644 --- a/test/e2e/action_test.go +++ b/test/e2e/action_test.go @@ -28,10 +28,10 @@ import ( ) const ( - actionPassingPath = "cap.interface.capactio.capact.validation.action.passing" - uploadTypePath = "cap.type.capactio.capact.validation.upload" - builtinStorageTypePath = "cap.core.type.hub.storage.neo4j" - singleKeyTypePath = "cap.type.capactio.capact.validation.single-key" + actionPassingInterfacePath = "cap.interface.capactio.capact.validation.action.passing" + uploadTypePath = "cap.type.capactio.capact.validation.upload" + builtinStorageTypePath = "cap.core.type.hub.storage.neo4j" + singleKeyTypePath = "cap.type.capactio.capact.validation.single-key" ) func getActionName() string { @@ -61,7 +61,6 @@ var _ = Describe("Action", func() { engineClient.DeleteAction(ctx, actionName) engineClient.DeleteAction(ctx, failingActionName) }) - const actionPath = "cap.interface.capactio.capact.validation.action.passing" Context("Action execution", func() { @@ -83,7 +82,7 @@ var _ = Describe("Action", func() { defer updateTICleanup() By("1.3 Creating TypeInstance that describes Helm storage") - helmStorage := getTypeInstanceHelmStorage() + helmStorage := fixHelmStorageTypeInstanceCreateInput() helmStorageTI, helmStorageTICleanup := createTypeInstance(ctx, hubClient, helmStorage) defer helmStorageTICleanup() @@ -99,7 +98,7 @@ var _ = Describe("Action", func() { By("2. Expecting Implementation A is picked and builtin storage is used...") - action := createActionAndWaitForReadyToRunPhase(ctx, engineClient, actionName, actionPath, inputData) + action := createActionAndWaitForReadyToRunPhase(ctx, engineClient, actionName, actionPassingInterfacePath, inputData) assertActionRenderedWorkflowContains(action, "echo '%s'", implIndicatorValue) runActionAndWaitForSucceeded(ctx, engineClient, actionName) @@ -127,7 +126,7 @@ var _ = Describe("Action", func() { setGlobalTestPolicy(ctx, engineClient, withHelmBackendForUploadTypeRef(helmStorageTI.ID)) By("7. Expecting Implementation A is picked and the Helm storage is used for uploaded TypeInstance...") - action = createActionAndWaitForReadyToRunPhase(ctx, engineClient, actionName, actionPath, inputData) + action = createActionAndWaitForReadyToRunPhase(ctx, engineClient, actionName, actionPassingInterfacePath, inputData) assertActionRenderedWorkflowContains(action, "echo '%s'", implIndicatorValue) runActionAndWaitForSucceeded(ctx, engineClient, actionName) @@ -159,7 +158,7 @@ var _ = Describe("Action", func() { defer updateTICleanup() By("1.3 Creating TypeInstance that describes Helm storage") - helmStorage := getTypeInstanceHelmStorage() + helmStorage := fixHelmStorageTypeInstanceCreateInput() helmStorageTI, helmStorageTICleanup := createTypeInstance(ctx, hubClient, helmStorage) defer helmStorageTICleanup() @@ -189,7 +188,7 @@ var _ = Describe("Action", func() { setGlobalTestPolicy(ctx, engineClient, prependInjectRuleForPassingActionInterface(globalPolicyRequiredTypeInstances)) By("3. Expecting Implementation B is picked and injected Helm storage is used...") - action := createActionAndWaitForReadyToRunPhase(ctx, engineClient, actionName, actionPath, inputData) + action := createActionAndWaitForReadyToRunPhase(ctx, engineClient, actionName, actionPassingInterfacePath, inputData) assertActionRenderedWorkflowContains(action, "echo '%s'", implIndicatorValue) runActionAndWaitForSucceeded(ctx, engineClient, actionName) @@ -395,7 +394,7 @@ func getTypeInstanceInputForUpdate() *hublocalgraphql.CreateTypeInstanceInput { } } -func getTypeInstanceHelmStorage() *hublocalgraphql.CreateTypeInstanceInput { +func fixHelmStorageTypeInstanceCreateInput() *hublocalgraphql.CreateTypeInstanceInput { return &hublocalgraphql.CreateTypeInstanceInput{ TypeRef: &hublocalgraphql.TypeInstanceTypeReferenceInput{ Path: "cap.type.helm.storage", @@ -530,7 +529,7 @@ func prependInjectRuleForPassingActionInterface(reqInput []*enginegraphql.Requir } return func(policy *enginegraphql.PolicyInput) { for idx, rule := range policy.Interface.Rules { - if rule.Interface.Path != actionPassingPath { + if rule.Interface.Path != actionPassingInterfacePath { continue } policy.Interface.Rules[idx].OneOf = append([]*enginegraphql.PolicyRuleInput{ @@ -656,7 +655,7 @@ func fixGQLTestPolicyInput() *enginegraphql.PolicyInput { Interface: &enginegraphql.InterfacePolicyInput{ Rules: []*enginegraphql.RulesForInterfaceInput{ { - Interface: manifestRef(actionPassingPath), + Interface: manifestRef(actionPassingInterfacePath), OneOf: []*enginegraphql.PolicyRuleInput{ { ImplementationConstraints: &enginegraphql.PolicyRuleImplementationConstraintsInput{ From ca1c5516fb2cc98eb5c7cb5c53d4c4056c7347a3 Mon Sep 17 00:00:00 2001 From: Mateusz Szostok Date: Thu, 17 Feb 2022 09:21:51 +0100 Subject: [PATCH 15/15] Add quotes, change manifests to official ones --- hack/lib/const.sh | 4 ++-- pkg/sdk/renderer/argo/dedicated_renderer.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/hack/lib/const.sh b/hack/lib/const.sh index b91c68e0f..00f670cfe 100644 --- a/hack/lib/const.sh +++ b/hack/lib/const.sh @@ -46,7 +46,7 @@ readonly CAPACT_USE_TEST_SETUP="false" # readonly CAPACT_INCREASE_RESOURCE_LIMITS="true" -readonly CAPACT_HUB_MANIFESTS_SOURCE_REPO_URL="github.com/mszostok/os-hub-manifests" +readonly CAPACT_HUB_MANIFESTS_SOURCE_REPO_URL="github.com/capactio/hub-manifests" # The git ref to checkout. It can point to a commit SHA, a branch name, or a tag. # If you want to use your forked version, remember to update CAPACT_HUB_MANIFESTS_SOURCE_REPO_URL respectively. -readonly CAPACT_HUB_MANIFESTS_SOURCE_REPO_REF="42fe61d2bee271c9829adc37c9fa64047fa81085" +readonly CAPACT_HUB_MANIFESTS_SOURCE_REPO_REF="main" diff --git a/pkg/sdk/renderer/argo/dedicated_renderer.go b/pkg/sdk/renderer/argo/dedicated_renderer.go index 0be34338f..415026183 100644 --- a/pkg/sdk/renderer/argo/dedicated_renderer.go +++ b/pkg/sdk/renderer/argo/dedicated_renderer.go @@ -1001,7 +1001,7 @@ func (r *dedicatedRenderer) addOutputTypeInstancesToGraph(step *WorkflowStep, pr upperLayerStepOutputBackendAlias := ptr.StringPtrToString(mappings[name].Backend) backendAlias, err := r.selectBackendAlias(upperLayerStepOutputBackendAlias, stepOutputBackendAlias) if err != nil { - return errors.Wrapf(err, "while resolving backend alias for %s", step.Name) + return errors.Wrapf(err, "while resolving backend alias for %q", step.Name) } log := r.log.With(zap.String("artifactName", *artifactName)) @@ -1009,7 +1009,7 @@ func (r *dedicatedRenderer) addOutputTypeInstancesToGraph(step *WorkflowStep, pr backend, err := r.selectBackend(backendAlias, typeRef, backends) if err != nil { - return errors.Wrapf(err, "while resolving backend ID for %s", name) + return errors.Wrapf(err, "while resolving backend ID for %q", name) } log.Debug("Selected TypeInstance Backend", zap.Any("backend", backend))