Skip to content
Draft
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
177 changes: 176 additions & 1 deletion internal/controller/funcs.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,24 @@ import (
"context"
"errors"
"fmt"
"strings"

"github.com/go-logr/logr"
rabbitmqv1 "github.com/openstack-k8s-operators/infra-operator/apis/rabbitmq/v1beta1"
topologyv1 "github.com/openstack-k8s-operators/infra-operator/apis/topology/v1beta1"
keystonev1 "github.com/openstack-k8s-operators/keystone-operator/api/v1beta1"
corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
k8s_errors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"

"github.com/openstack-k8s-operators/lib-common/modules/common/condition"
"github.com/openstack-k8s-operators/lib-common/modules/common/helper"
common_rbac "github.com/openstack-k8s-operators/lib-common/modules/common/rbac"
"github.com/openstack-k8s-operators/lib-common/modules/common/secret"
"github.com/openstack-k8s-operators/lib-common/modules/common/util"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// Static errors for ironic controllers
Expand Down Expand Up @@ -103,6 +106,27 @@ func getCommonRbacRules() []rbacv1.PolicyRule {
}
}

func getGraphicalConsoleRbacRules() []rbacv1.PolicyRule {
return []rbacv1.PolicyRule{
{
APIGroups: []string{"security.openshift.io"},
ResourceNames: []string{"anyuid", "privileged"},
Resources: []string{"securitycontextconstraints"},
Verbs: []string{"use"},
},
{
APIGroups: []string{""},
Resources: []string{"pods"},
Verbs: []string{"create", "get", "list", "watch", "update", "patch", "delete"},
},
{
APIGroups: []string{""},
Resources: []string{"secrets"},
Verbs: []string{"create", "get", "list", "watch", "update", "patch", "delete"},
},
}
}

type conditionUpdater interface {
Set(c *condition.Condition)
MarkTrue(t condition.Type, messageFormat string, messageArgs ...any)
Expand Down Expand Up @@ -169,6 +193,157 @@ func getQuorumQueues(
return quorumQueues, nil
}

// getConsoleNamespaceName returns the namespace name for console pods based on the service namespace.
// The prefix is extracted from the service namespace (e.g., "openstack" -> "openstack-ironic-consoles")
func getConsoleNamespaceName(serviceNamespace string) string {
// Extract the prefix from the service namespace (before any hyphen or use full name)
prefix := serviceNamespace
if idx := strings.Index(serviceNamespace, "-"); idx > 0 {
prefix = serviceNamespace[:idx]
}
return prefix + "-ironic-consoles"
}

// ensureConsoleNamespace creates the console namespace if it doesn't exist
func ensureConsoleNamespace(
ctx context.Context,
h *helper.Helper,
serviceNamespace string,
) error {
consolesNamespace := getConsoleNamespaceName(serviceNamespace)

ns := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: consolesNamespace,
},
}

op, err := controllerutil.CreateOrPatch(ctx, h.GetClient(), ns, func() error {
// Set labels
if ns.Labels == nil {
ns.Labels = make(map[string]string)
}
ns.Labels["app"] = "ironic"
return nil
})

if err != nil {
return fmt.Errorf("failed to reconcile console namespace %s: %w", consolesNamespace, err)
}

if op != controllerutil.OperationResultNone {
h.GetLogger().Info(fmt.Sprintf("Namespace %s %s", consolesNamespace, op))
}

return nil
}

// reconcileGraphicalConsoleRbac creates a Role and RoleBinding in the console namespace
// that grants the ServiceAccount from the service namespace permissions to create console pods.
// This enables cross-namespace RBAC where the ironic ServiceAccount in the 'openstack' namespace
// can create pods and secrets in the 'openstack-ironic-consoles' namespace.
// Note: These resources cannot have owner references since cross-namespace ownership is not allowed.
func reconcileGraphicalConsoleRbac(
ctx context.Context,
h *helper.Helper,
instance common_rbac.Reconciler,
serviceAccountName string,
consoleNamespace string,
rules []rbacv1.PolicyRule,
) (ctrl.Result, error) {
serviceNamespace := instance.RbacNamespace()
roleName := serviceAccountName + "-console-role"
roleBindingName := serviceAccountName + "-console-rolebinding"

labels := map[string]string{
"app": "ironic",
"ironic.openstack.org/service": serviceAccountName,
}

// Create or update Role in the console namespace without owner references
role := &rbacv1.Role{
ObjectMeta: metav1.ObjectMeta{
Name: roleName,
Namespace: consoleNamespace,
},
}

op, err := controllerutil.CreateOrPatch(ctx, h.GetClient(), role, func() error {
// Set labels
role.Labels = labels
// Set rules
role.Rules = rules
return nil
})

if err != nil {
instance.RbacConditionsSet(condition.FalseCondition(
condition.RoleReadyCondition,
condition.ErrorReason,
condition.SeverityWarning,
condition.RoleReadyErrorMessage,
err.Error()))
return ctrl.Result{}, err
}

if op != controllerutil.OperationResultNone {
h.GetLogger().Info(fmt.Sprintf("Role %s %s in namespace %s", roleName, op, consoleNamespace))
}

instance.RbacConditionsSet(condition.TrueCondition(
condition.RoleReadyCondition,
condition.RoleReadyMessage))

// Create or update RoleBinding in the console namespace that references the ServiceAccount
// from the service namespace (cross-namespace reference)
roleBinding := &rbacv1.RoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: roleBindingName,
Namespace: consoleNamespace,
},
}

op, err = controllerutil.CreateOrPatch(ctx, h.GetClient(), roleBinding, func() error {
// Set labels
roleBinding.Labels = labels
// Set RoleRef (immutable, but safe to set on create)
roleBinding.RoleRef = rbacv1.RoleRef{
APIGroup: "rbac.authorization.k8s.io",
Kind: "Role",
Name: roleName,
}
// Set Subjects
roleBinding.Subjects = []rbacv1.Subject{
{
Kind: "ServiceAccount",
Name: serviceAccountName,
Namespace: serviceNamespace,
},
}
return nil
})

if err != nil {
instance.RbacConditionsSet(condition.FalseCondition(
condition.RoleBindingReadyCondition,
condition.ErrorReason,
condition.SeverityWarning,
condition.RoleBindingReadyErrorMessage,
err.Error()))
return ctrl.Result{}, err
}

if op != controllerutil.OperationResultNone {
h.GetLogger().Info(fmt.Sprintf("RoleBinding %s %s in namespace %s", roleBindingName, op, consoleNamespace))
}

instance.RbacConditionsSet(condition.TrueCondition(
condition.RoleBindingReadyCondition,
condition.RoleBindingReadyMessage))

return ctrl.Result{}, nil
}

// setApplicationCredentialParams - shared function to set ApplicationCredential template parameters
// secretName is the name of the secret containing the application credentials
// Returns true if application credentials are available and configured
Expand Down
6 changes: 6 additions & 0 deletions internal/controller/ironic_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -830,6 +830,12 @@ func (r *IronicReconciler) conductorDeploymentCreateOrUpdate(
Region: keystoneRegion,
TLS: instance.Spec.IronicAPI.TLS.Ca,
Auth: instance.Spec.Auth,
GraphicalConsoles: instance.Spec.GraphicalConsoles,
// FIXME(stevebaker) drop this when https://github.com/openstack-k8s-operators/openstack-operator/pull/1633 lands
// ConsoleImage: instance.Spec.Images.GraphicalConsole,
// NoVNCProxyImage: instance.Spec.Images.NoVNCProxy,
ConsoleImage: "quay.io/steveb/ironic-vnc-container:firefox",
NoVNCProxyImage: "quay.io/steveb/openstack-ironic-novncproxy:steveb-dev-1761687778",
}

if instance.Status.NotificationsURLSecret != nil {
Expand Down
81 changes: 81 additions & 0 deletions internal/controller/ironicconductor_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,49 @@ func (r *IronicConductorReconciler) reconcileServices(
}
}
}
if instance.Spec.GraphicalConsoles == "Enabled" {

//
// Create the conductor pod route to enable traffic to the
// novnc service, which graphical consoles are enabled
//
conductorRouteLabels := map[string]string{
common.AppSelector: ironic.ServiceName,
common.ComponentSelector: ironic.NoVNCComponent,
ironic.ConductorGroupSelector: ironicv1.ConductorGroupNull,
}
if instance.Spec.ConductorGroup != "" {
conductorRouteLabels[ironic.ConductorGroupSelector] = strings.ToLower(instance.Spec.ConductorGroup)
}

novncRoute := ironicconductor.RouteNoVNC(conductorPod.Name, instance, conductorRouteLabels)
err = controllerutil.SetOwnerReference(&conductorPod, novncRoute, helper.GetScheme())
if err != nil {
return ctrl.Result{}, err
}
err = r.Get(
ctx,
types.NamespacedName{
Name: novncRoute.Name,
Namespace: novncRoute.Namespace,
},
novncRoute,
)
if err != nil && k8s_errors.IsNotFound(err) {
Log.Info(fmt.Sprintf("Route %s does not exist, creating it", novncRoute.Name))
err = r.Create(ctx, novncRoute)
if err != nil {
return ctrl.Result{}, err
}
} else {
Log.Info(fmt.Sprintf("Route %s exists, updating it", novncRoute.Name))
err = r.Update(ctx, novncRoute)
if err != nil {
return ctrl.Result{}, err
}
}

}
}

Log.Info("Reconciled Conductor Services successfully")
Expand Down Expand Up @@ -510,6 +553,36 @@ func (r *IronicConductorReconciler) reconcileNormal(ctx context.Context, instanc
}
}

// Roles and binding for existing service account for graphical consoles
if instance.Spec.GraphicalConsoles == "Enabled" {
// TODO: (stevebaker) Uncomment this when the role.yaml
// rule which allows namespace operations is applied.
// Until then, proceed as if the namespace has been created.
// //
// // Create the console namespace for graphical console pods
// //
// err := ensureConsoleNamespace(ctx, helper, instance.Namespace)
// if err != nil {
// return ctrl.Result{}, err
// }

consoleNamespace := getConsoleNamespaceName(instance.Namespace)
serviceAccountName := instance.RbacResourceName()
gcRbacResult, err := reconcileGraphicalConsoleRbac(
ctx,
helper,
instance,
serviceAccountName,
consoleNamespace,
getGraphicalConsoleRbacRules(),
)
if err != nil {
return gcRbacResult, err
} else if (gcRbacResult != ctrl.Result{}) {
return gcRbacResult, nil
}
}

// ConfigMap
configMapVars := make(map[string]env.Setter)

Expand Down Expand Up @@ -967,6 +1040,12 @@ func (r *IronicConductorReconciler) generateServiceConfigMaps(
templateParameters["Standalone"] = instance.Spec.Standalone
templateParameters["ConductorGroup"] = instance.Spec.ConductorGroup
templateParameters["LogPath"] = ironicconductor.LogPath
graphicalConsolesEnabled := instance.Spec.GraphicalConsoles == "Enabled"
templateParameters["GraphicalConsolesEnabled"] = graphicalConsolesEnabled
templateParameters["ConsoleNamespace"] = getConsoleNamespaceName(instance.Namespace)
if graphicalConsolesEnabled {
templateParameters["ConsoleImage"] = instance.Spec.ConsoleImage
}

// Set GracefulShutdownTimeout for conductor pods
templateParameters["GracefulShutdownTimeout"] = instance.Spec.TerminationGracePeriodSeconds
Expand Down Expand Up @@ -1009,8 +1088,10 @@ func (r *IronicConductorReconciler) generateServiceConfigMaps(
AdditionalTemplate: map[string]string{
"ironic.conf": "/common/config/ironic.conf",
"01-conductor.conf": "/ironicconductor/config/01-conductor.conf",
"01-novnc.conf": "/ironicconductor/config/01-novnc.conf",
"03-init-container-conductor.conf": "/ironicconductor/config/03-init-container-conductor.conf",
"dnsmasq.conf": "/common/config/dnsmasq.conf",
"ironic-console-pod.yaml.template": "/ironicconductor/config/ironic-console-pod.yaml.template",
},
Labels: cmLabels,
},
Expand Down
2 changes: 2 additions & 0 deletions internal/ironic/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ const (
APIComponent = "api"
// InspectorComponent -
InspectorComponent = "inspector"
// NoVNCComponent -
NoVNCComponent = "novnc"
// ConductorGroupSelector -
ConductorGroupSelector = "conductorGroup"
// ImageDirectory -
Expand Down
2 changes: 2 additions & 0 deletions internal/ironic/initcontainer.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ type APIDetails struct {
PxeInit bool
ConductorInit bool
DeployHTTPURL string
NoVNCProxyURL string
IngressDomain string
ProvisionNetwork string
ImageDirectory string
Expand All @@ -57,6 +58,7 @@ func InitContainer(init APIDetails) []corev1.Container {
envVars["DatabaseHost"] = env.SetValue(init.DatabaseHost)
envVars["DatabaseName"] = env.SetValue(init.DatabaseName)
envVars["DeployHTTPURL"] = env.SetValue(init.DeployHTTPURL)
envVars["NoVNCProxyURL"] = env.SetValue(init.NoVNCProxyURL)
envVars["IngressDomain"] = env.SetValue(init.IngressDomain)

envs := []corev1.EnvVar{
Expand Down
Loading
Loading