Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,25 @@ unit: manifests generate setup-envtest ## Run unit tests (envtest). V=1 for verb
KUBEBUILDER_ASSETS="$(shell "$(ENVTEST)" use $(ENVTEST_K8S_VERSION) --bin-dir "$(LOCALBIN)" -p path)" go test $(if $(V),-v) $(if $(RUN),-run $(RUN)) $$(go list ./... | grep -v /test/e2e)

.PHONY: lint
lint: golangci-lint ## Run golangci-lint linter.
lint: golangci-lint ## Run golangci-lint linter and go-lines
"$(GOLANGCI_LINT)" run
$(MAKE) go-lines

.PHONY: lint-fix
lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes.
"$(GOLANGCI_LINT)" run --fix

.PHONY: go-lines
go-lines: get-golines
@if [ "$$("$(GOLINES_BIN)" --dry-run . | wc -l)" -gt 0 ]; then \
echo "Run make go-lines-fix"; \
exit 1; \
fi

.PHONY: go-lines-fix
go-lines-fix: get-golines
"$(GOLINES_BIN)" -w .

.PHONY: e2e
e2e: ## Run e2e tests (requires: make deploy-bink). V=1 for verbose. RUN=<regex> to filter.
# NB: we `cd` here instead of passing a package path to `go test` so that `-v`
Expand Down Expand Up @@ -180,6 +192,7 @@ KUSTOMIZE ?= $(LOCALBIN)/kustomize
CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen
ENVTEST ?= $(LOCALBIN)/setup-envtest
GOLANGCI_LINT = $(LOCALBIN)/golangci-lint
GOLINES_BIN = $(LOCALBIN)/golines
YQ ?= $(LOCALBIN)/yq

KUSTOMIZE_VERSION ?= v5.8.1
Expand Down Expand Up @@ -224,6 +237,11 @@ golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary.
$(GOLANGCI_LINT): $(LOCALBIN)
$(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/v2/cmd/golangci-lint,$(GOLANGCI_LINT_VERSION))

.PHONY: get-golines
get-golines: $(GOLINES_BIN) ## Download golines locally if necessary.
$(GOLINES_BIN): $(LOCALBIN)
$(call go-install-tool,$(GOLINES_BIN),github.com/golangci/golines,latest)

.PHONY: yq
yq: $(YQ) ## Download yq locally if necessary.
$(YQ): $(LOCALBIN)
Expand Down
22 changes: 18 additions & 4 deletions cmd/controller/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,24 @@ func main() {
var probeAddr string
var tagResolutionInterval time.Duration
var allowInsecureRegistry bool
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
flag.DurationVar(&tagResolutionInterval, "tag-resolution-interval", 5*time.Minute, "How often to re-resolve tag-based image refs.")
flag.BoolVar(&allowInsecureRegistry, "allow-insecure-registry", false,
"Allow falling back to HTTP when resolving tag-based image refs against registries that do not serve TLS.")
flag.StringVar(
&probeAddr,
"health-probe-bind-address",
":8081",
"The address the probe endpoint binds to.",
)
flag.DurationVar(
&tagResolutionInterval,
"tag-resolution-interval",
5*time.Minute,
"How often to re-resolve tag-based image refs.",
)
flag.BoolVar(
&allowInsecureRegistry,
"allow-insecure-registry",
false,
"Allow falling back to HTTP when resolving tag-based image refs against registries that do not serve TLS.",
)
flag.BoolVar(&enableLeaderElection, "leader-elect", false,
"Enable leader election for controller manager. "+
"Enabling this will ensure there is only one active controller manager.")
Expand Down
12 changes: 10 additions & 2 deletions cmd/daemon/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,12 @@ func init() {

func main() {
var pollInterval time.Duration
flag.DurationVar(&pollInterval, "bootc-poll-interval", 5*time.Minute, "Interval for polling bootc status as a fallback to fsnotify")
flag.DurationVar(
&pollInterval,
"bootc-poll-interval",
5*time.Minute,
"Interval for polling bootc status as a fallback to fsnotify",
)

opts := zap.Options{
Development: true,
Expand All @@ -47,7 +52,10 @@ func main() {

nodeName := os.Getenv("NODE_NAME")
if nodeName == "" {
setupLog.Error(fmt.Errorf("NODE_NAME not set"), "NODE_NAME environment variable is required")
setupLog.Error(
fmt.Errorf("NODE_NAME not set"),
"NODE_NAME environment variable is required",
)
os.Exit(1)
}

Expand Down
77 changes: 62 additions & 15 deletions internal/controller/bootcnodepool_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,10 @@ func (r *BootcNodePoolReconciler) SetupWithManager(mgr ctrl.Manager) error {
// pool that owns it. The second set is needed so the owning pool can clean up
// when a node's labels change such that it no longer matches, or when the node
// is deleted entirely.
func (r *BootcNodePoolReconciler) mapNodeToPoolRequests(ctx context.Context, obj client.Object) []reconcile.Request {
func (r *BootcNodePoolReconciler) mapNodeToPoolRequests(
ctx context.Context,
obj client.Object,
) []reconcile.Request {
node, ok := obj.(*corev1.Node)
if !ok {
return nil
Expand Down Expand Up @@ -218,7 +221,10 @@ func nodeUnschedulableChanged(oldNode, newNode *corev1.Node) bool {

// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
func (r *BootcNodePoolReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
func (r *BootcNodePoolReconciler) Reconcile(
ctx context.Context,
req ctrl.Request,
) (ctrl.Result, error) {
log := logf.FromContext(ctx).WithValues("pool", req.Name)

// Fetch the pool.
Expand Down Expand Up @@ -306,12 +312,17 @@ func (r *BootcNodePoolReconciler) Reconcile(ctx context.Context, req ctrl.Reques
// resolveTargetDigest resolves the target digest from the pool's image
// ref. Digest refs are extracted directly. Tag refs are resolved via
// the registry, respecting the re-resolution interval.
func (r *BootcNodePoolReconciler) resolveTargetDigest(ctx context.Context, pool *bootcv1alpha1.BootcNodePool) (ctrl.Result, error) {
func (r *BootcNodePoolReconciler) resolveTargetDigest(
ctx context.Context,
pool *bootcv1alpha1.BootcNodePool,
) (ctrl.Result, error) {
log := logf.FromContext(ctx)

ref, err := parseImageRef(pool.Spec.Image.Ref)
if err != nil {
return ctrl.Result{}, newInvalidSpecError(fmt.Sprintf("invalid image ref %q: %v", pool.Spec.Image.Ref, err))
return ctrl.Result{}, newInvalidSpecError(
fmt.Sprintf("invalid image ref %q: %v", pool.Spec.Image.Ref, err),
)
}

digested, ok := ref.(reference.Digested)
Expand All @@ -325,7 +336,8 @@ func (r *BootcNodePoolReconciler) resolveTargetDigest(ctx context.Context, pool

// Tag ref — check if resolution is due.
now := time.Now()
if pool.Status.NextTagResolutionTime != nil && now.Before(pool.Status.NextTagResolutionTime.Time) {
if pool.Status.NextTagResolutionTime != nil &&
now.Before(pool.Status.NextTagResolutionTime.Time) {
remaining := pool.Status.NextTagResolutionTime.Sub(now)
log.V(1).Info("Tag resolution not yet due", "remaining", remaining)
return ctrl.Result{RequeueAfter: remaining}, nil
Expand Down Expand Up @@ -375,7 +387,11 @@ func isInvalidSpecError(err error) bool {

// setInvalidSpecCondition sets Degraded/InvalidSpec on the pool and
// returns (Result, nil) so Reconcile stops without requeueing.
func (r *BootcNodePoolReconciler) setInvalidSpecCondition(ctx context.Context, pool *bootcv1alpha1.BootcNodePool, specErr error) (ctrl.Result, error) {
func (r *BootcNodePoolReconciler) setInvalidSpecCondition(
ctx context.Context,
pool *bootcv1alpha1.BootcNodePool,
specErr error,
) (ctrl.Result, error) {
setPoolDegraded(pool, bootcv1alpha1.PoolInvalidSpec, specErr.Error())
if err := r.Status().Update(ctx, pool); err != nil {
return ctrl.Result{}, fmt.Errorf("updating pool status: %w", err)
Expand All @@ -386,7 +402,10 @@ func (r *BootcNodePoolReconciler) setInvalidSpecCondition(ctx context.Context, p
// syncMembership reconciles the set of BootcNodes owned by this pool
// against the set of Nodes matching the pool's nodeSelector. It returns
// the owned BootcNodes after mutations (creates and deletes) are applied.
func (r *BootcNodePoolReconciler) syncMembership(ctx context.Context, pool *bootcv1alpha1.BootcNodePool) (map[string]*bootcv1alpha1.BootcNode, error) {
func (r *BootcNodePoolReconciler) syncMembership(
ctx context.Context,
pool *bootcv1alpha1.BootcNodePool,
) (map[string]*bootcv1alpha1.BootcNode, error) {
log := logf.FromContext(ctx).WithValues("pool", pool.Name)

// List all nodes matching the pool's selector.
Expand Down Expand Up @@ -473,21 +492,30 @@ func (r *BootcNodePoolReconciler) syncMembership(ctx context.Context, pool *boot

// listMatchingNodes returns all Nodes whose labels match the pool's
// nodeSelector.
func (r *BootcNodePoolReconciler) listMatchingNodes(ctx context.Context, pool *bootcv1alpha1.BootcNodePool) ([]corev1.Node, error) {
func (r *BootcNodePoolReconciler) listMatchingNodes(
ctx context.Context,
pool *bootcv1alpha1.BootcNodePool,
) ([]corev1.Node, error) {
selector, err := metav1.LabelSelectorAsSelector(pool.Spec.NodeSelector)
if err != nil {
return nil, newInvalidSpecError(fmt.Sprintf("invalid nodeSelector: %v", err))
}

var nodeList corev1.NodeList
if err := r.List(ctx, &nodeList, client.MatchingLabelsSelector{Selector: selector}); err != nil {
if err := r.List(
ctx,
&nodeList,
client.MatchingLabelsSelector{Selector: selector},
); err != nil {
return nil, fmt.Errorf("listing nodes: %w", err)
}
return nodeList.Items, nil
}

// listAllBootcNodes returns all BootcNodes keyed by name.
func (r *BootcNodePoolReconciler) listAllBootcNodes(ctx context.Context) (map[string]*bootcv1alpha1.BootcNode, error) {
func (r *BootcNodePoolReconciler) listAllBootcNodes(
ctx context.Context,
) (map[string]*bootcv1alpha1.BootcNode, error) {
var bnList bootcv1alpha1.BootcNodeList
if err := r.List(ctx, &bnList); err != nil {
return nil, fmt.Errorf("listing BootcNodes: %w", err)
Expand All @@ -501,7 +529,11 @@ func (r *BootcNodePoolReconciler) listAllBootcNodes(ctx context.Context) (map[st
}

// syncBootcNodeSpec updates a BootcNode's spec fields to match the pool.
func (r *BootcNodePoolReconciler) syncBootcNodeSpec(ctx context.Context, pool *bootcv1alpha1.BootcNodePool, bn *bootcv1alpha1.BootcNode) error {
func (r *BootcNodePoolReconciler) syncBootcNodeSpec(
ctx context.Context,
pool *bootcv1alpha1.BootcNodePool,
bn *bootcv1alpha1.BootcNode,
) error {
modified := bn.DeepCopy()
desiredImage := desiredImageFromPool(pool)
needPatch := false
Expand Down Expand Up @@ -540,7 +572,11 @@ func desiredImageFromPool(pool *bootcv1alpha1.BootcNodePool) string {

// createBootcNode creates a BootcNode for a node joining the pool and
// labels the node as managed.
func (r *BootcNodePoolReconciler) createBootcNode(ctx context.Context, pool *bootcv1alpha1.BootcNodePool, node *corev1.Node) (*bootcv1alpha1.BootcNode, error) {
func (r *BootcNodePoolReconciler) createBootcNode(
ctx context.Context,
pool *bootcv1alpha1.BootcNodePool,
node *corev1.Node,
) (*bootcv1alpha1.BootcNode, error) {
bn := &bootcv1alpha1.BootcNode{
ObjectMeta: metav1.ObjectMeta{
Name: node.Name,
Expand Down Expand Up @@ -575,7 +611,11 @@ func (r *BootcNodePoolReconciler) createBootcNode(ctx context.Context, pool *boo
}

// ensureManagedLabel adds or removes the bootc.dev/managed label on a Node.
func (r *BootcNodePoolReconciler) ensureManagedLabel(ctx context.Context, node *corev1.Node, managed bool) error {
func (r *BootcNodePoolReconciler) ensureManagedLabel(
ctx context.Context,
node *corev1.Node,
managed bool,
) error {
_, hasLabel := node.Labels[bootcv1alpha1.LabelManaged]
if managed && hasLabel {
return nil
Expand All @@ -602,7 +642,10 @@ func (r *BootcNodePoolReconciler) ensureManagedLabel(ctx context.Context, node *

// removeBootcNode deletes a BootcNode for a node leaving the pool,
// removes the managed label, and restores prior cordon state.
func (r *BootcNodePoolReconciler) removeBootcNode(ctx context.Context, bn *bootcv1alpha1.BootcNode) error {
func (r *BootcNodePoolReconciler) removeBootcNode(
ctx context.Context,
bn *bootcv1alpha1.BootcNode,
) error {
// Cancel any active drain goroutine for this node. The goroutine will
// exit on its own and send a result on the channel, but since we've
// removed the entry from the map, collectDrainResults will never see
Expand Down Expand Up @@ -643,7 +686,11 @@ func (r *BootcNodePoolReconciler) removeBootcNode(ctx context.Context, bn *bootc
// restoreCordonState uncordons the Node if the BootcNode's was-cordoned
// annotation indicates the operator cordoned it. If the annotation is absent
// or "true" (node was already cordoned before us), this is a no-op.
func (r *BootcNodePoolReconciler) restoreCordonState(ctx context.Context, bn *bootcv1alpha1.BootcNode, node *corev1.Node) error {
func (r *BootcNodePoolReconciler) restoreCordonState(
ctx context.Context,
bn *bootcv1alpha1.BootcNode,
node *corev1.Node,
) error {
if bn.Annotations[bootcv1alpha1.AnnotationWasCordoned] != "false" {
return nil
}
Expand Down
17 changes: 14 additions & 3 deletions internal/controller/membership_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,11 @@ func TestMembershipCreatesBootcNodes(t *testing.T) {

// Wait for BootcNode to be deleted.
g.Eventually(func() error {
return k8sClient.Get(ctx, client.ObjectKey{Name: "mem-worker-1"}, &bootcv1alpha1.BootcNode{})
return k8sClient.Get(
ctx,
client.ObjectKey{Name: "mem-worker-1"},
&bootcv1alpha1.BootcNode{},
)
}).Should(MatchError(apierrors.IsNotFound, "IsNotFound"))

// Verify managed label is removed.
Expand All @@ -154,7 +158,11 @@ func TestMembershipCreatesBootcNodes(t *testing.T) {
g.Expect(k8sClient.Delete(ctx, node2)).To(Succeed())

g.Eventually(func() error {
return k8sClient.Get(ctx, client.ObjectKey{Name: "mem-worker-2"}, &bootcv1alpha1.BootcNode{})
return k8sClient.Get(
ctx,
client.ObjectKey{Name: "mem-worker-2"},
&bootcv1alpha1.BootcNode{},
)
}).Should(MatchError(apierrors.IsNotFound, "IsNotFound"))
}

Expand Down Expand Up @@ -216,7 +224,10 @@ func TestMembershipConflictDetection(t *testing.T) {
// node1: pool1 only, node2: pool2 only, node3: both (contested).
node1 := testutil.NewK8sNode("mem-conflict-1", map[string]string{"pool1": "true"})
node2 := testutil.NewK8sNode("mem-conflict-2", map[string]string{"pool2": "true"})
node3 := testutil.NewK8sNode("mem-conflict-3", map[string]string{"pool1": "true", "pool2": "true"})
node3 := testutil.NewK8sNode(
"mem-conflict-3",
map[string]string{"pool1": "true", "pool2": "true"},
)
for _, n := range []*corev1.Node{node1, node2, node3} {
g.Expect(k8sClient.Create(ctx, n)).To(Succeed())
t.Cleanup(func() {
Expand Down
Loading
Loading