Skip to content

Commit 5a2e15c

Browse files
author
Per Goncalves da Silva
committed
Add Boxcutter applier
Signed-off-by: Per Goncalves da Silva <[email protected]>
1 parent d8f1f5f commit 5a2e15c

File tree

4 files changed

+934
-42
lines changed

4 files changed

+934
-42
lines changed
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
package applier
2+
3+
import (
4+
"cmp"
5+
"context"
6+
"crypto/sha256"
7+
"encoding/hex"
8+
"fmt"
9+
"hash"
10+
"io/fs"
11+
"maps"
12+
"slices"
13+
14+
"github.com/davecgh/go-spew/spew"
15+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
16+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
17+
"k8s.io/apimachinery/pkg/runtime"
18+
"sigs.k8s.io/controller-runtime/pkg/client"
19+
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
20+
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
21+
22+
ocv1 "github.com/operator-framework/operator-controller/api/v1"
23+
"github.com/operator-framework/operator-controller/internal/operator-controller/controllers"
24+
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/bundle/source"
25+
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render"
26+
)
27+
28+
const (
29+
RevisionHashAnnotation = "olm.operatorframework.io/hash"
30+
// revisionHistoryLimit = 5
31+
)
32+
33+
type ClusterExtensionRevisionGenerator interface {
34+
GenerateRevision(bundleFS fs.FS, ext *ocv1.ClusterExtension, objectLabels map[string]string) (*ocv1.ClusterExtensionRevision, error)
35+
}
36+
37+
type SimpleRevisionGenerator struct {
38+
Scheme *runtime.Scheme
39+
BundleRenderer BundleRenderer
40+
}
41+
42+
func (r *SimpleRevisionGenerator) GenerateRevision(bundleFS fs.FS, ext *ocv1.ClusterExtension, objectLabels map[string]string) (*ocv1.ClusterExtensionRevision, error) {
43+
// extract plain manifests
44+
plain, err := r.BundleRenderer.Render(bundleFS, ext)
45+
if err != nil {
46+
return nil, err
47+
}
48+
49+
// objectLabels
50+
objs := make([]ocv1.ClusterExtensionRevisionObject, 0, len(plain))
51+
for _, obj := range plain {
52+
if len(obj.GetLabels()) > 0 {
53+
labels := maps.Clone(obj.GetLabels())
54+
if labels == nil {
55+
labels = map[string]string{}
56+
}
57+
maps.Copy(labels, objectLabels)
58+
obj.SetLabels(labels)
59+
}
60+
61+
gvk, err := apiutil.GVKForObject(obj, r.Scheme)
62+
if err != nil {
63+
return nil, err
64+
}
65+
66+
unstrObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
67+
if err != nil {
68+
return nil, err
69+
}
70+
unstr := unstructured.Unstructured{Object: unstrObj}
71+
unstr.SetGroupVersionKind(gvk)
72+
73+
objs = append(objs, ocv1.ClusterExtensionRevisionObject{
74+
Object: unstr,
75+
})
76+
}
77+
78+
// Build desired revision
79+
return &ocv1.ClusterExtensionRevision{
80+
ObjectMeta: metav1.ObjectMeta{
81+
Annotations: map[string]string{},
82+
Labels: map[string]string{
83+
controllers.ClusterExtensionRevisionOwnerLabel: ext.Name,
84+
},
85+
},
86+
Spec: ocv1.ClusterExtensionRevisionSpec{
87+
Phases: []ocv1.ClusterExtensionRevisionPhase{
88+
{
89+
Name: "everything",
90+
Objects: objs,
91+
},
92+
},
93+
},
94+
}, nil
95+
}
96+
97+
type Boxcutter struct {
98+
Client client.Client
99+
Scheme *runtime.Scheme
100+
RevisionGenerator ClusterExtensionRevisionGenerator
101+
}
102+
103+
func (bc *Boxcutter) Apply(ctx context.Context, contentFS fs.FS, ext *ocv1.ClusterExtension, objectLabels, storageLabels map[string]string) ([]client.Object, string, error) {
104+
objs, err := bc.apply(ctx, contentFS, ext, objectLabels, storageLabels)
105+
return objs, "", err
106+
}
107+
108+
func (bc *Boxcutter) apply(ctx context.Context, contentFS fs.FS, ext *ocv1.ClusterExtension, objectLabels, _ map[string]string) ([]client.Object, error) {
109+
// Generate desired revision
110+
desiredRevision, err := bc.RevisionGenerator.GenerateRevision(contentFS, ext, objectLabels)
111+
if err != nil {
112+
return nil, err
113+
}
114+
115+
// List all existing revisions
116+
existingRevisions, err := bc.getExistingRevisions(ctx, ext.GetName())
117+
if err != nil {
118+
return nil, err
119+
}
120+
desiredHash := computeSHA256Hash(desiredRevision.Spec.Phases)
121+
122+
// Sort into current and previous revisions.
123+
var (
124+
currentRevision *ocv1.ClusterExtensionRevision
125+
// prevRevisions []ocv1.ClusterExtensionRevision
126+
)
127+
if len(existingRevisions) > 0 {
128+
maybeCurrentRevision := existingRevisions[len(existingRevisions)-1]
129+
annotations := maybeCurrentRevision.GetAnnotations()
130+
if annotations != nil {
131+
if revisionHash, ok := annotations[RevisionHashAnnotation]; ok && revisionHash == desiredHash {
132+
currentRevision = &maybeCurrentRevision
133+
//prevRevisions = existingRevisions[0 : len(existingRevisions)-1] // previous is everything excluding current
134+
}
135+
}
136+
}
137+
138+
if currentRevision == nil {
139+
// all Revisions are outdated => create a new one.
140+
prevRevisions := existingRevisions
141+
revisionNumber := latestRevisionNumber(prevRevisions) + 1
142+
143+
newRevision := desiredRevision
144+
newRevision.Name = fmt.Sprintf("%s-%d", ext.Name, revisionNumber)
145+
if newRevision.GetAnnotations() == nil {
146+
newRevision.Annotations = map[string]string{}
147+
}
148+
newRevision.Annotations[RevisionHashAnnotation] = desiredHash
149+
newRevision.Spec.Revision = revisionNumber
150+
for _, prevRevision := range prevRevisions {
151+
newRevision.Spec.Previous = append(newRevision.Spec.Previous, ocv1.ClusterExtensionRevisionPrevious{
152+
Name: prevRevision.Name,
153+
UID: prevRevision.UID,
154+
})
155+
}
156+
157+
if err := controllerutil.SetControllerReference(ext, newRevision, bc.Scheme); err != nil {
158+
return nil, fmt.Errorf("set ownerref: %w", err)
159+
}
160+
if err := bc.Client.Create(ctx, newRevision); err != nil {
161+
return nil, fmt.Errorf("creating new Revision: %w", err)
162+
}
163+
}
164+
165+
// Delete archived previous revisions over revisionHistory limit
166+
//numToDelete := len(prevRevisions) - revisionHistoryLimit
167+
//slices.Reverse(prevRevisions)
168+
//
169+
//for _, prevRev := range prevRevisions {
170+
// if numToDelete <= 0 {
171+
// break
172+
// }
173+
//
174+
// if err := client.IgnoreNotFound(bc.Client.Delete(ctx, &prevRev)); err != nil {
175+
// return nil, fmt.Errorf("failed to delete revision (history limit): %w", err)
176+
// }
177+
// numToDelete--
178+
//}
179+
180+
// TODO: Read status from revision.
181+
182+
// Collect objects
183+
var plain []client.Object
184+
for _, phase := range desiredRevision.Spec.Phases {
185+
for _, phaseObject := range phase.Objects {
186+
plain = append(plain, &phaseObject.Object)
187+
}
188+
}
189+
return plain, nil
190+
}
191+
192+
// getExistingRevisions returns the list of ClusterExtensionRevisions for a ClusterExtension with name extName in revision order (oldest to newest)
193+
func (bc *Boxcutter) getExistingRevisions(ctx context.Context, extName string) ([]ocv1.ClusterExtensionRevision, error) {
194+
existingRevisionList := &ocv1.ClusterExtensionRevisionList{}
195+
if err := bc.Client.List(ctx, existingRevisionList, client.MatchingLabels{
196+
controllers.ClusterExtensionRevisionOwnerLabel: extName,
197+
}); err != nil {
198+
return nil, fmt.Errorf("listing revisions: %w", err)
199+
}
200+
slices.SortFunc(existingRevisionList.Items, func(a, b ocv1.ClusterExtensionRevision) int {
201+
return cmp.Compare(a.Spec.Revision, b.Spec.Revision)
202+
})
203+
return existingRevisionList.Items, nil
204+
}
205+
206+
// computeSHA256Hash returns a sha236 hash value calculated from object.
207+
func computeSHA256Hash(obj any) string {
208+
hasher := sha256.New()
209+
deepHashObject(hasher, obj)
210+
return hex.EncodeToString(hasher.Sum(nil))
211+
}
212+
213+
// deepHashObject writes specified object to hash using the spew library
214+
// which follows pointers and prints actual values of the nested objects
215+
// ensuring the hash does not change when a pointer changes.
216+
func deepHashObject(hasher hash.Hash, objectToWrite any) {
217+
hasher.Reset()
218+
219+
printer := spew.ConfigState{
220+
Indent: " ",
221+
SortKeys: true,
222+
DisableMethods: true,
223+
SpewKeys: true,
224+
}
225+
if _, err := printer.Fprintf(hasher, "%#v", objectToWrite); err != nil {
226+
panic(err)
227+
}
228+
}
229+
230+
func latestRevisionNumber(prevRevisions []ocv1.ClusterExtensionRevision) int64 {
231+
if len(prevRevisions) == 0 {
232+
return 0
233+
}
234+
return prevRevisions[len(prevRevisions)-1].Spec.Revision
235+
}
236+
237+
type BundleRenderer interface {
238+
Render(bundleFS fs.FS, ext *ocv1.ClusterExtension) ([]client.Object, error)
239+
}
240+
241+
type RegistryV1BundleRenderer struct {
242+
BundleRenderer render.BundleRenderer
243+
}
244+
245+
func (r *RegistryV1BundleRenderer) Render(bundleFS fs.FS, ext *ocv1.ClusterExtension) ([]client.Object, error) {
246+
reg, err := source.FromFS(bundleFS).GetBundle()
247+
if err != nil {
248+
return nil, err
249+
}
250+
watchNamespace, err := GetWatchNamespace(ext)
251+
if err != nil {
252+
return nil, err
253+
}
254+
return r.BundleRenderer.Render(reg, ext.Spec.Namespace, render.WithTargetNamespaces(watchNamespace))
255+
}

0 commit comments

Comments
 (0)