Skip to content

Commit e9755d8

Browse files
authored
Preserve labels and annos from v1alpha1 to CRD during migration (#1072)
Signed-off-by: Fiachra Corcoran <fiachra.corcoran@est.tech>
1 parent d83a6b4 commit e9755d8

3 files changed

Lines changed: 363 additions & 6 deletions

File tree

controllers/repositories/pkg/controllers/repository/pkgrevsync.go

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -149,19 +149,30 @@ func (r *RepositoryReconciler) applyPackageRevision(ctx context.Context, pr *por
149149
}
150150

151151
// applySeedFields applies non-repo-controller-owned fields (lifecycle, revision,
152-
// Kptfile-derived, publish metadata) on initial creation only. Uses a
153-
// separate field manager without ForceOwnership so these fields seed the value
154-
// for discovered packages but never overwrite values already set by the user
155-
// or the PR controller.
152+
// Kptfile-derived, publish metadata, source labels/annotations) on initial
153+
// creation only. Uses a separate field manager without ForceOwnership so these
154+
// fields seed the value for discovered packages but never overwrite values
155+
// already set by the user or the PR controller.
156156
func (r *RepositoryReconciler) applySeedFields(ctx context.Context, repo *configapi.Repository, pkgRev repository.PackageRevision, crd *porchv1alpha2.PackageRevision) {
157157
log := log.FromContext(ctx)
158158

159159
lifecycle := porchv1alpha2.PackageRevisionLifecycle(pkgRev.Lifecycle(ctx))
160160
kf, _ := pkgRev.GetKptfile(ctx)
161161

162+
// Carry over labels/annotations from the source PackageRevision's metadata.
163+
// System-managed labels are excluded (owned by the main field manager).
164+
seedObjMeta := metav1.ObjectMeta{Name: crd.Name, Namespace: crd.Namespace}
165+
labels, annotations := sourceMetadata(pkgRev)
166+
if len(labels) > 0 {
167+
seedObjMeta.Labels = labels
168+
}
169+
if len(annotations) > 0 {
170+
seedObjMeta.Annotations = annotations
171+
}
172+
162173
seedSpec := &porchv1alpha2.PackageRevision{
163174
TypeMeta: crd.TypeMeta,
164-
ObjectMeta: metav1.ObjectMeta{Name: crd.Name, Namespace: crd.Namespace},
175+
ObjectMeta: seedObjMeta,
165176
Spec: porchv1alpha2.PackageRevisionSpec{
166177
Lifecycle: lifecycle,
167178
ReadinessGates: porchv1alpha2.KptfileToReadinessGates(kf),
@@ -343,3 +354,38 @@ func packageRevisionLabels(repoName string, pkgRev repository.PackageRevision) m
343354
}
344355
return labels
345356
}
357+
358+
// systemLabels are labels managed by controllers that should not be carried
359+
// over as source metadata. Includes both v1alpha1 and v1alpha2 variants.
360+
var systemLabels = map[string]bool{
361+
porchv1alpha2.RepositoryLabelKey: true,
362+
porchv1alpha2.LatestPackageRevisionKey: true,
363+
"kpt.dev/latest-revision": true, // v1alpha1 variant
364+
}
365+
366+
// sourceMetadata extracts labels and annotations from the source package
367+
// revision's Kubernetes ObjectMeta. System-managed labels are excluded.
368+
func sourceMetadata(pkgRev repository.PackageRevision) (map[string]string, map[string]string) {
369+
meta := pkgRev.GetMeta()
370+
371+
var labels map[string]string
372+
for k, v := range meta.Labels {
373+
if systemLabels[k] {
374+
continue
375+
}
376+
if labels == nil {
377+
labels = make(map[string]string)
378+
}
379+
labels[k] = v
380+
}
381+
382+
var annotations map[string]string
383+
for k, v := range meta.Annotations {
384+
if annotations == nil {
385+
annotations = make(map[string]string)
386+
}
387+
annotations[k] = v
388+
}
389+
390+
return labels, annotations
391+
}

controllers/repositories/pkg/controllers/repository/pkgrevsync_test.go

Lines changed: 155 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,14 +93,15 @@ type fakePackageRevision struct {
9393
commitAuthor string
9494
isLatest bool
9595
resources map[string]string
96+
meta metav1.ObjectMeta
9697
}
9798

9899
func (f *fakePackageRevision) KubeObjectNamespace() string { return f.key.RKey().Namespace }
99100
func (f *fakePackageRevision) KubeObjectName() string { return repository.ComposePkgRevObjName(f.key) }
100101
func (f *fakePackageRevision) Key() repository.PackageRevisionKey { return f.key }
101102
func (f *fakePackageRevision) UID() types.UID { return "" }
102103
func (f *fakePackageRevision) ResourceVersion() string { return "" }
103-
func (f *fakePackageRevision) GetMeta() metav1.ObjectMeta { return metav1.ObjectMeta{} }
104+
func (f *fakePackageRevision) GetMeta() metav1.ObjectMeta { return f.meta }
104105
func (f *fakePackageRevision) SetMeta(_ context.Context, _ metav1.ObjectMeta) error { return nil }
105106
func (f *fakePackageRevision) Lifecycle(_ context.Context) porchv1alpha1.PackageRevisionLifecycle {
106107
return f.lifecycle
@@ -759,3 +760,156 @@ func TestSeedFieldsNotCalledOnUpdate(t *testing.T) {
759760
err := r.syncPackageRevisions(ctx, repo, []repository.PackageRevision{pkgRev})
760761
assert.NoError(t, err)
761762
}
763+
764+
// --- Tests: sourceMetadata ---
765+
766+
func TestSourceMetadata(t *testing.T) {
767+
t.Run("empty meta returns nil", func(t *testing.T) {
768+
pkgRev := &fakePackageRevision{}
769+
labels, annotations := sourceMetadata(pkgRev)
770+
assert.Nil(t, labels)
771+
assert.Nil(t, annotations)
772+
})
773+
774+
t.Run("system labels are excluded", func(t *testing.T) {
775+
pkgRev := &fakePackageRevision{
776+
meta: metav1.ObjectMeta{
777+
Labels: map[string]string{
778+
porchv1alpha2.RepositoryLabelKey: "my-repo",
779+
porchv1alpha2.LatestPackageRevisionKey: "true",
780+
"kpt.dev/latest-revision": "true", // v1alpha1 variant
781+
"app.kubernetes.io/name": "my-app",
782+
},
783+
},
784+
}
785+
labels, annotations := sourceMetadata(pkgRev)
786+
assert.Equal(t, map[string]string{"app.kubernetes.io/name": "my-app"}, labels)
787+
assert.Nil(t, annotations)
788+
})
789+
790+
t.Run("only system labels returns nil", func(t *testing.T) {
791+
pkgRev := &fakePackageRevision{
792+
meta: metav1.ObjectMeta{
793+
Labels: map[string]string{
794+
porchv1alpha2.RepositoryLabelKey: "my-repo",
795+
porchv1alpha2.LatestPackageRevisionKey: "true",
796+
"kpt.dev/latest-revision": "true",
797+
},
798+
},
799+
}
800+
labels, annotations := sourceMetadata(pkgRev)
801+
assert.Nil(t, labels)
802+
assert.Nil(t, annotations)
803+
})
804+
805+
t.Run("annotations are passed through", func(t *testing.T) {
806+
pkgRev := &fakePackageRevision{
807+
meta: metav1.ObjectMeta{
808+
Annotations: map[string]string{
809+
"team": "platform",
810+
"purpose": "infra",
811+
},
812+
},
813+
}
814+
labels, annotations := sourceMetadata(pkgRev)
815+
assert.Nil(t, labels)
816+
assert.Equal(t, map[string]string{"team": "platform", "purpose": "infra"}, annotations)
817+
})
818+
819+
t.Run("mixed labels and annotations", func(t *testing.T) {
820+
pkgRev := &fakePackageRevision{
821+
meta: metav1.ObjectMeta{
822+
Labels: map[string]string{
823+
porchv1alpha2.RepositoryLabelKey: "repo",
824+
"env": "production",
825+
"app": "frontend",
826+
},
827+
Annotations: map[string]string{
828+
"description": "production frontend",
829+
},
830+
},
831+
}
832+
labels, annotations := sourceMetadata(pkgRev)
833+
assert.Equal(t, map[string]string{"env": "production", "app": "frontend"}, labels)
834+
assert.Equal(t, map[string]string{"description": "production frontend"}, annotations)
835+
})
836+
}
837+
838+
// --- Tests: applySeedFields with source metadata ---
839+
840+
func TestApplySeedFieldsWithSourceMetadata(t *testing.T) {
841+
ctx := context.Background()
842+
repo := newTestRepo()
843+
844+
t.Run("source labels included in seed spec apply", func(t *testing.T) {
845+
pkgRev := newFakePkgRev("labeled-pkg", "v1", porchv1alpha2.PackageRevisionLifecyclePublished)
846+
pkgRev.key.Revision = 1
847+
pkgRev.meta = metav1.ObjectMeta{
848+
Labels: map[string]string{
849+
porchv1alpha2.RepositoryLabelKey: "my-repo", // system — should be excluded
850+
"team": "networking", // source — should be included
851+
"env": "staging", // source — should be included
852+
},
853+
Annotations: map[string]string{
854+
"contact": "team-net@example.com",
855+
},
856+
}
857+
858+
// Build the CRD first (as applyDesiredPackageRevisions would)
859+
crd, err := buildPackageRevision(ctx, repo, pkgRev, true)
860+
require.NoError(t, err)
861+
862+
mockClient := mockclient.NewMockClient(t)
863+
864+
// Expect seed spec Patch with source labels in ObjectMeta
865+
var seedSpecObj *porchv1alpha2.PackageRevision
866+
mockClient.EXPECT().Patch(mock.Anything, mock.AnythingOfType("*v1alpha2.PackageRevision"), mock.Anything, mock.Anything).
867+
Run(func(_ context.Context, obj client.Object, _ client.Patch, _ ...client.PatchOption) {
868+
seedSpecObj = obj.(*porchv1alpha2.PackageRevision)
869+
}).Return(nil).Once()
870+
871+
// Expect seed status Patch
872+
sw := mockclient.NewMockSubResourceWriter(t)
873+
mockClient.EXPECT().Status().Return(sw)
874+
sw.EXPECT().Patch(mock.Anything, mock.AnythingOfType("*v1alpha2.PackageRevision"), mock.Anything, mock.Anything).Return(nil).Once()
875+
876+
r := &RepositoryReconciler{Client: mockClient}
877+
r.applySeedFields(ctx, repo, pkgRev, crd)
878+
879+
// Verify source labels (excluding system) are in the seed apply ObjectMeta
880+
require.NotNil(t, seedSpecObj)
881+
assert.Equal(t, "networking", seedSpecObj.Labels["team"])
882+
assert.Equal(t, "staging", seedSpecObj.Labels["env"])
883+
assert.NotContains(t, seedSpecObj.Labels, porchv1alpha2.RepositoryLabelKey)
884+
885+
// Verify annotations are included
886+
assert.Equal(t, "team-net@example.com", seedSpecObj.Annotations["contact"])
887+
})
888+
889+
t.Run("no source metadata means no labels on seed ObjectMeta", func(t *testing.T) {
890+
pkgRev := newFakePkgRev("bare-pkg", "v1", porchv1alpha2.PackageRevisionLifecycleDraft)
891+
// meta left empty — no source labels/annotations
892+
893+
crd, err := buildPackageRevision(ctx, repo, pkgRev, false)
894+
require.NoError(t, err)
895+
896+
mockClient := mockclient.NewMockClient(t)
897+
898+
var seedSpecObj *porchv1alpha2.PackageRevision
899+
mockClient.EXPECT().Patch(mock.Anything, mock.AnythingOfType("*v1alpha2.PackageRevision"), mock.Anything, mock.Anything).
900+
Run(func(_ context.Context, obj client.Object, _ client.Patch, _ ...client.PatchOption) {
901+
seedSpecObj = obj.(*porchv1alpha2.PackageRevision)
902+
}).Return(nil).Once()
903+
904+
sw := mockclient.NewMockSubResourceWriter(t)
905+
mockClient.EXPECT().Status().Return(sw)
906+
sw.EXPECT().Patch(mock.Anything, mock.AnythingOfType("*v1alpha2.PackageRevision"), mock.Anything, mock.Anything).Return(nil).Once()
907+
908+
r := &RepositoryReconciler{Client: mockClient}
909+
r.applySeedFields(ctx, repo, pkgRev, crd)
910+
911+
require.NotNil(t, seedSpecObj)
912+
assert.Nil(t, seedSpecObj.Labels)
913+
assert.Nil(t, seedSpecObj.Annotations)
914+
})
915+
}

0 commit comments

Comments
 (0)