diff --git a/cmd/team-operator/main.go b/cmd/team-operator/main.go index 7e2940c..7002ae7 100644 --- a/cmd/team-operator/main.go +++ b/cmd/team-operator/main.go @@ -4,6 +4,7 @@ package main import ( + "context" "flag" "os" "strconv" @@ -11,6 +12,10 @@ import ( "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" @@ -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 @@ -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{ @@ -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 diff --git a/flightdeck/internal/kube.go b/flightdeck/internal/kube.go index 91e825d..5afbe06 100644 --- a/flightdeck/internal/kube.go +++ b/flightdeck/internal/kube.go @@ -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" @@ -109,6 +111,13 @@ 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 } @@ -116,3 +125,9 @@ func (c *siteClient) Get(name string, namespace string, opts metav1.GetOptions, 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") +} diff --git a/flightdeck/internal/kube_test.go b/flightdeck/internal/kube_test.go new file mode 100644 index 0000000..c25af9b --- /dev/null +++ b/flightdeck/internal/kube_test.go @@ -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) + }) + } +}