diff --git a/internal/dinosaur/pkg/gitops/service.go b/internal/dinosaur/pkg/gitops/service.go index 90aea7903d..e28042f08b 100644 --- a/internal/dinosaur/pkg/gitops/service.go +++ b/internal/dinosaur/pkg/gitops/service.go @@ -2,18 +2,26 @@ package gitops import ( "encoding/json" + "fmt" "strings" + "sync" "text/template" "github.com/pkg/errors" + "github.com/stackrox/acs-fleet-manager/fleetshard/pkg/util" "github.com/stackrox/rox/operator/apis/platform/v1alpha1" "k8s.io/apimachinery/pkg/util/strategicpatch" "sigs.k8s.io/yaml" ) +var ( + centralCRYAMLCache = make(map[string]string) + centralCacheMutex = sync.RWMutex{} +) + // Service applies GitOps configuration to Central instances. type Service interface { - GetCentral(ctx CentralParams) (v1alpha1.Central, error) + GetCentral(ctx CentralParams) (string, error) } type service struct { @@ -26,12 +34,74 @@ func NewService(configProvider ConfigProvider) Service { } // GetCentral returns a Central instance with the given parameters. -func (s *service) GetCentral(params CentralParams) (v1alpha1.Central, error) { +func (s *service) GetCentral(params CentralParams) (string, error) { cfg, err := s.configProvider.Get() if err != nil { - return v1alpha1.Central{}, errors.Wrap(err, "failed to get GitOps configuration") + return "", errors.Wrap(err, "failed to get GitOps configuration") + } + + result, err := readFromCache(params, cfg) + if err != nil { + return "", err + } + if result != "" { + return result, nil + } + + centralCR, err := renderCentral(params, cfg) + if err != nil { + return "", err + } + + centralYaml, err := yaml.Marshal(centralCR) + if err != nil { + return "", errors.Wrap(err, "failed to marshal Central CR") + } + + centralYAMLString := string(centralYaml) + err = addCache(params, cfg, centralYAMLString) + if err != nil { + return "", err + } + return centralYAMLString, nil +} + +// getCacheKey returns a key in the format of ":" +func getCacheKey(params CentralParams, config Config) (string, error) { + paramsHash, err := util.MD5SumFromJSONStruct(params) + if err != nil { + return "", fmt.Errorf("could not create MD5 from CentralParams name: %s, id: %s, %w", params.Name, params.ID, err) + } + configHash, err := util.MD5SumFromJSONStruct(config) + if err != nil { + return "", fmt.Errorf("could not create MD5 from gitops Configame: %s, id: %s, %w", params.Name, params.ID, err) + } + return fmt.Sprintf("%s:%s", string(paramsHash[:]), string(configHash[:])), nil +} + +func addCache(params CentralParams, config Config, centralYAML string) error { + centralCacheMutex.Lock() + defer centralCacheMutex.Unlock() + key, err := getCacheKey(params, config) + if err != nil { + return fmt.Errorf("could not add to cache: %w", err) + } + + centralCRYAMLCache[key] = centralYAML + return nil +} + +// readFromCache returns a CentralYAML from the cache +func readFromCache(params CentralParams, config Config) (string, error) { + cacheKey, err := getCacheKey(params, config) + if err != nil { + return "", fmt.Errorf("Could not get cache key: %w", err) + } + + if val, ok := centralCRYAMLCache[cacheKey]; ok { + return val, nil } - return renderCentral(params, cfg) + return "", nil } func renderCentral(params CentralParams, config Config) (v1alpha1.Central, error) { diff --git a/internal/dinosaur/pkg/gitops/service_test.go b/internal/dinosaur/pkg/gitops/service_test.go index 53b98dd4be..8f53ca360c 100644 --- a/internal/dinosaur/pkg/gitops/service_test.go +++ b/internal/dinosaur/pkg/gitops/service_test.go @@ -119,11 +119,56 @@ func TestService_GetCentral(t *testing.T) { t.Run(tt.name, func(t *testing.T) { svc := NewService(newMockProvider(tt.config)) got, err := svc.GetCentral(tt.params) - tt.assert(t, got, err) + centralGot := v1alpha1.Central{} + errUnmarshal := yaml.Unmarshal([]byte(got), ¢ralGot) + require.NoError(t, errUnmarshal) + tt.assert(t, centralGot, err) }) } } +func TestServiceGetCache(t *testing.T) { + c := Config{ + Centrals: CentralsConfig{ + Overrides: []CentralOverride{ + { + InstanceIDs: []string{"*"}, + Patch: `metadata: {"labels": {"foo": "bar"}}`, + }, + }, + }, + } + params := CentralParams{ + ID: "id-123", + Name: "Central-Name", + } + + gitOpsService := NewService(newMockProvider(c)) + centralYAML, err := gitOpsService.GetCentral(params) + require.NoError(t, err) + + central := v1alpha1.Central{} + err = yaml.Unmarshal([]byte(centralYAML), ¢ral) + require.NoError(t, err) + + // assert that the rendered Central is present in the cache + require.Len(t, centralCRYAMLCache, 1) + key, err := getCacheKey(params, c) + assert.Equal(t, centralYAML, centralCRYAMLCache[key]) + require.NoError(t, err) + + // assert that the Central is returned correctly from the cache + centralYAMLFromCache, err := readFromCache(params, c) + require.NoError(t, err) + assert.Equal(t, centralYAML, centralYAMLFromCache) + + // assert that the Central is NOT returned with a different config + c.Centrals = CentralsConfig{} + result, err := readFromCache(params, c) + require.NoError(t, err) + assert.Empty(t, result) +} + // TestDefaultTemplateIsValid tests that the default template is valid and // can be unmarshaled to a functional v1alpha1.Central object. func Test_defaultTemplate_isValid(t *testing.T) { diff --git a/internal/dinosaur/pkg/presenters/managedcentral.go b/internal/dinosaur/pkg/presenters/managedcentral.go index 836150c2fd..a9b51503a9 100644 --- a/internal/dinosaur/pkg/presenters/managedcentral.go +++ b/internal/dinosaur/pkg/presenters/managedcentral.go @@ -12,7 +12,6 @@ import ( "github.com/stackrox/acs-fleet-manager/internal/dinosaur/pkg/config" "github.com/stackrox/acs-fleet-manager/internal/dinosaur/pkg/gitops" "k8s.io/apimachinery/pkg/api/resource" - "sigs.k8s.io/yaml" ) // ManagedCentralPresenter helper service which converts Central DB representation to the private API representation @@ -32,16 +31,11 @@ func NewManagedCentralPresenter(config *config.CentralConfig, gitopsService gito // PresentManagedCentral converts DB representation of Central to the private API representation func (c *ManagedCentralPresenter) PresentManagedCentral(from *dbapi.CentralRequest) (private.ManagedCentral, error) { - centralCR, err := c.gitopsService.GetCentral(centralParamsFromRequest(from)) + centralYaml, err := c.gitopsService.GetCentral(centralParamsFromRequest(from)) if err != nil { return private.ManagedCentral{}, errors.Wrap(err, "failed to apply GitOps overrides to Central") } - centralYaml, err := yaml.Marshal(centralCR) - if err != nil { - return private.ManagedCentral{}, errors.Wrap(err, "failed to marshal Central CR") - } - res := private.ManagedCentral{ Id: from.ID, Kind: "ManagedCentral",