Skip to content

Commit 8a86a76

Browse files
committed
PackageRevision field selectors for Kptfile labels
1 parent 772c834 commit 8a86a76

4 files changed

Lines changed: 156 additions & 1 deletion

File tree

pkg/cache/dbcache/dbsqlfiltering.go

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package dbcache
1616

1717
import (
1818
"fmt"
19+
"strings"
1920

2021
porchapi "github.com/nephio-project/porch/api/porch/v1alpha1"
2122
"github.com/nephio-project/porch/pkg/repository"
@@ -59,7 +60,8 @@ func prListFilter2WhereClause(filter repository.ListPackageRevisionFilter) strin
5960
whereStatement, first = filter2SubClauseInt(whereStatement, prKey.Revision, "package_revisions.revision", first)
6061
whereStatement, first = filter2SubClauseWorkspace(whereStatement, prKey.WorkspaceName, "package_revisions.k8s_name", first)
6162

62-
whereStatement, _ = filter2SubClauseLifecycle(whereStatement, filter.Lifecycles, "package_revisions.lifecycle", first)
63+
whereStatement, first = filter2SubClauseLifecycle(whereStatement, filter.Lifecycles, "package_revisions.lifecycle", first)
64+
whereStatement, _ = filter2SubClauseKptfileLabels(whereStatement, filter.KptfileLabels, first)
6365

6466
if whereStatement == "" {
6567
return whereStatement
@@ -131,3 +133,24 @@ func filter2SubClauseLifecycle(whereStatement string, filterField []porchapi.Pac
131133
return whereStatement + "AND " + subClause, false
132134
}
133135
}
136+
137+
func filter2SubClauseKptfileLabels(whereStatement string, filterLabels map[string]string, first bool) (string, bool) {
138+
if len(filterLabels) == 0 {
139+
return whereStatement, first
140+
}
141+
142+
var subClauses []string
143+
for labelKey, labelValue := range filterLabels {
144+
subClause := fmt.Sprintf("(package_revisions.spec::jsonb->'packageMetadata'->'labels'->>'%s' = '%s')",
145+
labelKey, labelValue)
146+
subClauses = append(subClauses, subClause)
147+
}
148+
149+
combinedClause := "(" + strings.Join(subClauses, " AND ") + ")"
150+
151+
if first {
152+
return whereStatement + combinedClause, false
153+
} else {
154+
return whereStatement + "AND " + combinedClause, false
155+
}
156+
}

pkg/registry/porch/fieldselector.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,18 @@ func convertPackageRevisionFieldSelector(label, value string) (internalLabel, in
9696
if slices.Contains(porchapi.PackageRevisionSelectableFields, porchapi.PkgRevFieldSelector(label)) {
9797
return label, value, nil
9898
}
99+
100+
if strings.HasPrefix(label, "spec.packageMetadata.labels[") && strings.HasSuffix(label, "]") {
101+
start := len("spec.packageMetadata.labels[")
102+
end := len(label) - 1
103+
labelKey := label[start:end]
104+
if labelKey == "" {
105+
return "", "", fmt.Errorf("label key cannot be empty in field selector %q", label)
106+
}
107+
108+
return label, value, nil
109+
}
110+
99111
return "", "", fmt.Errorf("%q is not a known field selector", label)
100112
}
101113

@@ -166,6 +178,25 @@ func parsePackageRevisionFieldSelector(options *metainternalversion.ListOptions)
166178
return filter, apierrors.NewBadRequest(fmt.Sprintf("unsupported fieldSelector operator %q for field %q", requirement.Operator, requirement.Field))
167179
}
168180

181+
if strings.HasPrefix(requirement.Field, "spec.packageMetadata.labels[") && strings.HasSuffix(requirement.Field, "]") {
182+
start := len("spec.packageMetadata.labels[")
183+
end := len(requirement.Field) - 1
184+
labelKey := requirement.Field[start:end]
185+
186+
if labelKey == "" {
187+
return filter, apierrors.NewBadRequest(fmt.Sprintf("label key cannot be empty in field selector %q", requirement.Field))
188+
}
189+
190+
if filter.KptfileLabels == nil {
191+
filter.KptfileLabels = make(map[string]string)
192+
}
193+
194+
filter.KptfileLabels[labelKey] = requirement.Value
195+
196+
// skip the upcoming requirement.Field check
197+
return filter, nil
198+
}
199+
169200
filteredField := porchapi.PkgRevFieldSelector(requirement.Field)
170201
if filterFunc, fieldSelectable := PrFilterFieldMappings[filteredField]; fieldSelectable {
171202
if err := filterFunc(filter, requirement.Value); err != nil {

pkg/repository/repository.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,9 @@ type ListPackageRevisionFilter struct {
329329
// Lifecycle matches the spec.lifecycle of the package
330330
Lifecycles []porchapi.PackageRevisionLifecycle
331331

332+
// KptfileLabels matches labels specified in the Kptfile
333+
KptfileLabels map[string]string
334+
332335
Label labels.Selector
333336
}
334337

@@ -342,6 +345,24 @@ func (f *ListPackageRevisionFilter) Matches(ctx context.Context, p PackageRevisi
342345
return false
343346
}
344347

348+
if len(f.KptfileLabels) > 0 {
349+
packageRevision, err := p.GetPackageRevision(ctx)
350+
if err != nil {
351+
return false
352+
}
353+
354+
if packageRevision.Spec.PackageMetadata == nil {
355+
return false
356+
}
357+
358+
for labelKey, expectedlValue := range f.KptfileLabels {
359+
actualValue, exists := packageRevision.Spec.PackageMetadata.Labels[labelKey]
360+
if !exists || actualValue != expectedlValue {
361+
return false
362+
}
363+
}
364+
}
365+
345366
if !f.MatchesLabels(ctx, p) {
346367
return false
347368
}

test/e2e/metadata_test.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,12 @@
1515
package e2e
1616

1717
import (
18+
"fmt"
19+
1820
porchapi "github.com/nephio-project/porch/api/porch/v1alpha1"
1921
"github.com/nephio-project/porch/pkg/repository"
2022
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
23+
"k8s.io/apimachinery/pkg/fields"
2124
"sigs.k8s.io/controller-runtime/pkg/client"
2225
)
2326

@@ -92,6 +95,7 @@ func (t *PorchSuite) TestPackageMetadataFromKptfile() {
9295
"config.kubernetes.io/local-config": "true", // from upstream
9396
"porch.dev/new-annotation": "new-annotation-value", // from PackageMetadata
9497
}
98+
t.Require().NotNil(clonePr.Spec.PackageMetadata)
9599
t.Require().Equal(expectedLabels, clonePr.Spec.PackageMetadata.Labels)
96100
for k, v := range expectedAnnotations {
97101
actual, ok := clonePr.Spec.PackageMetadata.Annotations[k]
@@ -131,6 +135,7 @@ func (t *PorchSuite) TestPackageMetadataFromKptfile() {
131135
"porch.dev/test-annotation": "e2e-test-annotation-value",
132136
}
133137

138+
t.Require().NotNil(clonePr.Spec.PackageMetadata)
134139
t.Require().Equal(expectedLabelsAfterManual, clonePr.Spec.PackageMetadata.Labels)
135140
for k, v := range expectedAnnotationsAfterManual {
136141
actual, ok := clonePr.Spec.PackageMetadata.Annotations[k]
@@ -171,6 +176,7 @@ func (t *PorchSuite) TestPackageMetadataFromKptfile() {
171176
"porch.dev/added-annotation": "added-annotation-value",
172177
}
173178

179+
t.Require().NotNil(clonePr.Spec.PackageMetadata)
174180
t.Require().Equal(expectedLabels, clonePr.Spec.PackageMetadata.Labels)
175181
for k, v := range expectedAnnotations {
176182
actual, ok := clonePr.Spec.PackageMetadata.Annotations[k]
@@ -189,3 +195,77 @@ func (t *PorchSuite) TestPackageMetadataFromKptfile() {
189195
}
190196
})
191197
}
198+
199+
func (t *PorchSuite) TestPackageMetadataFieldSelectors() {
200+
const (
201+
repositoryName = "test-package-field-selector-repo"
202+
)
203+
204+
t.RegisterMainGitRepositoryF(repositoryName)
205+
206+
t.RegisterTestBlueprintRepository("test-blueprints", "")
207+
208+
t.Run("filter by existing label", func() {
209+
var list porchapi.PackageRevisionList
210+
211+
t.ListE(&list, client.InNamespace(t.Namespace))
212+
213+
simplePackage := t.MustFindPackageRevision(&list, repository.PackageRevisionKey{
214+
PkgKey: repository.PackageKey{
215+
RepoKey: repository.RepositoryKey{
216+
Name: "test-blueprints",
217+
},
218+
Package: "simple",
219+
},
220+
Revision: 1,
221+
})
222+
223+
t.Require().NotNil(simplePackage.Spec.PackageMetadata)
224+
t.Require().NotNil(simplePackage.Spec.PackageMetadata.Labels)
225+
t.Require().NotEmpty(simplePackage.Spec.PackageMetadata.Labels)
226+
227+
var testLabelKey, testLabelValue string
228+
for key, value := range simplePackage.Spec.PackageMetadata.Labels {
229+
testLabelKey = key
230+
testLabelValue = value
231+
break
232+
}
233+
234+
fieldSelector := fmt.Sprintf("spec.packageMetadata.labels[%s]=%s", testLabelKey, testLabelValue)
235+
listOptions := &client.ListOptions{
236+
FieldSelector: fields.ParseSelectorOrDie(fieldSelector),
237+
}
238+
239+
var filteredList porchapi.PackageRevisionList
240+
t.ListE(&filteredList, client.InNamespace(t.Namespace), listOptions)
241+
242+
found := false
243+
for _, item := range filteredList.Items {
244+
t.Require().NotNil(item.Spec.PackageMetadata)
245+
t.Require().NotNil(item.Spec.PackageMetadata.Labels)
246+
247+
actualValue, exists := item.Spec.PackageMetadata.Labels[testLabelKey]
248+
t.Require().True(exists, testLabelKey)
249+
t.Require().Equal(testLabelValue, actualValue)
250+
251+
if item.Spec.PackageName == "simple" && item.Spec.RepositoryName == "test-blueprints" {
252+
found = true
253+
t.Logf("Found expected simple package: %s", item.Name)
254+
}
255+
}
256+
257+
t.Require().True(found)
258+
})
259+
260+
t.Run("filter by non-existing label", func() {
261+
fieldSelector := "spec.packageMetadata.labels[non-existing-label-key]=non-existing-value"
262+
listOptions := &client.ListOptions{
263+
FieldSelector: fields.ParseSelectorOrDie(fieldSelector),
264+
}
265+
266+
var filteredList porchapi.PackageRevisionList
267+
t.ListE(&filteredList, client.InNamespace(t.Namespace), listOptions)
268+
269+
t.Require().Empty(filteredList.Items, len(filteredList.Items))
270+
})
271+
}

0 commit comments

Comments
 (0)