Skip to content
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
2 changes: 2 additions & 0 deletions api/v1beta1/grafana_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ type GrafanaSpec struct {
Ingress *IngressNetworkingV1 `json:"ingress,omitempty"`
// Route sets how the ingress object should look like with your grafana instance, this only works in Openshift.
Route *RouteOpenshiftV1 `json:"route,omitempty"`
// HTTPRoute customizes the GatewayAPI HTTPRoute Object. It will not be created if this is not set
HTTPRoute *HTTPRouteV1 `json:"httpRoute,omitempty"`
// Service sets how the service object should look like with your grafana instance, contains a number of defaults.
Service *ServiceV1 `json:"service,omitempty"`
// Version sets the tag of the default image: docker.io/grafana/grafana.
Expand Down
7 changes: 7 additions & 0 deletions api/v1beta1/typeoverrides.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
networkingv1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/strategicpatch"
gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"
)

// +kubebuilder:object:generate=true
Expand Down Expand Up @@ -525,6 +526,12 @@ type ServiceAccountV1 struct {
AutomountServiceAccountToken *bool `json:"automountServiceAccountToken,omitempty" protobuf:"varint,4,opt,name=automountServiceAccountToken"`
}

// +kubebuilder:object:generate=true
type HTTPRouteV1 struct {
ObjectMeta ObjectMeta `json:"metadata,omitempty"`
Spec gwapiv1.HTTPRouteSpec `json:"spec,omitempty"`
}

// Merge merges `overrides` into `base` using the SMP (structural merge patch) approach.
// - It intentionally does not remove fields present in base but missing from overrides
// - It merges slices only if the `patchStrategy:"merge"` tag is present and the `patchMergeKey` identifies the unique field
Expand Down
22 changes: 22 additions & 0 deletions api/v1beta1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3,139 changes: 3,139 additions & 0 deletions config/crd/bases/grafana.integreatly.org_grafanas.yaml

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions config/rbac/role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,18 @@ rules:
- patch
- update
- watch
- apiGroups:
- gateway.networking.k8s.io
resources:
- httproutes
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- grafana.integreatly.org
resources:
Expand Down
18 changes: 14 additions & 4 deletions controllers/autodetect/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ var _ AutoDetect = (*autoDetect)(nil)
// AutoDetect provides an assortment of routines that auto-detect traits based on the runtime.
type AutoDetect interface {
IsOpenshift() (bool, error)
HasGatewayAPI() (bool, error)
}

type autoDetect struct {
Expand All @@ -32,19 +33,28 @@ func New(restConfig *rest.Config) (AutoDetect, error) {
}, nil
}

// Platform returns the detected platform this operator is running on. Possible values: Kubernetes, OpenShift.
func (a *autoDetect) IsOpenshift() (bool, error) {
func (a *autoDetect) hasAPIGroup(target string) (bool, error) {
apiList, err := a.dcl.ServerGroups()
if err != nil {
return false, err
}

apiGroups := apiList.Groups
for i := range apiGroups {
if apiGroups[i].Name == "route.openshift.io" {
for _, group := range apiGroups {
if group.Name == target {
return true, nil
}
}

return false, nil
}

// Tests for the presence of the `route.openshift.io` api group as an indicator of if we're running in OpenShift
func (a *autoDetect) IsOpenshift() (bool, error) {
return a.hasAPIGroup("route.openshift.io")
}

// Tests if the GatewayAPI CRDs are present
func (a *autoDetect) HasGatewayAPI() (bool, error) {
return a.hasAPIGroup("gateway.networking.k8s.io")
}
9 changes: 8 additions & 1 deletion controllers/grafana_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import (
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"

"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
Expand All @@ -57,6 +58,7 @@ type GrafanaReconciler struct {
client.Client
Scheme *runtime.Scheme
IsOpenShift bool
HasGatewayAPI bool
ClusterDomain string
}

Expand All @@ -65,6 +67,7 @@ type GrafanaReconciler struct {
// +kubebuilder:rbac:groups="",resources=events,verbs=get;list;watch;create;patch
// +kubebuilder:rbac:groups="",resources=configmaps;secrets;serviceaccounts;services;persistentvolumeclaims,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=networking.k8s.io,resources=ingresses,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=httproutes,verbs=get;list;watch;create;update;patch;delete

func (r *GrafanaReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := logf.FromContext(ctx).WithName("GrafanaReconciler")
Expand Down Expand Up @@ -312,6 +315,10 @@ func (r *GrafanaReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manag
b.Owns(&routev1.Route{}, builder.WithPredicates(ignoreStatusUpdates()))
}

if r.HasGatewayAPI {
b.Owns(&gwapiv1.HTTPRoute{}, builder.WithPredicates(ignoreStatusUpdates()))
}

err := b.Complete(r)
if err != nil {
return err
Expand Down Expand Up @@ -380,7 +387,7 @@ func (r *GrafanaReconciler) getReconcilerForStage(stage grafanav1beta1.OperatorS
case grafanav1beta1.OperatorStageService:
return grafana.NewServiceReconciler(r.Client, r.ClusterDomain)
case grafanav1beta1.OperatorStageIngress:
return grafana.NewIngressReconciler(r.Client, r.IsOpenShift)
return grafana.NewIngressReconciler(r.Client, r.IsOpenShift, r.HasGatewayAPI)
case grafanav1beta1.OperatorStagePlugins:
return grafana.NewPluginsReconciler(r.Client)
case grafanav1beta1.OperatorStageDeployment:
Expand Down
14 changes: 14 additions & 0 deletions controllers/model/grafana_resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"
)

func GetCommonLabels() map[string]string {
Expand Down Expand Up @@ -127,6 +128,19 @@ func GetGrafanaRoute(cr *grafanav1beta1.Grafana, scheme *runtime.Scheme) *routev
return route
}

func GetGrafanaHTTPRoute(cr *grafanav1beta1.Grafana, scheme *runtime.Scheme) *gwapiv1.HTTPRoute {
httpRoute := &gwapiv1.HTTPRoute{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("%s-httproute", cr.Name),
Namespace: cr.Namespace,
Labels: GetCommonLabels(),
},
}
controllerutil.SetControllerReference(cr, httpRoute, scheme) //nolint:errcheck

return httpRoute
}

func GetGrafanaDeployment(cr *grafanav1beta1.Grafana, scheme *runtime.Scheme) *v13.Deployment {
deployment := &v13.Deployment{
ObjectMeta: metav1.ObjectMeta{
Expand Down
110 changes: 105 additions & 5 deletions controllers/reconcilers/grafana/ingress_reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,24 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
logf "sigs.k8s.io/controller-runtime/pkg/log"
gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"
)

const (
RouteKind = "Route"
)

type IngressReconciler struct {
client client.Client
isOpenShift bool
client client.Client
isOpenShift bool
hasGatewayAPI bool
}

func NewIngressReconciler(client client.Client, isOpenShift bool) reconcilers.OperatorGrafanaReconciler {
func NewIngressReconciler(client client.Client, isOpenShift bool, hasGatewayAPI bool) reconcilers.OperatorGrafanaReconciler {
return &IngressReconciler{
client: client,
isOpenShift: isOpenShift,
client: client,
isOpenShift: isOpenShift,
hasGatewayAPI: hasGatewayAPI,
}
}

Expand All @@ -46,6 +49,13 @@ func (r *IngressReconciler) Reconcile(ctx context.Context, cr *v1beta1.Grafana,
}
}

if r.hasGatewayAPI {
err := r.deleteHTTPRouteIfNil(ctx, cr, scheme)
if err != nil {
return v1beta1.OperatorStageResultFailed, err
}
}

err := r.deleteIngressIfNil(ctx, cr, scheme)
if err != nil {
return v1beta1.OperatorStageResultFailed, err
Expand All @@ -57,6 +67,11 @@ func (r *IngressReconciler) Reconcile(ctx context.Context, cr *v1beta1.Grafana,
return r.reconcileRoute(ctx, cr, vars, scheme)
}

if cr.Spec.HTTPRoute != nil {
Comment thread
weisdd marked this conversation as resolved.
log.Info("reconciling HTTPRoute")
return r.reconcileHTTPRoute(ctx, cr, vars, scheme)
}

log.Info("reconciling ingress")

return r.reconcileIngress(ctx, cr, vars, scheme)
Expand Down Expand Up @@ -88,6 +103,32 @@ func (r *IngressReconciler) deleteIngressIfNil(ctx context.Context, cr *v1beta1.
return r.client.Delete(ctx, ingress)
}

func (r *IngressReconciler) deleteHTTPRouteIfNil(ctx context.Context, cr *v1beta1.Grafana, scheme *runtime.Scheme) error {
if cr.Spec.HTTPRoute != nil {
return nil
}

route := model.GetGrafanaHTTPRoute(cr, scheme)

req := ctrl.Request{
NamespacedName: types.NamespacedName{
Name: route.Name,
Namespace: route.Namespace,
},
}

err := r.client.Get(ctx, req.NamespacedName, route)
if err != nil {
if kuberr.IsNotFound(err) {
return nil
}

return fmt.Errorf("error getting HTTPRoute: %w", err)
}

return r.client.Delete(ctx, route)
}

func (r *IngressReconciler) reconcileIngress(ctx context.Context, cr *v1beta1.Grafana, _ *v1beta1.OperatorReconcileVars, scheme *runtime.Scheme) (v1beta1.OperatorStageStatus, error) {
if cr.Spec.Ingress == nil {
return v1beta1.OperatorStageResultSuccess, nil
Expand Down Expand Up @@ -206,6 +247,42 @@ func (r *IngressReconciler) reconcileRoute(ctx context.Context, cr *v1beta1.Graf
return v1beta1.OperatorStageResultSuccess, nil
}

func (r *IngressReconciler) reconcileHTTPRoute(ctx context.Context, cr *v1beta1.Grafana, _ *v1beta1.OperatorReconcileVars, scheme *runtime.Scheme) (v1beta1.OperatorStageStatus, error) {
if cr.Spec.HTTPRoute == nil {
return v1beta1.OperatorStageResultSuccess, nil
}

route := model.GetGrafanaHTTPRoute(cr, scheme)

_, err := controllerutil.CreateOrUpdate(ctx, r.client, route, func() error {
route.Spec = getHTTPRouteSpec(cr, scheme)

err := v1beta1.Merge(route, cr.Spec.HTTPRoute)
if err != nil {
setInvalidMergeCondition(cr, "HTTPRoute", err)
return err
}

removeInvalidMergeCondition(cr, "HTTPRoute")

if scheme != nil {
err = controllerutil.SetControllerReference(cr, route, scheme)
if err != nil {
return err
}
}

model.SetInheritedLabels(route, cr.Labels)

return nil
})
if err != nil {
return v1beta1.OperatorStageResultFailed, err
}

return v1beta1.OperatorStageResultSuccess, nil
}

// getIngressAdminURL returns the first valid URL (Host field is set) from the ingress spec
func (r *IngressReconciler) getIngressAdminURL(ingress *v1.Ingress) string {
if ingress == nil {
Expand Down Expand Up @@ -296,6 +373,29 @@ func getRouteSpec(cr *v1beta1.Grafana, scheme *runtime.Scheme) routev1.RouteSpec
}
}

func getHTTPRouteSpec(cr *v1beta1.Grafana, scheme *runtime.Scheme) gwapiv1.HTTPRouteSpec {
service := model.GetGrafanaService(cr, scheme)
port := gwapiv1.PortNumber(GetGrafanaPort(cr)) //nolint:gosec
backendRefs := []gwapiv1.HTTPBackendRef{
{
BackendRef: gwapiv1.BackendRef{
BackendObjectReference: gwapiv1.BackendObjectReference{
Name: gwapiv1.ObjectName(service.Name),
Port: &port,
},
},
},
}

return gwapiv1.HTTPRouteSpec{
Rules: []gwapiv1.HTTPRouteRule{
{
BackendRefs: backendRefs,
},
},
}
}

func getIngressSpec(cr *v1beta1.Grafana, scheme *runtime.Scheme) v1.IngressSpec {
service := model.GetGrafanaService(cr, scheme)

Expand Down
Loading