diff --git a/internal/graph/generated/generated.go b/internal/graph/generated/generated.go index 9fa1e8d..624ffb9 100644 --- a/internal/graph/generated/generated.go +++ b/internal/graph/generated/generated.go @@ -312,6 +312,16 @@ type ComplexityRoot struct { Resource func(childComplexity int) int } + CrossplaneResourceTreeConnection struct { + Nodes func(childComplexity int) int + TotalCount func(childComplexity int) int + } + + CrossplaneResourceTreeNode struct { + ParentID func(childComplexity int) int + Resource func(childComplexity int) int + } + CustomResourceDefinition struct { APIVersion func(childComplexity int) int DefinedResources func(childComplexity int, version *string) int @@ -570,6 +580,7 @@ type ComplexityRoot struct { ConfigMap func(childComplexity int, namespace string, name string) int ConfigurationRevisions func(childComplexity int, configuration *model.ReferenceID, active *bool) int Configurations func(childComplexity int) int + CrossplaneResourceTree func(childComplexity int, id model.ReferenceID) int CustomResourceDefinitions func(childComplexity int, revision *model.ReferenceID) int Events func(childComplexity int, involved *model.ReferenceID) int KubernetesResource func(childComplexity int, id model.ReferenceID) int @@ -712,6 +723,7 @@ type QueryResolver interface { ConfigurationRevisions(ctx context.Context, configuration *model.ReferenceID, active *bool) (*model.ConfigurationRevisionConnection, error) CompositeResourceDefinitions(ctx context.Context, revision *model.ReferenceID, dangling *bool) (*model.CompositeResourceDefinitionConnection, error) Compositions(ctx context.Context, revision *model.ReferenceID, dangling *bool) (*model.CompositionConnection, error) + CrossplaneResourceTree(ctx context.Context, id model.ReferenceID) (*model.CrossplaneResourceTreeConnection, error) } type SecretResolver interface { Events(ctx context.Context, obj *model.Secret) (*model.EventConnection, error) @@ -1769,6 +1781,34 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.CreateKubernetesResourcePayload.Resource(childComplexity), true + case "CrossplaneResourceTreeConnection.nodes": + if e.complexity.CrossplaneResourceTreeConnection.Nodes == nil { + break + } + + return e.complexity.CrossplaneResourceTreeConnection.Nodes(childComplexity), true + + case "CrossplaneResourceTreeConnection.totalCount": + if e.complexity.CrossplaneResourceTreeConnection.TotalCount == nil { + break + } + + return e.complexity.CrossplaneResourceTreeConnection.TotalCount(childComplexity), true + + case "CrossplaneResourceTreeNode.parentId": + if e.complexity.CrossplaneResourceTreeNode.ParentID == nil { + break + } + + return e.complexity.CrossplaneResourceTreeNode.ParentID(childComplexity), true + + case "CrossplaneResourceTreeNode.resource": + if e.complexity.CrossplaneResourceTreeNode.Resource == nil { + break + } + + return e.complexity.CrossplaneResourceTreeNode.Resource(childComplexity), true + case "CustomResourceDefinition.apiVersion": if e.complexity.CustomResourceDefinition.APIVersion == nil { break @@ -2883,6 +2923,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Query.Configurations(childComplexity), true + case "Query.crossplaneResourceTree": + if e.complexity.Query.CrossplaneResourceTree == nil { + break + } + + args, err := ec.field_Query_crossplaneResourceTree_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Query.CrossplaneResourceTree(childComplexity, args["id"].(model.ReferenceID)), true + case "Query.customResourceDefinitions": if e.complexity.Query.CustomResourceDefinitions == nil { break @@ -5260,6 +5312,42 @@ type Query { """ dangling: Boolean = false ): CompositionConnection! + + """ + Get an ` + "`" + `KubernetesResource` + "`" + ` and its descendants which form a tree. The two + ` + "`" + `KubernetesResource` + "`" + `s that have descendants are ` + "`" + `CompositeResourceClaim` + "`" + ` (its + ` + "`" + `CompositeResource` + "`" + `) and ` + "`" + `CompositeResource` + "`" + ` (the ` + "`" + `KubernetesResource` + "`" + `s it + composes via a ` + "`" + `Composition` + "`" + `). + """ + crossplaneResourceTree( + "The ` + "`" + `ID` + "`" + ` of an ` + "`" + `CrossplaneResource` + "`" + `" + id: ID! + ): CrossplaneResourceTreeConnection! +} + +""" +A ` + "`" + `CrossplaneResourceTreeConnection` + "`" + ` represents a connection to ` + "`" + `CrossplaneResourceTreeNode` + "`" + `s +""" +type CrossplaneResourceTreeConnection { + "Connected nodes." + nodes: [CrossplaneResourceTreeNode!] + + "The total number of connected nodes." + totalCount: Int! +} + +""" +An ` + "`" + `CrossplaneResourceTreeNode` + "`" + ` is an ` + "`" + `KubernetesResource` + "`" + ` with a ` + "`" + `ID` + "`" + ` of its parent +` + "`" + `CrossplaneResource` + "`" + `. + +Note: A ` + "`" + `NULL` + "`" + ` ` + "`" + `parentId` + "`" + ` represents the root of the descendant tree. +""" +type CrossplaneResourceTreeNode { + "The ` + "`" + `ID` + "`" + ` of the parent ` + "`" + `KubernetesResource` + "`" + ` (` + "`" + `NULL` + "`" + ` is the root of the tree)" + parentId: ID + + "The ` + "`" + `KubernetesResource` + "`" + ` object of this ` + "`" + `CrossplaneResourceTreeNode` + "`" + `" + resource: KubernetesResource! } """ @@ -5590,6 +5678,21 @@ func (ec *executionContext) field_Query_configurationRevisions_args(ctx context. return args, nil } +func (ec *executionContext) field_Query_crossplaneResourceTree_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 model.ReferenceID + if tmp, ok := rawArgs["id"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("id")) + arg0, err = ec.unmarshalNID2githubᚗcomᚋupboundᚋxgqlᚋinternalᚋgraphᚋmodelᚐReferenceID(ctx, tmp) + if err != nil { + return nil, err + } + } + args["id"] = arg0 + return args, nil +} + func (ec *executionContext) field_Query_customResourceDefinitions_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -12933,6 +13036,182 @@ func (ec *executionContext) fieldContext_CreateKubernetesResourcePayload_resourc return fc, nil } +func (ec *executionContext) _CrossplaneResourceTreeConnection_nodes(ctx context.Context, field graphql.CollectedField, obj *model.CrossplaneResourceTreeConnection) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_CrossplaneResourceTreeConnection_nodes(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Nodes, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.([]model.CrossplaneResourceTreeNode) + fc.Result = res + return ec.marshalOCrossplaneResourceTreeNode2ᚕgithubᚗcomᚋupboundᚋxgqlᚋinternalᚋgraphᚋmodelᚐCrossplaneResourceTreeNodeᚄ(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_CrossplaneResourceTreeConnection_nodes(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "CrossplaneResourceTreeConnection", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "parentId": + return ec.fieldContext_CrossplaneResourceTreeNode_parentId(ctx, field) + case "resource": + return ec.fieldContext_CrossplaneResourceTreeNode_resource(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type CrossplaneResourceTreeNode", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _CrossplaneResourceTreeConnection_totalCount(ctx context.Context, field graphql.CollectedField, obj *model.CrossplaneResourceTreeConnection) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_CrossplaneResourceTreeConnection_totalCount(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.TotalCount, 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.(int) + fc.Result = res + return ec.marshalNInt2int(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_CrossplaneResourceTreeConnection_totalCount(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "CrossplaneResourceTreeConnection", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Int does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _CrossplaneResourceTreeNode_parentId(ctx context.Context, field graphql.CollectedField, obj *model.CrossplaneResourceTreeNode) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_CrossplaneResourceTreeNode_parentId(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.ParentID, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*model.ReferenceID) + fc.Result = res + return ec.marshalOID2ᚖgithubᚗcomᚋupboundᚋxgqlᚋinternalᚋgraphᚋmodelᚐReferenceID(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_CrossplaneResourceTreeNode_parentId(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "CrossplaneResourceTreeNode", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type ID does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _CrossplaneResourceTreeNode_resource(ctx context.Context, field graphql.CollectedField, obj *model.CrossplaneResourceTreeNode) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_CrossplaneResourceTreeNode_resource(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Resource, 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.(model.KubernetesResource) + fc.Result = res + return ec.marshalNKubernetesResource2githubᚗcomᚋupboundᚋxgqlᚋinternalᚋgraphᚋmodelᚐKubernetesResource(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_CrossplaneResourceTreeNode_resource(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "CrossplaneResourceTreeNode", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("FieldContext.Child cannot be called on type INTERFACE") + }, + } + return fc, nil +} + func (ec *executionContext) _CustomResourceDefinition_id(ctx context.Context, field graphql.CollectedField, obj *model.CustomResourceDefinition) (ret graphql.Marshaler) { fc, err := ec.fieldContext_CustomResourceDefinition_id(ctx, field) if err != nil { @@ -20552,6 +20831,67 @@ func (ec *executionContext) fieldContext_Query_compositions(ctx context.Context, return fc, nil } +func (ec *executionContext) _Query_crossplaneResourceTree(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Query_crossplaneResourceTree(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Query().CrossplaneResourceTree(rctx, fc.Args["id"].(model.ReferenceID)) + }) + 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.(*model.CrossplaneResourceTreeConnection) + fc.Result = res + return ec.marshalNCrossplaneResourceTreeConnection2ᚖgithubᚗcomᚋupboundᚋxgqlᚋinternalᚋgraphᚋmodelᚐCrossplaneResourceTreeConnection(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Query_crossplaneResourceTree(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "nodes": + return ec.fieldContext_CrossplaneResourceTreeConnection_nodes(ctx, field) + case "totalCount": + return ec.fieldContext_CrossplaneResourceTreeConnection_totalCount(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type CrossplaneResourceTreeConnection", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Query_crossplaneResourceTree_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return + } + return fc, nil +} + func (ec *executionContext) _Query___type(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Query___type(ctx, field) if err != nil { @@ -25391,6 +25731,70 @@ func (ec *executionContext) _CreateKubernetesResourcePayload(ctx context.Context return out } +var crossplaneResourceTreeConnectionImplementors = []string{"CrossplaneResourceTreeConnection"} + +func (ec *executionContext) _CrossplaneResourceTreeConnection(ctx context.Context, sel ast.SelectionSet, obj *model.CrossplaneResourceTreeConnection) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, crossplaneResourceTreeConnectionImplementors) + out := graphql.NewFieldSet(fields) + var invalids uint32 + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("CrossplaneResourceTreeConnection") + case "nodes": + + out.Values[i] = ec._CrossplaneResourceTreeConnection_nodes(ctx, field, obj) + + case "totalCount": + + out.Values[i] = ec._CrossplaneResourceTreeConnection_totalCount(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 crossplaneResourceTreeNodeImplementors = []string{"CrossplaneResourceTreeNode"} + +func (ec *executionContext) _CrossplaneResourceTreeNode(ctx context.Context, sel ast.SelectionSet, obj *model.CrossplaneResourceTreeNode) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, crossplaneResourceTreeNodeImplementors) + out := graphql.NewFieldSet(fields) + var invalids uint32 + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("CrossplaneResourceTreeNode") + case "parentId": + + out.Values[i] = ec._CrossplaneResourceTreeNode_parentId(ctx, field, obj) + + case "resource": + + out.Values[i] = ec._CrossplaneResourceTreeNode_resource(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 customResourceDefinitionImplementors = []string{"CustomResourceDefinition", "Node", "KubernetesResource", "ManagedResourceDefinition", "ProviderConfigDefinition"} func (ec *executionContext) _CustomResourceDefinition(ctx context.Context, sel ast.SelectionSet, obj *model.CustomResourceDefinition) graphql.Marshaler { @@ -27457,6 +27861,29 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr return ec.OperationContext.RootResolverMiddleware(ctx, innerFunc) } + out.Concurrently(i, func() graphql.Marshaler { + return rrm(innerCtx) + }) + case "crossplaneResourceTree": + field := field + + innerFunc := func(ctx context.Context) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_crossplaneResourceTree(ctx, field) + if res == graphql.Null { + atomic.AddUint32(&invalids, 1) + } + return res + } + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, innerFunc) + } + out.Concurrently(i, func() graphql.Marshaler { return rrm(innerCtx) }) @@ -28210,6 +28637,24 @@ func (ec *executionContext) marshalNCreateKubernetesResourcePayload2ᚖgithubᚗ return ec._CreateKubernetesResourcePayload(ctx, sel, v) } +func (ec *executionContext) marshalNCrossplaneResourceTreeConnection2githubᚗcomᚋupboundᚋxgqlᚋinternalᚋgraphᚋmodelᚐCrossplaneResourceTreeConnection(ctx context.Context, sel ast.SelectionSet, v model.CrossplaneResourceTreeConnection) graphql.Marshaler { + return ec._CrossplaneResourceTreeConnection(ctx, sel, &v) +} + +func (ec *executionContext) marshalNCrossplaneResourceTreeConnection2ᚖgithubᚗcomᚋupboundᚋxgqlᚋinternalᚋgraphᚋmodelᚐCrossplaneResourceTreeConnection(ctx context.Context, sel ast.SelectionSet, v *model.CrossplaneResourceTreeConnection) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._CrossplaneResourceTreeConnection(ctx, sel, v) +} + +func (ec *executionContext) marshalNCrossplaneResourceTreeNode2githubᚗcomᚋupboundᚋxgqlᚋinternalᚋgraphᚋmodelᚐCrossplaneResourceTreeNode(ctx context.Context, sel ast.SelectionSet, v model.CrossplaneResourceTreeNode) graphql.Marshaler { + return ec._CrossplaneResourceTreeNode(ctx, sel, &v) +} + func (ec *executionContext) marshalNCustomResourceDefinition2githubᚗcomᚋupboundᚋxgqlᚋinternalᚋgraphᚋmodelᚐCustomResourceDefinition(ctx context.Context, sel ast.SelectionSet, v model.CustomResourceDefinition) graphql.Marshaler { return ec._CustomResourceDefinition(ctx, sel, &v) } @@ -29342,6 +29787,53 @@ func (ec *executionContext) marshalOConfigurationStatus2ᚖgithubᚗcomᚋupboun return ec._ConfigurationStatus(ctx, sel, v) } +func (ec *executionContext) marshalOCrossplaneResourceTreeNode2ᚕgithubᚗcomᚋupboundᚋxgqlᚋinternalᚋgraphᚋmodelᚐCrossplaneResourceTreeNodeᚄ(ctx context.Context, sel ast.SelectionSet, v []model.CrossplaneResourceTreeNode) graphql.Marshaler { + if v == nil { + return graphql.Null + } + 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.marshalNCrossplaneResourceTreeNode2githubᚗcomᚋupboundᚋxgqlᚋinternalᚋgraphᚋmodelᚐCrossplaneResourceTreeNode(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + func (ec *executionContext) marshalOCustomResourceDefinition2ᚕgithubᚗcomᚋupboundᚋxgqlᚋinternalᚋgraphᚋmodelᚐCustomResourceDefinitionᚄ(ctx context.Context, sel ast.SelectionSet, v []model.CustomResourceDefinition) graphql.Marshaler { if v == nil { return graphql.Null diff --git a/internal/graph/model/generated.go b/internal/graph/model/generated.go index a606ac3..332a0a6 100644 --- a/internal/graph/model/generated.go +++ b/internal/graph/model/generated.go @@ -475,6 +475,25 @@ type CreateKubernetesResourcePayload struct { Resource KubernetesResource `json:"resource"` } +// A `CrossplaneResourceTreeConnection` represents a connection to `CrossplaneResourceTreeNode`s +type CrossplaneResourceTreeConnection struct { + // Connected nodes. + Nodes []CrossplaneResourceTreeNode `json:"nodes"` + // The total number of connected nodes. + TotalCount int `json:"totalCount"` +} + +// An `CrossplaneResourceTreeNode` is an `KubernetesResource` with a `ID` of its parent +// `CrossplaneResource`. +// +// Note: A `NULL` `parentId` represents the root of the descendant tree. +type CrossplaneResourceTreeNode struct { + // The `ID` of the parent `KubernetesResource` (`NULL` is the root of the tree) + ParentID *ReferenceID `json:"parentId"` + // The `KubernetesResource` object of this `CrossplaneResourceTreeNode` + Resource KubernetesResource `json:"resource"` +} + // A CustomResourceDefinition defines a type of custom resource that extends the // set of resources supported by the Kubernetes API. type CustomResourceDefinition struct { diff --git a/internal/graph/resolvers/query.go b/internal/graph/resolvers/query.go index bd3093e..491b9a5 100644 --- a/internal/graph/resolvers/query.go +++ b/internal/graph/resolvers/query.go @@ -50,6 +50,71 @@ type query struct { clients ClientCache } +// Recursively collect `CrossplaneResourceTreeNode`s from the given KubernetesResource +func (r *query) getAllDecedents(ctx context.Context, res model.KubernetesResource, parentID *model.ReferenceID) ([]model.CrossplaneResourceTreeNode, error) { //nolint:gocyclo + // This isn't _really_ that complex; it's a long but simple switch. + + switch typedRes := res.(type) { + case model.CompositeResource: + list := []model.CrossplaneResourceTreeNode{{ParentID: parentID, Resource: typedRes}} + + compositeResolver := compositeResourceSpec{clients: r.clients} + resources, err := compositeResolver.Resources(ctx, typedRes.Spec) + if err != nil || len(graphql.GetErrors(ctx)) > 0 { + return nil, err + } + + for _, childRes := range resources.Nodes { + childList, err := r.getAllDecedents(ctx, childRes, &typedRes.ID) + if err != nil || len(graphql.GetErrors(ctx)) > 0 { + return nil, err + } + + list = append(list, childList...) + } + + return list, nil + case model.CompositeResourceClaim: + list := []model.CrossplaneResourceTreeNode{{ParentID: parentID, Resource: typedRes}} + + claimResolver := compositeResourceClaimSpec{clients: r.clients} + composite, err := claimResolver.Resource(ctx, typedRes.Spec) + if err != nil || len(graphql.GetErrors(ctx)) > 0 { + return nil, err + } + + if composite == nil { + return list, nil + } + + childList, err := r.getAllDecedents(ctx, *composite, &typedRes.ID) + if err != nil || len(graphql.GetErrors(ctx)) > 0 { + return nil, err + } + + return append(list, childList...), nil + default: + return []model.CrossplaneResourceTreeNode{{ParentID: parentID, Resource: typedRes}}, nil + } +} + +func (r *query) CrossplaneResourceTree(ctx context.Context, id model.ReferenceID) (*model.CrossplaneResourceTreeConnection, error) { + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + rootRes, err := r.KubernetesResource(ctx, id) + if err != nil || len(graphql.GetErrors(ctx)) > 0 { + return nil, err + } + + list, err := r.getAllDecedents(ctx, rootRes, nil) + if err != nil || len(graphql.GetErrors(ctx)) > 0 { + return nil, err + } + + return &model.CrossplaneResourceTreeConnection{Nodes: list, TotalCount: len(list)}, nil +} + func (r *query) KubernetesResource(ctx context.Context, id model.ReferenceID) (model.KubernetesResource, error) { ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() diff --git a/internal/graph/resolvers/query_test.go b/internal/graph/resolvers/query_test.go index ec15ad6..ea17f41 100644 --- a/internal/graph/resolvers/query_test.go +++ b/internal/graph/resolvers/query_test.go @@ -32,6 +32,7 @@ import ( "github.com/google/go-cmp/cmp/cmpopts" "sigs.k8s.io/controller-runtime/pkg/client" + "github.com/crossplane/crossplane-runtime/pkg/fieldpath" "github.com/crossplane/crossplane-runtime/pkg/test" extv1 "github.com/crossplane/crossplane/apis/apiextensions/v1" pkgv1 "github.com/crossplane/crossplane/apis/pkg/v1" @@ -44,6 +45,195 @@ import ( var _ generated.QueryResolver = &query{} +func TestCrossplaneResourceTree(t *testing.T) { + errBoom := errors.New("boom") + + type args struct { + ctx context.Context + id model.ReferenceID + } + type want struct { + kr *model.CrossplaneResourceTreeConnection + err error + errs gqlerror.List + } + + namespace := "default" + deletionPolicyDelete := model.DeletionPolicyDelete + + cases := map[string]struct { + reason string + clients ClientCache + args args + want want + }{ + "GetKubernetesResourceError": { + reason: "If we can't get a client we should add the error to the GraphQL context and return early.", + clients: ClientCacheFn(func(_ auth.Credentials, _ ...clients.GetOption) (client.Client, error) { + return &test.MockClient{}, errBoom + }), + args: args{ + ctx: graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter, graphql.DefaultRecover), + }, + want: want{ + errs: gqlerror.List{ + gqlerror.Errorf(errors.Wrap(errBoom, errGetClient).Error()), + }, + }, + }, + "SuccessWithNoComposite": { + reason: "It is a successful call", + clients: ClientCacheFn(func(_ auth.Credentials, _ ...clients.GetOption) (client.Client, error) { + return &test.MockClient{ + MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error { + switch key.Name { + case "root": + u := *obj.(*unstructured.Unstructured) + u.SetNamespace(namespace) + fieldpath.Pave(u.Object).SetValue("spec.compositionRef", &corev1.ObjectReference{ + Name: "coolcomposition", + }) + default: + t.Fatalf("unknown get with name: %s", key.Name) + } + return nil + }, + }, nil + }), + args: args{ + ctx: graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter, graphql.DefaultRecover), + id: model.ReferenceID{Name: "root"}, + }, + want: want{ + kr: &model.CrossplaneResourceTreeConnection{TotalCount: 1, Nodes: []model.CrossplaneResourceTreeNode{ + { + Resource: model.CompositeResourceClaim{ + ID: model.ReferenceID{Namespace: namespace}, + Metadata: &model.ObjectMeta{Namespace: &namespace}, + Spec: &model.CompositeResourceClaimSpec{CompositionReference: &corev1.ObjectReference{Name: "coolcomposition"}}, + }, + }, + }}, + }, + }, + "Success": { + reason: "It is a successful call", + clients: ClientCacheFn(func(_ auth.Credentials, _ ...clients.GetOption) (client.Client, error) { + return &test.MockClient{ + MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error { + u := *obj.(*unstructured.Unstructured) + u.SetName(key.Name) + + switch key.Name { + case "root": + u.SetNamespace(namespace) + fieldpath.Pave(u.Object).SetValue("spec.resourceRef", &corev1.ObjectReference{Name: "composite"}) + case "composite": + fieldpath.Pave(u.Object).SetValue("spec.resourceRefs", []corev1.ObjectReference{{Name: "managed1"}, {Name: "child-composite"}}) + case "child-composite": + fieldpath.Pave(u.Object).SetValue("spec.resourceRefs", []corev1.ObjectReference{{Name: "managed2"}, {Name: "provider-config"}}) + case "managed1": + fallthrough + case "managed2": + fieldpath.Pave(u.Object).SetValue("spec.providerConfigRef.name", "") + case "provider-config": + u.SetKind("ProviderConfig") + default: + t.Fatalf("unknown get with name: %s", key.Name) + } + return nil + }, + }, nil + }), + args: args{ + ctx: graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter, graphql.DefaultRecover), + id: model.ReferenceID{Name: "root"}, + }, + want: want{ + kr: &model.CrossplaneResourceTreeConnection{TotalCount: 6, Nodes: []model.CrossplaneResourceTreeNode{ + { + Resource: model.CompositeResourceClaim{ + ID: model.ReferenceID{Namespace: namespace, Name: "root"}, + Metadata: &model.ObjectMeta{Namespace: &namespace, Name: "root"}, + Spec: &model.CompositeResourceClaimSpec{ResourceReference: &corev1.ObjectReference{Name: "composite"}}, + }, + }, + { + ParentID: &model.ReferenceID{Namespace: "default", Name: "root"}, + Resource: model.CompositeResource{ + ID: model.ReferenceID{Name: "composite"}, + Metadata: &model.ObjectMeta{Name: "composite"}, + Spec: &model.CompositeResourceSpec{ResourceReferences: []corev1.ObjectReference{{Name: "managed1"}, {Name: "child-composite"}}}, + }, + }, + { + ParentID: &model.ReferenceID{Name: "composite"}, + Resource: model.CompositeResource{ + ID: model.ReferenceID{Name: "child-composite"}, + Metadata: &model.ObjectMeta{Name: "child-composite"}, + Spec: &model.CompositeResourceSpec{ResourceReferences: []corev1.ObjectReference{{Name: "managed2"}, {Name: "provider-config"}}}, + }, + }, + { + ParentID: &model.ReferenceID{Name: "child-composite"}, + Resource: model.ProviderConfig{ + ID: model.ReferenceID{Kind: "ProviderConfig", Name: "provider-config"}, + Kind: "ProviderConfig", + Metadata: &model.ObjectMeta{Name: "provider-config"}, + }, + }, + { + ParentID: &model.ReferenceID{Name: "child-composite"}, + Resource: model.ManagedResource{ + ID: model.ReferenceID{Name: "managed2"}, + Metadata: &model.ObjectMeta{Name: "managed2"}, + Spec: &model.ManagedResourceSpec{ProviderConfigRef: &model.ProviderConfigReference{}, DeletionPolicy: &deletionPolicyDelete}, + }, + }, + { + ParentID: &model.ReferenceID{Name: "composite"}, + Resource: model.ManagedResource{ + ID: model.ReferenceID{Name: "managed1"}, + Metadata: &model.ObjectMeta{Name: "managed1"}, + Spec: &model.ManagedResourceSpec{ProviderConfigRef: &model.ProviderConfigReference{}, DeletionPolicy: &deletionPolicyDelete}, + }, + }, + }}, + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + q := &query{clients: tc.clients} + + // Our GraphQL resolvers never return errors. We instead add an + // error to the GraphQL context and return early. + got, err := q.CrossplaneResourceTree(tc.args.ctx, tc.args.id) + errs := graphql.GetErrors(tc.args.ctx) + + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("\n%s\ns.KubernetesResource(...): -want error, +got error:\n%s\n", tc.reason, diff) + } + if diff := cmp.Diff(tc.want.errs, errs, test.EquateErrors()); diff != "" { + t.Errorf("\n%s\ns.KubernetesResource(...): -want GraphQL errors, +got GraphQL errors:\n%s\n", tc.reason, diff) + } + + diffOptions := []cmp.Option{ + cmpopts.IgnoreFields(model.CompositeResourceClaim{}, "Unstructured"), + cmpopts.IgnoreFields(model.CompositeResource{}, "Unstructured"), + cmpopts.IgnoreFields(model.ManagedResource{}, "Unstructured"), + cmpopts.IgnoreFields(model.ProviderConfig{}, "Unstructured"), + cmpopts.IgnoreUnexported(model.ObjectMeta{}), + } + + if diff := cmp.Diff(tc.want.kr, got, diffOptions...); diff != "" { + t.Errorf("\n%s\ns.KubernetesResource(...): -want, +got:\n%s\n", tc.reason, diff) + } + }) + } +} + func TestQueryKubernetesResource(t *testing.T) { errBoom := errors.New("boom") @@ -96,7 +286,7 @@ func TestQueryKubernetesResource(t *testing.T) { }, }, "Success": { - reason: "If we can get and model the resource we should return it.", + reason: "If we can get and model the resource we should return it.", clients: ClientCacheFn(func(_ auth.Credentials, _ ...clients.GetOption) (client.Client, error) { return &test.MockClient{ MockGet: test.NewMockGetFn(nil), diff --git a/schema/queries.gql b/schema/queries.gql index 0f1b60c..764d8be 100644 --- a/schema/queries.gql +++ b/schema/queries.gql @@ -155,6 +155,42 @@ type Query { """ dangling: Boolean = false ): CompositionConnection! + + """ + Get an `KubernetesResource` and its descendants which form a tree. The two + `KubernetesResource`s that have descendants are `CompositeResourceClaim` (its + `CompositeResource`) and `CompositeResource` (the `KubernetesResource`s it + composes via a `Composition`). + """ + crossplaneResourceTree( + "The `ID` of an `CrossplaneResource`" + id: ID! + ): CrossplaneResourceTreeConnection! +} + +""" +A `CrossplaneResourceTreeConnection` represents a connection to `CrossplaneResourceTreeNode`s +""" +type CrossplaneResourceTreeConnection { + "Connected nodes." + nodes: [CrossplaneResourceTreeNode!] + + "The total number of connected nodes." + totalCount: Int! +} + +""" +An `CrossplaneResourceTreeNode` is an `KubernetesResource` with a `ID` of its parent +`CrossplaneResource`. + +Note: A `NULL` `parentId` represents the root of the descendant tree. +""" +type CrossplaneResourceTreeNode { + "The `ID` of the parent `KubernetesResource` (`NULL` is the root of the tree)" + parentId: ID + + "The `KubernetesResource` object of this `CrossplaneResourceTreeNode`" + resource: KubernetesResource! } """