Skip to content

Commit a462304

Browse files
committed
feat(stoneintg-1350): add support for nested componentGroups
Allows users to add a ComponentGroup to another ComponentGroup's spec.Components field. The resulting data structure is a DAG in which all leaf nodes are Components and all non-leaves are ComponentGroups. When a Component is built, its ComponentGroups will be gathered and snapshots will be created for them. The Snapshot will contain the GCL and updated images for that ComponentGroup and all ComponentGroups nested within it. After that snapshot is created the snapshot controller will create snapshots for all ComponentGroups that contain the first ComponentGroup. This allows Konflux to support complex use-cases for ComponentGroups in which users have multi-tiered applications with subgroups that must be tested independently Signed-off-by: Ryan Cole <rcyoalne@gmail.com>
1 parent bd638fb commit a462304

27 files changed

Lines changed: 1616 additions & 164 deletions

api/v1beta2/componentgroup_types.go

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,6 @@ type ComponentGroupSpec struct {
4141
// +required
4242
Components []ComponentReference `json:"components"`
4343

44-
// Dependents is a list of ComponentGroup names that are dependent on this ComponentGroup.
45-
// When a snapshot is created for this ComponentGroup, snapshots will also be created for all dependents.
46-
// +optional
47-
Dependents []string `json:"dependents,omitempty"`
48-
4944
// TestGraph describes the desired order in which tests associated with the ComponentGroup should be executed.
5045
// If not specified, all tests will run in parallel.
5146
// The map key is the test scenario name, and the value is a list of parent test scenarios it depends on.
@@ -58,17 +53,24 @@ type ComponentGroupSpec struct {
5853
SnapshotCreator *SnapshotCreatorSpec `json:"snapshotCreator,omitempty"`
5954
}
6055

61-
// ComponentReference references a Component and its specific branch/version
56+
// ComponentReference references a Component or ComponentGroup and its specific branch/version
6257
type ComponentReference struct {
63-
// Name is the name of the Component
58+
// Name is the name of the Component or ComponentGroup
6459
// +kubebuilder:validation:Pattern=^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
6560
// +required
6661
Name string `json:"name"`
6762

63+
// Kind is the type of resource being referenced
64+
// Must be 'component' or 'componentGroup', case-insensitive
65+
// +kubebuilder:default:="component"
66+
// +kubebuilder:validation:Pattern=`(?i)^(component|componentGroup)$`
67+
Kind string `json:"kind,omitempty"`
68+
6869
// ComponentVersion references the ComponentVersion for this Component.
70+
// Can only be set if 'Kind' is set to 'Component' or empty
6971
// The ComponentVersion CRD will be implemented by the build team as part of STONEBLD-3604.
7072
// For now, this contains the branch name and GCL (Global Candidate List) information.
71-
// +required
73+
// +optional
7274
ComponentVersion ComponentVersionReference `json:"componentVersion"`
7375
}
7476

api/v1beta2/componentgroup_types_test.go

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ func TestComponentGroupSpec(t *testing.T) {
5252
},
5353
},
5454
},
55-
Dependents: []string{"child-cg-1", "child-cg-2"},
5655
TestGraph: map[string][]TestGraphNode{
5756
"verify": {
5857
{Name: "clamav-scan"},
@@ -116,10 +115,6 @@ func TestComponentGroupSpec(t *testing.T) {
116115
t.Errorf("Expected second component branch 'components/second-component', got '%s'", cg.Spec.Components[1].ComponentVersion.Name)
117116
}
118117

119-
if len(cg.Spec.Dependents) != 2 {
120-
t.Errorf("Expected 2 dependents, got %d", len(cg.Spec.Dependents))
121-
}
122-
123118
if len(cg.Spec.TestGraph) != 2 {
124119
t.Errorf("Expected 2 test graph entries, got %d", len(cg.Spec.TestGraph))
125120
}
@@ -192,9 +187,6 @@ func TestComponentGroupMinimalSpec(t *testing.T) {
192187
if cg.Spec.Components[0].ComponentVersion.Revision != "" {
193188
t.Errorf("Expected empty Revision, got '%s'", cg.Spec.Components[0].ComponentVersion.Revision)
194189
}
195-
if len(cg.Spec.Dependents) != 0 {
196-
t.Errorf("Expected 0 dependents, got %d", len(cg.Spec.Dependents))
197-
}
198190
if cg.Spec.TestGraph != nil {
199191
t.Error("Expected nil TestGraph")
200192
}
@@ -257,7 +249,6 @@ func TestComponentGroupDeepCopy(t *testing.T) {
257249
},
258250
},
259251
},
260-
Dependents: []string{"dependent-1"},
261252
},
262253
}
263254

@@ -360,7 +351,6 @@ func TestComponentGroupSpecDeepCopy(t *testing.T) {
360351
Components: []ComponentReference{
361352
{Name: "comp-1", ComponentVersion: ComponentVersionReference{Name: "main"}},
362353
},
363-
Dependents: []string{"dep-1"},
364354
TestGraph: map[string][]TestGraphNode{
365355
"test-1": {{Name: "node-1", FailFast: true}},
366356
},

api/v1beta2/zz_generated.deepcopy.go

Lines changed: 0 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/crd/bases/appstudio.redhat.com_componentgroups.yaml

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,13 @@ spec:
5454
Components is a list of Components (name and branch) that belong to the ComponentGroup.
5555
This is the source of truth for logical groupings of versioned Components.
5656
items:
57-
description: ComponentReference references a Component and its specific
58-
branch/version
57+
description: ComponentReference references a Component or ComponentGroup
58+
and its specific branch/version
5959
properties:
6060
componentVersion:
6161
description: |-
6262
ComponentVersion references the ComponentVersion for this Component.
63+
Can only be set if 'Kind' is set to 'Component' or empty
6364
The ComponentVersion CRD will be implemented by the build team as part of STONEBLD-3604.
6465
For now, this contains the branch name and GCL (Global Candidate List) information.
6566
properties:
@@ -82,22 +83,21 @@ spec:
8283
required:
8384
- name
8485
type: object
86+
kind:
87+
default: component
88+
description: |-
89+
Kind is the type of resource being referenced
90+
Must be 'component' or 'componentGroup', case-insensitive
91+
pattern: (?i)^(component|componentGroup)$
92+
type: string
8593
name:
86-
description: Name is the name of the Component
94+
description: Name is the name of the Component or ComponentGroup
8795
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
8896
type: string
8997
required:
90-
- componentVersion
9198
- name
9299
type: object
93100
type: array
94-
dependents:
95-
description: |-
96-
Dependents is a list of ComponentGroup names that are dependent on this ComponentGroup.
97-
When a snapshot is created for this ComponentGroup, snapshots will also be created for all dependents.
98-
items:
99-
type: string
100-
type: array
101101
snapshotCreator:
102102
description: |-
103103
SnapshotCreator is an optional field that allows custom logic for Snapshot creation.

config/samples/appstudio_v1beta2_componentgroup.yaml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,16 @@ spec:
2424
componentVersion:
2525
name: "main"
2626
- name: second-component
27+
kind: component
2728
componentBranch:
2829
name: "v1"
2930
revision: "v1branch"
3031
context: "components/second-component"
3132
- name: python-component
3233
componentBranch:
3334
name: "3.12.4"
34-
dependents:
35-
# when a snapshot for this ComponentGroup is created,
36-
# the integration service will also create one for child-cg
37-
- child-cg
35+
- name: child-cg
36+
kind: componentGroup
3837
testGraph:
3938
# in this graph clamav-scan, dast-tests, and deployment would run first
4039
# e2e-test would start when clamav-scan finishes

gitops/snapshot.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,10 @@ const (
251251
// SnapshotAutoReleasedCondition is the condition for marking if Snapshot was auto-released released with AppStudio.
252252
SnapshotAutoReleasedCondition = "AutoReleased"
253253

254+
// ParentSnapshotsCreatedCondition is the condition marking whether snapshots for all parent ComponentGroups for the
255+
// ComponentGroup that the snapshot belongs to have been created
256+
ParentSnapshotsCreatedCondition = "ParentSnapshotsCreated"
257+
254258
// SnapshotAddedToGlobalCandidateListCondition is the condition for marking if Snapshot's component was added to
255259
// the global candidate list.
256260
SnapshotAddedToGlobalCandidateListCondition = "AddedToGlobalCandidateList"
@@ -351,6 +355,8 @@ type ComponentSnapshotInfo struct {
351355
Namespace string `json:"namespace"`
352356
// Component name
353357
Component string `json:"component"`
358+
// Component version
359+
Version string `json:"version,omitempty"`
354360
// The build PLR name building the container image triggered by pull request
355361
BuildPipelineRun string `json:"buildPipelineRun"`
356362
// The built component snapshot from build PLR
@@ -665,6 +671,31 @@ func MarkSnapshotAsAutoReleased(ctx context.Context, adapterClient client.Client
665671
return nil
666672
}
667673

674+
func ParentSnapshotsCreated(snapshot *applicationapiv1alpha1.Snapshot) bool {
675+
return IsSnapshotStatusConditionSet(snapshot, ParentSnapshotsCreatedCondition, metav1.ConditionTrue, "")
676+
}
677+
678+
func SetParentSnapshotsCreatedCondition(snapshot *applicationapiv1alpha1.Snapshot, status metav1.ConditionStatus, reason, message string) {
679+
condition := metav1.Condition{
680+
Type: ParentSnapshotsCreatedCondition,
681+
Status: status,
682+
Reason: reason,
683+
Message: message,
684+
}
685+
meta.SetStatusCondition(&snapshot.Status.Conditions, condition)
686+
}
687+
688+
func AddParentSnapshotDataToSnapshotStatus(snapshot *applicationapiv1alpha1.Snapshot, created bool, parentCG, parentSnapshot, message string) {
689+
if snapshot.Status.ParentSnapshots == nil {
690+
snapshot.Status.ParentSnapshots = make(map[string]applicationapiv1alpha1.ParentSnapshotData)
691+
}
692+
snapshot.Status.ParentSnapshots[parentCG] = applicationapiv1alpha1.ParentSnapshotData{
693+
Created: created,
694+
Name: parentSnapshot,
695+
Message: message,
696+
}
697+
}
698+
668699
// IsSnapshotMarkedAsAddedToGlobalCandidateList returns true if snapshot's AddedToGlobalCandidateListAnnotation result is marked as true to global candidate list
669700
func IsSnapshotMarkedAsAddedToGlobalCandidateList(snapshot *applicationapiv1alpha1.Snapshot) bool {
670701
annotationValue, ok := snapshot.GetAnnotations()[AddedToGlobalCandidateListAnnotation]

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ go 1.25.0
44

55
require (
66
codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v3 v3.0.0
7-
github.com/konflux-ci/application-api v0.0.0-20260312190025-5154ad273e17
7+
github.com/konflux-ci/application-api v0.0.0-20260603073049-dd8c9b1a64c2
88
github.com/konflux-ci/coverport/instrumentation/go v0.0.0-20251127115143-b5207b335f8b
99
github.com/konflux-ci/image-controller v0.0.0-20241128141349-9986c9955e05
1010
github.com/konflux-ci/operator-toolkit v0.0.0-20251118152634-b4f41f073069

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -351,8 +351,8 @@ github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3J
351351
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
352352
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
353353
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
354-
github.com/konflux-ci/application-api v0.0.0-20260312190025-5154ad273e17 h1:auNv0idITCdlV3fP9i+V6EeT2MbZc/9+05GtKzOk1Ss=
355-
github.com/konflux-ci/application-api v0.0.0-20260312190025-5154ad273e17/go.mod h1:948Z+a1IbfRT0RtoHzWWSN9YEucSbMJTHaMhz7dVICc=
354+
github.com/konflux-ci/application-api v0.0.0-20260603073049-dd8c9b1a64c2 h1:ldBiggDxskW+X1Edqa1REuonbCo+hVmdwA4bC8v674Q=
355+
github.com/konflux-ci/application-api v0.0.0-20260603073049-dd8c9b1a64c2/go.mod h1:948Z+a1IbfRT0RtoHzWWSN9YEucSbMJTHaMhz7dVICc=
356356
github.com/konflux-ci/coverport/instrumentation/go v0.0.0-20251127115143-b5207b335f8b h1:NoriO1KRc+7d2/JA07JizqxP0LlA2oJdD5AuQOvEIjE=
357357
github.com/konflux-ci/coverport/instrumentation/go v0.0.0-20251127115143-b5207b335f8b/go.mod h1:WVMHU9A2464s/vjH1xOTm4LJDD4xP+VlEiU+KM0gkSU=
358358
github.com/konflux-ci/image-controller v0.0.0-20241128141349-9986c9955e05 h1:5Xawkybl99uEiXhkdkxWtHDWitgnf+kAjpNVTanVGRE=

internal/controller/buildpipeline/buildpipeline_adapter.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ func (a *Adapter) EnsureSnapshotExists() (result controller.OperationResult, err
219219
}
220220

221221
for _, componentGroup := range *a.componentGroups {
222-
expectedSnapshot, err := snapshot.PrepareSnapshotForPipelineRun(a.context, a.client, a.pipelineRun, a.component.Name, &componentGroup)
222+
expectedSnapshot, err := snapshot.PrepareSnapshotForPipelineRun(a.context, a.client, a.pipelineRun, a.component.Name, &componentGroup, a.loader)
223223
if err != nil {
224224
return a.updatePipelineRunWithCustomizedError(&canRemoveFinalizer, err, a.context, a.pipelineRun, a.client, a.logger)
225225
}

internal/controller/buildpipeline/buildpipeline_adapter_test.go

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,10 @@ var _ = Describe("Pipeline Adapter", Ordered, func() {
583583
ContextKey: loader.ComponentGroupContextKey,
584584
Resource: hasCompGroup,
585585
},
586+
{
587+
ContextKey: loader.NestedComponentGroupsContextKey,
588+
Resource: []v1beta2.ComponentGroup{},
589+
},
586590
{
587591
ContextKey: loader.ComponentContextKey,
588592
Resource: hasComp,
@@ -723,6 +727,10 @@ var _ = Describe("Pipeline Adapter", Ordered, func() {
723727
ContextKey: loader.ComponentGroupContextKey,
724728
Resource: hasCompGroup,
725729
},
730+
{
731+
ContextKey: loader.NestedComponentGroupsContextKey,
732+
Resource: []v1beta2.ComponentGroup{},
733+
},
726734
{
727735
ContextKey: loader.ComponentContextKey,
728736
Resource: hasComp,
@@ -742,7 +750,7 @@ var _ = Describe("Pipeline Adapter", Ordered, func() {
742750
},
743751
},
744752
})
745-
_, err := snapshot.PrepareSnapshotForPipelineRun(adapter.context, adapter.client, adapter.pipelineRun, adapter.component.Name, hasCompGroup)
753+
_, err := snapshot.PrepareSnapshotForPipelineRun(adapter.context, adapter.client, adapter.pipelineRun, adapter.component.Name, hasCompGroup, adapter.loader)
746754
Expect(helpers.IsInvalidImageDigestError(err)).To(BeTrue())
747755
Eventually(func() bool {
748756
result, err := adapter.EnsureSnapshotExists()
@@ -1305,6 +1313,10 @@ var _ = Describe("Pipeline Adapter", Ordered, func() {
13051313
ContextKey: loader.ComponentGroupContextKey,
13061314
Resource: hasCompGroup,
13071315
},
1316+
{
1317+
ContextKey: loader.NestedComponentGroupsContextKey,
1318+
Resource: []v1beta2.ComponentGroup{},
1319+
},
13081320
{
13091321
ContextKey: loader.ComponentContextKey,
13101322
Resource: hasComp,
@@ -1345,6 +1357,10 @@ var _ = Describe("Pipeline Adapter", Ordered, func() {
13451357
ContextKey: loader.ComponentGroupContextKey,
13461358
Resource: hasCompGroup,
13471359
},
1360+
{
1361+
ContextKey: loader.NestedComponentGroupsContextKey,
1362+
Resource: []v1beta2.ComponentGroup{},
1363+
},
13481364
{
13491365
ContextKey: loader.ComponentContextKey,
13501366
Resource: hasComp,
@@ -1723,6 +1739,10 @@ var _ = Describe("Pipeline Adapter", Ordered, func() {
17231739
hasCompGroup.Name: nil,
17241740
},
17251741
},
1742+
{
1743+
ContextKey: loader.NestedComponentGroupsContextKey,
1744+
Resource: []v1beta2.ComponentGroup{},
1745+
},
17261746
})
17271747

17281748
Eventually(func() bool {
@@ -1973,6 +1993,10 @@ var _ = Describe("Pipeline Adapter", Ordered, func() {
19731993
Resource: nil,
19741994
Err: notFoundErr,
19751995
},
1996+
{
1997+
ContextKey: loader.NestedComponentGroupsContextKey,
1998+
Resource: []v1beta2.ComponentGroup{},
1999+
},
19762000
})
19772001

19782002
Eventually(func() bool {
@@ -2008,6 +2032,10 @@ var _ = Describe("Pipeline Adapter", Ordered, func() {
20082032
Resource: buildPipelineRun,
20092033
Err: conflictErr,
20102034
},
2035+
{
2036+
ContextKey: loader.NestedComponentGroupsContextKey,
2037+
Resource: []v1beta2.ComponentGroup{},
2038+
},
20112039
})
20122040

20132041
Eventually(func() bool {
@@ -2049,6 +2077,10 @@ var _ = Describe("Pipeline Adapter", Ordered, func() {
20492077
ContextKey: loader.AllSnapshotsContextKey,
20502078
Resource: []applicationapiv1alpha1.Snapshot{*hasSnapshot},
20512079
},
2080+
{
2081+
ContextKey: loader.NestedComponentGroupsContextKey,
2082+
Resource: []v1beta2.ComponentGroup{},
2083+
},
20522084
})
20532085

20542086
Eventually(func() bool {

0 commit comments

Comments
 (0)