Skip to content
This repository was archived by the owner on Sep 9, 2026. It is now read-only.
Merged
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
316 changes: 213 additions & 103 deletions internal/api/osac/private/v1/event_type.pb.go

Large diffs are not rendered by default.

314 changes: 211 additions & 103 deletions internal/api/osac/private/v1/event_type_protoopaque.pb.go

Large diffs are not rendered by default.

222 changes: 166 additions & 56 deletions internal/api/osac/public/v1/event_type.pb.go

Large diffs are not rendered by default.

220 changes: 164 additions & 56 deletions internal/api/osac/public/v1/event_type_protoopaque.pb.go

Large diffs are not rendered by default.

74 changes: 74 additions & 0 deletions internal/cmd/service/start/controller/start_controller_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ import (
"github.com/osac-project/fulfillment-service/internal/controllers/organization"
"github.com/osac-project/fulfillment-service/internal/controllers/publicip"
"github.com/osac-project/fulfillment-service/internal/controllers/publicippool"
"github.com/osac-project/fulfillment-service/internal/controllers/role"
"github.com/osac-project/fulfillment-service/internal/controllers/rolebinding"
"github.com/osac-project/fulfillment-service/internal/controllers/securitygroup"
"github.com/osac-project/fulfillment-service/internal/controllers/subnet"
"github.com/osac-project/fulfillment-service/internal/controllers/virtualnetwork"
Expand Down Expand Up @@ -617,6 +619,78 @@ func (r *runnerContext) run(cmd *cobra.Command, argv []string) error {
}
}()

// Create the role reconciler:
r.logger.InfoContext(ctx, "Creating role reconciler")
roleReconcilerFunction, err := role.NewFunction().
SetLogger(r.logger).
SetConnection(r.client).
Build()
if err != nil {
return fmt.Errorf("failed to create role reconciler function: %w", err)
}
roleReconciler, err := controllers.NewReconciler[*privatev1.Role]().
SetLogger(r.logger).
SetName("role").
SetClient(r.client).
SetFunction(roleReconcilerFunction.Run).
SetEventFilter("has(event.role)").
SetHealthReporter(healthAggregator).
Build()
if err != nil {
return fmt.Errorf("failed to create role reconciler: %w", err)
}

// Start the role reconciler:
r.logger.InfoContext(ctx, "Starting role reconciler")
go func() {
err := roleReconciler.Start(ctx)
if err == nil || errors.Is(err, context.Canceled) {
r.logger.InfoContext(ctx, "Role reconciler finished")
} else {
r.logger.InfoContext(
ctx,
"Role reconciler failed",
slog.Any("error", err),
)
}
}()

// Create the role binding reconciler:
r.logger.InfoContext(ctx, "Creating role binding reconciler")
roleBindingReconcilerFunction, err := rolebinding.NewFunction().
SetLogger(r.logger).
SetConnection(r.client).
Build()
if err != nil {
return fmt.Errorf("failed to create role binding reconciler function: %w", err)
}
roleBindingReconciler, err := controllers.NewReconciler[*privatev1.RoleBinding]().
SetLogger(r.logger).
SetName("role_binding").
SetClient(r.client).
SetFunction(roleBindingReconcilerFunction.Run).
SetEventFilter("has(event.role_binding)").
SetHealthReporter(healthAggregator).
Build()
if err != nil {
return fmt.Errorf("failed to create role binding reconciler: %w", err)
}

// Start the role binding reconciler:
r.logger.InfoContext(ctx, "Starting role binding reconciler")
go func() {
err := roleBindingReconciler.Start(ctx)
if err == nil || errors.Is(err, context.Canceled) {
r.logger.InfoContext(ctx, "Role binding reconciler finished")
} else {
r.logger.InfoContext(
ctx,
"Role binding reconciler failed",
slog.Any("error", err),
)
}
}()

// Create the organization reconciler if IDP is configured:
if idpManager != nil {
r.logger.InfoContext(ctx, "Creating organization reconciler")
Expand Down
177 changes: 177 additions & 0 deletions internal/controllers/role/role_reconciler_function.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
/*
Copyright (c) 2026 Red Hat Inc.

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the
License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific
language governing permissions and limitations under the License.
*/

package role

import (
"context"
"errors"
"log/slog"
"slices"

"google.golang.org/grpc"
"google.golang.org/protobuf/proto"

privatev1 "github.com/osac-project/fulfillment-service/internal/api/osac/private/v1"
"github.com/osac-project/fulfillment-service/internal/controllers/finalizers"
"github.com/osac-project/fulfillment-service/internal/masks"
)

// FunctionBuilder contains the data needed to build instances of the reconciler function.
type FunctionBuilder struct {
logger *slog.Logger
connection *grpc.ClientConn
}

// NewFunction creates a builder that can be used to configure and create reconciler functions.
func NewFunction() *FunctionBuilder {
return &FunctionBuilder{}
}

// SetLogger sets the logger that the reconciler will use to write log messages.
func (b *FunctionBuilder) SetLogger(value *slog.Logger) *FunctionBuilder {
b.logger = value
return b
}

// SetConnection sets the gRPC connection that the reconciler will use to communicate with the API server.
func (b *FunctionBuilder) SetConnection(value *grpc.ClientConn) *FunctionBuilder {
b.connection = value
return b
}

// Build uses the data stored in the builder to create and configure a new reconciler function.
func (b *FunctionBuilder) Build() (result *function, err error) {
if b.logger == nil {
err = errors.New("logger is mandatory")
return
}
if b.connection == nil {
err = errors.New("connection is mandatory")
return
}

result = &function{
logger: b.logger,
rolesClient: privatev1.NewRolesClient(b.connection),
maskCalculator: masks.NewCalculator().
Build(),
}
return
}

// function is the implementation of the reconciler function.
type function struct {
logger *slog.Logger
rolesClient privatev1.RolesClient
maskCalculator *masks.Calculator
}

// Run executes the reconciliation logic for the given role.
func (r *function) Run(ctx context.Context, role *privatev1.Role) error {
oldRole := proto.Clone(role).(*privatev1.Role)

task := &task{
r: r,
role: role,
}

var err error
if role.HasMetadata() && role.GetMetadata().HasDeletionTimestamp() {
err = task.delete(ctx)
} else {
err = task.update(ctx)
}
if err != nil {
return err
}

updateMask := r.maskCalculator.Calculate(oldRole, role)

if len(updateMask.GetPaths()) > 0 {
_, err = r.rolesClient.Update(ctx, privatev1.RolesUpdateRequest_builder{
Object: role,
UpdateMask: updateMask,
}.Build())
}

return err
}

// task contains the data needed to reconcile a single role.
type task struct {
r *function
role *privatev1.Role
}

func (t *task) update(ctx context.Context) error {
if t.addFinalizer() {
return nil
}

t.setDefaults()

t.r.logger.InfoContext(
ctx,
"Reconciling role",
slog.Any("role", t.role),
)

return nil
}

func (t *task) delete(ctx context.Context) error {
t.r.logger.InfoContext(
ctx,
"Reconciling deleted role",
slog.Any("role", t.role),
)

t.removeFinalizer()
return nil
}

func (t *task) setDefaults() {
if !t.role.HasStatus() {
t.role.SetStatus(&privatev1.RoleStatus{})
}
if t.role.GetStatus().GetState() == privatev1.RoleState_ROLE_STATE_UNSPECIFIED {
t.role.GetStatus().SetState(privatev1.RoleState_ROLE_STATE_PENDING)
}
}

func (t *task) addFinalizer() bool {
if !t.role.HasMetadata() {
t.role.SetMetadata(&privatev1.Metadata{})
}
list := t.role.GetMetadata().GetFinalizers()
if !slices.Contains(list, finalizers.Controller) {
list = append(list, finalizers.Controller)
t.role.GetMetadata().SetFinalizers(list)
return true
}
return false
}

func (t *task) removeFinalizer() {
if !t.role.HasMetadata() {
return
}
list := t.role.GetMetadata().GetFinalizers()
if slices.Contains(list, finalizers.Controller) {
list = slices.DeleteFunc(list, func(item string) bool {
return item == finalizers.Controller
})
t.role.GetMetadata().SetFinalizers(list)
}
}
Loading
Loading