diff --git a/cmd/deploy.go b/cmd/deploy.go index f1b9e04..8dad765 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -449,6 +449,10 @@ func configureConfig(log *logger.Logger, components component.Component, deployS return fmt.Errorf("configuring operator configuration: %w", err) } + if deploySettings.Roxie.KonfluxImagesEnabled() { + deployer.PopulateKonfluxEnvVars(deploySettings) + } + if components.IncludesCentral() { if err := deploySettings.Central.ConfigureSpec(&deploySettings.Roxie); err != nil { return fmt.Errorf("configuring Central spec: %w", err) @@ -511,9 +515,6 @@ func deployValidate(components component.Component, deploySettings *deployer.Con if deploySettings.Operator.DeployViaOlmEnabled() { return errors.New("using Konflux images while deploying operator via OLM is not supported") } - if !clusterType.IsOpenShift() { - return fmt.Errorf("--konflux flag is only supported on OpenShift 4 clusters (current cluster type: %s)", clusterType) - } } return nil diff --git a/internal/deployer/konflux.go b/internal/deployer/konflux.go new file mode 100644 index 0000000..5c5eff2 --- /dev/null +++ b/internal/deployer/konflux.go @@ -0,0 +1,45 @@ +package deployer + +import ( + "fmt" + + "github.com/stackrox/roxie/internal/constants" +) + +var konfluxRelatedImages = map[string]string{ + "RELATED_IMAGE_MAIN": "main", + "RELATED_IMAGE_CENTRAL_DB": "central-db", + "RELATED_IMAGE_SCANNER": "scanner", + "RELATED_IMAGE_SCANNER_SLIM": "scanner-slim", + "RELATED_IMAGE_SCANNER_DB": "scanner-db", + "RELATED_IMAGE_SCANNER_DB_SLIM": "scanner-db-slim", + "RELATED_IMAGE_COLLECTOR": "collector", + "RELATED_IMAGE_SCANNER_V4_DB": "scanner-v4-db", + "RELATED_IMAGE_SCANNER_V4": "scanner-v4", + "RELATED_IMAGE_FACT": "fact", +} + +// KonfluxOperatorImage returns the Konflux-built operator image reference. +func KonfluxOperatorImage(config *Config) string { + return fmt.Sprintf("%s/release-operator:%s", constants.DefaultRegistry, config.Operator.Version) +} + +// PopulateKonfluxEnvVars populates config.Operator.EnvVars with RELATED_IMAGE_* +// entries for Konflux image rewriting. Explicitly-provided env vars (e.g. from +// --operator-env) take precedence and are not overwritten. +func PopulateKonfluxEnvVars(config *Config) { + if config.Operator.EnvVars == nil { + config.Operator.EnvVars = make(map[string]string) + } + for envName, imageSuffix := range konfluxRelatedImages { + if _, exists := config.Operator.EnvVars[envName]; exists { + continue + } + config.Operator.EnvVars[envName] = fmt.Sprintf( + "%s/release-%s:%s", + constants.DefaultRegistry, + imageSuffix, + config.Operator.Version, // Konflux built images use the "operator tag". + ) + } +} diff --git a/internal/deployer/konflux_test.go b/internal/deployer/konflux_test.go new file mode 100644 index 0000000..7da65f4 --- /dev/null +++ b/internal/deployer/konflux_test.go @@ -0,0 +1,73 @@ +package deployer + +import ( + "fmt" + "testing" + + "github.com/stackrox/roxie/internal/constants" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestKonfluxOperatorImage(t *testing.T) { + config := &Config{ + Operator: OperatorConfig{Version: "4.9.2"}, + } + expected := fmt.Sprintf("%s/release-operator:4.9.2", constants.DefaultRegistry) + assert.Equal(t, expected, KonfluxOperatorImage(config)) +} + +func TestPopulateKonfluxEnvVars_AllEntries(t *testing.T) { + config := &Config{ + Operator: OperatorConfig{Version: "4.9.2"}, + } + + PopulateKonfluxEnvVars(config) + + require.Len(t, config.Operator.EnvVars, len(konfluxRelatedImages)) + + for envName, imageSuffix := range konfluxRelatedImages { + expected := fmt.Sprintf("%s/release-%s:%s", constants.DefaultRegistry, imageSuffix, "4.9.2") + assert.Equal(t, expected, config.Operator.EnvVars[envName], "mismatch for %s", envName) + } +} + +func TestPopulateKonfluxEnvVars_UserOverridePreserved(t *testing.T) { + userValue := "quay.io/custom/my-main:latest" + config := &Config{ + Operator: OperatorConfig{ + Version: "4.9.2", + EnvVars: map[string]string{ + "RELATED_IMAGE_MAIN": userValue, + }, + }, + } + + PopulateKonfluxEnvVars(config) + + assert.Equal(t, userValue, config.Operator.EnvVars["RELATED_IMAGE_MAIN"], + "user override should be preserved") + + assert.Len(t, config.Operator.EnvVars, len(konfluxRelatedImages), + "all other entries should be populated") + + for envName, imageSuffix := range konfluxRelatedImages { + if envName == "RELATED_IMAGE_MAIN" { + continue + } + expected := fmt.Sprintf("%s/release-%s:%s", constants.DefaultRegistry, imageSuffix, "4.9.2") + assert.Equal(t, expected, config.Operator.EnvVars[envName], "mismatch for %s", envName) + } +} + +func TestPopulateKonfluxEnvVars_NilEnvVarsMap(t *testing.T) { + config := &Config{ + Operator: OperatorConfig{Version: "4.9.2"}, + } + assert.Nil(t, config.Operator.EnvVars) + + PopulateKonfluxEnvVars(config) + + require.NotNil(t, config.Operator.EnvVars) + assert.Len(t, config.Operator.EnvVars, len(konfluxRelatedImages)) +} diff --git a/internal/deployer/operator.go b/internal/deployer/operator.go index 7f0bba3..094fc5c 100644 --- a/internal/deployer/operator.go +++ b/internal/deployer/operator.go @@ -13,8 +13,8 @@ import ( "time" "gopkg.in/yaml.v3" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "github.com/stackrox/roxie/internal/constants" "github.com/stackrox/roxie/internal/k8s" "github.com/stackrox/roxie/internal/ocihelper" ) @@ -23,6 +23,7 @@ const ( adminPasswordSecretName = "admin-password" operatorNamespace = "rhacs-operator-system" operatorDeploymentName = "rhacs-operator-controller-manager" + managerContainerName = "manager" ) var requiredCRDs = []string{ @@ -34,15 +35,6 @@ var requiredCRDs = []string{ // deployOperatorNonOLM deploys the RHACS operator without OLM func (d *Deployer) deployOperatorNonOLM(ctx context.Context) error { d.logger.Infof("Operator tag: %s", d.config.Operator.Version) - if d.config.Roxie.KonfluxImagesEnabled() { - if err := d.ensureKonfluxImageRewriting(ctx); err != nil { - return fmt.Errorf("failed to configure Konflux image rewriting: %w", err) - } - } else { - if err := d.removeKonfluxImageRewriting(ctx); err != nil { - return fmt.Errorf("failed to remove Konflux ImageContentSourcePolicy: %v", err) - } - } bundleImage := OperatorBundleImage(d.config) bundleDir, err := d.downloadAndExtractOperatorBundle(ctx, bundleImage) @@ -189,111 +181,6 @@ func (d *Deployer) ensureCRDsInstalled(ctx context.Context) error { return nil } -// ensureKonfluxImageRewriting configures image rewriting for Konflux images -func (d *Deployer) ensureKonfluxImageRewriting(ctx context.Context) error { - if !d.config.Roxie.ClusterType.IsOpenShift() { - return errors.New("image rewriting for Konflux is only supported on OpenShift4 clusters") - } - - d.logger.Info("Configuring ImageContentSourcePolicy for Konflux images on OpenShift4...") - return d.applyImageContentSourcePolicy(ctx) -} - -// applyImageContentSourcePolicy creates the ImageContentSourcePolicy for Konflux image mirrors -func (d *Deployer) applyImageContentSourcePolicy(ctx context.Context) error { - // Define repository digest mirrors as Go data structures - rewrite := func(from, to string) map[string]interface{} { - source := fmt.Sprintf("registry.redhat.io/advanced-cluster-security/%s", from) - mirror := fmt.Sprintf("%s/%s", constants.DefaultRegistry, to) - if d.verbose { - d.logger.Dimf("Image rewriting rule: %s -> %s", source, mirror) - } - return map[string]interface{}{ - "source": source, - "mirrors": []string{mirror}, - } - } - repositoryDigestMirrors := []map[string]interface{}{ - rewrite("rhacs-operator-bundle", "release-operator-bundle"), - rewrite("rhacs-rhel8-operator", "release-operator"), - rewrite("rhacs-main-rhel8", "release-main"), - rewrite("rhacs-scanner-rhel8", "release-scanner"), - rewrite("rhacs-scanner-slim-rhel8", "release-scanner-slim"), - rewrite("rhacs-scanner-db-rhel8", "release-scanner-db"), - rewrite("rhacs-scanner-db-slim-rhel8", "release-scanner-db-slim"), - rewrite("rhacs-collector-slim-rhel8", "release-collector-slim"), - rewrite("rhacs-collector-rhel8", "release-collector"), - rewrite("rhacs-fact-rhel8", "release-fact"), - rewrite("rhacs-roxctl-rhel8", "release-roxctl"), - rewrite("rhacs-central-db-rhel8", "release-central-db"), - rewrite("rhacs-scanner-v4-db-rhel8", "release-scanner-v4-db"), - rewrite("rhacs-scanner-v4-rhel8", "release-scanner-v4"), - - // Also support downstream image rewriting for upcoming UBI9/rhel9 images. - rewrite("rhacs-operator-bundle-rhel9", "release-operator-bundle"), - rewrite("rhacs-rhel9-operator", "release-operator"), - rewrite("rhacs-main-rhel9", "release-main"), - rewrite("rhacs-scanner-rhel9", "release-scanner"), - rewrite("rhacs-scanner-slim-rhel9", "release-scanner-slim"), - rewrite("rhacs-scanner-db-rhel9", "release-scanner-db"), - rewrite("rhacs-scanner-db-slim-rhel9", "release-scanner-db-slim"), - rewrite("rhacs-collector-slim-rhel9", "release-collector-slim"), - rewrite("rhacs-collector-rhel9", "release-collector"), - rewrite("rhacs-fact-rhel9", "release-fact"), - rewrite("rhacs-roxctl-rhel9", "release-roxctl"), - rewrite("rhacs-central-db-rhel9", "release-central-db"), - rewrite("rhacs-scanner-v4-db-rhel9", "release-scanner-v4-db"), - rewrite("rhacs-scanner-v4-rhel9", "release-scanner-v4"), - } - - icsp := map[string]interface{}{ - "apiVersion": "operator.openshift.io/v1alpha1", - "kind": "ImageContentSourcePolicy", - "metadata": map[string]interface{}{ - "name": "acs-konflux-builds", - }, - "spec": map[string]interface{}{ - "repositoryDigestMirrors": repositoryDigestMirrors, - }, - } - - yamlData, err := yaml.Marshal(icsp) - if err != nil { - return fmt.Errorf("failed to marshal ImageContentSourcePolicy: %w", err) - } - - d.logger.Dim("Applying ImageContentSourcePolicy...") - _, err = d.runKubectl(ctx, k8s.KubectlOptions{ - Args: []string{"apply", "-f", "-"}, - Stdin: bytes.NewReader(yamlData), - }) - if err != nil { - return fmt.Errorf("failed to apply ImageContentSourcePolicy: %w", err) - } - - d.logger.Successf("✓ ImageContentSourcePolicy 'acs-konflux-builds' applied") - d.logger.Info("Note: OpenShift nodes may need to restart to apply the image mirroring configuration") - - return nil -} - -// removeKonfluxImageRewriting removes the ImageContentSourcePolicy for Konflux images if it exists -func (d *Deployer) removeKonfluxImageRewriting(ctx context.Context) error { - if !d.config.Roxie.ClusterType.IsOpenShift() { - return nil - } - - d.logger.Dim("Removing Konflux ImageContentSourcePolicy if present...") - _, err := d.runKubectl(ctx, k8s.KubectlOptions{ - Args: []string{"delete", "imagecontentsourcepolicy", "acs-konflux-builds", "--ignore-not-found=true"}, - }) - if err != nil { - return fmt.Errorf("failed to delete ImageContentSourcePolicy: %w", err) - } - - return nil -} - // deployOperatorFromCSV deploys the operator from CSV func (d *Deployer) deployOperatorFromCSV(ctx context.Context, bundleDir string) error { csvFile := filepath.Join(bundleDir, "rhacs-operator.clusterserviceversion.yaml") @@ -318,7 +205,7 @@ func (d *Deployer) deployOperatorFromCSV(ctx context.Context, bundleDir string) if len(d.config.Operator.EnvVars) > 0 { d.logger.Dimf(" • Custom operator env vars: %d", len(d.config.Operator.EnvVars)) for _, envVar := range envVarsToSortedList(d.config.Operator.EnvVars) { - ev := envVar.(map[string]interface{}) + ev := envVar.(map[string]any) d.logger.Dimf(" %s=%s", ev["name"], ev["value"]) } } @@ -355,26 +242,26 @@ func (d *Deployer) deployOperatorFromCSV(ctx context.Context, bundleDir string) } // parseCSVDeploymentSpec parses the CSV file -func (d *Deployer) parseCSVDeploymentSpec(csvFile string) (map[string]interface{}, error) { +func (d *Deployer) parseCSVDeploymentSpec(csvFile string) (map[string]any, error) { content, err := os.ReadFile(csvFile) if err != nil { return nil, fmt.Errorf("failed to read CSV file: %w", err) } - var csvContent map[string]interface{} + var csvContent map[string]any if err := yaml.Unmarshal(content, &csvContent); err != nil { return nil, fmt.Errorf("failed to parse CSV: %w", err) } - spec := csvContent["spec"].(map[string]interface{}) - installSpec := spec["install"].(map[string]interface{})["spec"].(map[string]interface{}) + spec := csvContent["spec"].(map[string]any) + installSpec := spec["install"].(map[string]any)["spec"].(map[string]any) - deployments := installSpec["deployments"].([]interface{}) - clusterPermissions := installSpec["clusterPermissions"].([]interface{}) + deployments := installSpec["deployments"].([]any) + clusterPermissions := installSpec["clusterPermissions"].([]any) - metadata := csvContent["metadata"].(map[string]interface{}) + metadata := csvContent["metadata"].(map[string]any) - deploymentSpec := map[string]interface{}{ + deploymentSpec := map[string]any{ "name": metadata["name"], "deployments": deployments, "cluster_permissions": clusterPermissions, @@ -382,7 +269,7 @@ func (d *Deployer) parseCSVDeploymentSpec(csvFile string) (map[string]interface{ } if len(clusterPermissions) > 0 { - firstPerm := clusterPermissions[0].(map[string]interface{}) + firstPerm := clusterPermissions[0].(map[string]any) if sa, ok := firstPerm["serviceAccountName"]; ok { deploymentSpec["service_account"] = sa } @@ -393,10 +280,10 @@ func (d *Deployer) parseCSVDeploymentSpec(csvFile string) (map[string]interface{ // createServiceAccount creates a service account func (d *Deployer) createServiceAccount(ctx context.Context, namespace, name string) error { - sa := map[string]interface{}{ + sa := map[string]any{ "apiVersion": "v1", "kind": "ServiceAccount", - "metadata": map[string]interface{}{ + "metadata": map[string]any{ "name": name, "namespace": namespace, "labels": map[string]string{"app": "rhacs-operator"}, @@ -425,20 +312,20 @@ func (d *Deployer) createServiceAccount(ctx context.Context, namespace, name str } // createClusterRoleFromCSV creates ClusterRole from CSV -func (d *Deployer) createClusterRoleFromCSV(ctx context.Context, deploymentSpec map[string]interface{}) error { - clusterPermissions := deploymentSpec["cluster_permissions"].([]interface{}) +func (d *Deployer) createClusterRoleFromCSV(ctx context.Context, deploymentSpec map[string]any) error { + clusterPermissions := deploymentSpec["cluster_permissions"].([]any) if len(clusterPermissions) == 0 { d.logger.Warning("No cluster permissions found in CSV") return nil } - firstPerm := clusterPermissions[0].(map[string]interface{}) + firstPerm := clusterPermissions[0].(map[string]any) rules := firstPerm["rules"] - clusterRole := map[string]interface{}{ + clusterRole := map[string]any{ "apiVersion": "rbac.authorization.k8s.io/v1", "kind": "ClusterRole", - "metadata": map[string]interface{}{ + "metadata": map[string]any{ "name": "rhacs-operator-manager-role", "labels": map[string]string{"app": "rhacs-operator"}, }, @@ -462,20 +349,20 @@ func (d *Deployer) createClusterRoleFromCSV(ctx context.Context, deploymentSpec // createClusterRoleBinding creates ClusterRoleBinding func (d *Deployer) createClusterRoleBinding(ctx context.Context, namespace, serviceAccountName string) error { - crb := map[string]interface{}{ + crb := map[string]any{ "apiVersion": "rbac.authorization.k8s.io/v1", "kind": "ClusterRoleBinding", - "metadata": map[string]interface{}{ + "metadata": map[string]any{ "name": "rhacs-operator-manager-rolebinding", "labels": map[string]string{"app": "rhacs-operator"}, }, - "roleRef": map[string]interface{}{ + "roleRef": map[string]any{ "apiGroup": "rbac.authorization.k8s.io", "kind": "ClusterRole", "name": "rhacs-operator-manager-role", }, - "subjects": []interface{}{ - map[string]interface{}{ + "subjects": []any{ + map[string]any{ "kind": "ServiceAccount", "name": serviceAccountName, "namespace": namespace, @@ -499,24 +386,24 @@ func (d *Deployer) createClusterRoleBinding(ctx context.Context, namespace, serv } // createDeploymentFromCSV creates Deployment from CSV -func (d *Deployer) createDeploymentFromCSV(ctx context.Context, namespace string, deploymentSpec map[string]interface{}) error { - deployments := deploymentSpec["deployments"].([]interface{}) +func (d *Deployer) createDeploymentFromCSV(ctx context.Context, namespace string, deploymentSpec map[string]any) error { + deployments := deploymentSpec["deployments"].([]any) if len(deployments) == 0 { return errors.New("no deployments found in CSV") } - csvDeployment := deployments[0].(map[string]interface{}) + csvDeployment := deployments[0].(map[string]any) deploymentName, _ := csvDeployment["name"].(string) if deploymentName == "" { deploymentName = operatorDeploymentName } - deploymentTemplate := csvDeployment["spec"].(map[string]interface{}) + deploymentTemplate := csvDeployment["spec"].(map[string]any) - deployment := map[string]interface{}{ + deployment := map[string]any{ "apiVersion": "apps/v1", "kind": "Deployment", - "metadata": map[string]interface{}{ + "metadata": map[string]any{ "name": deploymentName, "namespace": namespace, "labels": csvDeployment["label"], @@ -524,21 +411,30 @@ func (d *Deployer) createDeploymentFromCSV(ctx context.Context, namespace string "spec": deploymentTemplate, } - spec := deployment["spec"].(map[string]interface{}) - if template, ok := spec["template"].(map[string]interface{}); ok { - if podSpec, ok := template["spec"].(map[string]interface{}); ok { - podSpec["serviceAccountName"] = deploymentSpec["service_account"] + podSpecAny, found, err := unstructured.NestedFieldNoCopy(deployment, "spec", "template", "spec") + if err != nil { + return fmt.Errorf("extracting pod spec from operator deployment object: %w", err) + } + if !found { + return errors.New("missing pod spec in deployment object") + } + podSpec, ok := podSpecAny.(map[string]any) + if !ok { + return fmt.Errorf("pod spec in deployment object of invalid type %T", podSpecAny) + } - if len(d.config.Operator.EnvVars) > 0 { - containers, ok := podSpec["containers"].([]interface{}) - if !ok { - return errors.New("no containers found in deployment pod spec") - } - if err := d.injectEnvVarsIntoManagerContainer(containers); err != nil { - return fmt.Errorf("failed to inject operator env vars: %w", err) - } - } - } + managerContainer, err := managerContainerFromPodSpec(podSpec) + if err != nil { + return fmt.Errorf("extracting manager container from operator pod spec: %w", err) + } + + podSpec["serviceAccountName"] = deploymentSpec["service_account"] + if d.config.Roxie.KonfluxImagesEnabled() { + d.rewriteKonfluxOperatorImage(managerContainer) + } + + if len(d.config.Operator.EnvVars) > 0 { + d.injectEnvVarsIntoManagerContainer(managerContainer) } yamlData, err := yaml.Marshal(deployment) @@ -556,43 +452,55 @@ func (d *Deployer) createDeploymentFromCSV(ctx context.Context, namespace string return nil } -const managerContainerName = "manager" +func managerContainerFromPodSpec(podSpec map[string]any) (map[string]any, error) { + containers, ok := podSpec["containers"].([]any) + if !ok { + return nil, errors.New("no containers found in deployment pod spec") + } -// injectEnvVarsIntoManagerContainer merges configured operator env vars into -// the manager container, overriding any existing env vars with the same name. -func (d *Deployer) injectEnvVarsIntoManagerContainer(containers []interface{}) error { for _, c := range containers { - container, ok := c.(map[string]interface{}) + container, ok := c.(map[string]any) if !ok { continue } - if container["name"] != managerContainerName { - continue + if container["name"] == managerContainerName { + return container, nil } + } + return nil, fmt.Errorf("container %q missing from operator pod spec", managerContainerName) +} - existing := make(map[string]int) - envList, _ := container["env"].([]interface{}) - for i, item := range envList { - if envVar, ok := item.(map[string]interface{}); ok { - if name, ok := envVar["name"].(string); ok { - existing[name] = i - } +// injectEnvVarsIntoManagerContainer merges configured operator env vars into +// the manager container, overriding any existing env vars with the same name. +func (d *Deployer) injectEnvVarsIntoManagerContainer(container map[string]any) { + existing := make(map[string]int) + envList, _ := container["env"].([]any) + for i, item := range envList { + if envVar, ok := item.(map[string]any); ok { + if name, ok := envVar["name"].(string); ok { + existing[name] = i } } + } - for _, envVar := range envVarsToSortedList(d.config.Operator.EnvVars) { - name := envVar.(map[string]interface{})["name"].(string) - if idx, found := existing[name]; found { - envList[idx] = envVar - } else { - envList = append(envList, envVar) - } + for _, envVar := range envVarsToSortedList(d.config.Operator.EnvVars) { + name := envVar.(map[string]any)["name"].(string) + if idx, found := existing[name]; found { + envList[idx] = envVar + } else { + envList = append(envList, envVar) } - - container["env"] = envList - return nil } - return fmt.Errorf("container %q not found in deployment", managerContainerName) + + container["env"] = envList +} + +// rewriteKonfluxOperatorImage replaces the manager container's image with the +// Konflux-built operator image. +func (d *Deployer) rewriteKonfluxOperatorImage(container map[string]any) { + newImage := KonfluxOperatorImage(&d.config) + d.logger.Infof("Rewriting operator image to %s", newImage) + container["image"] = newImage } func (d *Deployer) applyBundleServiceResources(ctx context.Context, bundleDir, namespace string) error {