diff --git a/internal/dinosaur/pkg/gitops/README.md b/internal/dinosaur/pkg/gitops/README.md new file mode 100644 index 0000000000..6334ffc08a --- /dev/null +++ b/internal/dinosaur/pkg/gitops/README.md @@ -0,0 +1,14 @@ +# GitOps Workflow + +![GitOps workflow](gitops-workflow.png) + +1. `fleetshard` polls `fleetmanager` for a list of `Centrals` by sending an api request +2. `fleetmanager` lists the central instances from the database +3. `fleetmanager` applies the default configuration to the central instances +4. `fleetmanager` retrieves the gitops configuration +5. `fleetmanager` applies the gitops configuration to the central instances +6. `fleetmanager` returns the list of central instances to `fleetshard` +7. `fleetshard` applies the cluster-specific configuration/overrides to the central instances +8. `fleetshard` performs reconciliation of the central instances + +The `gitops` configuration repository is located at https://gitlab.cee.redhat.com/stackrox/acs-cloud-service/config diff --git a/internal/dinosaur/pkg/gitops/config.go b/internal/dinosaur/pkg/gitops/config.go index 299ad62538..d3a464833c 100644 --- a/internal/dinosaur/pkg/gitops/config.go +++ b/internal/dinosaur/pkg/gitops/config.go @@ -3,8 +3,8 @@ package gitops import ( "github.com/stackrox/rox/operator/apis/platform/v1alpha1" - "gopkg.in/yaml.v2" - field "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/apimachinery/pkg/util/validation/field" + "sigs.k8s.io/yaml" ) // Config represents the gitops configuration @@ -14,8 +14,6 @@ type Config struct { // CentralsConfig represents the declarative configuration for Central instances defaults and overrides. type CentralsConfig struct { - // Default configuration for Central instances. - Default v1alpha1.Central `json:"default"` // Overrides are the overrides for Central instances. Overrides []CentralOverride `json:"overrides"` } diff --git a/internal/dinosaur/pkg/gitops/config_test.go b/internal/dinosaur/pkg/gitops/config_test.go index fdbaa275b2..a164d63ab8 100644 --- a/internal/dinosaur/pkg/gitops/config_test.go +++ b/internal/dinosaur/pkg/gitops/config_test.go @@ -24,20 +24,19 @@ func TestValidateGitOpsConfig(t *testing.T) { }, yaml: ` centrals: - default: {} overrides: - - instanceId: id1 + - instanceIds: + - id1 patch: | {}`, }, { name: "invalid yaml in patch", assert: func(t *testing.T, c *Config, err field.ErrorList) { require.Len(t, err, 1) - assert.Equal(t, field.Invalid(field.NewPath("centrals", "overrides").Index(0).Child("patch"), "foo", "invalid patch: yaml: unmarshal errors:\n line 1: cannot unmarshal !!str `foo` into v1alpha1.Central"), err[0]) + assert.Equal(t, field.Invalid(field.NewPath("centrals", "overrides").Index(0).Child("patch"), "foo", "invalid patch: error unmarshaling JSON: while decoding JSON: json: cannot unmarshal string into Go value of type v1alpha1.Central"), err[0]) }, yaml: ` centrals: - default: {} overrides: - instanceIds: - id1 @@ -47,11 +46,10 @@ centrals: name: "patch contains un-mergeable fields", assert: func(t *testing.T, c *Config, err field.ErrorList) { require.Len(t, err, 1) - assert.Equal(t, field.Invalid(field.NewPath("centrals", "overrides").Index(0).Child("patch"), "spec: 123\n", "invalid patch: yaml: unmarshal errors:\n line 1: cannot unmarshal !!int `123` into v1alpha1.CentralSpec"), err[0]) + assert.Equal(t, field.Invalid(field.NewPath("centrals", "overrides").Index(0).Child("patch"), "spec: 123\n", "invalid patch: error unmarshaling JSON: while decoding JSON: json: cannot unmarshal number into Go struct field Central.spec of type v1alpha1.CentralSpec"), err[0]) }, yaml: ` centrals: - default: {} overrides: - instanceIds: - id1 diff --git a/internal/dinosaur/pkg/gitops/default_central.go b/internal/dinosaur/pkg/gitops/default_central.go new file mode 100644 index 0000000000..d4fa535b88 --- /dev/null +++ b/internal/dinosaur/pkg/gitops/default_central.go @@ -0,0 +1,6 @@ +package gitops + +import _ "embed" + +//go:embed default_central.yaml +var defaultCentralTemplate []byte diff --git a/internal/dinosaur/pkg/gitops/default_central.yaml b/internal/dinosaur/pkg/gitops/default_central.yaml new file mode 100644 index 0000000000..f5273d8fa4 --- /dev/null +++ b/internal/dinosaur/pkg/gitops/default_central.yaml @@ -0,0 +1,52 @@ +metadata: + name: "{{ .Name }}" + namespace: "{{ .Namespace }}" + labels: + rhacs.redhat.com/instance-type: "{{ .InstanceType }}" + rhacs.redhat.com/org-id: "{{ .OrganizationID }}" + rhacs.redhat.com/tenant: "{{ .ID }}" + annotations: + platform.stackrox.io/managed-services: "true" + rhacs.redhat.com/org-name: {{ .OrganizationName }} +spec: + central: + adminPasswordGenerationDisabled: true #pragma: allowlist secret + # db: {} -- managed by fleetshard-sync + # exposure: {} -- managed by fleetshard-sync + monitoring: + exposeEndpoint: Enabled + openshift: + enabled: false + resources: + limits: + cpu: "4" + memory: 8Gi + requests: + cpu: "2" + memory: 4Gi + # telemetry: {} -- managed by fleetshard-sync + scanner: + analyzer: + resources: + limits: + cpu: "3" + memory: 8Gi + requests: + cpu: "1.5" + memory: 4Gi + scaling: + autoScaling: Enabled + maxReplicas: 3 + minReplicas: 1 + replicas: 1 + scannerComponent: Enabled + db: + resources: + limits: + cpu: "2.5" + memory: 4Gi + requests: + cpu: "1.25" + memory: 2Gi + monitoring: + exposeEndpoint: Enabled diff --git a/internal/dinosaur/pkg/gitops/gitops-workflow.png b/internal/dinosaur/pkg/gitops/gitops-workflow.png new file mode 100644 index 0000000000..5f80ee7c0e Binary files /dev/null and b/internal/dinosaur/pkg/gitops/gitops-workflow.png differ diff --git a/internal/dinosaur/pkg/gitops/gitops-workflow.puml b/internal/dinosaur/pkg/gitops/gitops-workflow.puml new file mode 100644 index 0000000000..8b359cd6d1 --- /dev/null +++ b/internal/dinosaur/pkg/gitops/gitops-workflow.puml @@ -0,0 +1,28 @@ +@startuml +'https://plantuml.com/sequence-diagram + +autonumber + +box FleetShard +participant FS as "FleetShard" +end box + +box "FleetManager" +participant FM as "FleetManager" +participant DC as "Default Central" +participant DB as "Database" +participant GitOps as GitOps +end box + +FS -> FM: Poll Centrals +FM -> DB: List Instances +FM -> DC: Get Default Central +FM -> FM: Apply Defaults to List +FM -> GitOps: Get GitOps Config +FM -> FM: Apply GitOps Config to List +FM -> FS: Central List +FS -> FS: Apply Cluster-Specific Defaults +FS -> FS: Reconcile + + +@enduml diff --git a/internal/dinosaur/pkg/gitops/provider.go b/internal/dinosaur/pkg/gitops/provider.go new file mode 100644 index 0000000000..3a9ccc39da --- /dev/null +++ b/internal/dinosaur/pkg/gitops/provider.go @@ -0,0 +1,76 @@ +package gitops + +import ( + "sync/atomic" + + "github.com/golang/glog" + "github.com/pkg/errors" + "github.com/prometheus/client_golang/prometheus" +) + +var ( + errorCounter = prometheus.NewCounterVec(prometheus.CounterOpts{ + Name: "dinosaur_gitops_config_provider_error_total", + Help: "Number of errors encountered by the GitOps configuration provider.", + }, []string{}) +) + +func init() { + prometheus.MustRegister(errorCounter) +} + +// ConfigProvider is the interface for GitOps configuration providers. +type ConfigProvider interface { + // Get returns the GitOps configuration. + Get() (Config, error) +} + +type validationFn func(config Config) error + +type provider struct { + reader Reader + lastWorkingConfig atomic.Pointer[Config] + validationFn validationFn +} + +// NewProvider returns a new ConfigProvider. +func NewProvider(reader Reader) ConfigProvider { + return &provider{ + reader: reader, + lastWorkingConfig: atomic.Pointer[Config]{}, + validationFn: func(config Config) error { + return ValidateConfig(config).ToAggregate() + }, + } +} + +// Get implements ConfigProvider.Get +func (p *provider) Get() (Config, error) { + // Load the config from the reader + cfg, err := p.reader.Read() + if err != nil { + p.increaseErrorCount() + return p.tryGetLastWorkingConfig(errors.Wrap(err, "failed to read GitOps configuration")) + } + // Validate the config + if err := p.validationFn(cfg); err != nil { + p.increaseErrorCount() + return p.tryGetLastWorkingConfig(errors.Wrap(err, "failed to validate GitOps configuration")) + } + // Store the config as the last working config + p.lastWorkingConfig.Store(&cfg) + return cfg, nil +} + +func (p *provider) increaseErrorCount() { + errorCounter.WithLabelValues().Inc() +} + +func (p *provider) tryGetLastWorkingConfig(err error) (Config, error) { + lastWorkingConfig := p.lastWorkingConfig.Load() + if lastWorkingConfig == nil { + return Config{}, errors.Wrap(err, "no last working gitops config available") + } + glog.Warningf("Failed to get GitOps configuration. Using last working config: %s", err) + return *lastWorkingConfig, nil +} diff --git a/internal/dinosaur/pkg/gitops/provider_test.go b/internal/dinosaur/pkg/gitops/provider_test.go new file mode 100644 index 0000000000..d15a853c11 --- /dev/null +++ b/internal/dinosaur/pkg/gitops/provider_test.go @@ -0,0 +1,133 @@ +package gitops + +import ( + "sync/atomic" + "testing" + + "github.com/prometheus/client_golang/prometheus/testutil" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestProvider_Get(t *testing.T) { + + var failingValidation validationFn = func(config Config) error { + return assert.AnError + } + var successfulValidation validationFn = func(config Config) error { + return nil + } + var failingReader Reader = &mockReader{err: assert.AnError} + var successfulReader Reader = &mockReader{config: Config{}} + + type tc struct { + name string + hasLastWorkingConfig bool + reader Reader + validator validationFn + expectedErrorMetricCount int + expectError bool + } + tcs := []tc{ + { + name: "Successful without last working config", + hasLastWorkingConfig: false, + reader: successfulReader, + validator: successfulValidation, + expectedErrorMetricCount: 0, + expectError: false, + }, { + name: "Successful with last working config", + hasLastWorkingConfig: true, + reader: successfulReader, + validator: successfulValidation, + expectedErrorMetricCount: 0, + expectError: false, + }, { + name: "Reader fails without last working config", + hasLastWorkingConfig: false, + reader: failingReader, + validator: successfulValidation, + expectedErrorMetricCount: 1, + expectError: true, + }, { + name: "Reader fails with last working config", + hasLastWorkingConfig: true, + reader: failingReader, + validator: successfulValidation, + expectedErrorMetricCount: 1, + expectError: false, + }, { + name: "Validation fails without last working config", + hasLastWorkingConfig: false, + reader: failingReader, + validator: failingValidation, + expectedErrorMetricCount: 1, + expectError: true, + }, { + name: "Validation fails with last working config", + hasLastWorkingConfig: true, + reader: failingReader, + validator: failingValidation, + expectedErrorMetricCount: 1, + expectError: false, + }, + } + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + p := &provider{} + p.lastWorkingConfig = atomic.Pointer[Config]{} + + if tc.hasLastWorkingConfig { + // Get the config once to set the last working config + p.reader = successfulReader + p.validationFn = successfulValidation + _, err := p.Get() + require.NoError(t, err) + } + + p.reader = tc.reader + p.validationFn = tc.validator + + errorCounter.Reset() + _, err := p.Get() + if tc.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + } + + count := testutil.CollectAndCount(errorCounter) + assert.Equal(t, tc.expectedErrorMetricCount, count) + + }) + } +} + +type mockReader struct { + config Config + err error +} + +func NewMockReader(config Config) *mockReader { + return &mockReader{ + config: Config{}, + err: nil, + } +} + +func (r *mockReader) Read() (Config, error) { + return r.config, r.err +} + +func (r *mockReader) WillFail() *mockReader { + r.err = assert.AnError + return r +} + +func (r *mockReader) WillSucceed() *mockReader { + r.err = nil + return r +} + +var _ Reader = &mockReader{} diff --git a/internal/dinosaur/pkg/gitops/reader.go b/internal/dinosaur/pkg/gitops/reader.go new file mode 100644 index 0000000000..3d0570819e --- /dev/null +++ b/internal/dinosaur/pkg/gitops/reader.go @@ -0,0 +1,68 @@ +package gitops + +import ( + // embed needed for embedding the default central template + _ "embed" + "os" + + "github.com/pkg/errors" + "sigs.k8s.io/yaml" +) + +// Reader reads a Config from a source. +type Reader interface { + Read() (Config, error) +} + +// fileReader is a Reader that reads a Config from a file. +type fileReader struct { + path string +} + +// NewFileReader returns a new fileReader. +func NewFileReader(path string) Reader { + return &fileReader{path: path} +} + +// Read implements Reader.Read +func (r *fileReader) Read() (Config, error) { + fileBytes, err := os.ReadFile(r.path) + if err != nil { + return Config{}, errors.Wrap(err, "failed to read GitOps configuration file") + } + var config Config + if err := yaml.Unmarshal(fileBytes, &config); err != nil { + return Config{}, errors.Wrap(err, "failed to unmarshal GitOps configuration") + } + return config, nil +} + +// staticReader is a Reader that returns a static Config. +type staticReader struct { + config Config +} + +// NewStaticReader returns a new staticReader. +// Useful for testing. +func NewStaticReader(config Config) Reader { + return &staticReader{config: config} +} + +// Read implements Reader.Read +func (r *staticReader) Read() (Config, error) { + return r.config, nil +} + +// emptyReader is a Reader that returns an empty Config. +type emptyReader struct{} + +// NewEmptyReader returns a new emptyReader. +// Useful for testing. +func NewEmptyReader() Reader { + return &emptyReader{} +} + +// Read implements Reader.Read +func (r *emptyReader) Read() (Config, error) { + return Config{}, nil +} diff --git a/internal/dinosaur/pkg/gitops/reader_test.go b/internal/dinosaur/pkg/gitops/reader_test.go new file mode 100644 index 0000000000..4768e5da49 --- /dev/null +++ b/internal/dinosaur/pkg/gitops/reader_test.go @@ -0,0 +1,50 @@ +package gitops + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestFileReader_Get_FailsIfFileDoesNotExist(t *testing.T) { + tmpDir := t.TempDir() + tmpFilePath := tmpDir + "/config.yaml" + provider := NewFileReader(tmpFilePath) + _, err := provider.Read() + assert.Error(t, err) +} + +func TestFileReader_Get_FailsIfFileIsInvalidYAML(t *testing.T) { + tmpDir := t.TempDir() + tmpFilePath := tmpDir + "/config.yaml" + file, err := os.Create(tmpFilePath) + require.NoError(t, err) + _, err = file.WriteString("invalid yaml") + require.NoError(t, err) + provider := NewFileReader(tmpFilePath) + _, err = provider.Read() + assert.Error(t, err) +} + +func TestFileReader_Get(t *testing.T) { + tmpDir := t.TempDir() + tmpFilePath := tmpDir + "/config.yaml" + file, err := os.Create(tmpFilePath) + require.NoError(t, err) + _, err = file.WriteString(` +centrals: + overrides: [] +`) + require.NoError(t, err) + provider := NewFileReader(tmpFilePath) + _, err = provider.Read() + require.NoError(t, err) +} + +func TestStaticReader_Read(t *testing.T) { + provider := NewStaticReader(Config{}) + _, err := provider.Read() + require.NoError(t, err) +} diff --git a/internal/dinosaur/pkg/gitops/service.go b/internal/dinosaur/pkg/gitops/service.go new file mode 100644 index 0000000000..19f6c232d0 --- /dev/null +++ b/internal/dinosaur/pkg/gitops/service.go @@ -0,0 +1,160 @@ +package gitops + +import ( + "encoding/json" + "strings" + "text/template" + + "github.com/pkg/errors" + "github.com/stackrox/rox/operator/apis/platform/v1alpha1" + "k8s.io/apimachinery/pkg/util/strategicpatch" + "sigs.k8s.io/yaml" +) + +// Service applies GitOps configuration to Central instances. +type Service interface { + GetCentral(ctx CentralParams) (v1alpha1.Central, error) +} + +type service struct { + configProvider ConfigProvider +} + +// NewService returns a new Service. +func NewService(configProvider ConfigProvider) Service { + return &service{configProvider: configProvider} +} + +// GetCentral returns a Central instance with the given parameters. +func (s *service) GetCentral(params CentralParams) (v1alpha1.Central, error) { + wr := new(strings.Builder) + if err := defaultTemplate.Execute(wr, params); err != nil { + return v1alpha1.Central{}, errors.Wrap(err, "failed to render default template") + } + var central v1alpha1.Central + if err := yaml.Unmarshal([]byte(wr.String()), ¢ral); err != nil { + return v1alpha1.Central{}, errors.Wrap(err, "failed to unmarshal default central") + } + cfg, err := s.configProvider.Get() + if err != nil { + return v1alpha1.Central{}, errors.Wrap(err, "failed to get GitOps configuration") + } + return applyConfigToCentral(cfg, central, params) +} + +// CentralParams represents the parameters for a Central instance. +type CentralParams struct { + // ID is the ID of the Central instance. + ID string + // Name is the name of the Central instance. + Name string + // Namespace is the namespace of the Central instance. + Namespace string + // Region is the region of the Central instance. + Region string + // ClusterID is the ID of the cluster of the Central instance. + ClusterID string + // CloudProvider is the cloud provider of the Central instance. + CloudProvider string + // CloudAccountID is the cloud account ID of the Central instance. + CloudAccountID string + // SubscriptionID is the subscription ID of the Central instance. + SubscriptionID string + // Owner is the owner of the Central instance. + Owner string + // OwnerAccountID is the owner account ID of the Central instance. + OwnerAccountID string + // OwnerUserID is the owner user ID of the Central instance. + OwnerUserID string + // Host is the host of the Central instance. + Host string + // OrganizationID is the organization ID of the Central instance. + OrganizationID string + // OrganizationName is the organization name of the Central instance. + OrganizationName string + // InstanceType is the instance type of the Central instance. + InstanceType string + // IsInternal is true if the Central instance is internal. + IsInternal bool +} + +// applyConfigToCentral will apply the given GitOps configuration to the given Central instance. +func applyConfigToCentral(config Config, central v1alpha1.Central, ctx CentralParams) (v1alpha1.Central, error) { + var overrides []CentralOverride + for _, override := range config.Centrals.Overrides { + if !shouldApplyCentralOverride(override, ctx) { + continue + } + overrides = append(overrides, override) + } + if len(overrides) == 0 { + return central, nil + } + // render override path templates + for i, override := range overrides { + var err error + overrides[i].Patch, err = renderPatchTemplate(override.Patch, ctx) + if err != nil { + return v1alpha1.Central{}, err + } + } + centralBytes, err := json.Marshal(central) + if err != nil { + return v1alpha1.Central{}, errors.Wrap(err, "failed to marshal Central instance") + } + for _, override := range overrides { + patchBytes := []byte(override.Patch) + centralBytes, err = applyPatchToCentral(centralBytes, patchBytes) + if err != nil { + return v1alpha1.Central{}, err + } + } + var result v1alpha1.Central + if err := json.Unmarshal(centralBytes, &result); err != nil { + return v1alpha1.Central{}, errors.Wrap(err, "failed to unmarshal Central instance") + } + return result, nil +} + +// shouldApplyCentralOverride returns true if the given Central override should be applied to the given Central instance. +func shouldApplyCentralOverride(override CentralOverride, ctx CentralParams) bool { + for _, d := range override.InstanceIDs { + if d == "*" { + return true + } + if d == ctx.ID { + return true + } + } + return false +} + +// applyPatchToCentral will apply the given patch to the given Central instance. +func applyPatchToCentral(centralBytes, patch []byte) ([]byte, error) { + // convert patch from yaml to json + jsonPath, err := yaml.YAMLToJSON(patch) + if err != nil { + return []byte{}, errors.Wrap(err, "failed to convert override patch from yaml to json") + } + // apply patch + patchedBytes, err := strategicpatch.StrategicMergePatch(centralBytes, jsonPath, v1alpha1.Central{}) + if err != nil { + return []byte{}, errors.Wrap(err, "failed to apply override to Central instance") + } + return patchedBytes, nil +} + +func renderPatchTemplate(patchTemplate string, ctx CentralParams) (string, error) { + tpl, err := template.New("patch").Parse(patchTemplate) + if err != nil { + return "", errors.Wrap(err, "failed to parse patch template") + } + var writer = new(strings.Builder) + if err := tpl.Execute(writer, ctx); err != nil { + return "", errors.Wrap(err, "failed to render patch template") + } + return writer.String(), nil +} + +// defaultTemplate is the default template for Central instances. +var defaultTemplate = template.Must(template.New("default").Parse(string(defaultCentralTemplate))) diff --git a/internal/dinosaur/pkg/gitops/service_test.go b/internal/dinosaur/pkg/gitops/service_test.go new file mode 100644 index 0000000000..53b98dd4be --- /dev/null +++ b/internal/dinosaur/pkg/gitops/service_test.go @@ -0,0 +1,166 @@ +package gitops + +import ( + "strings" + "testing" + + "github.com/stackrox/rox/operator/apis/platform/v1alpha1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "sigs.k8s.io/yaml" +) + +func TestService_GetCentral(t *testing.T) { + tests := []struct { + name string + config Config + params CentralParams + assert func(t *testing.T, got v1alpha1.Central, err error) + }{ + { + name: "no overrides", + params: CentralParams{ + ID: "central-1", + }, + config: Config{}, + assert: func(t *testing.T, got v1alpha1.Central, err error) { + require.NoError(t, err) + }, + }, + { + name: "multiple overrides", + params: CentralParams{ + ID: "central-1", + }, + config: Config{ + Centrals: CentralsConfig{ + Overrides: []CentralOverride{ + { + InstanceIDs: []string{"central-1"}, + Patch: `metadata: {"labels": {"foo": "bar"}}`, + }, { + InstanceIDs: []string{"central-1"}, + Patch: `metadata: {"annotations": {"foo": "bar"}}`, + }, + }, + }, + }, + assert: func(t *testing.T, got v1alpha1.Central, err error) { + require.NoError(t, err) + assert.Equal(t, "bar", got.Labels["foo"]) + assert.Equal(t, "bar", got.Annotations["foo"]) + }, + }, + { + name: "multiple overrides, one not matching", + params: CentralParams{ + ID: "central-1", + }, + config: Config{ + Centrals: CentralsConfig{ + Overrides: []CentralOverride{ + { + InstanceIDs: []string{"central-1"}, + Patch: `metadata: {"labels": {"foo": "bar"}}`, + }, { + InstanceIDs: []string{"central-2"}, + Patch: `metadata: {"labels": {"foo": "baz"}}`, + }, + }, + }, + }, + assert: func(t *testing.T, got v1alpha1.Central, err error) { + require.NoError(t, err) + assert.Equal(t, "bar", got.Labels["foo"]) + }, + }, + { + name: "with templated patch", + params: CentralParams{ + ID: "central-1", + }, + config: Config{ + Centrals: CentralsConfig{ + Overrides: []CentralOverride{ + { + InstanceIDs: []string{"central-1"}, + Patch: `metadata: {"labels": {"foo": "{{ .ID }}"}}`, + }, + }, + }, + }, + assert: func(t *testing.T, got v1alpha1.Central, err error) { + require.NoError(t, err) + assert.Equal(t, "central-1", got.Labels["foo"]) + }, + }, + { + name: "wildcard override", + params: CentralParams{ + ID: "central-1", + }, + config: Config{ + Centrals: CentralsConfig{ + Overrides: []CentralOverride{ + { + InstanceIDs: []string{"*"}, + Patch: `metadata: {"labels": {"foo": "bar"}}`, + }, + }, + }, + }, + assert: func(t *testing.T, got v1alpha1.Central, err error) { + require.NoError(t, err) + assert.Equal(t, "bar", got.Labels["foo"]) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + svc := NewService(newMockProvider(tt.config)) + got, err := svc.GetCentral(tt.params) + tt.assert(t, got, err) + }) + } +} + +// 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) { + + var wr strings.Builder + err := defaultTemplate.Execute(&wr, CentralParams{ + ID: "id", + Name: "name", + Namespace: "namespace", + Region: "region", + ClusterID: "cluster-id", + CloudProvider: "cloud-provider", + CloudAccountID: "cloud-account-id", + SubscriptionID: "subscription-id", + Owner: "owner", + OwnerAccountID: "owner-account-id", + OwnerUserID: "owner-user-id", + Host: "host", + OrganizationID: "organization-id", + OrganizationName: "organization-name", + InstanceType: "instance-type", + IsInternal: false, + }) + require.NoError(t, err) + + var central v1alpha1.Central + require.NoError(t, yaml.Unmarshal([]byte(wr.String()), ¢ral)) +} + +type mockProvider struct { + config Config +} + +func (m *mockProvider) Get() (Config, error) { + return m.config, nil +} + +func newMockProvider(config Config) *mockProvider { + return &mockProvider{config: config} +} diff --git a/internal/dinosaur/pkg/handlers/data_plane_dinosaur.go b/internal/dinosaur/pkg/handlers/data_plane_dinosaur.go index 898425ab15..2fb85abe11 100644 --- a/internal/dinosaur/pkg/handlers/data_plane_dinosaur.go +++ b/internal/dinosaur/pkg/handlers/data_plane_dinosaur.go @@ -5,13 +5,12 @@ import ( "github.com/stackrox/acs-fleet-manager/pkg/features" "net/http" + "github.com/gorilla/mux" "github.com/stackrox/acs-fleet-manager/internal/dinosaur/pkg/api/private" "github.com/stackrox/acs-fleet-manager/internal/dinosaur/pkg/presenters" "github.com/stackrox/acs-fleet-manager/internal/dinosaur/pkg/services" - "github.com/stackrox/acs-fleet-manager/pkg/handlers" - - "github.com/gorilla/mux" "github.com/stackrox/acs-fleet-manager/pkg/errors" + "github.com/stackrox/acs-fleet-manager/pkg/handlers" ) type dataPlaneDinosaurHandler struct { @@ -21,7 +20,11 @@ type dataPlaneDinosaurHandler struct { } // NewDataPlaneDinosaurHandler ... -func NewDataPlaneDinosaurHandler(service services.DataPlaneCentralService, dinosaurService services.DinosaurService, presenter *presenters.ManagedCentralPresenter) *dataPlaneDinosaurHandler { +func NewDataPlaneDinosaurHandler( + service services.DataPlaneCentralService, + dinosaurService services.DinosaurService, + presenter *presenters.ManagedCentralPresenter, +) *dataPlaneDinosaurHandler { return &dataPlaneDinosaurHandler{ service: service, dinosaurService: dinosaurService, @@ -56,7 +59,7 @@ func (h *dataPlaneDinosaurHandler) GetAll(w http.ResponseWriter, r *http.Request handlers.ValidateLength(&clusterID, "id", &handlers.MinRequiredFieldLength, nil), }, Action: func() (interface{}, *errors.ServiceError) { - centralRequests, err := h.dinosaurService.ListByClusterID(clusterID) + centralRequests, err := h.service.ListByClusterID(clusterID) if err != nil { return nil, err } @@ -72,7 +75,10 @@ func (h *dataPlaneDinosaurHandler) GetAll(w http.ResponseWriter, r *http.Request } for i := range centralRequests { - converted := h.presenter.PresentManagedCentral(centralRequests[i]) + converted, err := h.presenter.PresentManagedCentral(centralRequests[i]) + if err != nil { + return nil, errors.GeneralError("failed to convert central request to managed central: %v", err) + } managedDinosaurList.Items = append(managedDinosaurList.Items, converted) } return managedDinosaurList, nil @@ -87,12 +93,15 @@ func (h *dataPlaneDinosaurHandler) GetByID(w http.ResponseWriter, r *http.Reques centralID := mux.Vars(r)["id"] cfg := &handlers.HandlerConfig{ Action: func() (interface{}, *errors.ServiceError) { - centralRequest, err := h.dinosaurService.GetByID(centralID) - if err != nil { - return nil, err + centralRequest, svcErr := h.dinosaurService.GetByID(centralID) + if svcErr != nil { + return nil, svcErr } - converted := h.presenter.PresentManagedCentralWithSecrets(centralRequest) + converted, err := h.presenter.PresentManagedCentralWithSecrets(centralRequest) + if err != nil { + return nil, errors.GeneralError("failed to convert central request to managed central: %v", err) + } return converted, nil }, diff --git a/internal/dinosaur/pkg/presenters/managedcentral.go b/internal/dinosaur/pkg/presenters/managedcentral.go index 98d3bdd8f9..f5e5b6e21b 100644 --- a/internal/dinosaur/pkg/presenters/managedcentral.go +++ b/internal/dinosaur/pkg/presenters/managedcentral.go @@ -7,26 +7,33 @@ import ( "time" "github.com/golang/glog" + "github.com/pkg/errors" "github.com/stackrox/acs-fleet-manager/internal/dinosaur/pkg/api/dbapi" "github.com/stackrox/acs-fleet-manager/internal/dinosaur/pkg/api/private" "github.com/stackrox/acs-fleet-manager/internal/dinosaur/pkg/config" "github.com/stackrox/acs-fleet-manager/internal/dinosaur/pkg/defaults" + "github.com/stackrox/acs-fleet-manager/internal/dinosaur/pkg/gitops" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" + "sigs.k8s.io/yaml" ) // ManagedCentralPresenter helper service which converts Central DB representation to the private API representation type ManagedCentralPresenter struct { centralConfig *config.CentralConfig + gitopsService gitops.Service } // NewManagedCentralPresenter creates a new instance of ManagedCentralPresenter -func NewManagedCentralPresenter(config *config.CentralConfig) *ManagedCentralPresenter { - return &ManagedCentralPresenter{centralConfig: config} +func NewManagedCentralPresenter(config *config.CentralConfig, gitopsService gitops.Service) *ManagedCentralPresenter { + return &ManagedCentralPresenter{ + centralConfig: config, + gitopsService: gitopsService, + } } // PresentManagedCentral converts DB representation of Central to the private API representation -func (c *ManagedCentralPresenter) PresentManagedCentral(from *dbapi.CentralRequest) private.ManagedCentral { +func (c *ManagedCentralPresenter) PresentManagedCentral(from *dbapi.CentralRequest) (private.ManagedCentral, error) { var central dbapi.CentralSpec var scanner dbapi.ScannerSpec @@ -51,6 +58,16 @@ func (c *ManagedCentralPresenter) PresentManagedCentral(from *dbapi.CentralReque } } + centralCR, 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", @@ -134,6 +151,7 @@ func (c *ManagedCentralPresenter) PresentManagedCentral(from *dbapi.CentralReque }, }, }, + CentralCRYAML: string(centralYaml), }, RequestStatus: from.Status, ForceReconcile: from.ForceReconcile, @@ -143,18 +161,20 @@ func (c *ManagedCentralPresenter) PresentManagedCentral(from *dbapi.CentralReque res.Metadata.DeletionTimestamp = from.DeletionTimestamp.Format(time.RFC3339) } - return res + return res, nil } // PresentManagedCentralWithSecrets return a private.ManagedCentral including secret data -func (c *ManagedCentralPresenter) PresentManagedCentralWithSecrets(from *dbapi.CentralRequest) private.ManagedCentral { - managedCentral := c.PresentManagedCentral(from) +func (c *ManagedCentralPresenter) PresentManagedCentralWithSecrets(from *dbapi.CentralRequest) (private.ManagedCentral, error) { + managedCentral, err := c.PresentManagedCentral(from) + if err != nil { + return private.ManagedCentral{}, err + } secretInterfaceMap, err := from.Secrets.Object() secretStringMap := make(map[string]string, len(secretInterfaceMap)) if err != nil { - glog.Errorf("Failed to get Secrets for central request as map %q/%s: %v", from.Name, from.ID, err) - return managedCentral + return managedCentral, errors.Wrapf(err, "failed to get Secrets for central request as map %q/%s", from.Name, from.ID) } for k, v := range secretInterfaceMap { @@ -162,7 +182,7 @@ func (c *ManagedCentralPresenter) PresentManagedCentralWithSecrets(from *dbapi.C } managedCentral.Metadata.Secrets = secretStringMap // pragma: allowlist secret - return managedCentral + return managedCentral, nil } func orDefaultQty(qty resource.Quantity, def resource.Quantity) *resource.Quantity { @@ -204,3 +224,24 @@ func getSecretNames(from *dbapi.CentralRequest) []string { return secretNames } + +func centralParamsFromRequest(centralRequest *dbapi.CentralRequest) gitops.CentralParams { + return gitops.CentralParams{ + ID: centralRequest.ID, + Name: centralRequest.Name, + Namespace: centralRequest.Namespace, + Region: centralRequest.Region, + ClusterID: centralRequest.ClusterID, + CloudProvider: centralRequest.CloudProvider, + CloudAccountID: centralRequest.CloudAccountID, + SubscriptionID: centralRequest.SubscriptionID, + Owner: centralRequest.Owner, + OwnerAccountID: centralRequest.OwnerAccountID, + OwnerUserID: centralRequest.OwnerUserID, + Host: centralRequest.Host, + OrganizationID: centralRequest.OrganisationID, + OrganizationName: centralRequest.OrganisationName, + InstanceType: centralRequest.InstanceType, + IsInternal: centralRequest.Internal, + } +} diff --git a/internal/dinosaur/pkg/services/data_plane_cluster.go b/internal/dinosaur/pkg/services/data_plane_cluster.go index 66ae41f658..18b5457c37 100644 --- a/internal/dinosaur/pkg/services/data_plane_cluster.go +++ b/internal/dinosaur/pkg/services/data_plane_cluster.go @@ -35,7 +35,7 @@ type dataPlaneClusterService struct { } // NewDataPlaneClusterService ... -func NewDataPlaneClusterService(config dataPlaneClusterService) *dataPlaneClusterService { +func NewDataPlaneClusterService(config dataPlaneClusterService) DataPlaneClusterService { return &config } diff --git a/internal/dinosaur/pkg/services/data_plane_dinosaur.go b/internal/dinosaur/pkg/services/data_plane_dinosaur.go index faae4a64e2..a7c0357a88 100644 --- a/internal/dinosaur/pkg/services/data_plane_dinosaur.go +++ b/internal/dinosaur/pkg/services/data_plane_dinosaur.go @@ -6,13 +6,12 @@ import ( "strings" "time" - constants2 "github.com/stackrox/acs-fleet-manager/internal/dinosaur/constants" - "github.com/golang/glog" "github.com/pkg/errors" + "github.com/stackrox/acs-fleet-manager/internal/dinosaur/constants" "github.com/stackrox/acs-fleet-manager/internal/dinosaur/pkg/api/dbapi" - "github.com/stackrox/acs-fleet-manager/internal/dinosaur/pkg/config" "github.com/stackrox/acs-fleet-manager/pkg/api" + "github.com/stackrox/acs-fleet-manager/pkg/db" serviceError "github.com/stackrox/acs-fleet-manager/pkg/errors" "github.com/stackrox/acs-fleet-manager/pkg/logger" "github.com/stackrox/acs-fleet-manager/pkg/metrics" @@ -32,26 +31,31 @@ const ( // DataPlaneCentralService ... type DataPlaneCentralService interface { UpdateDataPlaneCentralService(ctx context.Context, clusterID string, status []*dbapi.DataPlaneCentralStatus) *serviceError.ServiceError + ListByClusterID(clusterID string) (dbapi.CentralList, *serviceError.ServiceError) } type dataPlaneCentralService struct { - dinosaurService DinosaurService - clusterService ClusterService - dinosaurConfig *config.CentralConfig + dinosaurService DinosaurService + clusterService ClusterService + connectionFactory *db.ConnectionFactory } // NewDataPlaneCentralService ... -func NewDataPlaneCentralService(dinosaurSrv DinosaurService, clusterSrv ClusterService, dinosaurConfig *config.CentralConfig) *dataPlaneCentralService { +func NewDataPlaneCentralService( + dinosaurSrv DinosaurService, + clusterSrv ClusterService, + connectionFactory *db.ConnectionFactory, +) DataPlaneCentralService { return &dataPlaneCentralService{ - dinosaurService: dinosaurSrv, - clusterService: clusterSrv, - dinosaurConfig: dinosaurConfig, + dinosaurService: dinosaurSrv, + clusterService: clusterSrv, + connectionFactory: connectionFactory, } } // UpdateDataPlaneCentralService ... -func (d *dataPlaneCentralService) UpdateDataPlaneCentralService(ctx context.Context, clusterID string, status []*dbapi.DataPlaneCentralStatus) *serviceError.ServiceError { - cluster, err := d.clusterService.FindClusterByID(clusterID) +func (s *dataPlaneCentralService) UpdateDataPlaneCentralService(ctx context.Context, clusterID string, status []*dbapi.DataPlaneCentralStatus) *serviceError.ServiceError { + cluster, err := s.clusterService.FindClusterByID(clusterID) log := logger.NewUHCLogger(ctx) if err != nil { return err @@ -61,7 +65,7 @@ func (d *dataPlaneCentralService) UpdateDataPlaneCentralService(ctx context.Cont return serviceError.BadRequest("Cluster id %s not found", clusterID) } for _, ks := range status { - dinosaur, getErr := d.dinosaurService.GetByID(ks.CentralClusterID) + dinosaur, getErr := s.dinosaurService.GetByID(ks.CentralClusterID) if getErr != nil { glog.Error(errors.Wrapf(getErr, "failed to get central cluster by id %s", ks.CentralClusterID)) continue @@ -71,22 +75,22 @@ func (d *dataPlaneCentralService) UpdateDataPlaneCentralService(ctx context.Cont continue } var e *serviceError.ServiceError - switch s := getStatus(ks); s { + switch getStatus(ks) { case statusReady: // Persist values only known once central is in statusReady e.g. routes, secrets - e = d.persistCentralValues(dinosaur, ks, cluster) + e = s.persistCentralValues(dinosaur, ks, cluster) if e == nil { - e = d.setCentralClusterReady(dinosaur) + e = s.setCentralClusterReady(dinosaur) } case statusError: // when getStatus returns statusError we know that the ready // condition will be there so there's no need to check for it readyCondition, _ := ks.GetReadyCondition() - e = d.setCentralClusterFailed(dinosaur, readyCondition.Message) + e = s.setCentralClusterFailed(dinosaur, readyCondition.Message) case statusDeleted: - e = d.setCentralClusterDeleting(dinosaur) + e = s.setCentralClusterDeleting(dinosaur) case statusRejected: - e = d.reassignCentralCluster(dinosaur) + e = s.reassignCentralCluster(dinosaur) case statusUnknown: log.Infof("central cluster %s status is unknown", ks.CentralClusterID) default: @@ -100,7 +104,22 @@ func (d *dataPlaneCentralService) UpdateDataPlaneCentralService(ctx context.Cont return nil } -func (d *dataPlaneCentralService) setCentralClusterReady(centralRequest *dbapi.CentralRequest) *serviceError.ServiceError { +// ListByClusterID returns a list of CentralRequests with specified clusterID +func (s *dataPlaneCentralService) ListByClusterID(clusterID string) (dbapi.CentralList, *serviceError.ServiceError) { + dbConn := s.connectionFactory.New(). + Where("cluster_id = ?", clusterID). + Where("status IN (?)", dinosaurManagedCRStatuses). + Where("host != ''") + + var centralRequests dbapi.CentralList + if err := dbConn.Find(¢ralRequests).Error; err != nil { + return nil, serviceError.NewWithCause(serviceError.ErrorGeneral, err, "unable to list central requests") + } + + return centralRequests, nil +} + +func (s *dataPlaneCentralService) setCentralClusterReady(centralRequest *dbapi.CentralRequest) *serviceError.ServiceError { if !centralRequest.RoutesCreated { logger.Logger.V(10).Infof("routes for central %s are not created", centralRequest.ID) return nil @@ -108,72 +127,72 @@ func (d *dataPlaneCentralService) setCentralClusterReady(centralRequest *dbapi.C logger.Logger.Infof("routes for central %s are created", centralRequest.ID) // only send metrics data if the current dinosaur request is in "provisioning" status as this is the only case we want to report - shouldSendMetric, err := d.checkCentralRequestCurrentStatus(centralRequest, constants2.CentralRequestStatusProvisioning) + shouldSendMetric, err := s.checkCentralRequestCurrentStatus(centralRequest, constants.CentralRequestStatusProvisioning) if err != nil { return err } - err = d.dinosaurService.Updates(centralRequest, map[string]interface{}{"failed_reason": "", "status": constants2.CentralRequestStatusReady.String()}) + err = s.dinosaurService.Updates(centralRequest, map[string]interface{}{"failed_reason": "", "status": constants.CentralRequestStatusReady.String()}) if err != nil { - return serviceError.NewWithCause(err.Code, err, "failed to update status %s for central cluster %s", constants2.CentralRequestStatusReady, centralRequest.ID) + return serviceError.NewWithCause(err.Code, err, "failed to update status %s for central cluster %s", constants.CentralRequestStatusReady, centralRequest.ID) } if shouldSendMetric { - metrics.UpdateCentralRequestsStatusSinceCreatedMetric(constants2.CentralRequestStatusReady, centralRequest.ID, centralRequest.ClusterID, time.Since(centralRequest.CreatedAt)) + metrics.UpdateCentralRequestsStatusSinceCreatedMetric(constants.CentralRequestStatusReady, centralRequest.ID, centralRequest.ClusterID, time.Since(centralRequest.CreatedAt)) metrics.UpdateCentralCreationDurationMetric(metrics.JobTypeCentralCreate, time.Since(centralRequest.CreatedAt)) - metrics.IncreaseCentralSuccessOperationsCountMetric(constants2.CentralOperationCreate) - metrics.IncreaseCentralTotalOperationsCountMetric(constants2.CentralOperationCreate) + metrics.IncreaseCentralSuccessOperationsCountMetric(constants.CentralOperationCreate) + metrics.IncreaseCentralTotalOperationsCountMetric(constants.CentralOperationCreate) } return nil } -func (d *dataPlaneCentralService) setCentralClusterFailed(centralRequest *dbapi.CentralRequest, errMessage string) *serviceError.ServiceError { +func (s *dataPlaneCentralService) setCentralClusterFailed(centralRequest *dbapi.CentralRequest, errMessage string) *serviceError.ServiceError { // if dinosaur was already reported as failed we don't do anything - if centralRequest.Status == string(constants2.CentralRequestStatusFailed) { + if centralRequest.Status == string(constants.CentralRequestStatusFailed) { return nil } // only send metrics data if the current dinosaur request is in "provisioning" status as this is the only case we want to report - shouldSendMetric, err := d.checkCentralRequestCurrentStatus(centralRequest, constants2.CentralRequestStatusProvisioning) + shouldSendMetric, err := s.checkCentralRequestCurrentStatus(centralRequest, constants.CentralRequestStatusProvisioning) if err != nil { return err } - centralRequest.Status = string(constants2.CentralRequestStatusFailed) + centralRequest.Status = string(constants.CentralRequestStatusFailed) centralRequest.FailedReason = fmt.Sprintf("Central reported as failed: '%s'", errMessage) - err = d.dinosaurService.Update(centralRequest) + err = s.dinosaurService.Update(centralRequest) if err != nil { - return serviceError.NewWithCause(err.Code, err, "failed to update central cluster to %s status for central cluster %s", constants2.CentralRequestStatusFailed, centralRequest.ID) + return serviceError.NewWithCause(err.Code, err, "failed to update central cluster to %s status for central cluster %s", constants.CentralRequestStatusFailed, centralRequest.ID) } if shouldSendMetric { - metrics.UpdateCentralRequestsStatusSinceCreatedMetric(constants2.CentralRequestStatusFailed, centralRequest.ID, centralRequest.ClusterID, time.Since(centralRequest.CreatedAt)) - metrics.IncreaseCentralTotalOperationsCountMetric(constants2.CentralOperationCreate) + metrics.UpdateCentralRequestsStatusSinceCreatedMetric(constants.CentralRequestStatusFailed, centralRequest.ID, centralRequest.ClusterID, time.Since(centralRequest.CreatedAt)) + metrics.IncreaseCentralTotalOperationsCountMetric(constants.CentralOperationCreate) } logger.Logger.Errorf("Central status for Central ID '%s' in ClusterID '%s' reported as failed by Fleet Shard Operator: '%s'", centralRequest.ID, centralRequest.ClusterID, errMessage) return nil } -func (d *dataPlaneCentralService) setCentralClusterDeleting(centralRequest *dbapi.CentralRequest) *serviceError.ServiceError { +func (s *dataPlaneCentralService) setCentralClusterDeleting(centralRequest *dbapi.CentralRequest) *serviceError.ServiceError { // If the Dinosaur cluster is deleted from the data plane cluster, we will make it as "deleting" in db and the reconcilier will ensure it is cleaned up properly - if ok, updateErr := d.dinosaurService.UpdateStatus(centralRequest.ID, constants2.CentralRequestStatusDeleting); ok { + if ok, updateErr := s.dinosaurService.UpdateStatus(centralRequest.ID, constants.CentralRequestStatusDeleting); ok { if updateErr != nil { - return serviceError.NewWithCause(updateErr.Code, updateErr, "failed to update status %s for central cluster %s", constants2.CentralRequestStatusDeleting, centralRequest.ID) + return serviceError.NewWithCause(updateErr.Code, updateErr, "failed to update status %s for central cluster %s", constants.CentralRequestStatusDeleting, centralRequest.ID) } - metrics.UpdateCentralRequestsStatusSinceCreatedMetric(constants2.CentralRequestStatusDeleting, centralRequest.ID, centralRequest.ClusterID, time.Since(centralRequest.CreatedAt)) + metrics.UpdateCentralRequestsStatusSinceCreatedMetric(constants.CentralRequestStatusDeleting, centralRequest.ID, centralRequest.ClusterID, time.Since(centralRequest.CreatedAt)) } return nil } -func (d *dataPlaneCentralService) reassignCentralCluster(centralRequest *dbapi.CentralRequest) *serviceError.ServiceError { - if centralRequest.Status == constants2.CentralRequestStatusProvisioning.String() { +func (s *dataPlaneCentralService) reassignCentralCluster(centralRequest *dbapi.CentralRequest) *serviceError.ServiceError { + if centralRequest.Status == constants.CentralRequestStatusProvisioning.String() { // If a Dinosaur cluster is rejected by the fleetshard-operator, it should be assigned to another OSD cluster (via some scheduler service in the future). // But now we only have one OSD cluster, so we need to change the placementId field so that the fleetshard-operator will try it again // In the future, we may consider adding a new table to track the placement history for dinosaur clusters if there are multiple OSD clusters and the value here can be the key of that table centralRequest.PlacementID = api.NewID() - if err := d.dinosaurService.Update(centralRequest); err != nil { + if err := s.dinosaurService.Update(centralRequest); err != nil { return err } - metrics.UpdateCentralRequestsStatusSinceCreatedMetric(constants2.CentralRequestStatusProvisioning, centralRequest.ID, centralRequest.ClusterID, time.Since(centralRequest.CreatedAt)) + metrics.UpdateCentralRequestsStatusSinceCreatedMetric(constants.CentralRequestStatusProvisioning, centralRequest.ID, centralRequest.ClusterID, time.Since(centralRequest.CreatedAt)) } else { logger.Logger.Infof("central cluster %s is rejected and current status is %s", centralRequest.ID, centralRequest.Status) } @@ -206,9 +225,9 @@ func getStatus(status *dbapi.DataPlaneCentralStatus) centralStatus { } return statusInstalling } -func (d *dataPlaneCentralService) checkCentralRequestCurrentStatus(centralRequest *dbapi.CentralRequest, status constants2.CentralStatus) (bool, *serviceError.ServiceError) { +func (s *dataPlaneCentralService) checkCentralRequestCurrentStatus(centralRequest *dbapi.CentralRequest, status constants.CentralStatus) (bool, *serviceError.ServiceError) { matchStatus := false - if currentInstance, err := d.dinosaurService.GetByID(centralRequest.ID); err != nil { + if currentInstance, err := s.dinosaurService.GetByID(centralRequest.ID); err != nil { return matchStatus, err } else if currentInstance.Status == status.String() { matchStatus = true @@ -216,29 +235,29 @@ func (d *dataPlaneCentralService) checkCentralRequestCurrentStatus(centralReques return matchStatus, nil } -func (d *dataPlaneCentralService) persistCentralValues(centralRequest *dbapi.CentralRequest, centralStatus *dbapi.DataPlaneCentralStatus, cluster *api.Cluster) *serviceError.ServiceError { - if err := d.addRoutesToRequest(centralRequest, centralStatus, cluster); err != nil { +func (s *dataPlaneCentralService) persistCentralValues(centralRequest *dbapi.CentralRequest, centralStatus *dbapi.DataPlaneCentralStatus, cluster *api.Cluster) *serviceError.ServiceError { + if err := s.addRoutesToRequest(centralRequest, centralStatus, cluster); err != nil { return err } - if err := d.addSecretsToRequest(centralRequest, centralStatus, cluster); err != nil { + if err := s.addSecretsToRequest(centralRequest, centralStatus, cluster); err != nil { return err } - if err := d.dinosaurService.Update(centralRequest); err != nil { + if err := s.dinosaurService.Update(centralRequest); err != nil { return serviceError.NewWithCause(err.Code, err, "failed to update routes for central cluster %s", centralRequest.ID) } return nil } -func (d *dataPlaneCentralService) addRoutesToRequest(centralRequest *dbapi.CentralRequest, centralStatus *dbapi.DataPlaneCentralStatus, cluster *api.Cluster) *serviceError.ServiceError { +func (s *dataPlaneCentralService) addRoutesToRequest(centralRequest *dbapi.CentralRequest, centralStatus *dbapi.DataPlaneCentralStatus, cluster *api.Cluster) *serviceError.ServiceError { if centralRequest.Routes != nil { logger.Logger.V(10).Infof("skip persisting routes for Central %s as they are already stored", centralRequest.ID) return nil } logger.Logger.Infof("store routes information for central %s", centralRequest.ID) - clusterDNS, err := d.clusterService.GetClusterDNS(cluster.ClusterID) + clusterDNS, err := s.clusterService.GetClusterDNS(cluster.ClusterID) if err != nil { return serviceError.NewWithCause(err.Code, err, "failed to get DNS entry for cluster %s", cluster.ClusterID) } diff --git a/internal/dinosaur/pkg/services/dinosaur.go b/internal/dinosaur/pkg/services/dinosaur.go index 70a72aea20..9a397d1be2 100644 --- a/internal/dinosaur/pkg/services/dinosaur.go +++ b/internal/dinosaur/pkg/services/dinosaur.go @@ -15,9 +15,6 @@ import ( "github.com/stackrox/acs-fleet-manager/internal/dinosaur/pkg/config" "github.com/stackrox/acs-fleet-manager/internal/dinosaur/pkg/dinosaurs/types" "github.com/stackrox/acs-fleet-manager/pkg/services" - "github.com/stackrox/acs-fleet-manager/pkg/services/sso" - - "github.com/stackrox/acs-fleet-manager/pkg/services/authorization" coreServices "github.com/stackrox/acs-fleet-manager/pkg/services/queryparser" "github.com/golang/glog" @@ -83,7 +80,6 @@ type DinosaurService interface { // The Dinosaur Request in the database will be updated with a deleted_at timestamp. Delete(centralRequest *dbapi.CentralRequest, force bool) *errors.ServiceError List(ctx context.Context, listArgs *services.ListArguments) (dbapi.CentralList, *api.PagingMeta, *errors.ServiceError) - ListByClusterID(clusterID string) ([]*dbapi.CentralRequest, *errors.ServiceError) RegisterDinosaurJob(dinosaurRequest *dbapi.CentralRequest) *errors.ServiceError ListByStatus(status ...dinosaurConstants.CentralStatus) ([]*dbapi.CentralRequest, *errors.ServiceError) // UpdateStatus change the status of the Dinosaur cluster @@ -118,13 +114,11 @@ var _ DinosaurService = &dinosaurService{} type dinosaurService struct { connectionFactory *db.ConnectionFactory clusterService ClusterService - iamService sso.IAMService dinosaurConfig *config.CentralConfig awsConfig *config.AWSConfig quotaServiceFactory QuotaServiceFactory mu sync.Mutex awsClientFactory aws.ClientFactory - authService authorization.Authorization dataplaneClusterConfig *config.DataplaneClusterConfig clusterPlacementStrategy ClusterPlacementStrategy amsClient ocm.AMSClient @@ -134,20 +128,18 @@ type dinosaurService struct { } // NewDinosaurService ... -func NewDinosaurService(connectionFactory *db.ConnectionFactory, clusterService ClusterService, iamService sso.IAMService, +func NewDinosaurService(connectionFactory *db.ConnectionFactory, clusterService ClusterService, iamConfig *iam.IAMConfig, dinosaurConfig *config.CentralConfig, dataplaneClusterConfig *config.DataplaneClusterConfig, awsConfig *config.AWSConfig, - quotaServiceFactory QuotaServiceFactory, awsClientFactory aws.ClientFactory, authorizationService authorization.Authorization, - clusterPlacementStrategy ClusterPlacementStrategy, amsClient ocm.AMSClient, centralDefaultVersionService CentralDefaultVersionService) *dinosaurService { + quotaServiceFactory QuotaServiceFactory, awsClientFactory aws.ClientFactory, + clusterPlacementStrategy ClusterPlacementStrategy, amsClient ocm.AMSClient, centralDefaultVersionService CentralDefaultVersionService) DinosaurService { return &dinosaurService{ connectionFactory: connectionFactory, clusterService: clusterService, - iamService: iamService, iamConfig: iamConfig, dinosaurConfig: dinosaurConfig, awsConfig: awsConfig, quotaServiceFactory: quotaServiceFactory, awsClientFactory: awsClientFactory, - authService: authorizationService, dataplaneClusterConfig: dataplaneClusterConfig, clusterPlacementStrategy: clusterPlacementStrategy, amsClient: amsClient, @@ -676,21 +668,6 @@ func (k *dinosaurService) List(ctx context.Context, listArgs *services.ListArgum return dinosaurRequestList, pagingMeta, nil } -// ListByClusterID returns a list of CentralRequests with specified clusterID -func (k *dinosaurService) ListByClusterID(clusterID string) ([]*dbapi.CentralRequest, *errors.ServiceError) { - dbConn := k.connectionFactory.New(). - Where("cluster_id = ?", clusterID). - Where("status IN (?)", dinosaurManagedCRStatuses). - Where("host != ''") - - var dinosaurRequestList dbapi.CentralList - if err := dbConn.Find(&dinosaurRequestList).Error; err != nil { - return nil, errors.NewWithCause(errors.ErrorGeneral, err, "unable to list central requests") - } - - return dinosaurRequestList, nil -} - // Update ... func (k *dinosaurService) Update(dinosaurRequest *dbapi.CentralRequest) *errors.ServiceError { dbConn := k.connectionFactory.New(). diff --git a/internal/dinosaur/pkg/services/dinosaurservice_moq.go b/internal/dinosaur/pkg/services/dinosaurservice_moq.go index a9231f48bf..7f4e2a6593 100644 --- a/internal/dinosaur/pkg/services/dinosaurservice_moq.go +++ b/internal/dinosaur/pkg/services/dinosaurservice_moq.go @@ -64,9 +64,6 @@ var _ DinosaurService = &DinosaurServiceMock{} // ListFunc: func(ctx context.Context, listArgs *services.ListArguments) (dbapi.CentralList, *api.PagingMeta, *serviceError.ServiceError) { // panic("mock out the List method") // }, -// ListByClusterIDFunc: func(clusterID string) ([]*dbapi.CentralRequest, *serviceError.ServiceError) { -// panic("mock out the ListByClusterID method") -// }, // ListByStatusFunc: func(status ...dinosaurConstants.CentralStatus) ([]*dbapi.CentralRequest, *serviceError.ServiceError) { // panic("mock out the ListByStatus method") // }, @@ -149,9 +146,6 @@ type DinosaurServiceMock struct { // ListFunc mocks the List method. ListFunc func(ctx context.Context, listArgs *services.ListArguments) (dbapi.CentralList, *api.PagingMeta, *serviceError.ServiceError) - // ListByClusterIDFunc mocks the ListByClusterID method. - ListByClusterIDFunc func(clusterID string) ([]*dbapi.CentralRequest, *serviceError.ServiceError) - // ListByStatusFunc mocks the ListByStatus method. ListByStatusFunc func(status ...dinosaurConstants.CentralStatus) ([]*dbapi.CentralRequest, *serviceError.ServiceError) @@ -261,11 +255,6 @@ type DinosaurServiceMock struct { // ListArgs is the listArgs argument value. ListArgs *services.ListArguments } - // ListByClusterID holds details about calls to the ListByClusterID method. - ListByClusterID []struct { - // ClusterID is the clusterID argument value. - ClusterID string - } // ListByStatus holds details about calls to the ListByStatus method. ListByStatus []struct { // Status is the status argument value. @@ -348,7 +337,6 @@ type DinosaurServiceMock struct { lockGetCNAMERecordStatus sync.RWMutex lockHasAvailableCapacityInRegion sync.RWMutex lockList sync.RWMutex - lockListByClusterID sync.RWMutex lockListByStatus sync.RWMutex lockListCentralsWithoutAuthConfig sync.RWMutex lockListDinosaursWithRoutesNotCreated sync.RWMutex @@ -790,38 +778,6 @@ func (mock *DinosaurServiceMock) ListCalls() []struct { return calls } -// ListByClusterID calls ListByClusterIDFunc. -func (mock *DinosaurServiceMock) ListByClusterID(clusterID string) ([]*dbapi.CentralRequest, *serviceError.ServiceError) { - if mock.ListByClusterIDFunc == nil { - panic("DinosaurServiceMock.ListByClusterIDFunc: method is nil but DinosaurService.ListByClusterID was just called") - } - callInfo := struct { - ClusterID string - }{ - ClusterID: clusterID, - } - mock.lockListByClusterID.Lock() - mock.calls.ListByClusterID = append(mock.calls.ListByClusterID, callInfo) - mock.lockListByClusterID.Unlock() - return mock.ListByClusterIDFunc(clusterID) -} - -// ListByClusterIDCalls gets all the calls that were made to ListByClusterID. -// Check the length with: -// -// len(mockedDinosaurService.ListByClusterIDCalls()) -func (mock *DinosaurServiceMock) ListByClusterIDCalls() []struct { - ClusterID string -} { - var calls []struct { - ClusterID string - } - mock.lockListByClusterID.RLock() - calls = mock.calls.ListByClusterID - mock.lockListByClusterID.RUnlock() - return calls -} - // ListByStatus calls ListByStatusFunc. func (mock *DinosaurServiceMock) ListByStatus(status ...dinosaurConstants.CentralStatus) ([]*dbapi.CentralRequest, *serviceError.ServiceError) { if mock.ListByStatusFunc == nil { diff --git a/internal/dinosaur/providers.go b/internal/dinosaur/providers.go index 212d52ce7a..0809a46d4b 100644 --- a/internal/dinosaur/providers.go +++ b/internal/dinosaur/providers.go @@ -6,6 +6,7 @@ import ( "github.com/stackrox/acs-fleet-manager/internal/dinosaur/pkg/clusters" "github.com/stackrox/acs-fleet-manager/internal/dinosaur/pkg/config" "github.com/stackrox/acs-fleet-manager/internal/dinosaur/pkg/environments" + "github.com/stackrox/acs-fleet-manager/internal/dinosaur/pkg/gitops" "github.com/stackrox/acs-fleet-manager/internal/dinosaur/pkg/handlers" "github.com/stackrox/acs-fleet-manager/internal/dinosaur/pkg/migrations" "github.com/stackrox/acs-fleet-manager/internal/dinosaur/pkg/presenters" @@ -55,13 +56,13 @@ func ConfigProviders() di.Option { func ServiceProviders() di.Option { return di.Options( di.Provide(services.NewClusterService), - di.Provide(services.NewDinosaurService, di.As(new(services.DinosaurService))), + di.Provide(services.NewDinosaurService), di.Provide(services.NewCloudProvidersService), di.Provide(services.NewObservatoriumService), di.Provide(services.NewFleetshardOperatorAddon), di.Provide(services.NewClusterPlacementStrategy), - di.Provide(services.NewDataPlaneClusterService, di.As(new(services.DataPlaneClusterService))), - di.Provide(services.NewDataPlaneCentralService, di.As(new(services.DataPlaneCentralService))), + di.Provide(services.NewDataPlaneClusterService), + di.Provide(services.NewDataPlaneCentralService), di.Provide(handlers.NewAuthenticationBuilder), di.Provide(clusters.NewDefaultProviderFactory, di.As(new(clusters.ProviderFactory))), di.Provide(routes.NewRouteLoader), @@ -75,6 +76,9 @@ func ServiceProviders() di.Option { di.Provide(dinosaurmgrs.NewReadyDinosaurManager, di.As(new(workers.Worker))), di.Provide(dinosaurmgrs.NewDinosaurCNAMEManager, di.As(new(workers.Worker))), di.Provide(dinosaurmgrs.NewCentralAuthConfigManager, di.As(new(workers.Worker))), + di.Provide(gitops.NewEmptyReader), + di.Provide(gitops.NewProvider), + di.Provide(gitops.NewService), di.Provide(presenters.NewManagedCentralPresenter), ) }