diff --git a/Makefile b/Makefile index 3d76e01..7f8f1e2 100644 --- a/Makefile +++ b/Makefile @@ -118,7 +118,7 @@ test: manifests generate fmt vet setup-envtest ## Run tests. # - CERT_MANAGER_INSTALL_SKIP=true .PHONY: test-e2e test-e2e: manifests generate fmt vet ## Run the e2e tests. Expected an isolated environment using Kind. - go test ./test/e2e/ -v -ginkgo.v + go test ./test/e2e/ -v -timeout 30m -ginkgo.v -ginkgo.flake-attempts=1 .PHONY: lint lint: golangci-lint ## Run golangci-lint linter diff --git a/charts/castai-castware-operator/templates/cleanup-job.yaml b/charts/castai-castware-operator/templates/cleanup-job.yaml index 0af39d9..d5ba56d 100644 --- a/charts/castai-castware-operator/templates/cleanup-job.yaml +++ b/charts/castai-castware-operator/templates/cleanup-job.yaml @@ -1,3 +1,66 @@ +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "castware-operator.fullname" . }}-cleanup + namespace: {{ .Release.Namespace }} + labels: + {{- include "castware-operator.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": pre-delete + "helm.sh/hook-weight": "-10" + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded,hook-failed +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "castware-operator.fullname" . }}-cleanup + labels: + {{- include "castware-operator.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": pre-delete + "helm.sh/hook-weight": "-10" + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded,hook-failed +rules: + - apiGroups: + - castware.cast.ai + resources: + - clusters + - components + verbs: + - get + - list + - update + - patch + - delete + - apiGroups: + - apiextensions.k8s.io + resources: + - customresourcedefinitions + verbs: + - delete + - get + - list +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "castware-operator.fullname" . }}-cleanup + labels: + {{- include "castware-operator.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": pre-delete + "helm.sh/hook-weight": "-10" + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded,hook-failed +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "castware-operator.fullname" . }}-cleanup +subjects: + - kind: ServiceAccount + name: {{ include "castware-operator.fullname" . }}-cleanup + namespace: {{ .Release.Namespace }} +--- apiVersion: batch/v1 kind: Job metadata: @@ -22,7 +85,7 @@ spec: {{- toYaml . | nindent 8 }} {{- end }} - serviceAccountName: {{ include "castware-operator.fullname" . }}-controller-manager + serviceAccountName: {{ include "castware-operator.fullname" . }}-cleanup restartPolicy: OnFailure securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }} diff --git a/charts/castai-castware-operator/templates/preflight-install-check.yaml b/charts/castai-castware-operator/templates/preflight-install-check.yaml new file mode 100644 index 0000000..b110519 --- /dev/null +++ b/charts/castai-castware-operator/templates/preflight-install-check.yaml @@ -0,0 +1,82 @@ +{{- if .Values.preflightInstallCheck.enabled }} +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ include "castware-operator.fullname" . }}-preflight-install-check + namespace: {{ .Release.Namespace }} + labels: + {{- include "castware-operator.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": pre-install + "helm.sh/hook-weight": "1" + "helm.sh/hook-delete-policy": before-hook-creation +spec: + backoffLimit: 0 + activeDeadlineSeconds: 300 + ttlSecondsAfterFinished: 300 + template: + metadata: + name: {{ include "castware-operator.fullname" . }}-preflight-install-check + labels: + {{- include "castware-operator.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + restartPolicy: Never + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + args: + - preflight-install-check + command: + - /manager + env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: OPERATOR_NAME + value: {{ include "castware-operator.fullname" . }} + - name: HELM_RELEASE_NAME + value: {{ .Release.Name }} + - name: LOG_LEVEL + value: "info" + - name: API_URL + value: {{ .Values.defaultCluster.api.apiUrl }} + - name: HELM_REPO_URL + value: {{ .Values.defaultCluster.helmRepoURL }} + - name: API_KEY + value: {{ .Values.apiKeySecret.apiKey }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + volumeMounts: + - mountPath: /.cache/helm + name: helm-cache + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + volumes: + - name: helm-cache + emptyDir: + sizeLimit: 500Mi +{{- end }} diff --git a/charts/castai-castware-operator/values.yaml b/charts/castai-castware-operator/values.yaml index 643a150..9bfc2eb 100644 --- a/charts/castai-castware-operator/values.yaml +++ b/charts/castai-castware-operator/values.yaml @@ -81,6 +81,9 @@ crdUpgrade: preflightCheck: enabled: true +preflightInstallCheck: + enabled: true + # Webhook configuration webhook: enabled: true diff --git a/cmd/main.go b/cmd/main.go index bdb0c7a..df1fa8c 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -3,21 +3,21 @@ package main import ( "os" - "github.com/castai/castware-operator/internal/castai" - "github.com/castai/castware-operator/internal/config" "github.com/spf13/cobra" - // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) - // to ensure that exec-entrypoint and run can make use of them. - _ "k8s.io/client-go/plugin/pkg/client/auth" - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" + + // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) + // to ensure that exec-entrypoint and run can make use of them. + _ "k8s.io/client-go/plugin/pkg/client/auth" ctrl "sigs.k8s.io/controller-runtime" castwarev1alpha1 "github.com/castai/castware-operator/api/v1alpha1" + "github.com/castai/castware-operator/internal/castai" + "github.com/castai/castware-operator/internal/config" // +kubebuilder:scaffold:imports ) @@ -70,7 +70,8 @@ func newRootCmd() *cobra.Command { rootCmd.Flags().StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+ "Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.") - rootCmd.Flags().StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") + rootCmd.Flags().StringVar(&probeAddr, "health-probe-bind-address", ":8081", + "The address the probe endpoint binds to.") rootCmd.Flags().BoolVar(&enableLeaderElection, "leader-elect", false, "Enable leader election for controller manager. "+ "Enabling this will ensure there is only one active controller manager.") @@ -80,7 +81,8 @@ func newRootCmd() *cobra.Command { "The directory that contains the metrics server certificate.") rootCmd.Flags().StringVar(&metricsCertName, "metrics-cert-name", "tls.crt", "The name of the metrics server certificate file.") - rootCmd.Flags().StringVar(&metricsCertKey, "metrics-cert-key", "tls.key", "The name of the metrics server key file.") + rootCmd.Flags().StringVar(&metricsCertKey, "metrics-cert-key", "tls.key", + "The name of the metrics server key file.") rootCmd.Flags().BoolVar(&enableHTTP2, "enable-http2", false, "If set, HTTP/2 will be enabled for the metrics and webhook servers") @@ -92,6 +94,7 @@ func main() { rootCmd.AddCommand(newUpgradeCmd()) rootCmd.AddCommand(newCleanupCmd()) rootCmd.AddCommand(newPreflightCheckCmd()) + rootCmd.AddCommand(newPreflightInstallCheckCmd()) version = config.CastwareOperatorVersion{ GitCommit: GitCommit, diff --git a/cmd/preflight.go b/cmd/preflight.go index 94ede11..6647f33 100644 --- a/cmd/preflight.go +++ b/cmd/preflight.go @@ -8,14 +8,18 @@ import ( "time" "github.com/bombsimon/logrusr/v4" - "github.com/castai/castware-operator/internal/config" - "github.com/castai/castware-operator/internal/helm" "github.com/sirupsen/logrus" "github.com/spf13/cobra" v1 "k8s.io/api/core/v1" controllerruntime "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/cluster" + + "github.com/castai/castware-operator/internal/castai" + "github.com/castai/castware-operator/internal/castai/auth" + components "github.com/castai/castware-operator/internal/component" + "github.com/castai/castware-operator/internal/config" + "github.com/castai/castware-operator/internal/helm" ) func newPreflightCheckCmd() *cobra.Command { @@ -30,34 +34,38 @@ func newPreflightCheckCmd() *cobra.Command { logrus.StandardLogger().Fatalf("failed to get config from environment: %v", err) } logrus.StandardLogger().SetLevel(cfg.LogLevel.Level()) - log := logrus.StandardLogger().WithField("gitCommit", version.GitCommit).WithField("version", version.Version) + log := logrus.StandardLogger().WithField("gitCommit", version.GitCommit).WithField( + "version", version.Version, + ) controllerruntime.SetLogger(logrusr.New(log)) restConfig := controllerruntime.GetConfigOrDie() - client, err := cluster.New(restConfig, func(options *cluster.Options) { - options.Scheme = scheme - options.Client.Cache = &client.CacheOptions{ - DisableFor: []client.Object{ - &v1.Secret{}, - }, - } - }) + runtimeClient, err := cluster.New( + restConfig, func(options *cluster.Options) { + options.Scheme = scheme + options.Client.Cache = &client.CacheOptions{ + DisableFor: []client.Object{ + &v1.Secret{}, + }, + } + }, + ) if err != nil { - return fmt.Errorf("failed to create cluster client: %w", err) + return fmt.Errorf("failed to create cluster runtimeClient: %w", err) } ctx, cancel := context.WithTimeout(controllerruntime.SetupSignalHandler(), time.Minute*5) defer cancel() go func() { - err = client.Start(ctx) + err = runtimeClient.Start(ctx) if err != nil { - log.WithError(err).Error("failed to start cluster client") + log.WithError(err).Error("failed to start cluster runtimeClient") cancel() } }() - cacheSynced := client.GetCache().WaitForCacheSync(ctx) + cacheSynced := runtimeClient.GetCache().WaitForCacheSync(ctx) if !cacheSynced { return errors.New("failed to sync cache") } @@ -65,10 +73,12 @@ func newPreflightCheckCmd() *cobra.Command { chartLoader := helm.NewChartLoader(log) helmClient := helm.NewClient(log, chartLoader, restConfig) - helmRelease, err := helmClient.GetRelease(helm.GetReleaseOptions{ - Namespace: cfg.PodNamespace, - ReleaseName: cfg.HelmReleaseName, - }) + helmRelease, err := helmClient.GetRelease( + helm.GetReleaseOptions{ + Namespace: cfg.PodNamespace, + ReleaseName: cfg.HelmReleaseName, + }, + ) if err != nil { return fmt.Errorf("failed to get helm release: %w", err) @@ -100,3 +110,259 @@ func newPreflightCheckCmd() *cobra.Command { return preflightCheckCmd } + +// validatePreflightInstallConfig validates the configuration for preflight install check. +func validatePreflightInstallConfig(cfg *config.Config) error { + // Validate namespace is castai-agent + if cfg.PodNamespace != "castai-agent" { + return fmt.Errorf(` +========================================== +PREFLIGHT CHECK FAILED +Operator must be installed in namespace 'castai-agent' +========================================== +Current namespace: %s + +Action: Install with --namespace castai-agent +==========================================`, cfg.PodNamespace) + } + + // Validate release name is castware-operator + if cfg.HelmReleaseName != "castware-operator" { + return fmt.Errorf(` +========================================== +PREFLIGHT CHECK FAILED +Operator must be installed with release name 'castware-operator' +========================================== +Current release name: %s + +Action: Install with release name castware-operator +==========================================`, cfg.HelmReleaseName) + } + + return nil +} + +func newPreflightInstallCheckCmd() *cobra.Command { + preflightInstallCheckCmd := &cobra.Command{ + Use: "preflight-install-check", + Short: "Checks that the operator can be installed", + Long: "Preflight install check command validates that the operator can be installed " + + "by checking HTTP connectivity and helm availability.", + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := config.GetFromEnvironment() + if err != nil { + logrus.StandardLogger().Fatalf("failed to get config from environment: %v", err) + } + logrus.StandardLogger().SetLevel(cfg.LogLevel.Level()) + log := logrus.StandardLogger().WithField("gitCommit", version.GitCommit).WithField( + "version", version.Version, + ) + controllerruntime.SetLogger(logrusr.New(log)) + + ctx, cancel := context.WithTimeout(controllerruntime.SetupSignalHandler(), time.Minute*5) + defer cancel() + + // Validate configuration + if err := validatePreflightInstallConfig(cfg); err != nil { + return err + } + + // Get API key from environment + apiKey := os.Getenv("API_KEY") + if apiKey == "" { + log.Error("API_KEY environment variable not set") + return errors.New("API_KEY environment variable not set. Please provide a valid API key to connect to CAST AI") + } + + // Get API URL from environment + apiURL := os.Getenv("API_URL") + if apiURL == "" { + log.Error("API_URL environment variable not set") + return errors.New("API_URL environment variable not set") + } + + // Get helm repo URL from environment + helmRepoURL := os.Getenv("HELM_REPO_URL") + if helmRepoURL == "" { + log.Error("HELM_REPO_URL environment variable not set") + return errors.New("HELM_REPO_URL environment variable not set") + } + + // Check 1: HTTP Client - call GetComponentByName for castware-operator + log.Info("Running preflight check 1: HTTP client connectivity") + if err := checkHTTPClient(ctx, log, cfg, apiURL, apiKey); err != nil { + return fmt.Errorf("preflight check failed: HTTP client connectivity: %w", err) + } + log.Info("HTTP client preflight check passed") + + // Check 2: Helm Availability - pull castai-agent chart + log.Info("Running preflight check 2: helm availability") + if err := checkHelmAvailability(ctx, log, helmRepoURL); err != nil { + return fmt.Errorf("preflight check failed: helm availability: %w", err) + } + log.Info("Helm availability preflight check passed") + + log.Info("All preflight install checks passed successfully") + return nil + }, + } + + return preflightInstallCheckCmd +} + +// checkHTTPClient validates that the operator can connect to the CAST AI API +// by attempting to call GetComponentByName for the castware-operator component. +func checkHTTPClient( + ctx context.Context, + log logrus.FieldLogger, + cfg *config.Config, + apiURL string, + apiKey string, +) error { + log.Info("Checking HTTP client connectivity to CAST AI API") + + // Create CAST AI client + authProvider := auth.NewStaticAuth(apiKey) + restClient := castai.NewRestyClient(cfg, apiURL, authProvider) + apiClient := castai.NewClient(log, cfg, restClient) + + // Attempt to get castware-operator component + log.Info("Attempting to call GetComponentByName for castware-operator") + component, err := apiClient.GetComponentByName(ctx, "castware-operator") + if err != nil { + return fmt.Errorf( + ` +========================================== +PREFLIGHT CHECK FAILED: Cannot connect to CAST AI API +========================================== +API URL: %s + +Possible causes: + 1. Invalid API key - verify your credentials + 2. Network issue - check firewall/proxy settings + 3. Wrong API URL - verify the endpoint + +Error details: %v +==========================================`, apiURL, err, + ) + } + + // Validate that we got a valid component response + if component == nil || component.Id == "" || component.Name == "" { + return errors.New( + ` +========================================== +PREFLIGHT CHECK FAILED: Invalid API response +========================================== +Received empty component data - API may be unavailable +==========================================`, + ) + } + + log.Infof( + "Successfully retrieved castware-operator component from CAST AI (ID: %s, Latest Version: %s)", + component.Id, + component.LatestVersion, + ) + return nil +} + +// checkHelmAvailability validates that helm can access the castai-agent chart +// from the configured helm repository by checking the helm index and pulling the chart. +func checkHelmAvailability(ctx context.Context, log logrus.FieldLogger, helmRepoURL string) error { + log.Infof("Checking helm availability by accessing helm index at %s", helmRepoURL) + + // Download and parse the helm repository index + r, err := helm.NewHelmRepo(helmRepoURL) + if err != nil { + return fmt.Errorf( + "unable to initialize helm repository %s: %w. "+ + "Please verify the helm repository URL is valid", + helmRepoURL, + err, + ) + } + + log.Info("Downloading helm repository index") + index, err := r.DownloadIndex() + if err != nil { + + return fmt.Errorf( + ` +========================================== +PREFLIGHT CHECK FAILED +Cannot access Helm repository +========================================== +Helm Repo URL: %s + +Possible causes: + • Network connectivity issue + • Incorrect helm repository URL + • Repository temporarily unavailable + +Action: Verify helm repository URL and network +==========================================`, helmRepoURL, + ) + } + + // Check if castai-agent chart exists in the index + log.Info("Checking if castai-agent chart exists in helm repository") + chartEntries, exists := index.Entries[components.ComponentNameAgent] + if !exists || len(chartEntries) == 0 { + return fmt.Errorf( + "castai-agent chart not found in helm repository %s. "+ + "Please verify the helm repository URL is correct", + helmRepoURL, + ) + } + + log.Infof( + "Found %d versions of castai-agent chart in helm repository", + len(chartEntries), + ) + + // Try to pull the latest version to verify the chart is actually accessible + latestVersion := chartEntries[0].Version + log.Infof("Attempting to pull castai-agent chart version %s", latestVersion) + + chartLoader := helm.NewChartLoader(log) + chartSource := &helm.ChartSource{ + RepoURL: helmRepoURL, + Name: components.ComponentNameAgent, + Version: latestVersion, + } + + chart, err := chartLoader.Load(ctx, chartSource) + if err != nil { + + return fmt.Errorf( + ` +========================================== +PREFLIGHT CHECK FAILED +Cannot download Helm chart +========================================== +Chart: castai-agent version %s +Repo: %s + +Action: Verify helm repository is accessible and chart exists +==========================================`, latestVersion, helmRepoURL, + ) + } + + // Validate that we got a valid chart + if chart == nil || chart.Metadata == nil || chart.Metadata.Name == "" { + return fmt.Errorf( + "received invalid chart data for castai-agent from %s. "+ + "Please verify the helm repository is functioning correctly", + helmRepoURL, + ) + } + + log.Infof( + "Successfully pulled and validated castai-agent chart version %s (chart name: %s)", + chart.Metadata.Version, + chart.Metadata.Name, + ) + return nil +} diff --git a/cmd/preflight_test.go b/cmd/preflight_test.go new file mode 100644 index 0000000..c512a2e --- /dev/null +++ b/cmd/preflight_test.go @@ -0,0 +1,470 @@ +package main + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "context" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + + "github.com/castai/castware-operator/internal/castai" + "github.com/castai/castware-operator/internal/config" +) + +const indexYamlPath = "/index.yaml" + +func TestCheckHTTPClient(t *testing.T) { + log := logrus.New() + log.SetLevel(logrus.ErrorLevel) // Reduce noise in tests + + cfg := &config.Config{ + RequestTimeout: 10 * time.Second, + } + + t.Run( + "success - component found", func(t *testing.T) { + // Create a test server that returns a valid component + server := httptest.NewServer( + http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/cluster-management/v1/components:getByName" && + r.URL.Query().Get("name") == "castware-operator" { + component := &castai.Component{ + Id: "test-id", + Name: "castware-operator", + LatestVersion: "1.0.0", + } + w.Header().Set("Content-Type", "application/json") + err := json.NewEncoder(w).Encode(component) + assert.NoError(t, err) + return + } + http.NotFound(w, r) + }, + ), + ) + defer server.Close() + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + err := checkHTTPClient(ctx, log, cfg, server.URL, "test-api-key") + assert.NoError(t, err) + }, + ) + + t.Run( + "failure - component not found (404)", func(t *testing.T) { + // Create a test server that returns 404 + server := httptest.NewServer( + http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusNotFound) + err := json.NewEncoder(w).Encode(map[string]string{"message": "component not found"}) + assert.NoError(t, err) + }, + ), + ) + defer server.Close() + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + err := checkHTTPClient(ctx, log, cfg, server.URL, "test-api-key") + assert.Error(t, err) + assert.Contains(t, err.Error(), "Cannot connect to CAST AI API") + }, + ) + + t.Run( + "failure - invalid component response (empty ID)", func(t *testing.T) { + // Create a test server that returns a component with empty ID + server := httptest.NewServer( + http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + component := &castai.Component{ + Id: "", // Empty ID + Name: "castware-operator", + LatestVersion: "1.0.0", + } + w.Header().Set("Content-Type", "application/json") + err := json.NewEncoder(w).Encode(component) + assert.NoError(t, err) + }, + ), + ) + defer server.Close() + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + err := checkHTTPClient(ctx, log, cfg, server.URL, "test-api-key") + assert.Error(t, err) + assert.Contains(t, err.Error(), "Invalid API response") + }, + ) + + t.Run( + "failure - unauthorized (401)", func(t *testing.T) { + // Create a test server that returns 401 + server := httptest.NewServer( + http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + err := json.NewEncoder(w).Encode(map[string]string{"message": "unauthorized"}) + assert.NoError(t, err) + }, + ), + ) + defer server.Close() + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + err := checkHTTPClient(ctx, log, cfg, server.URL, "invalid-api-key") + assert.Error(t, err) + assert.Contains(t, err.Error(), "Cannot connect to CAST AI API") + }, + ) + + t.Run( + "failure - server unreachable", func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + // Use an unreachable address + err := checkHTTPClient(ctx, log, cfg, "http://localhost:1", "test-api-key") + assert.Error(t, err) + assert.Contains(t, err.Error(), "Cannot connect to CAST AI API") + }, + ) + + t.Run( + "failure - context timeout", func(t *testing.T) { + // Create a test server that delays response + server := httptest.NewServer( + http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + time.Sleep(100 * time.Millisecond) // Just long enough to trigger timeout + w.WriteHeader(http.StatusOK) + }, + ), + ) + defer server.Close() + + // Use a very short timeout to force a timeout + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond) + defer cancel() + + cfg := &config.Config{ + RequestTimeout: 10 * time.Millisecond, + } + + err := checkHTTPClient(ctx, log, cfg, server.URL, "test-api-key") + assert.Error(t, err) + }, + ) +} + +func TestCheckHelmAvailability(t *testing.T) { + log := logrus.New() + log.SetLevel(logrus.ErrorLevel) // Reduce noise in tests + + t.Run( + "failure - chart not found in index", func(t *testing.T) { + // Create a test server that returns an index without castai-agent + server := httptest.NewServer( + http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == indexYamlPath { + // Return empty index + w.Header().Set("Content-Type", "application/x-yaml") + _, err := w.Write( + []byte(`apiVersion: v1 +entries: {} +generated: "2024-01-01T00:00:00Z" +`), + ) + assert.NoError(t, err) + return + } + http.NotFound(w, r) + }, + ), + ) + defer server.Close() + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + err := checkHelmAvailability(ctx, log, server.URL) + assert.Error(t, err) + assert.Contains(t, err.Error(), "castai-agent chart not found") + }, + ) + + t.Run( + "failure - helm repo unreachable", func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + // Use an unreachable address + err := checkHelmAvailability(ctx, log, "http://localhost:1") + assert.Error(t, err) + assert.Contains(t, err.Error(), "helm repository") + }, + ) + + t.Run( + "failure - invalid helm repo URL", func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + err := checkHelmAvailability(ctx, log, "not-a-valid-url") + assert.Error(t, err) + // Could be either initialization error or download error + assert.True( + t, strings.Contains(err.Error(), "helm repository"), + ) + }, + ) + + t.Run( + "success - chart pulled successfully", func(t *testing.T) { + // Create a test server that returns a valid index and chart archive + var serverURL string + server := httptest.NewServer( + http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == indexYamlPath { + // Return index with castai-agent + w.Header().Set("Content-Type", "application/x-yaml") + indexYAML := "apiVersion: v1\n" + + "entries:\n" + + " castai-agent:\n" + + " - name: castai-agent\n" + + " version: 1.0.0\n" + + " urls:\n" + + " - " + serverURL + "/castai-agent-1.0.0.tgz\n" + + "generated: \"2024-01-01T00:00:00Z\"\n" + _, err := w.Write([]byte(indexYAML)) + assert.NoError(t, err) + return + } + if r.URL.Path == "/castai-agent-1.0.0.tgz" { + // Return a valid helm chart archive + w.Header().Set("Content-Type", "application/gzip") + _, err := w.Write(createMinimalHelmChart(t, "castai-agent", "1.0.0")) + assert.NoError(t, err) + + return + } + http.NotFound(w, r) + }, + ), + ) + serverURL = server.URL + defer server.Close() + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + err := checkHelmAvailability(ctx, log, server.URL) + assert.NoError(t, err) + }, + ) + + t.Run( + "failure - chart archive not accessible", func(t *testing.T) { + // Create a test server that returns index but 404 for chart archive + var serverURL string + server := httptest.NewServer( + http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == indexYamlPath { + // Return index with castai-agent + w.Header().Set("Content-Type", "application/x-yaml") + indexYAML := "apiVersion: v1\n" + + "entries:\n" + + " castai-agent:\n" + + " - name: castai-agent\n" + + " version: 1.0.0\n" + + " urls:\n" + + " - " + serverURL + "/castai-agent-1.0.0.tgz\n" + + "generated: \"2024-01-01T00:00:00Z\"\n" + _, err := w.Write([]byte(indexYAML)) + assert.NoError(t, err) + + return + } + // Return 404 for chart archive + http.NotFound(w, r) + }, + ), + ) + serverURL = server.URL + defer server.Close() + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + err := checkHelmAvailability(ctx, log, server.URL) + assert.Error(t, err) + assert.Contains(t, err.Error(), "Cannot download Helm chart") + }, + ) + + t.Run( + "failure - empty chart entries", func(t *testing.T) { + // Create a test server that returns an index with castai-agent but no versions + server := httptest.NewServer( + http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == indexYamlPath { + // Return index with empty castai-agent entries + w.Header().Set("Content-Type", "application/x-yaml") + indexYAML := "apiVersion: v1\n" + + "entries:\n" + + " castai-agent: []\n" + + "generated: \"2024-01-01T00:00:00Z\"\n" + _, err := w.Write([]byte(indexYAML)) + assert.NoError(t, err) + + return + } + http.NotFound(w, r) + }, + ), + ) + defer server.Close() + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + err := checkHelmAvailability(ctx, log, server.URL) + assert.Error(t, err) + assert.Contains(t, err.Error(), "castai-agent chart not found") + }, + ) +} + +// createMinimalHelmChart creates a minimal valid helm chart tar.gz archive for testing. +func createMinimalHelmChart(t *testing.T, chartName, version string) []byte { + t.Helper() + + var buf bytes.Buffer + gzipWriter := gzip.NewWriter(&buf) + tarWriter := tar.NewWriter(gzipWriter) + + // Create Chart.yaml content + chartYAML := fmt.Sprintf( + `apiVersion: v2 +name: %s +version: %s +description: Test chart for preflight checks +type: application +`, chartName, version, + ) + + // Add Chart.yaml to tar + chartYAMLHeader := &tar.Header{ + Name: chartName + "/Chart.yaml", + Mode: 0644, + Size: int64(len(chartYAML)), + } + if err := tarWriter.WriteHeader(chartYAMLHeader); err != nil { + t.Fatalf("failed to write chart.yaml header: %v", err) + } + if _, err := tarWriter.Write([]byte(chartYAML)); err != nil { + t.Fatalf("failed to write chart.yaml content: %v", err) + } + + // Close tar and gzip writers + if err := tarWriter.Close(); err != nil { + t.Fatalf("failed to close tar writer: %v", err) + } + if err := gzipWriter.Close(); err != nil { + t.Fatalf("failed to close gzip writer: %v", err) + } + + return buf.Bytes() +} + +func TestPreflightInstallCheckCmd_ReleaseNameValidation(t *testing.T) { + t.Run( + "failure - invalid release name", func(t *testing.T) { + cfg := &config.Config{ + PodNamespace: "castai-agent", + HelmReleaseName: "invalid-release-name", + } + + // Test the validation logic directly + err := validatePreflightInstallConfig(cfg) + + assert.Error(t, err) + assert.Contains( + t, err.Error(), "Operator must be installed with release name 'castware-operator'", + ) + assert.Contains(t, err.Error(), "invalid-release-name") + }, + ) + + t.Run( + "failure - namespace check runs before release name check", func(t *testing.T) { + cfg := &config.Config{ + PodNamespace: "wrong-namespace", + HelmReleaseName: "invalid-release-name", + } + + // Test the validation logic directly + err := validatePreflightInstallConfig(cfg) + + assert.Error(t, err) + // Should fail on namespace check first + assert.Contains(t, err.Error(), "Operator must be installed in namespace 'castai-agent'") + assert.NotContains(t, err.Error(), "release name") + }, + ) + + t.Run( + "passes release name validation with correct name", func(t *testing.T) { + cfg := &config.Config{ + PodNamespace: "castai-agent", + HelmReleaseName: "castware-operator", + } + + // Test the validation logic directly + err := validatePreflightInstallConfig(cfg) + + assert.NoError(t, err) + }, + ) + + t.Run( + "failure - empty release name", func(t *testing.T) { + cfg := &config.Config{ + PodNamespace: "castai-agent", + HelmReleaseName: "", + } + + // Test the validation logic directly + err := validatePreflightInstallConfig(cfg) + + assert.Error(t, err) + assert.Contains( + t, err.Error(), "Operator must be installed with release name 'castware-operator'", + ) + }, + ) +} diff --git a/internal/castai/auth/auth.go b/internal/castai/auth/auth.go index c93a376..a821e7a 100644 --- a/internal/castai/auth/auth.go +++ b/internal/castai/auth/auth.go @@ -6,9 +6,10 @@ import ( "fmt" "sync" - castwarev1alpha1 "github.com/castai/castware-operator/api/v1alpha1" corev1 "k8s.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/client" + + castwarev1alpha1 "github.com/castai/castware-operator/api/v1alpha1" ) type Auth interface { @@ -82,3 +83,28 @@ func (a *auth) ApiKey() string { defer a.lock.RUnlock() return a.apiKey } + +// staticAuth is a simple implementation of Auth that returns a static API key +// without needing to load it from a Kubernetes secret. This is useful for +// preflight checks and other scenarios where the API key is provided directly. +type staticAuth struct { + apiKey string +} + +// NewStaticAuth creates a new Auth implementation with a static API key. +func NewStaticAuth(apiKey string) Auth { + return &staticAuth{apiKey: apiKey} +} + +func (s *staticAuth) LoadApiKey(_ context.Context, _ client.Reader) error { + // No-op for static auth as the key is already loaded + return nil +} + +func (s *staticAuth) GetApiKey(_ context.Context, _ client.Reader) (string, error) { + return s.apiKey, nil +} + +func (s *staticAuth) ApiKey() string { + return s.apiKey +} diff --git a/internal/helm/chart_loader.go b/internal/helm/chart_loader.go index 357494b..d6b19c7 100644 --- a/internal/helm/chart_loader.go +++ b/internal/helm/chart_loader.go @@ -151,3 +151,33 @@ func (cl *remoteChartLoader) chartURL(index *repo.IndexFile, name, version strin return "", ErrChartNotFound } + +// HelmRepo is a wrapper around helm's ChartRepository that provides +// a simpler interface for accessing helm repository indexes. +type HelmRepo struct { + repo *repo.ChartRepository +} + +// NewHelmRepo creates a new HelmRepo for the given repository URL. +func NewHelmRepo(repoURL string) (*HelmRepo, error) { + r, err := repo.NewChartRepository(&repo.Entry{URL: repoURL}, getter.All(&cli.EnvSettings{})) + if err != nil { + return nil, fmt.Errorf("initializing chart repo: %w", err) + } + return &HelmRepo{repo: r}, nil +} + +// DownloadIndex downloads and parses the helm repository index file. +func (hr *HelmRepo) DownloadIndex() (*repo.IndexFile, error) { + indexFilepath, err := hr.repo.DownloadIndexFile() + if err != nil { + return nil, fmt.Errorf("downloading index file: %w", err) + } + + index, err := repo.LoadIndexFile(indexFilepath) + if err != nil { + return nil, fmt.Errorf("loading index file: %w", err) + } + + return index, nil +} diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 1ff3fd5..4d72d71 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -11,11 +11,12 @@ import ( "strings" "time" - components "github.com/castai/castware-operator/internal/component" - "github.com/castai/castware-operator/test/utils" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/samber/lo" + + components "github.com/castai/castware-operator/internal/component" + "github.com/castai/castware-operator/test/utils" ) // namespace where the project is deployed in @@ -80,12 +81,314 @@ spec: GKE_REGION: e2e ` -var _ = Describe("Manager", Ordered, func() { +// Preflight Install Check tests - run BEFORE operator installation +var _ = Describe("Preflight Install Check", Ordered, Serial, func() { + var apiKey string + var apiURL = os.Getenv("API_URL") + if apiURL == "" { + apiURL = "https://api.dev-master.cast.ai" + } + var operatorChartPath string + + // Extract image repository and tag from projectImage (format: repository:tag) + imageParts := strings.Split(projectImage, ":") + Expect(imageParts).To(HaveLen(2), "invalid projectImage format") + + BeforeAll(func() { + apiKey = os.Getenv("API_KEY") + Expect(apiKey).NotTo(BeEmpty(), "API_KEY environment variable is not set") + + wd, _ := os.Getwd() + operatorChartPath = filepath.Join(wd, "charts", "castai-castware-operator") + }) + + AfterEach(func() { + By("cleaning up any helm releases in test namespace") + cmd := exec.Command("helm", "list", "-n", namespace, "--short") + output, _ := utils.Run(cmd) + releases := utils.GetNonEmptyLines(output) + for _, release := range releases { + cmd = exec.Command("helm", "uninstall", release, "-n", namespace) + _, _ = utils.Run(cmd) + } + // Wait for cleanup + time.Sleep(3 * time.Second) + + By("removing namespace if exists") + cmd = exec.Command("kubectl", "delete", "ns", namespace) + _, _ = utils.Run(cmd) + }) + + // Helper function to install operator with preflight check + installOperatorWithPreflight := func(releaseName, namespace, apiKeyValue, apiURLValue, helmRepoURL string) *exec.Cmd { + args := []string{ + "upgrade", "--install", releaseName, + "--namespace", namespace, + "--create-namespace", + "--set", fmt.Sprintf("image.repository=%s", imageParts[0]), + "--set", fmt.Sprintf("image.tag=%s", imageParts[1]), + "--set", "image.pullPolicy=IfNotPresent", + "--set", fmt.Sprintf("apiKeySecret.apiKey=%s", apiKeyValue), + "--set", fmt.Sprintf("defaultCluster.api.apiUrl=%s", apiURLValue), + "--set", "defaultCluster.provider=gke", + "--set", "defaultCluster.terraform=false", + "--set", "defaultComponents.enabled=false", + "--set", "preflightInstallCheck.enabled=true", + "--set", "webhook.env.GKE_CLUSTER_NAME=castware-operator-e2e", + "--set", "webhook.env.GKE_LOCATION=e2e", + "--set", "webhook.env.GKE_PROJECT_ID=e2e", + "--set", "webhook.env.GKE_REGION=e2e", + } + + if helmRepoURL != "" { + args = append(args, "--set", fmt.Sprintf("defaultCluster.helmRepoURL=%s", helmRepoURL)) + } + + args = append(args, "--atomic", "--timeout", "5m", operatorChartPath) + + fmt.Println("Running helm command: ", strings.Join(args, "")) + + return exec.Command("helm", args...) + } + + It("should fail preflight-install-check when namespace is different than castai-agent", func() { + By("attempting to install operator with invalid namespace") + invalidNamespace := "invalid-namespace" + cmd := installOperatorWithPreflight( + "castware-operator", + invalidNamespace, + apiKey, + apiURL, + "", + ) + + defer func() { + By("cleaning up test namespace") + cmd = exec.Command("kubectl", "delete", "ns", invalidNamespace) + _, _ = utils.Run(cmd) + }() + + _, err := utils.Run(cmd) + Expect(err).To(HaveOccurred(), "Installation should fail with invalid namespace") + Expect(err.Error()).To( + ContainSubstring("preflight-install-check"), + "Error should mention preflight-install-check", + ) + + By("verifying preflight-install-check job failed") + cmd = exec.Command( + "kubectl", "get", "job", + "-l", "app.kubernetes.io/name=castware-operator", + "-n", invalidNamespace, + "-o", "jsonpath={.items[*].status.conditions[?(@.type=='Failed')].status}", + ) + output, err := utils.Run(cmd) + Expect(err).NotTo(HaveOccurred()) + Expect(output).To(ContainSubstring("True"), "Preflight job should have failed") + + By("cleanup invalid namespace") + cmd = exec.Command("kubectl", "delete", "ns", invalidNamespace) + _, err = utils.Run(cmd) + Expect(err).NotTo(HaveOccurred()) + }) + + It("should fail preflight-install-check when API key is invalid", func() { + By("attempting to install operator with invalid API key") + cmd := installOperatorWithPreflight( + "castware-operator", + namespace, + "invalid-api-key-12345", + apiURL, + "", + ) + + _, err := utils.Run(cmd) + Expect(err).To(HaveOccurred(), "Installation should fail with invalid API key") + Expect(err.Error()).To( + ContainSubstring("preflight-install-check"), + "Error should mention preflight-install-check", + ) + + By("verifying preflight-install-check job failed") + cmd = exec.Command( + "kubectl", "get", "job", + "-l", "app.kubernetes.io/name=castware-operator", + "-n", namespace, + "-o", "jsonpath={.items[*].status.conditions[?(@.type=='Failed')].status}", + ) + output, err := utils.Run(cmd) + Expect(err).NotTo(HaveOccurred()) + Expect(output).To(ContainSubstring("True"), "Preflight job should have failed") + }) + + It("should fail preflight-install-check when API URL is unreachable", func() { + By("creating test namespace") + cmd := exec.Command("kubectl", "create", "ns", namespace) + _, err := utils.Run(cmd) + Expect(err).NotTo(HaveOccurred(), "Failed to create test namespace") + + By("attempting to install operator with unreachable API URL") + cmd = installOperatorWithPreflight( + "castware-operator", + namespace, + apiKey, + "http://localhost:1", + "", + ) + + _, err = utils.Run(cmd) + Expect(err).To(HaveOccurred(), "Installation should fail with unreachable API URL") + Expect(err.Error()).To( + ContainSubstring("preflight-install-check"), + "Error should mention preflight-install-check", + ) + + By("verifying preflight-install-check job failed") + cmd = exec.Command( + "kubectl", "get", "job", + "-l", "app.kubernetes.io/name=castware-operator", + "-n", namespace, + "-o", "jsonpath={.items[*].status.conditions[?(@.type=='Failed')].status}", + ) + output, err := utils.Run(cmd) + Expect(err).NotTo(HaveOccurred()) + Expect(output).To(ContainSubstring("True"), "Preflight job should have failed") + }) + + It("should fail preflight-install-check when helm repo URL is invalid", func() { + By("creating test namespace") + cmd := exec.Command("kubectl", "create", "ns", namespace) + _, err := utils.Run(cmd) + Expect(err).NotTo(HaveOccurred(), "Failed to create test namespace") + + By("attempting to install operator with invalid helm repo URL") + cmd = installOperatorWithPreflight( + "castware-operator", + namespace, + apiKey, + apiURL, + "http://localhost:1/invalid-helm-repo", + ) + + _, err = utils.Run(cmd) + Expect(err).To(HaveOccurred(), "Installation should fail with invalid helm repo URL") + Expect(err.Error()).To( + ContainSubstring("preflight-install-check"), + "Error should mention preflight-install-check", + ) + + By("verifying preflight-install-check job failed") + cmd = exec.Command( + "kubectl", "get", "job", + "-l", "app.kubernetes.io/name=castware-operator", + "-n", namespace, + "-o", "jsonpath={.items[*].status.conditions[?(@.type=='Failed')].status}", + ) + output, err := utils.Run(cmd) + Expect(err).NotTo(HaveOccurred()) + Expect(output).To(ContainSubstring("True"), "Preflight job should have failed") + }) + + It("should fail preflight-install-check when release name is different than castware-operator", func() { + By("creating test namespace") + cmd := exec.Command("kubectl", "create", "ns", namespace) + _, err := utils.Run(cmd) + Expect(err).NotTo(HaveOccurred(), "Failed to create test namespace") + + By("attempting to install operator with invalid release name") + invalidReleaseName := "invalid-release-name" + cmd = installOperatorWithPreflight( + invalidReleaseName, + namespace, + apiKey, + apiURL, + "", + ) + + _, err = utils.Run(cmd) + Expect(err).To(HaveOccurred(), "Installation should fail with invalid release name") + Expect(err.Error()).To( + ContainSubstring("preflight-install-check"), + "Error should mention preflight-install-check", + ) + + By("verifying preflight-install-check job failed") + cmd = exec.Command( + "kubectl", "get", "job", + "-l", "app.kubernetes.io/name=castware-operator", + "-n", namespace, + "-o", "jsonpath={.items[*].status.conditions[?(@.type=='Failed')].status}", + ) + output, err := utils.Run(cmd) + Expect(err).NotTo(HaveOccurred()) + Expect(output).To(ContainSubstring("True"), "Preflight job should have failed") + }) + + It("should pass preflight-install-check with valid configuration", func() { + //testReleaseName := "castware-operator-valid" + testReleaseName := "castware-operator" + By("creating test namespace") + cmd := exec.Command("kubectl", "create", "ns", namespace) + _, err := utils.Run(cmd) + Expect(err).NotTo(HaveOccurred(), "Failed to create test namespace") + + By("labeling the namespace to enforce the restricted security policy") + cmd = exec.Command("kubectl", "label", "--overwrite", "ns", namespace, + "pod-security.kubernetes.io/enforce=restricted") + _, err = utils.Run(cmd) + Expect(err).NotTo(HaveOccurred(), "Failed to label namespace with restricted policy") + + By("installing operator with valid configuration") + cmd = installOperatorWithPreflight( + testReleaseName, + namespace, + apiKey, + apiURL, + "", + ) + + _, err = utils.Run(cmd) + Expect(err).NotTo(HaveOccurred(), "Installation should succeed with valid configuration") + + By("verifying preflight-install-check job succeeded") + verifyPreflightSuccess := func(g Gomega) { + cmd := exec.Command( + "kubectl", "get", "job", testReleaseName+"-preflight-install-check", + "-n", namespace, + "-o", "jsonpath={.status.conditions[?(@.type=='Complete')].status}", + ) + + output, err := utils.Run(cmd) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(output).To(ContainSubstring("True"), "Preflight job should have succeeded") + } + Eventually(verifyPreflightSuccess).Should(Succeed()) + + By("verifying operator pod is running") + verifyOperatorRunning := func(g Gomega) { + cmd := exec.Command( + "kubectl", "get", "pods", + "-l", "app.kubernetes.io/instance="+testReleaseName, + "-n", namespace, + "-o", "jsonpath={.items[*].status.phase}", + ) + output, err := utils.Run(cmd) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(output).To(ContainSubstring("Running"), "Operator pod should be running") + } + Eventually(verifyOperatorRunning, 2*time.Minute).Should(Succeed()) + }) +}) + +var _ = Describe("Manager", Ordered, Serial, func() { var controllerPodName string var clusterID string var organizationID string var apiKey string - var apiURL = "https://api.dev-master.cast.ai" + var apiURL = os.Getenv("API_URL") + if apiURL == "" { + apiURL = "https://api.dev-master.cast.ai" + } var agentInstalled bool var spotHandlerInstalled bool var versionBeforeDowngrade string @@ -167,6 +470,7 @@ var _ = Describe("Manager", Ordered, func() { "--set", "defaultCluster.provider=gke", "--set", "defaultCluster.terraform=false", "--set", "defaultComponents.enabled=false", + "--set", "preflightInstallCheck.enabled=false", // disabled - there are separate tests for it "--set", "webhook.env.GKE_CLUSTER_NAME=castware-operator-e2e", "--set", "webhook.env.GKE_LOCATION=e2e", "--set", "webhook.env.GKE_PROJECT_ID=e2e", @@ -362,27 +666,27 @@ var _ = Describe("Manager", Ordered, func() { "--image=curlimages/curl:latest", "--overrides", fmt.Sprintf(`{ - "spec": { - "containers": [{ - "name": "curl", - "image": "curlimages/curl:latest", - "command": ["/bin/sh", "-c"], - "args": ["curl -v -k -H 'Authorization: Bearer %s' https://%s.%s.svc.cluster.local:8080/metrics"], - "securityContext": { - "allowPrivilegeEscalation": false, - "capabilities": { - "drop": ["ALL"] - }, - "runAsNonRoot": true, - "runAsUser": 1000, - "seccompProfile": { - "type": "RuntimeDefault" - } + "spec": { + "containers": [{ + "name": "curl", + "image": "curlimages/curl:latest", + "command": ["/bin/sh", "-c"], + "args": ["curl -v -k -H 'Authorization: Bearer %s' https://%s.%s.svc.cluster.local:8080/metrics"], + "securityContext": { + "allowPrivilegeEscalation": false, + "capabilities": { + "drop": ["ALL"] + }, + "runAsNonRoot": true, + "runAsUser": 1000, + "seccompProfile": { + "type": "RuntimeDefault" } - }], - "serviceAccount": "%s" - } - }`, token, metricsServiceName, namespace, serviceAccountName)) + } + }], + "serviceAccount": "%s" + } + }`, token, metricsServiceName, namespace, serviceAccountName)) _, err = utils.Run(cmd) Expect(err).NotTo(HaveOccurred(), "Failed to create curl-metrics pod") @@ -475,7 +779,8 @@ var _ = Describe("Manager", Ordered, func() { Eventually(verifyClusterID, 5*time.Minute).Should(Succeed()) By("verifying cluster name and location are also populated") - cmd = exec.Command("kubectl", "get", "cluster", clusterName, "-n", namespace, "-o", "jsonpath={.spec.cluster}") + cmd = exec.Command("kubectl", "get", "cluster", clusterName, "-n", namespace, "-o", + "jsonpath={.spec.cluster}") output, err := utils.Run(cmd) Expect(err).NotTo(HaveOccurred(), "Failed to get cluster metadata") Expect(output).To(ContainSubstring("clusterID"), "Cluster metadata should contain clusterID") @@ -489,7 +794,8 @@ var _ = Describe("Manager", Ordered, func() { It("should install castai-agent", func() { By("creating a component custom resource") - componentYAML := fmt.Sprintf(componentYaml, components.ComponentNameAgent, namespace, components.ComponentNameAgent) + componentYAML := fmt.Sprintf(componentYaml, components.ComponentNameAgent, namespace, + components.ComponentNameAgent) componentFile := filepath.Join("/tmp", fmt.Sprintf("%s-component.yaml", components.ComponentNameAgent)) err := os.WriteFile(componentFile, []byte(componentYAML), os.FileMode(0o644)) @@ -519,7 +825,8 @@ var _ = Describe("Manager", Ordered, func() { cmd := exec.Command("kubectl", "get", "pods", "-l", "app.kubernetes.io/name=castai-agent", "-n", namespace, - "-o", "jsonpath={range .items[*]}{.metadata.name}{'|'}{.status.conditions[?(@.type=='Ready')].status}{'\\n'}{end}") + "-o", + "jsonpath={range .items[*]}{.metadata.name}{'|'}{.status.conditions[?(@.type=='Ready')].status}{'\\n'}{end}") output, err := utils.Run(cmd) g.Expect(err).NotTo(HaveOccurred(), "Failed to get castai-agent pods") g.Expect(output).NotTo(BeEmpty(), "No castai-agent pods found") @@ -598,7 +905,8 @@ var _ = Describe("Manager", Ordered, func() { cmd := exec.Command("kubectl", "get", "pods", "-l", "app.kubernetes.io/name=castai-agent", "-n", namespace, - "-o", "jsonpath={range .items[*]}{.metadata.name}{'|'}{.status.conditions[?(@.type=='Ready')].status}{'\\n'}{end}") + "-o", + "jsonpath={range .items[*]}{.metadata.name}{'|'}{.status.conditions[?(@.type=='Ready')].status}{'\\n'}{end}") output, err := utils.Run(cmd) g.Expect(err).NotTo(HaveOccurred(), "Failed to get castai-agent pods") g.Expect(output).NotTo(BeEmpty(), "No castai-agent pods found") @@ -632,7 +940,8 @@ var _ = Describe("Manager", Ordered, func() { ) output, err = utils.Run(cmd) Expect(err).NotTo(HaveOccurred(), "Failed to get component status") - Expect(output).To(ContainSubstring(`"type":"Available"`), "Component should be in Available status after downgrade") + Expect(output).To(ContainSubstring(`"type":"Available"`), + "Component should be in Available status after downgrade") getClusterURL := fmt.Sprintf("%s/cluster-management/v1/organizations/%s/clusters/%s/components:view", apiURL, organizationID, clusterID) @@ -680,7 +989,8 @@ var _ = Describe("Manager", Ordered, func() { currentVersion, err := utils.Run(cmd) g.Expect(err).NotTo(HaveOccurred(), "Failed to get component version") g.Expect(currentVersion).NotTo(BeEmpty(), "Version should be set") - g.Expect(currentVersion).NotTo(Equal(versionBeforeUpgrade), "Version should have changed from previous version") + g.Expect(currentVersion).NotTo(Equal(versionBeforeUpgrade), + "Version should have changed from previous version") } Eventually(verifyUpgrade, 5*time.Minute).Should(Succeed()) @@ -698,7 +1008,8 @@ var _ = Describe("Manager", Ordered, func() { cmd := exec.Command("kubectl", "get", "pods", "-l", "app.kubernetes.io/name=castai-agent", "-n", namespace, - "-o", "jsonpath={range .items[*]}{.metadata.name}{'|'}{.status.conditions[?(@.type=='Ready')].status}{'\\n'}{end}") + "-o", + "jsonpath={range .items[*]}{.metadata.name}{'|'}{.status.conditions[?(@.type=='Ready')].status}{'\\n'}{end}") output, err := utils.Run(cmd) g.Expect(err).NotTo(HaveOccurred(), "Failed to get castai-agent pods") g.Expect(output).NotTo(BeEmpty(), "No castai-agent pods found") @@ -732,7 +1043,8 @@ var _ = Describe("Manager", Ordered, func() { ) output, err = utils.Run(cmd) Expect(err).NotTo(HaveOccurred(), "Failed to get component status") - Expect(output).To(ContainSubstring(`"type":"Available"`), "Component should be in Available status after upgrade") + Expect(output).To(ContainSubstring(`"type":"Available"`), + "Component should be in Available status after upgrade") getClusterURL := fmt.Sprintf("%s/cluster-management/v1/organizations/%s/clusters/%s/components:view", apiURL, organizationID, clusterID) @@ -755,7 +1067,8 @@ var _ = Describe("Manager", Ordered, func() { namespace, components.ComponentNameSpotHandler) componentYAML += " phase2Permissions: false" - componentFile := filepath.Join("/tmp", fmt.Sprintf("%s-component.yaml", components.ComponentNameSpotHandler)) + componentFile := filepath.Join("/tmp", + fmt.Sprintf("%s-component.yaml", components.ComponentNameSpotHandler)) err := os.WriteFile(componentFile, []byte(componentYAML), os.FileMode(0o644)) Expect(err).NotTo(HaveOccurred(), "Failed to write component manifest") @@ -783,7 +1096,8 @@ var _ = Describe("Manager", Ordered, func() { cmd := exec.Command("kubectl", "get", "daemonsets", "-l", "app.kubernetes.io/instance=castai-spot-handler", "-n", namespace, - "-o", "jsonpath={range .items[*]}{.metadata.name}{'|'}{.status.conditions[?(@.type=='Ready')].status}{'\\n'}{end}") + "-o", + "jsonpath={range .items[*]}{.metadata.name}{'|'}{.status.conditions[?(@.type=='Ready')].status}{'\\n'}{end}") output, err := utils.Run(cmd) g.Expect(err).NotTo(HaveOccurred(), "Failed to get spot-handler daemonset") g.Expect(output).NotTo(BeEmpty(), "No spot-handler daemonsets found") @@ -844,7 +1158,8 @@ var _ = Describe("Manager", Ordered, func() { cmd := exec.Command("kubectl", "get", "daemonsets", "-l", "helm.sh/chart=castai-spot-handler-"+downgradeVersion, "-n", namespace, - "-o", "jsonpath={range .items[*]}{.metadata.name}{'|'}{.status.conditions[?(@.type=='Ready')].status}{'\\n'}{end}") + "-o", + "jsonpath={range .items[*]}{.metadata.name}{'|'}{.status.conditions[?(@.type=='Ready')].status}{'\\n'}{end}") output, err := utils.Run(cmd) g.Expect(err).NotTo(HaveOccurred(), "Failed to get spot-handler daemonset") g.Expect(output).NotTo(BeEmpty(), "No spot-handler daemonsets found") @@ -859,7 +1174,8 @@ var _ = Describe("Manager", Ordered, func() { ) output, err := utils.Run(cmd) Expect(err).NotTo(HaveOccurred(), "Failed to get component status") - Expect(output).To(ContainSubstring(`"type":"Available"`), "Component should be in Available status after downgrade") + Expect(output).To(ContainSubstring(`"type":"Available"`), + "Component should be in Available status after downgrade") getClusterURL := fmt.Sprintf("%s/cluster-management/v1/organizations/%s/clusters/%s/components:view", apiURL, organizationID, clusterID) @@ -906,7 +1222,8 @@ var _ = Describe("Manager", Ordered, func() { currentVersion, err := utils.Run(cmd) g.Expect(err).NotTo(HaveOccurred(), "Failed to get component version") g.Expect(currentVersion).NotTo(BeEmpty(), "Version should be set") - g.Expect(currentVersion).NotTo(Equal(versionBeforeUpgrade), "Version should have changed from previous version") + g.Expect(currentVersion).NotTo(Equal(versionBeforeUpgrade), + "Version should have changed from previous version") } Eventually(verifyUpgrade, 5*time.Minute).Should(Succeed()) @@ -925,7 +1242,8 @@ var _ = Describe("Manager", Ordered, func() { cmd := exec.Command("kubectl", "get", "daemonsets", "-l", "app.kubernetes.io/instance=spot-handler", "-n", namespace, - "-o", "jsonpath={range .items[*]}{.metadata.name}{'|'}{.status.conditions[?(@.type=='Ready')].status}{'\\n'}{end}") + "-o", + "jsonpath={range .items[*]}{.metadata.name}{'|'}{.status.conditions[?(@.type=='Ready')].status}{'\\n'}{end}") output, err := utils.Run(cmd) g.Expect(err).NotTo(HaveOccurred(), "Failed to get spot-handler daemonset") g.Expect(output).NotTo(BeEmpty(), "No spot-handler daemonsets found") @@ -939,7 +1257,8 @@ var _ = Describe("Manager", Ordered, func() { ) output, err := utils.Run(cmd) Expect(err).NotTo(HaveOccurred(), "Failed to get component status") - Expect(output).To(ContainSubstring(`"type":"Available"`), "Component should be in Available status after upgrade") + Expect(output).To(ContainSubstring(`"type":"Available"`), + "Component should be in Available status after upgrade") getClusterURL := fmt.Sprintf("%s/cluster-management/v1/organizations/%s/clusters/%s/components:view", apiURL, organizationID, clusterID) @@ -970,11 +1289,21 @@ var _ = Describe("Manager", Ordered, func() { cmd := exec.Command("bash", "-c", scriptResp.Script) output, _ := utils.Run(cmd) + By("waiting for operator deployment to be ready") + waitForOperatorReady := func(g Gomega) { + cmd := exec.Command("kubectl", "rollout", "status", "deployment/castware-operator", + "-n", namespace, "--timeout=2m") + _, err := utils.Run(cmd) + g.Expect(err).NotTo(HaveOccurred(), "Operator deployment should be ready") + } + Eventually(waitForOperatorReady, 3*time.Minute).Should(Succeed()) // Phase2 script returns an error, but it's expected because it tries to // run "gcloud container clusters describe", but the cluster is not running in GKE. // Checking successful install of spot-handler and cluster-controller is enough for this test. - Expect(output).To(ContainSubstring("cluster-controller ready with version"), "Failed to install cluster-controller") - Expect(output).To(ContainSubstring("spot-handler ready with version "), "Phase2 spot handler install failed") + Expect(output).To(ContainSubstring("cluster-controller ready with version"), + "Failed to install cluster-controller") + Expect(output).To(ContainSubstring("spot-handler ready with version "), + "Phase2 spot handler install failed") // err = fetchFromAPI(getClusterURL, http.MethodGet, &componentsResp) }) @@ -994,12 +1323,14 @@ var _ = Describe("Manager", Ordered, func() { Eventually(verifyCRDsGone).Should(Succeed()) By("verifying that castai-agent still exists") - cmd = exec.Command("kubectl", "get", "deployment", "-l", "app.kubernetes.io/name=castai-agent", "-n", namespace) + cmd = exec.Command("kubectl", "get", "deployment", "-l", "app.kubernetes.io/name=castai-agent", "-n", + namespace) _, err = utils.Run(cmd) Expect(err).NotTo(HaveOccurred(), "castai-agent should still exist after operator uninstall") By("verifying that spot-handler still exists") - cmd = exec.Command("kubectl", "get", "daemonset", "-l", "app.kubernetes.io/instance=spot-handler", "-n", namespace) + cmd = exec.Command("kubectl", "get", "daemonset", "-l", "app.kubernetes.io/instance=spot-handler", "-n", + namespace) _, err = utils.Run(cmd) Expect(err).NotTo(HaveOccurred(), "spot-handler should still exist after operator uninstall") @@ -1036,7 +1367,8 @@ var _ = Describe("Manager", Ordered, func() { cmd := exec.Command("bash", "-c", scriptResp) output, _ := utils.Run(cmd) Expect(output).To(ContainSubstring("deployment.apps/castai-agent created"), "Agent not installed") - Expect(output).To(ContainSubstring("daemonset.apps/castai-spot-handler created"), "Spot handler not installed") + Expect(output).To(ContainSubstring("daemonset.apps/castai-spot-handler created"), + "Spot handler not installed") By("patching castai-agent deployment to add GKE environment variables") cmd = exec.Command("kubectl", "patch", "deployment", "castai-agent", @@ -1048,7 +1380,8 @@ var _ = Describe("Manager", Ordered, func() { By("waiting for deployment to be updated") verifyDeploymentUpdated := func(g Gomega) { - cmd := exec.Command("kubectl", "rollout", "status", "deployment/castai-agent", "-n", namespace, "--timeout=60s") + cmd := exec.Command("kubectl", "rollout", "status", "deployment/castai-agent", "-n", namespace, + "--timeout=60s") _, err := utils.Run(cmd) g.Expect(err).NotTo(HaveOccurred(), "Deployment rollout failed") } @@ -1059,7 +1392,8 @@ var _ = Describe("Manager", Ordered, func() { cmd := exec.Command("kubectl", "get", "pods", "-l", "app.kubernetes.io/name=castai-agent", "-n", namespace, - "-o", "jsonpath={range .items[*]}{.metadata.name}{'|'}{.status.conditions[?(@.type=='Ready')].status}{'\\n'}{end}") + "-o", + "jsonpath={range .items[*]}{.metadata.name}{'|'}{.status.conditions[?(@.type=='Ready')].status}{'\\n'}{end}") output, err := utils.Run(cmd) g.Expect(err).NotTo(HaveOccurred(), "Failed to get castai-agent pods") g.Expect(output).NotTo(BeEmpty(), "No castai-agent pods found") @@ -1092,6 +1426,7 @@ var _ = Describe("Manager", Ordered, func() { "--set", "defaultCluster.terraform=false", "--set", "defaultCluster.migrationMode=autoUpgrade", "--set", "defaultComponents.enabled=false", + "--set", "preflightInstallCheck.enabled=false", // disabled - there are separate tests for it "--set", "webhook.env.GKE_CLUSTER_NAME=castware-operator-e2e", "--set", "webhook.env.GKE_LOCATION=e2e", "--set", "webhook.env.GKE_PROJECT_ID=e2e", @@ -1164,8 +1499,10 @@ var _ = Describe("Manager", Ordered, func() { // Phase2 script returns an error, but it's expected because it tries to // run "gcloud container clusters describe", but the cluster is not running in GKE. // Checking successful install of spot-handler and cluster-controller is enough for this test. - Expect(output).To(ContainSubstring("cluster-controller ready with version"), "Failed to install cluster-controller") - Expect(output).To(ContainSubstring("spot-handler ready with version "), "Phase2 spot handler install failed") + Expect(output).To(ContainSubstring("cluster-controller ready with version"), + "Failed to install cluster-controller") + Expect(output).To(ContainSubstring("spot-handler ready with version "), + "Phase2 spot handler install failed") By("verifying spot-handler component CR exists and is ready") verifySpotHandlerComponent := func(g Gomega) { @@ -1207,7 +1544,8 @@ var _ = Describe("Manager", Ordered, func() { ) output, err = utils.Run(cmd) Expect(err).NotTo(HaveOccurred(), "Failed to get cluster-controller component status") - Expect(output).To(ContainSubstring(`"type":"Available"`), "cluster-controller component should be Available") + Expect(output).To(ContainSubstring(`"type":"Available"`), + "cluster-controller component should be Available") By("verifying spot-handler has phase2Permissions=true from helm values") verifyPhase2Permissions := func(g Gomega) { @@ -1243,7 +1581,8 @@ var _ = Describe("Manager", Ordered, func() { _, _ = utils.Run(cmd) By("deleting any existing spot-handler components") - cmd = exec.Command("kubectl", "delete", "daemonset", "castai-spot-handler", "-n", namespace, "--ignore-not-found") + cmd = exec.Command("kubectl", "delete", "daemonset", "castai-spot-handler", "-n", namespace, + "--ignore-not-found") _, _ = utils.Run(cmd) By("deleting the namespace") @@ -1260,7 +1599,8 @@ var _ = Describe("Manager", Ordered, func() { cmd = exec.Command("bash", "-c", scriptResp) output, _ := utils.Run(cmd) Expect(output).To(ContainSubstring("deployment.apps/castai-agent created"), "Agent not installed") - Expect(output).To(ContainSubstring("daemonset.apps/castai-spot-handler created"), "Spot handler not installed") + Expect(output).To(ContainSubstring("daemonset.apps/castai-spot-handler created"), + "Spot handler not installed") By("patching castai-agent deployment to add GKE environment variables") cmd = exec.Command("kubectl", "patch", "deployment", "castai-agent", @@ -1272,7 +1612,8 @@ var _ = Describe("Manager", Ordered, func() { By("waiting for castai-agent deployment to be updated") verifyDeploymentUpdated := func(g Gomega) { - cmd := exec.Command("kubectl", "rollout", "status", "deployment/castai-agent", "-n", namespace, "--timeout=60s") + cmd := exec.Command("kubectl", "rollout", "status", "deployment/castai-agent", "-n", namespace, + "--timeout=60s") _, err := utils.Run(cmd) g.Expect(err).NotTo(HaveOccurred(), "Deployment rollout failed") } @@ -1283,7 +1624,8 @@ var _ = Describe("Manager", Ordered, func() { cmd := exec.Command("kubectl", "get", "pods", "-l", "app.kubernetes.io/name=castai-agent", "-n", namespace, - "-o", "jsonpath={range .items[*]}{.metadata.name}{'|'}{.status.conditions[?(@.type=='Ready')].status}{'\\n'}{end}") + "-o", + "jsonpath={range .items[*]}{.metadata.name}{'|'}{.status.conditions[?(@.type=='Ready')].status}{'\\n'}{end}") output, err := utils.Run(cmd) g.Expect(err).NotTo(HaveOccurred(), "Failed to get castai-agent pods") g.Expect(output).NotTo(BeEmpty(), "No castai-agent pods found") @@ -1342,7 +1684,8 @@ var _ = Describe("Manager", Ordered, func() { cmd := exec.Command("kubectl", "get", "daemonsets", "-l", "app.kubernetes.io/instance=castai-spot-handler", "-n", namespace, - "-o", "jsonpath={range .items[*]}{.metadata.name}{'|'}{.status.conditions[?(@.type=='Ready')].status}{'\\n'}{end}") + "-o", + "jsonpath={range .items[*]}{.metadata.name}{'|'}{.status.conditions[?(@.type=='Ready')].status}{'\\n'}{end}") output, err := utils.Run(cmd) g.Expect(err).NotTo(HaveOccurred(), "Failed to get spot-handler daemonset") g.Expect(output).NotTo(BeEmpty(), "No spot-handler daemonsets found") @@ -1375,6 +1718,7 @@ var _ = Describe("Manager", Ordered, func() { "--set", "defaultCluster.terraform=false", "--set", "defaultCluster.extendedPermissions=true", "--set", "defaultComponents.enabled=false", + "--set", "preflightInstallCheck.enabled=false", // disabled - there are separate tests for it "--set", "webhook.env.GKE_CLUSTER_NAME=castware-operator-e2e", "--set", "webhook.env.GKE_LOCATION=e2e", "--set", "webhook.env.GKE_PROJECT_ID=e2e", @@ -1446,12 +1790,14 @@ var _ = Describe("Manager", Ordered, func() { Eventually(verifyCRDsGone).Should(Succeed()) By("verifying that castai-agent still exists") - cmd = exec.Command("kubectl", "get", "deployment", "-l", "app.kubernetes.io/name=castai-agent", "-n", namespace) + cmd = exec.Command("kubectl", "get", "deployment", "-l", "app.kubernetes.io/name=castai-agent", "-n", + namespace) _, err = utils.Run(cmd) Expect(err).NotTo(HaveOccurred(), "castai-agent should still exist after operator uninstall") By("verifying that spot-handler still exists") - cmd = exec.Command("kubectl", "get", "daemonset", "-l", "app.kubernetes.io/instance=spot-handler", "-n", namespace) + cmd = exec.Command("kubectl", "get", "daemonset", "-l", "app.kubernetes.io/instance=spot-handler", "-n", + namespace) _, err = utils.Run(cmd) Expect(err).NotTo(HaveOccurred(), "spot-handler should still exist after operator uninstall") @@ -1492,7 +1838,8 @@ var _ = Describe("Manager", Ordered, func() { cmd := exec.Command("bash", "-c", scriptResp) output, _ := utils.Run(cmd) Expect(output).To(ContainSubstring("deployment.apps/castai-agent created"), "Agent not installed") - Expect(output).To(ContainSubstring("daemonset.apps/castai-spot-handler created"), "Spot handler not installed") + Expect(output).To(ContainSubstring("daemonset.apps/castai-spot-handler created"), + "Spot handler not installed") By("patching castai-agent deployment to add GKE environment variables") cmd = exec.Command("kubectl", "patch", "deployment", "castai-agent", @@ -1504,7 +1851,8 @@ var _ = Describe("Manager", Ordered, func() { By("waiting for deployment to be updated") verifyDeploymentUpdated := func(g Gomega) { - cmd := exec.Command("kubectl", "rollout", "status", "deployment/castai-agent", "-n", namespace, "--timeout=60s") + cmd := exec.Command("kubectl", "rollout", "status", "deployment/castai-agent", "-n", namespace, + "--timeout=60s") _, err := utils.Run(cmd) g.Expect(err).NotTo(HaveOccurred(), "Deployment rollout failed") } @@ -1515,7 +1863,8 @@ var _ = Describe("Manager", Ordered, func() { cmd := exec.Command("kubectl", "get", "pods", "-l", "app.kubernetes.io/name=castai-agent", "-n", namespace, - "-o", "jsonpath={range .items[*]}{.metadata.name}{'|'}{.status.conditions[?(@.type=='Ready')].status}{'\\n'}{end}") + "-o", + "jsonpath={range .items[*]}{.metadata.name}{'|'}{.status.conditions[?(@.type=='Ready')].status}{'\\n'}{end}") output, err := utils.Run(cmd) g.Expect(err).NotTo(HaveOccurred(), "Failed to get castai-agent pods") g.Expect(output).NotTo(BeEmpty(), "No castai-agent pods found") @@ -1573,6 +1922,7 @@ var _ = Describe("Manager", Ordered, func() { "--set", "defaultCluster.terraform=false", "--set", "defaultCluster.migrationMode=autoUpgrade", "--set", "defaultComponents.enabled=false", + "--set", "preflightInstallCheck.enabled=false", // disabled - there are separate tests for it "--set", "webhook.env.GKE_CLUSTER_NAME=castware-operator-e2e", "--set", "webhook.env.GKE_LOCATION=e2e", "--set", "webhook.env.GKE_PROJECT_ID=e2e", @@ -1646,7 +1996,8 @@ var _ = Describe("Manager", Ordered, func() { ) output, err = utils.Run(cmd) Expect(err).NotTo(HaveOccurred(), "Failed to get cluster-controller component status") - Expect(output).To(ContainSubstring(`"type":"Available"`), "cluster-controller component should be Available") + Expect(output).To(ContainSubstring(`"type":"Available"`), + "cluster-controller component should be Available") }) It("should not allow to disable extended permissions once they are enabled", func() { @@ -1675,8 +2026,8 @@ var _ = Describe("Manager", Ordered, func() { // and parsing the resulting token from the API response. func serviceAccountToken() (string, error) { const tokenRequestRawString = `{ - "apiVersion": "authentication.k8s.io/v1", - "kind": "TokenRequest" + "apiVersion": "authentication.k8s.io/v1", + "kind": "TokenRequest" }` // Temporary file to store the token request @@ -1747,7 +2098,8 @@ func deleteClusterRoleResourcesWithAnnotation() error { // Delete ClusterRoles with the annotation // nolint: lll cmd := exec.Command("kubectl", "get", "clusterroles", - "-o", "jsonpath={range .items[?(@.metadata.annotations.meta\\.helm\\.sh/release-namespace=='castai-agent')]}{.metadata.name}{'\\n'}{end}") + "-o", + "jsonpath={range .items[?(@.metadata.annotations.meta\\.helm\\.sh/release-namespace=='castai-agent')]}{.metadata.name}{'\\n'}{end}") output, err := utils.Run(cmd) if err != nil { return fmt.Errorf("failed to list ClusterRoles with annotation %s: %w", annotation, err) @@ -1766,7 +2118,8 @@ func deleteClusterRoleResourcesWithAnnotation() error { // Delete ClusterRoleBindings with the annotation // nolint: lll cmd = exec.Command("kubectl", "get", "clusterrolebindings", - "-o", "jsonpath={range .items[?(@.metadata.annotations.meta\\.helm\\.sh/release-namespace=='castai-agent')]}{.metadata.name}{'\\n'}{end}") + "-o", + "jsonpath={range .items[?(@.metadata.annotations.meta\\.helm\\.sh/release-namespace=='castai-agent')]}{.metadata.name}{'\\n'}{end}") output, err = utils.Run(cmd) if err != nil { return fmt.Errorf("failed to list ClusterRoleBindings with annotation %s: %w", annotation, err)