diff --git a/fleetshard/config/config.go b/fleetshard/config/config.go index e2987cfc1e..7df6d8126e 100644 --- a/fleetshard/config/config.go +++ b/fleetshard/config/config.go @@ -28,7 +28,7 @@ type Config struct { CreateAuthProvider bool `env:"CREATE_AUTH_PROVIDER" envDefault:"false"` MetricsAddress string `env:"FLEETSHARD_METRICS_ADDRESS" envDefault:":8080"` EgressProxyImage string `env:"EGRESS_PROXY_IMAGE"` - BaseCrdURL string `env:"BASE_CRD_URL" envDefault:"https://raw.githubusercontent.com/stackrox/stackrox/%s/operator/bundle/manifests/"` + DefaultBaseCRDURL string `env:"DEFAULT_BASE_CRD_URL" envDefault:"https://raw.githubusercontent.com/stackrox/stackrox/%s/operator/bundle/manifests/"` ManagedDB ManagedDB Telemetry Telemetry diff --git a/fleetshard/pkg/central/charts/charts.go b/fleetshard/pkg/central/charts/charts.go index 8f90910ed0..7bfa01b74b 100644 --- a/fleetshard/pkg/central/charts/charts.go +++ b/fleetshard/pkg/central/charts/charts.go @@ -5,6 +5,7 @@ import ( "context" "embed" "fmt" + "github.com/stackrox/rox/pkg/httputil" "io" "io/fs" "net/http" @@ -74,6 +75,10 @@ func downloadTemplate(url string) (*loader.BufferedFile, error) { } defer resp.Body.Close() + if !httputil.Is2xxStatusCode(resp.StatusCode) { + return nil, fmt.Errorf("received bad status code: %d", resp.StatusCode) + } + bytes, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("cannot read bytes: %w", err) diff --git a/fleetshard/pkg/central/operator/config.go b/fleetshard/pkg/central/operator/config.go new file mode 100644 index 0000000000..15bc8c488b --- /dev/null +++ b/fleetshard/pkg/central/operator/config.go @@ -0,0 +1,103 @@ +package operator + +import ( + "fmt" + "github.com/golang/glog" + "github.com/stackrox/acs-fleet-manager/internal/dinosaur/pkg/api/private" + "sigs.k8s.io/yaml" +) + +func parseConfig(content []byte) (OperatorConfigs, error) { + var out OperatorConfigs + err := yaml.Unmarshal(content, &out) + if err != nil { + return OperatorConfigs{}, fmt.Errorf("unmarshalling operator config %w", err) + } + return out, nil +} + +// GetConfig returns the rhacs operator configurations +func GetConfig() OperatorConfigs { + // TODO: Read config from GitOps configuration + glog.Error("Reading RHACS Operator GitOps configuration not implemented yet") + return OperatorConfigs{} +} + +// Validate validates the operator configuration and can be used in different life-cycle stages like runtime and deploy time. +func Validate(configs OperatorConfigs) []error { + var errors []error + manager := ACSOperatorManager{} + manifests, err := manager.RenderChart(configs) + if err != nil { + errors = append(errors, fmt.Errorf("could not render operator helm charts, got invalid configuration: %s", err.Error())) + } else if len(manifests) == 0 { + errors = append(errors, fmt.Errorf("operator chart rendering succeed, but no manifests rendered")) + } + + return errors +} + +// CRDConfig represents the crd to be installed in the data-plane cluster. The CRD is downloaded automatically +// from the base URL. It takes a GitRef to resolve a GitHub link to the CRD definition. +type CRDConfig struct { + BaseURL string `json:"baseURL,omitempty"` + GitRef string `json:"gitRef"` +} + +// OperatorConfigs represents all operators and the CRD which should be installed in a data-plane cluster. +type OperatorConfigs struct { + CRD CRDConfig `json:"crd"` + Configs []OperatorConfig `json:"operators"` +} + +// OperatorConfig represents the configuration of an operator. +type OperatorConfig struct { + Image string `json:"image"` + GitRef string `json:"gitRef"` + HelmValues string `json:"helmValues,omitempty"` +} + +// ToAPIResponse transforms the config to an private API response. +func (o OperatorConfigs) ToAPIResponse() private.RhacsOperatorConfigs { + apiConfigs := private.RhacsOperatorConfigs{ + CRD: private.RhacsOperatorConfigsCrd{ + GitRef: o.CRD.GitRef, + BaseURL: o.CRD.BaseURL, + }, + } + + for _, config := range o.Configs { + apiConfigs.RHACSOperatorConfigs = append(apiConfigs.RHACSOperatorConfigs, config.ToAPIResponse()) + } + return apiConfigs +} + +// ToAPIResponse converts the internal OperatorConfig to the openapi generated private.RhacsOperatorConfig type. +func (o OperatorConfig) ToAPIResponse() private.RhacsOperatorConfig { + return private.RhacsOperatorConfig{ + Image: o.Image, + GitRef: o.GitRef, + HelmValues: o.HelmValues, + } +} + +// FromAPIResponse converts an openapi generated model to the internal OperatorConfigs type +func FromAPIResponse(config private.RhacsOperatorConfigs) OperatorConfigs { + var operatorConfigs []OperatorConfig + for _, apiConfig := range config.RHACSOperatorConfigs { + config := OperatorConfig{ + Image: apiConfig.Image, + GitRef: apiConfig.GitRef, + HelmValues: apiConfig.HelmValues, + } + operatorConfigs = append(operatorConfigs, config) + } + + return OperatorConfigs{ + Configs: operatorConfigs, + CRD: CRDConfig{ + GitRef: config.CRD.GitRef, + BaseURL: config.CRD.BaseURL, + }, + } +} diff --git a/fleetshard/pkg/central/operator/config_test.go b/fleetshard/pkg/central/operator/config_test.go new file mode 100644 index 0000000000..1fd49ed043 --- /dev/null +++ b/fleetshard/pkg/central/operator/config_test.go @@ -0,0 +1,40 @@ +package operator + +import ( + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "testing" +) + +func getExampleConfig() []byte { + return []byte(` +crd: + baseURL: https://raw.githubusercontent.com/stackrox/stackrox/{{ .GitRef }}/operator/bundle/manifests/ + gitRef: 4.1.1 +operators: +- gitRef: 4.1.1 + image: "quay.io/rhacs-eng/stackrox-operator:4.1.1" + helmValues: | + operator: + resources: + requests: + memory: 500Mi + cpu: 50m +`) +} + +func TestGetOperatorConfig(t *testing.T) { + conf, err := parseConfig(getExampleConfig()) + require.NoError(t, err) + assert.Len(t, conf.Configs, 1) + assert.Equal(t, "4.1.1", conf.Configs[0].GitRef) + assert.Equal(t, "quay.io/rhacs-eng/stackrox-operator:4.1.1", conf.Configs[0].Image) +} + +func TestValidate(t *testing.T) { + conf, err := parseConfig(getExampleConfig()) + require.NoError(t, err) + + errors := Validate(conf) + assert.Len(t, errors, 0) +} diff --git a/fleetshard/pkg/central/operator/upgrade.go b/fleetshard/pkg/central/operator/upgrade.go index 5c56e79ec1..41f605504d 100644 --- a/fleetshard/pkg/central/operator/upgrade.go +++ b/fleetshard/pkg/central/operator/upgrade.go @@ -7,30 +7,29 @@ import ( containerImage "github.com/containers/image/docker/reference" "github.com/stackrox/acs-fleet-manager/fleetshard/pkg/central/charts" "golang.org/x/exp/slices" + "gopkg.in/yaml.v2" "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chartutil" + "html/template" appsv1 "k8s.io/api/apps/v1" "k8s.io/apimachinery/pkg/api/errors" apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/util/validation" ctrlClient "sigs.k8s.io/controller-runtime/pkg/client" + "strings" ) const ( - operatorNamespace = "rhacs" - releaseName = "rhacs-operator" - operatorDeploymentPrefix = "rhacs-operator" + operatorNamespace = "rhacs" + releaseName = "rhacs-operator" + operatorDeploymentPrefix = "rhacs-operator" + defaultCRDBaseURLTemplate = "https://raw.githubusercontent.com/stackrox/stackrox/{{ .GitRef }}/operator/bundle/manifests/" ) -// DeploymentConfig represents operator configuration for deployment -type DeploymentConfig struct { - Image string - GitRef string -} - -func parseOperatorConfigs(operators []DeploymentConfig) ([]chartutil.Values, error) { +func parseOperatorConfigs(operators OperatorConfigs) ([]chartutil.Values, error) { var helmValues []chartutil.Values - for _, operator := range operators { + for _, operator := range operators.Configs { imageReference, err := containerImage.Parse(operator.Image) if err != nil { return nil, err @@ -51,6 +50,14 @@ func parseOperatorConfigs(operators []DeploymentConfig) ([]chartutil.Values, err "image": image, "labelSelector": operator.GitRef, } + + operatorHelmValues := make(map[string]interface{}) + err = yaml.Unmarshal([]byte(operator.HelmValues), operatorHelmValues) + if err != nil { + return nil, fmt.Errorf("Unmarshalling Helm values failed for operator %s: %w.", operator.GitRef, err) + } + + chartutil.CoalesceTables(operatorValues, operatorHelmValues) helmValues = append(helmValues, operatorValues) } return helmValues, nil @@ -59,19 +66,39 @@ func parseOperatorConfigs(operators []DeploymentConfig) ([]chartutil.Values, err // ACSOperatorManager keeps data necessary for managing ACS Operator type ACSOperatorManager struct { client ctrlClient.Client - crdURL string resourcesChart *chart.Chart } // InstallOrUpgrade provisions or upgrades an existing ACS Operator from helm chart template -func (u *ACSOperatorManager) InstallOrUpgrade(ctx context.Context, operators []DeploymentConfig, crdTag string) error { - if len(operators) == 0 { - return nil +func (u *ACSOperatorManager) InstallOrUpgrade(ctx context.Context, operators OperatorConfigs) error { + objs, err := u.RenderChart(operators) + if err != nil { + return err + } + + for _, obj := range objs { + if obj.GetNamespace() == "" { + obj.SetNamespace(operatorNamespace) + } + err := charts.InstallOrUpdateChart(ctx, obj, u.client) + if err != nil { + return fmt.Errorf("failed to update operator object %w", err) + } + } + + return nil + +} + +// RenderChart renders the operator helm chart manifests +func (u *ACSOperatorManager) RenderChart(operators OperatorConfigs) ([]*unstructured.Unstructured, error) { + if len(operators.Configs) == 0 { + return nil, nil } operatorImages, err := parseOperatorConfigs(operators) if err != nil { - return fmt.Errorf("failed to parse images: %w", err) + return nil, fmt.Errorf("failed to parse images: %w", err) } chartVals := chartutil.Values{ "operator": chartutil.Values{ @@ -80,28 +107,19 @@ func (u *ACSOperatorManager) InstallOrUpgrade(ctx context.Context, operators []D } var dynamicTemplatesUrls []string - if crdTag != "" { - dynamicTemplatesUrls = u.generateCRDTemplateUrls(crdTag) + if operators.CRD.GitRef != "" { + dynamicTemplatesUrls, err = u.generateCRDTemplateUrls(operators.CRD) + if err != nil { + return nil, err + } } u.resourcesChart = charts.MustGetChart("rhacs-operator", dynamicTemplatesUrls) objs, err := charts.RenderToObjects(releaseName, operatorNamespace, u.resourcesChart, chartVals) if err != nil { - return fmt.Errorf("failed rendering operator chart: %w", err) - } - - for _, obj := range objs { - if obj.GetNamespace() == "" { - obj.SetNamespace(operatorNamespace) - } - err := charts.InstallOrUpdateChart(ctx, obj, u.client) - if err != nil { - return fmt.Errorf("failed to update operator object %w", err) - } + return nil, fmt.Errorf("failed rendering operator chart: %w", err) } - - return nil - + return objs, nil } // ListVersionsWithReplicas returns currently running ACS Operator versions with number of ready replicas @@ -168,17 +186,36 @@ func generateDeploymentName(version string) string { return operatorDeploymentPrefix + "-" + version } -func (u *ACSOperatorManager) generateCRDTemplateUrls(tag string) []string { - stackroxWithTag := fmt.Sprintf(u.crdURL, tag) - centralCrdURL := stackroxWithTag + "platform.stackrox.io_centrals.yaml" - securedClusterCrdURL := stackroxWithTag + "platform.stackrox.io_securedclusters.yaml" - return []string{centralCrdURL, securedClusterCrdURL} +func (u *ACSOperatorManager) generateCRDTemplateUrls(crdConfig CRDConfig) ([]string, error) { + baseURL := defaultCRDBaseURLTemplate + if crdConfig.BaseURL != "" { + baseURL = crdConfig.BaseURL + } + + wr := new(strings.Builder) + crdURLTpl, err := template.New("crd_base_url").Parse(baseURL) + if err != nil { + return []string{}, fmt.Errorf("could not parse CRD base URL: %w", err) + } + + err = crdURLTpl.Execute(wr, struct { + GitRef string + }{ + GitRef: crdConfig.GitRef, + }) + if err != nil { + return []string{}, fmt.Errorf("could not parse CRD base URL: %w", err) + } + + //stackroxWithTag := fmt.Sprintf(baseURL, crdConfig.GitRef) + centralCrdURL := wr.String() + "platform.stackrox.io_centrals.yaml" + securedClusterCrdURL := wr.String() + "platform.stackrox.io_securedclusters.yaml" + return []string{centralCrdURL, securedClusterCrdURL}, nil } // NewACSOperatorManager creates a new ACS Operator Manager -func NewACSOperatorManager(k8sClient ctrlClient.Client, baseCrdURL string) *ACSOperatorManager { +func NewACSOperatorManager(k8sClient ctrlClient.Client) *ACSOperatorManager { return &ACSOperatorManager{ client: k8sClient, - crdURL: baseCrdURL, } } diff --git a/fleetshard/pkg/central/operator/upgrade_test.go b/fleetshard/pkg/central/operator/upgrade_test.go index 4f5d7ed442..08bf40169d 100644 --- a/fleetshard/pkg/central/operator/upgrade_test.go +++ b/fleetshard/pkg/central/operator/upgrade_test.go @@ -25,18 +25,23 @@ const ( operatorRepository = "quay.io/rhacs-eng/stackrox-operator" operatorImage1 = "quay.io/rhacs-eng/stackrox-operator:4.0.1" operatorImage2 = "quay.io/rhacs-eng/stackrox-operator:4.0.2" - crdTag1 = "4.0.1" crdURL = "https://raw.githubusercontent.com/stackrox/stackrox/%s/operator/bundle/manifests/" deploymentName1 = operatorDeploymentPrefix + "-4.0.1" deploymentName2 = operatorDeploymentPrefix + "-4.0.2" ) -var operatorConfig1 = DeploymentConfig{ +var crdTag1 = OperatorConfigs{ + CRD: CRDConfig{ + GitRef: "4.0.1", + }, +} + +var operatorConfig1 = OperatorConfig{ Image: operatorImage1, GitRef: "4.0.1", } -var operatorConfig2 = DeploymentConfig{ +var operatorConfig2 = OperatorConfig{ Image: operatorImage2, GitRef: "4.0.2", } @@ -104,11 +109,20 @@ func createOperatorDeployment(name string, image string) *appsv1.Deployment { } } +func getExampleOperatorConfigs(configs ...OperatorConfig) OperatorConfigs { + return OperatorConfigs{ + CRD: CRDConfig{ + GitRef: "4.0.1", + }, + Configs: configs, + } +} + func TestOperatorUpgradeFreshInstall(t *testing.T) { fakeClient := testutils.NewFakeClientBuilder(t).Build() - u := NewACSOperatorManager(fakeClient, crdURL) + u := NewACSOperatorManager(fakeClient) - err := u.InstallOrUpgrade(context.Background(), []DeploymentConfig{operatorConfig1}, crdTag1) + err := u.InstallOrUpgrade(context.Background(), getExampleOperatorConfigs(operatorConfig1)) require.NoError(t, err) // check Secured Cluster CRD exists and correct @@ -150,11 +164,9 @@ func TestOperatorUpgradeFreshInstall(t *testing.T) { func TestOperatorUpgradeMultipleVersions(t *testing.T) { fakeClient := testutils.NewFakeClientBuilder(t).Build() - u := NewACSOperatorManager(fakeClient, crdURL) - - operatorConfigs := []DeploymentConfig{operatorConfig1, operatorConfig2} + u := NewACSOperatorManager(fakeClient) - err := u.InstallOrUpgrade(context.Background(), operatorConfigs, crdTag1) + err := u.InstallOrUpgrade(context.Background(), getExampleOperatorConfigs(operatorConfig1, operatorConfig2)) require.NoError(t, err) err = fakeClient.Get(context.Background(), client.ObjectKey{Namespace: operatorNamespace, Name: operatorDeployment1.Name}, operatorDeployment1) @@ -170,14 +182,15 @@ func TestOperatorUpgradeMultipleVersions(t *testing.T) { func TestOperatorUpgradeDoNotInstallLongTagVersion(t *testing.T) { fakeClient := testutils.NewFakeClientBuilder(t).Build() - u := NewACSOperatorManager(fakeClient, crdURL) + u := NewACSOperatorManager(fakeClient) longVersionName := "4.0.1-with-ridiculously-long-version-name-like-really-long-one-which-has-more-than-63-characters" - operatorConfig := DeploymentConfig{ + operatorConfig := OperatorConfig{ Image: operatorImage1, GitRef: longVersionName, } - err := u.InstallOrUpgrade(context.Background(), []DeploymentConfig{operatorConfig}, crdTag1) + + err := u.InstallOrUpgrade(context.Background(), getExampleOperatorConfigs(operatorConfig)) require.Errorf(t, err, "zero tags parsed from images") deployments := &appsv1.DeploymentList{} @@ -188,14 +201,15 @@ func TestOperatorUpgradeDoNotInstallLongTagVersion(t *testing.T) { func TestOperatorUpgradeImageWithDigest(t *testing.T) { fakeClient := testutils.NewFakeClientBuilder(t).Build() - u := NewACSOperatorManager(fakeClient, crdURL) + u := NewACSOperatorManager(fakeClient) digestedImage := "quay.io/rhacs-eng/stackrox-operator:4.0.1@sha256:232a180dbcbcfa7250917507f3827d88a9ae89bb1cdd8fe3ac4db7b764ebb25a" - operatorConfig := DeploymentConfig{ + operatorConfig := OperatorConfig{ Image: digestedImage, GitRef: "4.0.1", } - err := u.InstallOrUpgrade(context.Background(), []DeploymentConfig{operatorConfig}, crdTag1) + + err := u.InstallOrUpgrade(context.Background(), getExampleOperatorConfigs(operatorConfig)) require.NoError(t, err) err = fakeClient.Get(context.Background(), client.ObjectKey{Namespace: operatorNamespace, Name: operatorDeployment1.Name}, operatorDeployment1) @@ -206,7 +220,7 @@ func TestOperatorUpgradeImageWithDigest(t *testing.T) { func TestRemoveUnusedEmpty(t *testing.T) { fakeClient := testutils.NewFakeClientBuilder(t).Build() - u := NewACSOperatorManager(fakeClient, crdURL) + u := NewACSOperatorManager(fakeClient) ctx := context.Background() err := u.RemoveUnusedOperators(ctx, []string{}) @@ -215,7 +229,7 @@ func TestRemoveUnusedEmpty(t *testing.T) { func TestRemoveOneUnusedOperator(t *testing.T) { fakeClient := testutils.NewFakeClientBuilder(t, operatorDeployment1, serviceAccount).Build() - u := NewACSOperatorManager(fakeClient, crdURL) + u := NewACSOperatorManager(fakeClient) ctx := context.Background() err := fakeClient.Get(context.Background(), client.ObjectKey{Namespace: operatorNamespace, Name: operatorDeployment1.Name}, operatorDeployment1) @@ -234,7 +248,7 @@ func TestRemoveOneUnusedOperator(t *testing.T) { func TestRemoveOneUnusedOperatorFromMany(t *testing.T) { fakeClient := testutils.NewFakeClientBuilder(t, operatorDeployment1, operatorDeployment2, serviceAccount).Build() - u := NewACSOperatorManager(fakeClient, crdURL) + u := NewACSOperatorManager(fakeClient) ctx := context.Background() err := u.RemoveUnusedOperators(ctx, []string{operatorImage2}) @@ -259,7 +273,7 @@ func TestRemoveOneUnusedOperatorFromMany(t *testing.T) { func TestRemoveMultipleUnusedOperators(t *testing.T) { fakeClient := testutils.NewFakeClientBuilder(t, operatorDeployment1, operatorDeployment2, serviceAccount).Build() - u := NewACSOperatorManager(fakeClient, crdURL) + u := NewACSOperatorManager(fakeClient) ctx := context.Background() err := u.RemoveUnusedOperators(ctx, []string{}) @@ -274,28 +288,28 @@ func TestRemoveMultipleUnusedOperators(t *testing.T) { func TestParseOperatorConfigs(t *testing.T) { cases := map[string]struct { - operatorConfigs []DeploymentConfig + operatorConfigs OperatorConfigs expected []map[string]string shouldFail bool }{ "should parse one valid operator image": { - operatorConfigs: []DeploymentConfig{operatorConfig1}, + operatorConfigs: getExampleOperatorConfigs(operatorConfig1), expected: []map[string]string{ {"deploymentName": deploymentName1, "image": operatorImage1, "labelSelector": "4.0.1"}, }, }, "should parse two valid operator images": { - operatorConfigs: []DeploymentConfig{operatorConfig1, operatorConfig2}, + operatorConfigs: getExampleOperatorConfigs(operatorConfig1, operatorConfig2), expected: []map[string]string{ {"deploymentName": deploymentName1, "image": operatorImage1, "labelSelector": "4.0.1"}, {"deploymentName": deploymentName2, "image": operatorImage2, "labelSelector": "4.0.2"}, }, }, "should parse image with tag and digest": { - operatorConfigs: []DeploymentConfig{{ + operatorConfigs: getExampleOperatorConfigs(OperatorConfig{ Image: "quay.io/image-with-tag-and-digest:1.2.3@sha256:4ff5cb2dcddaaa2a4b702516870c85177e53ccc3566509c36c2d84b01ef8f783", GitRef: "version1", - }}, + }), expected: []map[string]string{ { "deploymentName": operatorDeploymentPrefix + "-version1", @@ -305,45 +319,40 @@ func TestParseOperatorConfigs(t *testing.T) { }, }, "should parse image without tag": { - operatorConfigs: []DeploymentConfig{{ + operatorConfigs: getExampleOperatorConfigs(OperatorConfig{ Image: "quay.io/image-without-tag", GitRef: "version1", - }}, + }), expected: []map[string]string{ {"deploymentName": operatorDeploymentPrefix + "-version1", "image": "quay.io/image-without-tag", "labelSelector": "version1"}, }, }, "do not fail if images list is empty": { - operatorConfigs: []DeploymentConfig{}, + operatorConfigs: getExampleOperatorConfigs(), shouldFail: false, }, "should accept images from multiple repositories with the same tag": { - operatorConfigs: []DeploymentConfig{ - { - Image: "repo1:tag", - GitRef: "version1", - }, - { - Image: "repo2:tag", - GitRef: "version2", - }}, + operatorConfigs: getExampleOperatorConfigs( + OperatorConfig{Image: "repo1:tag", GitRef: "version1"}, + OperatorConfig{Image: "repo2:tag", GitRef: "version2"}, + ), expected: []map[string]string{ {"deploymentName": operatorDeploymentPrefix + "-version1", "image": "repo1:tag", "labelSelector": "version1"}, {"deploymentName": operatorDeploymentPrefix + "-version2", "image": "repo2:tag", "labelSelector": "version2"}, }, }, "fail if image contains more than one colon": { - operatorConfigs: []DeploymentConfig{{ + operatorConfigs: getExampleOperatorConfigs(OperatorConfig{ Image: "quay.io/image-name:1.2.3:", GitRef: "version1", - }}, + }), shouldFail: true, }, "fail if GitRef is way too long for the DeploymentName": { - operatorConfigs: []DeploymentConfig{{ + operatorConfigs: getExampleOperatorConfigs(OperatorConfig{ Image: "quay.io/image-name:tag", GitRef: "1.2.3-with-ridiculously-long-version-name-like-really-long-one-which-has-more-than-63-characters", - }}, + }), shouldFail: true, }, } diff --git a/fleetshard/pkg/runtime/runtime.go b/fleetshard/pkg/runtime/runtime.go index f705932206..1ee5b090a1 100644 --- a/fleetshard/pkg/runtime/runtime.go +++ b/fleetshard/pkg/runtime/runtime.go @@ -35,7 +35,7 @@ type reconcilerRegistry map[string]*centralReconciler.CentralReconciler var reconciledCentralCountCache int32 -var cachedOperatorConfigs []operator.DeploymentConfig +var cachedOperatorConfigs operator.OperatorConfigs var backoff = wait.Backoff{ Duration: 1 * time.Second, @@ -91,7 +91,7 @@ func NewRuntime(config *config.Config, k8sClient ctrlClient.Client) (*Runtime, e } } - operatorManager := operator.NewACSOperatorManager(k8sClient, config.BaseCrdURL) + operatorManager := operator.NewACSOperatorManager(k8sClient) return &Runtime{ config: config, @@ -249,36 +249,32 @@ func (r *Runtime) deleteStaleReconcilers(list *private.ManagedCentralList) { } func (r *Runtime) upgradeOperator(list private.ManagedCentralList) error { - var desiredOperatorConfigs []operator.DeploymentConfig + ctx := context.Background() var desiredOperatorImages []string - uniqueConfigs := make(map[string]bool) - for _, central := range list.Items { - if _, alreadyAdded := uniqueConfigs[central.Spec.OperatorImage]; !alreadyAdded { - uniqueConfigs[central.Spec.OperatorImage] = true - // TODO: read GitRef ManagedCentral list call - operatorConfiguration := operator.DeploymentConfig{ - Image: central.Spec.OperatorImage, - GitRef: "4.0.1", - } - desiredOperatorConfigs = append(desiredOperatorConfigs, operatorConfiguration) - desiredOperatorImages = append(desiredOperatorImages, central.Spec.OperatorImage) - } + for _, operatorDeployment := range list.RhacsOperators.RHACSOperatorConfigs { + glog.Infof("Installing Operator version: %s", operatorDeployment.GitRef) + desiredOperatorImages = append(desiredOperatorImages, operatorDeployment.Image) } - if reflect.DeepEqual(cachedOperatorConfigs, desiredOperatorConfigs) { - return nil + // TODO: Replace with list from API request + operators := operator.OperatorConfigs{ + Configs: []operator.OperatorConfig{{ + Image: "quay.io/rhacs-eng/stackrox-operator", + GitRef: "4.1.0", + }}, + CRD: operator.CRDConfig{ + GitRef: "4.1.0", + }, } - cachedOperatorConfigs = desiredOperatorConfigs - - ctx := context.Background() - for _, operatorDeployment := range desiredOperatorConfigs { - glog.Infof("Installing Operator version: %s", operatorDeployment.GitRef) + if reflect.DeepEqual(cachedOperatorConfigs, list.RhacsOperators.RHACSOperatorConfigs) { + return nil } + cachedOperatorConfigs = operators - //TODO(ROX-15080): Download CRD on operator upgrades to always install the latest CRD - crdTag := "4.0.1" - err := r.operatorManager.InstallOrUpgrade(ctx, desiredOperatorConfigs, crdTag) + // TODO: comment line in to use the API response for production usage after Fleet-Manager implementation is finished + // err = r.operatorManager.InstallOrUpgrade(ctx, operator.FromAPIResponse(list.RhacsOperators)) + err := r.operatorManager.InstallOrUpgrade(ctx, operators) if err != nil { return fmt.Errorf("ensuring initial operator installation failed: %w", err) } diff --git a/internal/dinosaur/compat/compat.go b/internal/dinosaur/compat/compat.go index 6e4ff8c251..6f49d74cf8 100644 --- a/internal/dinosaur/compat/compat.go +++ b/internal/dinosaur/compat/compat.go @@ -18,9 +18,6 @@ type GenericOpenAPIError = public.GenericOpenAPIError // PrivateError ... type PrivateError = private.Error -// WatchEvent ... -type WatchEvent = private.WatchEvent - // ErrorList ... type ErrorList = public.ErrorList diff --git a/internal/dinosaur/pkg/api/private/api/openapi.yaml b/internal/dinosaur/pkg/api/private/api/openapi.yaml index e578226928..83b8b77c3f 100644 --- a/internal/dinosaur/pkg/api/private/api/openapi.yaml +++ b/internal/dinosaur/pkg/api/private/api/openapi.yaml @@ -234,6 +234,15 @@ components: memory: 2500Mi db: host: dbhost.rhacs-psql-instance + RHACSOperatorConfigExample: + value: + gitRef: 4.1.1 + image: quay.io/rhacs-eng/stackrox-operator@sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08 + helmValues: | + operator: + resources: + limits: + cpu: 100m DataPlaneCentralStatusRequestExample: value: conditions: @@ -297,6 +306,24 @@ components: - $ref: '#/components/schemas/ListReference' - $ref: '#/components/schemas/ManagedCentralList_allOf' description: A list of ManagedCentral + RHACSOperatorConfigs: + properties: + CRD: + $ref: '#/components/schemas/RHACSOperatorConfigs_CRD' + RHACSOperatorConfigs: + items: + allOf: + - $ref: '#/components/schemas/RHACSOperatorConfig' + type: array + RHACSOperatorConfig: + description: RHACSOperatorConfig defines the configuration of an operator + properties: + gitRef: + type: string + image: + type: string + helmValues: + type: string DataPlaneClusterUpdateStatusRequest: description: Schema for the request to update a data plane cluster's status example: @@ -351,18 +378,6 @@ components: spec: $ref: '#/components/schemas/DataplaneClusterAgentConfig_spec' type: object - WatchEvent: - properties: - type: - type: string - error: - $ref: '#/components/schemas/Error' - object: - nullable: true - type: object - required: - - type - type: object Error: allOf: - $ref: '#/components/schemas/ObjectReference' @@ -510,6 +525,14 @@ components: allOf: - $ref: '#/components/schemas/ManagedCentral' type: array + rhacs_operators: + $ref: '#/components/schemas/RHACSOperatorConfigs' + RHACSOperatorConfigs_CRD: + properties: + baseURL: + type: string + gitRef: + type: string DataPlaneClusterUpdateStatusRequest_conditions: example: reason: reason diff --git a/internal/dinosaur/pkg/api/private/model_managed_central_list.go b/internal/dinosaur/pkg/api/private/model_managed_central_list.go index 7d961852c0..22c9ea3e67 100644 --- a/internal/dinosaur/pkg/api/private/model_managed_central_list.go +++ b/internal/dinosaur/pkg/api/private/model_managed_central_list.go @@ -12,6 +12,7 @@ package private // ManagedCentralList A list of ManagedCentral type ManagedCentralList struct { - Kind string `json:"kind"` - Items []ManagedCentral `json:"items"` + Kind string `json:"kind"` + Items []ManagedCentral `json:"items"` + RhacsOperators RhacsOperatorConfigs `json:"rhacs_operators,omitempty"` } diff --git a/internal/dinosaur/pkg/api/private/model_rhacs_operator_config.go b/internal/dinosaur/pkg/api/private/model_rhacs_operator_config.go new file mode 100644 index 0000000000..00dc8e24c8 --- /dev/null +++ b/internal/dinosaur/pkg/api/private/model_rhacs_operator_config.go @@ -0,0 +1,18 @@ +/* + * Red Hat Advanced Cluster Security Service Fleet Manager + * + * Red Hat Advanced Cluster Security (RHACS) Service Fleet Manager APIs that are used by internal services e.g fleetshard operators. + * + * API version: 1.4.0 + * Generated by: OpenAPI Generator (https://openapi-generator.tech) + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech). DO NOT EDIT. +package private + +// RhacsOperatorConfig RHACSOperatorConfig defines the configuration of an operator +type RhacsOperatorConfig struct { + GitRef string `json:"gitRef,omitempty"` + Image string `json:"image,omitempty"` + HelmValues string `json:"helmValues,omitempty"` +} diff --git a/internal/dinosaur/pkg/api/private/model_rhacs_operator_configs.go b/internal/dinosaur/pkg/api/private/model_rhacs_operator_configs.go new file mode 100644 index 0000000000..66adaed154 --- /dev/null +++ b/internal/dinosaur/pkg/api/private/model_rhacs_operator_configs.go @@ -0,0 +1,17 @@ +/* + * Red Hat Advanced Cluster Security Service Fleet Manager + * + * Red Hat Advanced Cluster Security (RHACS) Service Fleet Manager APIs that are used by internal services e.g fleetshard operators. + * + * API version: 1.4.0 + * Generated by: OpenAPI Generator (https://openapi-generator.tech) + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech). DO NOT EDIT. +package private + +// RhacsOperatorConfigs struct for RhacsOperatorConfigs +type RhacsOperatorConfigs struct { + CRD RhacsOperatorConfigsCrd `json:"CRD,omitempty"` + RHACSOperatorConfigs []RhacsOperatorConfig `json:"RHACSOperatorConfigs,omitempty"` +} diff --git a/internal/dinosaur/pkg/api/private/model_watch_event.go b/internal/dinosaur/pkg/api/private/model_rhacs_operator_configs_crd.go similarity index 64% rename from internal/dinosaur/pkg/api/private/model_watch_event.go rename to internal/dinosaur/pkg/api/private/model_rhacs_operator_configs_crd.go index dd1c6659de..f0740735c8 100644 --- a/internal/dinosaur/pkg/api/private/model_watch_event.go +++ b/internal/dinosaur/pkg/api/private/model_rhacs_operator_configs_crd.go @@ -10,9 +10,8 @@ // Code generated by OpenAPI Generator (https://openapi-generator.tech). DO NOT EDIT. package private -// WatchEvent struct for WatchEvent -type WatchEvent struct { - Type string `json:"type"` - Error Error `json:"error,omitempty"` - Object *map[string]interface{} `json:"object,omitempty"` +// RhacsOperatorConfigsCrd struct for RhacsOperatorConfigsCrd +type RhacsOperatorConfigsCrd struct { + BaseURL string `json:"baseURL,omitempty"` + GitRef string `json:"gitRef,omitempty"` } diff --git a/internal/dinosaur/pkg/handlers/data_plane_dinosaur.go b/internal/dinosaur/pkg/handlers/data_plane_dinosaur.go index 4e0c735fbc..26d73276c1 100644 --- a/internal/dinosaur/pkg/handlers/data_plane_dinosaur.go +++ b/internal/dinosaur/pkg/handlers/data_plane_dinosaur.go @@ -1,6 +1,8 @@ package handlers import ( + "github.com/stackrox/acs-fleet-manager/fleetshard/pkg/central/operator" + "github.com/stackrox/acs-fleet-manager/pkg/features" "net/http" "github.com/stackrox/acs-fleet-manager/internal/dinosaur/pkg/api/private" @@ -64,6 +66,11 @@ func (h *dataPlaneDinosaurHandler) GetAll(w http.ResponseWriter, r *http.Request Items: []private.ManagedCentral{}, } + // TODO: check that the correct GitOps configuration is added to the response + if features.TargetedOperatorUpgrades.Enabled() { + managedDinosaurList.RhacsOperators = operator.GetConfig().ToAPIResponse() + } + for i := range centralRequests { converted := h.presenter.PresentManagedCentral(centralRequests[i]) managedDinosaurList.Items = append(managedDinosaurList.Items, converted) diff --git a/openapi/fleet-manager-private.yaml b/openapi/fleet-manager-private.yaml index 5643be7f3f..0019e2f1cb 100644 --- a/openapi/fleet-manager-private.yaml +++ b/openapi/fleet-manager-private.yaml @@ -340,7 +340,34 @@ components: items: allOf: - $ref: "#/components/schemas/ManagedCentral" + rhacs_operators: + $ref: RHACSOperatorConfigs + RHACSOperatorConfigs: + properties: + CRD: + type: object + properties: + baseURL: + type: string + gitRef: + type: string + RHACSOperatorConfigs: + type: array + items: + allOf: + - $ref: '#/components/schemas/RHACSOperatorConfig' + + RHACSOperatorConfig: + description: >- + RHACSOperatorConfig defines the configuration of an operator + properties: + gitRef: + type: string + image: + type: string + helmValues: + type: string DataPlaneClusterUpdateStatusRequest: # TODO are there any fields that should be required? # TODO are there any fields that should be nullable? (this is, a pointer in the resulting generated Go code) @@ -420,20 +447,6 @@ components: tag: type: string - WatchEvent: - required: - - type - type: object - properties: - type: - type: string - error: - nullable: true - $ref: "fleet-manager.yaml#/components/schemas/Error" - object: - type: object - nullable: true - securitySchemes: Bearer: scheme: bearer @@ -492,6 +505,15 @@ components: memory: 2500Mi db: host: dbhost.rhacs-psql-instance + RHACSOperatorConfigExample: + value: + gitRef: 4.1.1 + image: quay.io/rhacs-eng/stackrox-operator@sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08 + helmValues: | + operator: + resources: + limits: + cpu: 100m DataPlaneCentralStatusRequestExample: value: conditions: diff --git a/pkg/handlers/framework.go b/pkg/handlers/framework.go index 8e3bcc6e4a..e8e3aff4a4 100644 --- a/pkg/handlers/framework.go +++ b/pkg/handlers/framework.go @@ -190,17 +190,12 @@ func HandleList(w http.ResponseWriter, r *http.Request, cfg *HandlerConfig) { result, err := stream.GetNextEvent() if err != nil { ulog := logger.NewUHCLogger(ctx) - operationID := logger.GetOperationID(ctx) // If this is a 400 error, its the user's issue, log as info rather than error if err.HTTPCode >= 400 && err.HTTPCode <= 499 { ulog.Infof(err.Error()) } else { ulog.Error(err) } - result := compat.WatchEvent{ - Type: "error", - Error: ConvertToPrivateError(err.AsOpenapiError(operationID, r.RequestURI)), - } _ = json.NewEncoder(w).Encode(result) return }