Skip to content

Commit

Permalink
ROX-21070: Implement the addon mgmt in fleet manager (#1522)
Browse files Browse the repository at this point in the history
  • Loading branch information
kovayur authored Jan 11, 2024
1 parent b53f738 commit 5c06671
Show file tree
Hide file tree
Showing 9 changed files with 1,193 additions and 309 deletions.
210 changes: 210 additions & 0 deletions internal/dinosaur/pkg/services/addon.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
package services

import (
"encoding/json"
"fmt"

"github.com/golang/glog"
"github.com/hashicorp/go-multierror"
clustersmgmtv1 "github.com/openshift-online/ocm-sdk-go/clustersmgmt/v1"
"github.com/stackrox/acs-fleet-manager/internal/dinosaur/pkg/api/dbapi"
"github.com/stackrox/acs-fleet-manager/internal/dinosaur/pkg/gitops"
"github.com/stackrox/acs-fleet-manager/pkg/api"
"github.com/stackrox/acs-fleet-manager/pkg/client/ocm"
"github.com/stackrox/acs-fleet-manager/pkg/features"
"github.com/stackrox/acs-fleet-manager/pkg/shared"
"golang.org/x/exp/maps"
)

// AddonProvisioner keeps addon installations on the data plane clusters up-to-date
type AddonProvisioner struct {
ocmClient ocm.Client
}

// NewAddonProvisioner creates a new instance of AddonProvisioner
func NewAddonProvisioner(ocmClient ocm.ClusterManagementClient) *AddonProvisioner {
return &AddonProvisioner{
ocmClient: ocmClient,
}
}

type updateDecision struct {
installedInOCM *clustersmgmtv1.AddOnInstallation
expectedConfig gitops.AddonConfig
ocmClient ocm.Client
multiErr *multierror.Error
}

// Provision installs, upgrades or uninstalls the addons based on a given config
func (p *AddonProvisioner) Provision(cluster api.Cluster, addons []gitops.AddonConfig) error {
var multiErr *multierror.Error
clusterID := cluster.ClusterID

updateDecisions := make(map[string]*updateDecision)
for _, addon := range addons {
addonInstallation, addonErr := p.ocmClient.GetAddonInstallation(clusterID, addon.ID)
if addonErr != nil {
if addonErr.Is404() {
// addon does not exist, install it
multiErr = multierror.Append(multiErr, p.installAddon(clusterID, addon))
} else {
multiErr = multierror.Append(multiErr, fmt.Errorf("failed to get addon %s: %w", addon.ID, addonErr))
}
} else {
updateDecisions[addonInstallation.ID()] = &updateDecision{
installedInOCM: addonInstallation,
expectedConfig: addon,
ocmClient: p.ocmClient,
multiErr: multiErr,
}
}
}
installedAddons, err := p.getInstalledAddons(cluster)
if err != nil {
multiErr = multierror.Append(multiErr, err)
}
for _, installedAddon := range installedAddons {
decision, exists := updateDecisions[installedAddon.ID]
if !exists {
// addon is installed on the cluster but not present in gitops config - uninstall it
multiErr = multierror.Append(multiErr, p.uninstallAddon(clusterID, installedAddon.ID))
} else {
if decision.updateInProgress() {
glog.V(10).Infof("Addon %s is not in a final state: %s, skip until the next worker iteration",
decision.installedInOCM.ID(), decision.installedInOCM.State())
} else if decision.needsUpdate(installedAddon) {
multiErr = multierror.Append(multiErr, p.updateAddon(clusterID, decision.expectedConfig))
} else {
glog.V(10).Infof("Addon %s is already up-to-date", installedAddon.ID)
multiErr = validateUpToDateAddon(multiErr, decision.installedInOCM, installedAddon)
}
}
}
return errorOrNil(multiErr)
}

func validateUpToDateAddon(multiErr *multierror.Error, ocmInstallation *clustersmgmtv1.AddOnInstallation, dataPlaneInstallation dbapi.AddonInstallation) *multierror.Error {
if ocmInstallation.State() == clustersmgmtv1.AddOnInstallationStateFailed {
// addon is already up-to-date with gitops config and still failed
multiErr = multierror.Append(multiErr, fmt.Errorf("addon %s is in a failed state", ocmInstallation.ID()))
}
if ocmInstallation.AddonVersion().ID() != dataPlaneInstallation.Version {
multiErr = multierror.Append(multiErr, fmt.Errorf("addon %s version mismatch: ocm - %s, data plane - %s",
ocmInstallation.ID(), ocmInstallation.AddonVersion().ID(), dataPlaneInstallation.Version))
}
if ocmSHA256Sum := convertParametersFromOCMAPI(ocmInstallation.Parameters()).SHA256Sum(); ocmSHA256Sum != dataPlaneInstallation.ParametersSHA256Sum {
multiErr = multierror.Append(multiErr, fmt.Errorf("addon %s parameters mismatch: ocm sha256sum - %s, data plane sha256sum - %s",
ocmInstallation.ID(), ocmSHA256Sum, dataPlaneInstallation.ParametersSHA256Sum))
}
return multiErr
}

func (p *AddonProvisioner) getInstalledAddons(cluster api.Cluster) ([]dbapi.AddonInstallation, error) {
if !features.AddonAutoUpgrade.Enabled() {
glog.V(10).Info("Addon auto upgrade feature is disabled, the existing addon installations will NOT be updated")
return []dbapi.AddonInstallation{}, nil
}
if len(cluster.Addons) == 0 {
glog.V(10).Info("No addons installed on the cluster, skipping")
return []dbapi.AddonInstallation{}, nil
}
var installedAddons []dbapi.AddonInstallation
if err := json.Unmarshal(cluster.Addons, &installedAddons); err != nil {
return []dbapi.AddonInstallation{}, fmt.Errorf("unmarshal installed addons: %w", err)
}
return installedAddons, nil
}

func errorOrNil(multiErr *multierror.Error) error {
if err := multiErr.ErrorOrNil(); err != nil {
return fmt.Errorf("provision addons: %w", err)
}
return nil
}

func (p *AddonProvisioner) installAddon(clusterID string, config gitops.AddonConfig) error {
addonInstallation, err := newInstallation(config)
if err != nil {
return err
}
if err = p.ocmClient.CreateAddonInstallation(clusterID, addonInstallation); err != nil {
return fmt.Errorf("create addon %s in ocm: %w", config.ID, err)
}
glog.V(5).Infof("Addon %s has been installed on the cluster %s", config.ID, clusterID)
return nil
}

func newInstallation(config gitops.AddonConfig) (*clustersmgmtv1.AddOnInstallation, error) {
installation, err := clustersmgmtv1.NewAddOnInstallation().
Addon(clustersmgmtv1.NewAddOn().ID(config.ID)).
AddonVersion(clustersmgmtv1.NewAddOnVersion().ID(config.Version)).
Parameters(convertParametersToOCMAPI(config.Parameters)).
Build()

if err != nil {
return nil, fmt.Errorf("build new addon installation %s: %w", config.ID, err)
}

return installation, nil
}

func (p *AddonProvisioner) updateAddon(clusterID string, config gitops.AddonConfig) error {
update, err := newInstallation(config)
if err != nil {
return err
}
if err := p.ocmClient.UpdateAddonInstallation(clusterID, update); err != nil {
return fmt.Errorf("update addon %s: %w", update.ID(), err)
}
glog.V(5).Infof("Addon %s has been updated on the cluster %s", config.ID, clusterID)
return nil
}

func (p *AddonProvisioner) uninstallAddon(clusterID string, addonID string) error {
if err := p.ocmClient.DeleteAddonInstallation(clusterID, addonID); err != nil {
return fmt.Errorf("uninstall addon %s: %w", addonID, err)
}
glog.V(5).Infof("Addon %s has been uninstalled from the cluster %s", addonID, clusterID)
return nil
}

func isFinalState(state clustersmgmtv1.AddOnInstallationState) bool {
return state == clustersmgmtv1.AddOnInstallationStateFailed || state == clustersmgmtv1.AddOnInstallationStateReady
}

func (c *updateDecision) updateInProgress() bool {
return !isFinalState(c.installedInOCM.State())
}

func (c *updateDecision) needsUpdate(current dbapi.AddonInstallation) bool {
if c.installedInOCM.AddonVersion().ID() != c.expectedConfig.Version ||
!maps.Equal(convertParametersFromOCMAPI(c.installedInOCM.Parameters()), c.expectedConfig.Parameters) {
return true
}

addonVersion, err := c.ocmClient.GetAddonVersion(c.expectedConfig.ID, c.expectedConfig.Version)
if err != nil {
c.multiErr = multierror.Append(c.multiErr, fmt.Errorf("get addon version object for addon %s with version %s: %w",
c.expectedConfig.ID, c.expectedConfig.Version, err))
return false
}

return current.SourceImage != addonVersion.SourceImage() || current.PackageImage != addonVersion.PackageImage()
}

func convertParametersToOCMAPI(parameters map[string]string) *clustersmgmtv1.AddOnInstallationParameterListBuilder {
var values []*clustersmgmtv1.AddOnInstallationParameterBuilder
for key, value := range parameters {
values = append(values, clustersmgmtv1.NewAddOnInstallationParameter().ID(key).Value(value))
}
return clustersmgmtv1.NewAddOnInstallationParameterList().Items(values...)
}

func convertParametersFromOCMAPI(parameters *clustersmgmtv1.AddOnInstallationParameterList) shared.AddonParameters {
result := make(map[string]string)
parameters.Each(func(item *clustersmgmtv1.AddOnInstallationParameter) bool {
result[item.ID()] = item.Value()
return true
})
return result
}
Loading

0 comments on commit 5c06671

Please sign in to comment.