Skip to content

Commit 87f6795

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 8f9e64f commit 87f6795

28 files changed

Lines changed: 1652 additions & 177 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: 29 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"
@@ -670,6 +674,31 @@ func MarkSnapshotAsAutoReleased(ctx context.Context, adapterClient client.Client
670674
return nil
671675
}
672676

677+
func ParentSnapshotsCreated(snapshot *applicationapiv1alpha1.Snapshot) bool {
678+
return IsSnapshotStatusConditionSet(snapshot, ParentSnapshotsCreatedCondition, metav1.ConditionTrue, "")
679+
}
680+
681+
func SetParentSnapshotsCreatedCondition(snapshot *applicationapiv1alpha1.Snapshot, status metav1.ConditionStatus, reason, message string) {
682+
condition := metav1.Condition{
683+
Type: ParentSnapshotsCreatedCondition,
684+
Status: status,
685+
Reason: reason,
686+
Message: message,
687+
}
688+
meta.SetStatusCondition(&snapshot.Status.Conditions, condition)
689+
}
690+
691+
func AddParentSnapshotDataToSnapshotStatus(snapshot *applicationapiv1alpha1.Snapshot, created bool, parentCG, parentSnapshot, message string) {
692+
if snapshot.Status.ParentSnapshots == nil {
693+
snapshot.Status.ParentSnapshots = make(map[string]applicationapiv1alpha1.ParentSnapshotData)
694+
}
695+
snapshot.Status.ParentSnapshots[parentCG] = applicationapiv1alpha1.ParentSnapshotData{
696+
Created: created,
697+
Name: parentSnapshot,
698+
Message: message,
699+
}
700+
}
701+
673702
// IsSnapshotMarkedAsAddedToGlobalCandidateList returns true if snapshot's AddedToGlobalCandidateListAnnotation result is marked as true to global candidate list
674703
func IsSnapshotMarkedAsAddedToGlobalCandidateList(snapshot *applicationapiv1alpha1.Snapshot) bool {
675704
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
@@ -664,6 +664,10 @@ var _ = Describe("Pipeline Adapter", Ordered, func() {
664664
ContextKey: loader.ComponentGroupContextKey,
665665
Resource: hasCompGroup,
666666
},
667+
{
668+
ContextKey: loader.NestedComponentGroupsContextKey,
669+
Resource: []v1beta2.ComponentGroup{},
670+
},
667671
{
668672
ContextKey: loader.ComponentContextKey,
669673
Resource: hasComp,
@@ -804,6 +808,10 @@ var _ = Describe("Pipeline Adapter", Ordered, func() {
804808
ContextKey: loader.ComponentGroupContextKey,
805809
Resource: hasCompGroup,
806810
},
811+
{
812+
ContextKey: loader.NestedComponentGroupsContextKey,
813+
Resource: []v1beta2.ComponentGroup{},
814+
},
807815
{
808816
ContextKey: loader.ComponentContextKey,
809817
Resource: hasComp,
@@ -823,7 +831,7 @@ var _ = Describe("Pipeline Adapter", Ordered, func() {
823831
},
824832
},
825833
})
826-
_, err := snapshot.PrepareSnapshotForPipelineRun(adapter.context, adapter.client, adapter.pipelineRun, adapter.component.Name, hasCompGroup)
834+
_, err := snapshot.PrepareSnapshotForPipelineRun(adapter.context, adapter.client, adapter.pipelineRun, adapter.component.Name, hasCompGroup, adapter.loader)
827835
Expect(helpers.IsInvalidImageDigestError(err)).To(BeTrue())
828836
Eventually(func() bool {
829837
result, err := adapter.EnsureSnapshotExists()
@@ -1386,6 +1394,10 @@ var _ = Describe("Pipeline Adapter", Ordered, func() {
13861394
ContextKey: loader.ComponentGroupContextKey,
13871395
Resource: hasCompGroup,
13881396
},
1397+
{
1398+
ContextKey: loader.NestedComponentGroupsContextKey,
1399+
Resource: []v1beta2.ComponentGroup{},
1400+
},
13891401
{
13901402
ContextKey: loader.ComponentContextKey,
13911403
Resource: hasComp,
@@ -1426,6 +1438,10 @@ var _ = Describe("Pipeline Adapter", Ordered, func() {
14261438
ContextKey: loader.ComponentGroupContextKey,
14271439
Resource: hasCompGroup,
14281440
},
1441+
{
1442+
ContextKey: loader.NestedComponentGroupsContextKey,
1443+
Resource: []v1beta2.ComponentGroup{},
1444+
},
14291445
{
14301446
ContextKey: loader.ComponentContextKey,
14311447
Resource: hasComp,
@@ -1804,6 +1820,10 @@ var _ = Describe("Pipeline Adapter", Ordered, func() {
18041820
hasCompGroup.Name: nil,
18051821
},
18061822
},
1823+
{
1824+
ContextKey: loader.NestedComponentGroupsContextKey,
1825+
Resource: []v1beta2.ComponentGroup{},
1826+
},
18071827
})
18081828

18091829
Eventually(func() bool {
@@ -2091,6 +2111,10 @@ var _ = Describe("Pipeline Adapter", Ordered, func() {
20912111
Resource: nil,
20922112
Err: notFoundErr,
20932113
},
2114+
{
2115+
ContextKey: loader.NestedComponentGroupsContextKey,
2116+
Resource: []v1beta2.ComponentGroup{},
2117+
},
20942118
})
20952119

20962120
Eventually(func() bool {
@@ -2126,6 +2150,10 @@ var _ = Describe("Pipeline Adapter", Ordered, func() {
21262150
Resource: buildPipelineRun,
21272151
Err: conflictErr,
21282152
},
2153+
{
2154+
ContextKey: loader.NestedComponentGroupsContextKey,
2155+
Resource: []v1beta2.ComponentGroup{},
2156+
},
21292157
})
21302158

21312159
Eventually(func() bool {
@@ -2167,6 +2195,10 @@ var _ = Describe("Pipeline Adapter", Ordered, func() {
21672195
ContextKey: loader.AllSnapshotsContextKey,
21682196
Resource: []applicationapiv1alpha1.Snapshot{*hasSnapshot},
21692197
},
2198+
{
2199+
ContextKey: loader.NestedComponentGroupsContextKey,
2200+
Resource: []v1beta2.ComponentGroup{},
2201+
},
21702202
})
21712203

21722204
Eventually(func() bool {

0 commit comments

Comments
 (0)