Skip to content
Closed
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
83 changes: 69 additions & 14 deletions cmd/team-operator/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,18 @@
package main

import (
"context"
"flag"
"os"
"strconv"

"github.com/posit-dev/team-operator/api/keycloak/v2alpha1"
"github.com/posit-dev/team-operator/api/product"
"github.com/traefik/traefik/v3/pkg/provider/kubernetes/crd/traefikio/v1alpha1"
"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/cache"
"sigs.k8s.io/controller-runtime/pkg/metrics/server"
"sigs.k8s.io/controller-runtime/pkg/webhook"
Expand Down Expand Up @@ -73,6 +78,29 @@ func init() {
LoadSchemes(scheme)
}

// isCRDPresent checks if a Custom Resource Definition exists on the cluster
func isCRDPresent(ctx context.Context, config *rest.Config, crdName string) (bool, error) {
// Create a clientset for CRD operations
crdClient, err := clientset.NewForConfig(config)
if err != nil {
return false, err
}

// Try to get the CRD
_, err = crdClient.ApiextensionsV1().CustomResourceDefinitions().Get(ctx, crdName, metav1.GetOptions{})
if err != nil {
if errors.IsNotFound(err) {
// CRD doesn't exist, which is okay
return false, nil
}
// Some other error occurred
return false, err
}

// CRD exists
return true, nil
}

func main() {
var (
metricsAddr string
Expand Down Expand Up @@ -132,13 +160,27 @@ func main() {
os.Exit(1)
}

if err = (&corecontroller.SiteReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
Log: setupLog,
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "Site")
os.Exit(1)
// Check if Site CRD exists before setting up Site controller
ctx := context.Background()
siteCRDExists, err := isCRDPresent(ctx, mgr.GetConfig(), "sites.core.posit.team")
if err != nil {
setupLog.Error(err, "unable to check if Site CRD exists")
// Continue without Site controller rather than exiting
siteCRDExists = false
}

if siteCRDExists {
setupLog.Info("Site CRD found, setting up Site controller")
if err = (&corecontroller.SiteReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
Log: setupLog,
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "Site")
os.Exit(1)
}
} else {
setupLog.Info("Site CRD not found, skipping Site controller setup")
}

if err = (&corecontroller.PostgresDatabaseReconciler{
Expand Down Expand Up @@ -185,13 +227,26 @@ func main() {
os.Exit(1)
}

if err = (&corecontroller.FlightdeckReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
Log: setupLog,
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "Flightdeck")
os.Exit(1)
// Check if Flightdeck CRD exists before setting up Flightdeck controller
flightdeckCRDExists, err := isCRDPresent(ctx, mgr.GetConfig(), "flightdecks.core.posit.team")
if err != nil {
setupLog.Error(err, "unable to check if Flightdeck CRD exists")
// Continue without Flightdeck controller rather than exiting
flightdeckCRDExists = false
}

if flightdeckCRDExists {
setupLog.Info("Flightdeck CRD found, setting up Flightdeck controller")
if err = (&corecontroller.FlightdeckReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
Log: setupLog,
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "Flightdeck")
os.Exit(1)
}
} else {
setupLog.Info("Flightdeck CRD not found, skipping Flightdeck controller setup")
}

//+kubebuilder:scaffold:builder
Expand Down
15 changes: 15 additions & 0 deletions flightdeck/internal/kube.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package internal
import (
"context"
"fmt"
"strings"

positcov1beta1 "github.com/posit-dev/team-operator/api/core/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
Expand Down Expand Up @@ -109,10 +111,23 @@ func (c *siteClient) Get(name string, namespace string, opts metav1.GetOptions,
Into(&result)

if err != nil {
if isCRDNotFoundError(err.Error()) {
slog.Info("Sites CRD not found on cluster, returning empty site", "name", name, "namespace", namespace)
// Return an empty Site with minimal info for display
result.Name = name
result.Namespace = namespace
return &result, nil
}
slog.Error("failed to fetch site", "name", name, "namespace", namespace, "error", err)
return &result, err
}

slog.Debug("site fetched successfully", "name", name, "namespace", namespace)
return &result, err
}

// isCRDNotFoundError checks if an error message indicates the Site CRD is not installed.
func isCRDNotFoundError(errMsg string) bool {
return strings.Contains(errMsg, "the server could not find the requested resource") ||
strings.Contains(errMsg, "no matches for kind")
}
48 changes: 48 additions & 0 deletions flightdeck/internal/kube_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package internal

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestIsCRDNotFoundError(t *testing.T) {
tests := []struct {
name string
errMsg string
isCRDAbsent bool
}{
{
name: "resource not found indicates missing CRD",
errMsg: "the server could not find the requested resource",
isCRDAbsent: true,
},
{
name: "no matches for kind indicates missing CRD",
errMsg: "no matches for kind \"Site\" in version \"core.posit.team/v1beta1\"",
isCRDAbsent: true,
},
{
name: "connection refused is not a missing CRD",
errMsg: "connection refused",
isCRDAbsent: false,
},
{
name: "timeout is not a missing CRD",
errMsg: "context deadline exceeded",
isCRDAbsent: false,
},
{
name: "empty string is not a missing CRD",
errMsg: "",
isCRDAbsent: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := isCRDNotFoundError(tt.errMsg)
assert.Equal(t, tt.isCRDAbsent, result)
})
}
}