@@ -17,18 +17,25 @@ limitations under the License.
1717package projection
1818
1919import (
20+ "errors"
21+ "fmt"
22+ "slices"
23+ "strings"
24+
2025 syncagentv1alpha1 "github.com/kcp-dev/api-syncagent/sdk/apis/syncagent/v1alpha1"
2126
27+ apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
2228 "k8s.io/apimachinery/pkg/runtime/schema"
29+ "k8s.io/apimachinery/pkg/util/sets"
30+ "k8s.io/apimachinery/pkg/version"
2331)
2432
25- // PublishedResourceSourceGVK returns the source GVK of the local resources
33+ // PublishedResourceSourceGK returns the source GK of the local resources
2634// that are supposed to be published.
27- func PublishedResourceSourceGVK (pubRes * syncagentv1alpha1.PublishedResource ) schema.GroupVersionKind {
28- return schema.GroupVersionKind {
29- Group : pubRes .Spec .Resource .APIGroup ,
30- Version : pubRes .Spec .Resource .Version ,
31- Kind : pubRes .Spec .Resource .Kind ,
35+ func PublishedResourceSourceGK (pubRes * syncagentv1alpha1.PublishedResource ) schema.GroupKind {
36+ return schema.GroupKind {
37+ Group : pubRes .Spec .Resource .APIGroup ,
38+ Kind : pubRes .Spec .Resource .Kind ,
3239 }
3340}
3441
@@ -59,3 +66,161 @@ func PublishedResourceProjectedGVK(pubRes *syncagentv1alpha1.PublishedResource)
5966 Kind : kind ,
6067 }
6168}
69+
70+ func ApplyProjection (crd * apiextensionsv1.CustomResourceDefinition , pubRes * syncagentv1alpha1.PublishedResource ) (* apiextensionsv1.CustomResourceDefinition , error ) {
71+ result := crd .DeepCopy ()
72+
73+ // reduce the CRD down to the selected versions
74+ result , err := stripUnwantedVersions (result , pubRes )
75+ if err != nil {
76+ return nil , err
77+ }
78+
79+ // if there is no storage version left, we use the latest served version
80+ result , err = adjustStorageVersion (result )
81+ if err != nil {
82+ return nil , err
83+ }
84+
85+ // now we get to actually project something, if desired
86+ result , err = projectCRD (result , pubRes )
87+ if err != nil {
88+ return nil , err
89+ }
90+
91+ return result , nil
92+ }
93+
94+ func stripUnwantedVersions (crd * apiextensionsv1.CustomResourceDefinition , pubRes * syncagentv1alpha1.PublishedResource ) (* apiextensionsv1.CustomResourceDefinition , error ) {
95+ src := pubRes .Spec .Resource
96+
97+ if src .Version != "" && len (src .Versions ) > 0 {
98+ return nil , errors .New ("cannot configure both .version and .versions in as the source of a PublishedResource" )
99+ }
100+
101+ crd .Spec .Versions = slices .DeleteFunc (crd .Spec .Versions , func (ver apiextensionsv1.CustomResourceDefinitionVersion ) bool {
102+ switch {
103+ case src .Version != "" :
104+ return ver .Name != src .Version
105+ case len (src .Versions ) > 0 :
106+ return ! slices .Contains (src .Versions , ver .Name )
107+ default :
108+ return false // i.e. keep all versions by default
109+ }
110+ })
111+
112+ if len (crd .Spec .Versions ) == 0 {
113+ switch {
114+ case src .Version != "" :
115+ return nil , fmt .Errorf ("CRD does not contain version %s" , src .Version )
116+ case len (src .Versions ) > 0 :
117+ return nil , fmt .Errorf ("CRD does not contain any of versions %v" , src .Versions )
118+ default :
119+ return nil , errors .New ("CRD contains no versions" )
120+ }
121+ }
122+
123+ return crd , nil
124+ }
125+
126+ func adjustStorageVersion (crd * apiextensionsv1.CustomResourceDefinition ) (* apiextensionsv1.CustomResourceDefinition , error ) {
127+ var hasStorage bool
128+ latestServed := - 1
129+ for i , v := range crd .Spec .Versions {
130+ if v .Storage {
131+ hasStorage = true
132+ }
133+ if v .Served {
134+ latestServed = i
135+ }
136+ }
137+
138+ if latestServed < 0 {
139+ return nil , errors .New ("no CRD version selected that is marked as served" )
140+ }
141+
142+ if ! hasStorage {
143+ crd .Spec .Versions [latestServed ].Storage = true
144+ }
145+
146+ return crd , nil
147+ }
148+
149+ func projectCRD (crd * apiextensionsv1.CustomResourceDefinition , pubRes * syncagentv1alpha1.PublishedResource ) (* apiextensionsv1.CustomResourceDefinition , error ) {
150+ projection := pubRes .Spec .Projection
151+ if projection == nil {
152+ return crd , nil
153+ }
154+
155+ if projection .Group != "" {
156+ crd .Spec .Group = projection .Group
157+ }
158+
159+ indexOfVersion := func (v string ) int {
160+ for i , version := range crd .Spec .Versions {
161+ if version .Name == v {
162+ return i
163+ }
164+ }
165+
166+ return - 1
167+ }
168+
169+ // We already validated that Version and Versions can be set at the same time.
170+
171+ if projection .Version != "" {
172+ if size := len (crd .Spec .Versions ); size != 1 {
173+ return nil , fmt .Errorf ("cannot project CRD version to a single version %q because it contains %d versions" , projection .Version , size )
174+ }
175+
176+ crd .Spec .Versions [0 ].Name = projection .Version
177+ } else if len (projection .Versions ) > 0 {
178+ for _ , mut := range projection .Versions {
179+ fromIdx := indexOfVersion (mut .From )
180+ if fromIdx < 0 {
181+ return nil , fmt .Errorf ("cannot project CRD version %s to %s because there is no %s version" , mut .From , mut .To , mut .From )
182+ }
183+
184+ crd .Spec .Versions [fromIdx ].Name = mut .To
185+ }
186+
187+ // ensure we ended up with a unique set of versions
188+ knownVersions := sets .New [string ]()
189+ for _ , version := range crd .Spec .Versions {
190+ if knownVersions .Has (version .Name ) {
191+ return nil , fmt .Errorf ("CRD contains multiple entries for %s after applying mutation rules" , version .Name )
192+ }
193+ }
194+
195+ // ensure proper Kubernetes-style version order
196+ slices .SortFunc (crd .Spec .Versions , func (a , b apiextensionsv1.CustomResourceDefinitionVersion ) int {
197+ return version .CompareKubeAwareVersionStrings (a .Name , b .Name )
198+ })
199+ }
200+
201+ if projection .Kind != "" {
202+ crd .Spec .Names .Kind = projection .Kind
203+ crd .Spec .Names .ListKind = projection .Kind + "List"
204+
205+ crd .Spec .Names .Singular = strings .ToLower (crd .Spec .Names .Kind )
206+ crd .Spec .Names .Plural = crd .Spec .Names .Singular + "s"
207+ }
208+
209+ if projection .Plural != "" {
210+ crd .Spec .Names .Plural = projection .Plural
211+ }
212+
213+ if projection .Scope != "" {
214+ crd .Spec .Scope = apiextensionsv1 .ResourceScope (projection .Scope )
215+ }
216+
217+ if projection .Categories != nil {
218+ crd .Spec .Names .Categories = projection .Categories
219+ }
220+
221+ if projection .ShortNames != nil {
222+ crd .Spec .Names .ShortNames = projection .ShortNames
223+ }
224+
225+ return crd , nil
226+ }
0 commit comments