Skip to content

Commit 93187c7

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

File tree

4 files changed

+845
-42
lines changed

4 files changed

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

0 commit comments

Comments
 (0)