diff --git a/app/services/infraprovider/infraprovider.go b/app/services/infraprovider/infraprovider.go index cf0478a33..6db4ab006 100644 --- a/app/services/infraprovider/infraprovider.go +++ b/app/services/infraprovider/infraprovider.go @@ -29,6 +29,7 @@ func NewService( templateStore store.InfraProviderTemplateStore, factory infraprovider.Factory, spaceFinder refcache.SpaceFinder, + gatewayStore store.CDEGatewayStore, ) *Service { return &Service{ tx: tx, @@ -38,6 +39,7 @@ func NewService( infraProviderFactory: factory, spaceFinder: spaceFinder, gitspaceConfigStore: gitspaceConfigStore, + gatewayStore: gatewayStore, } } @@ -49,4 +51,5 @@ type Service struct { infraProviderTemplateStore store.InfraProviderTemplateStore infraProviderFactory infraprovider.Factory spaceFinder refcache.SpaceFinder + gatewayStore store.CDEGatewayStore } diff --git a/app/services/infraprovider/list_gateways.go b/app/services/infraprovider/list_gateways.go new file mode 100644 index 000000000..1c665e320 --- /dev/null +++ b/app/services/infraprovider/list_gateways.go @@ -0,0 +1,70 @@ +// Copyright 2023 Harness, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package infraprovider + +import ( + "context" + "fmt" + "time" + + "github.com/harness/gitness/types" +) + +func (c *Service) ListGateways(ctx context.Context, filter *types.CDEGatewayFilter) ([]*types.CDEGateway, error) { + if filter == nil || len(filter.InfraProviderConfigIDs) == 0 { + return nil, fmt.Errorf("cde-gateway filter is required") + } + + if filter.HealthReportValidityInMins == 0 { + filter.HealthReportValidityInMins = 5 + } + + gateways, err := c.gatewayStore.List(ctx, filter) + if err != nil { + return nil, fmt.Errorf("failed to list gateways: %w", err) + } + + infraProviderConfigMap := make(map[int64]string) + for _, gateway := range gateways { + if _, ok := infraProviderConfigMap[gateway.InfraProviderConfigID]; !ok { + infraProviderConfig, err := c.infraProviderConfigStore.Find(ctx, gateway.InfraProviderConfigID, false) + if err != nil { + return nil, fmt.Errorf("failed to find infra provider config %d while listing gateways: %w", + gateway.InfraProviderConfigID, err) + } + infraProviderConfigMap[gateway.InfraProviderConfigID] = infraProviderConfig.Identifier + } + gateway.InfraProviderConfigIdentifier = infraProviderConfigMap[gateway.InfraProviderConfigID] + + spaceCore, err := c.spaceFinder.FindByID(ctx, gateway.SpaceID) + if err != nil { + return nil, fmt.Errorf("failed to find space %d while listing gateways: %w", gateway.SpaceID, err) + } + + gateway.SpacePath = spaceCore.Path + + if gateway.Updated < time.Now().Add(-time.Duration(filter.HealthReportValidityInMins)*time.Minute).UnixMilli() { + gateway.Health = types.GatewayHealthUnhealthy + } + + if gateway.Health != types.GatewayHealthHealthy || gateway.EnvoyHealth != types.GatewayHealthHealthy { + gateway.OverallHealth = types.GatewayHealthUnhealthy + } else { + gateway.OverallHealth = types.GatewayHealthHealthy + } + } + + return gateways, nil +} diff --git a/app/services/infraprovider/report_stats.go b/app/services/infraprovider/report_stats.go new file mode 100644 index 000000000..4e95b5ebf --- /dev/null +++ b/app/services/infraprovider/report_stats.go @@ -0,0 +1,47 @@ +// Copyright 2023 Harness, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package infraprovider + +import ( + "context" + "time" + + "github.com/harness/gitness/types" +) + +func (c *Service) ReportStats( + ctx context.Context, + spaceCore *types.SpaceCore, + infraProviderConfig *types.InfraProviderConfig, + in *types.CDEGatewayStats, +) error { + gateway := types.CDEGateway{ + InfraProviderConfigID: infraProviderConfig.ID, + InfraProviderConfigIdentifier: infraProviderConfig.Identifier, + SpaceID: spaceCore.ID, + SpacePath: spaceCore.Path, + } + gateway.Name = in.Name + gateway.GroupName = in.GroupName + gateway.Region = in.Region + gateway.Zone = in.Zone + gateway.Version = in.Version + gateway.Health = in.Health + gateway.EnvoyHealth = in.EnvoyHealth + gateway.Created = time.Now().UnixMilli() + gateway.Updated = gateway.Created + + return c.gatewayStore.Upsert(ctx, &gateway) +} diff --git a/app/services/infraprovider/wire.go b/app/services/infraprovider/wire.go index a54f93820..71e54776f 100644 --- a/app/services/infraprovider/wire.go +++ b/app/services/infraprovider/wire.go @@ -35,7 +35,8 @@ func ProvideInfraProvider( templateStore store.InfraProviderTemplateStore, infraProviderFactory infraprovider.Factory, spaceFinder refcache.SpaceFinder, + gatewayStore store.CDEGatewayStore, ) *Service { return NewService(tx, gitspaceConfigStore, resourceStore, configStore, templateStore, infraProviderFactory, - spaceFinder) + spaceFinder, gatewayStore) } diff --git a/app/store/database.go b/app/store/database.go index 46ebe9c53..943bb9c67 100644 --- a/app/store/database.go +++ b/app/store/database.go @@ -1352,4 +1352,9 @@ type ( end int64, ) ([]types.UsageMetric, error) } + + CDEGatewayStore interface { + Upsert(ctx context.Context, in *types.CDEGateway) error + List(ctx context.Context, filter *types.CDEGatewayFilter) ([]*types.CDEGateway, error) + } ) diff --git a/app/store/database/cde_gateway.go b/app/store/database/cde_gateway.go new file mode 100644 index 000000000..c5d43c8b0 --- /dev/null +++ b/app/store/database/cde_gateway.go @@ -0,0 +1,179 @@ +// Copyright 2023 Harness, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package database + +import ( + "context" + "time" + + "github.com/harness/gitness/app/store" + "github.com/harness/gitness/store/database" + "github.com/harness/gitness/store/database/dbtx" + "github.com/harness/gitness/types" + + "github.com/Masterminds/squirrel" + "github.com/jmoiron/sqlx" + "github.com/pkg/errors" +) + +var _ store.CDEGatewayStore = (*CDEGatewayStore)(nil) + +const ( + cdeGatewayIDColumn = `cgate_id` + cdeGatewayInsertColumns = ` + cgate_name, + cgate_group_name, + cgate_space_id, + cgate_infra_provider_config_id, + cgate_region, + cgate_zone, + cgate_version, + cgate_health, + cgate_envoy_health, + cgate_created, + cgate_updated + ` + cdeGatewaySelectColumns = cdeGatewayIDColumn + "," + cdeGatewayInsertColumns + cdeGatewayTable = `cde_gateways` +) + +// NewCDEGatewayStore returns a new CDEGatewayStore. +func NewCDEGatewayStore(db *sqlx.DB) *CDEGatewayStore { + return &CDEGatewayStore{ + db: db, + } +} + +// CDEGatewayStore implements store.CDEGatewayStore backed by a relational database. +type CDEGatewayStore struct { + db *sqlx.DB +} + +func (c *CDEGatewayStore) Upsert(ctx context.Context, in *types.CDEGateway) error { + stmt := database.Builder. + Insert(cdeGatewayTable). + Columns(cdeGatewayInsertColumns). + Values( + in.Name, + in.GroupName, + in.SpaceID, + in.InfraProviderConfigID, + in.Region, + in.Zone, + in.Version, + in.Health, + in.EnvoyHealth, + in.Created, + in.Updated). + Suffix(` +ON CONFLICT (cgate_space_id, cgate_infra_provider_config_id, cgate_region, cgate_group_name, cgate_name) +DO UPDATE +SET + cgate_health = EXCLUDED.cgate_health, + cgate_envoy_health = EXCLUDED.cgate_envoy_health, + cgate_updated = EXCLUDED.cgate_updated, + cgate_zone = EXCLUDED.cgate_zone, + cgate_version = EXCLUDED.cgate_version`) + + sql, args, err := stmt.ToSql() + if err != nil { + return errors.Wrap(err, "Failed to convert squirrel builder to sql") + } + db := dbtx.GetAccessor(ctx, c.db) + if _, err = db.ExecContext(ctx, sql, args...); err != nil { + return database.ProcessSQLErrorf( + ctx, err, "cde gateway upsert create query failed for %s", in.Name) + } + return nil +} + +func (c *CDEGatewayStore) List(ctx context.Context, filter *types.CDEGatewayFilter) ([]*types.CDEGateway, error) { + stmt := database.Builder. + Select(cdeGatewaySelectColumns). + From(cdeGatewayTable) + + if filter != nil && len(filter.InfraProviderConfigIDs) > 0 { + stmt = stmt.Where(squirrel.Eq{"cgate_infra_provider_config_id": filter.InfraProviderConfigIDs}) + } + + if filter != nil && filter.Health == types.GatewayHealthHealthy { + stmt = stmt.Where(squirrel.Eq{"cgate_health": filter.Health}). + Where(squirrel.Eq{"cgate_envoy_health": filter.Health}). + Where(squirrel.Gt{"cgate_updated": time.Now().Add( + -time.Duration(filter.HealthReportValidityInMins) * time.Minute).UnixMilli()}) + } + + if filter != nil && filter.Health == types.GatewayHealthUnhealthy { + stmt = stmt.Where( + squirrel.Or{ + squirrel.LtOrEq{"cgate_updated": time.Now().Add( + time.Minute * -time.Duration(filter.HealthReportValidityInMins)).UnixMilli()}, + squirrel.Eq{"cgate_envoy_health": filter.Health}, + }, + ) + } + + sql, args, err := stmt.ToSql() + if err != nil { + return nil, errors.Wrap(err, "Failed to convert squirrel builder to sql") + } + + db := dbtx.GetAccessor(ctx, c.db) + dst := new([]*cdeGateway) + if err := db.SelectContext(ctx, dst, sql, args...); err != nil { + return nil, database.ProcessSQLErrorf(ctx, err, "Failed to list cde gatways") + } + + return entitiesToDTOs(*dst), nil +} + +type cdeGateway struct { + ID int64 `db:"cgate_id"` + Name string `db:"cgate_name"` + GroupName string `db:"cgate_group_name"` + SpaceID int64 `db:"cgate_space_id"` + InfraProviderConfigID int64 `db:"cgate_infra_provider_config_id"` + Region string `db:"cgate_region"` + Zone string `db:"cgate_zone"` + Version string `db:"cgate_version"` + Health string `db:"cgate_health"` + EnvoyHealth string `db:"cgate_envoy_health"` + Created int64 `db:"cgate_created"` + Updated int64 `db:"cgate_updated"` +} + +func entitiesToDTOs(entities []*cdeGateway) []*types.CDEGateway { + var dtos []*types.CDEGateway + for _, entity := range entities { + dtos = append(dtos, entityToDTO(*entity)) + } + return dtos +} + +func entityToDTO(entity cdeGateway) *types.CDEGateway { + dto := &types.CDEGateway{} + dto.Name = entity.Name + dto.GroupName = entity.GroupName + dto.SpaceID = entity.SpaceID + dto.InfraProviderConfigID = entity.InfraProviderConfigID + dto.Region = entity.Region + dto.Zone = entity.Zone + dto.Version = entity.Version + dto.Health = entity.Health + dto.EnvoyHealth = entity.EnvoyHealth + dto.Created = entity.Created + dto.Updated = entity.Updated + return dto +} diff --git a/app/store/database/migrate/postgres/0107_create_table_cde_gateways.down.sql b/app/store/database/migrate/postgres/0107_create_table_cde_gateways.down.sql new file mode 100644 index 000000000..8733c66b2 --- /dev/null +++ b/app/store/database/migrate/postgres/0107_create_table_cde_gateways.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS cde_gateways; \ No newline at end of file diff --git a/app/store/database/migrate/postgres/0107_create_table_cde_gateways.up.sql b/app/store/database/migrate/postgres/0107_create_table_cde_gateways.up.sql new file mode 100644 index 000000000..72c348211 --- /dev/null +++ b/app/store/database/migrate/postgres/0107_create_table_cde_gateways.up.sql @@ -0,0 +1,26 @@ +CREATE TABLE IF NOT EXISTS cde_gateways +( + cgate_id BIGSERIAL PRIMARY KEY, + cgate_name TEXT NOT NULL, + cgate_group_name TEXT NOT NULL, + cgate_region TEXT NOT NULL, + cgate_zone TEXT NOT NULL, + cgate_version TEXT NOT NULL, + cgate_health TEXT NOT NULL, + cgate_space_id BIGINT NOT NULL, + cgate_infra_provider_config_id BIGINT NOT NULL, + cgate_envoy_health TEXT NOT NULL, + cgate_created BIGINT NOT NULL, + cgate_updated BIGINT NOT NULL, + CONSTRAINT fk_cgate_space_id FOREIGN KEY (cgate_space_id) + REFERENCES spaces (space_id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE NO ACTION, + CONSTRAINT fk_cgate_infra_provider_config_id FOREIGN KEY (cgate_infra_provider_config_id) + REFERENCES infra_provider_configs (ipconf_id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE NO ACTION +); + +CREATE UNIQUE INDEX IF NOT EXISTS cde_gateways_space_id_infra_config_id_region_group_name_name + ON cde_gateways (cgate_space_id, cgate_infra_provider_config_id, cgate_region, cgate_group_name, cgate_name); \ No newline at end of file diff --git a/app/store/database/migrate/sqlite/0107_create_table_cde_gateways.down.sql b/app/store/database/migrate/sqlite/0107_create_table_cde_gateways.down.sql new file mode 100644 index 000000000..8733c66b2 --- /dev/null +++ b/app/store/database/migrate/sqlite/0107_create_table_cde_gateways.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS cde_gateways; \ No newline at end of file diff --git a/app/store/database/migrate/sqlite/0107_create_table_cde_gateways.up.sql b/app/store/database/migrate/sqlite/0107_create_table_cde_gateways.up.sql new file mode 100644 index 000000000..cc3c83b65 --- /dev/null +++ b/app/store/database/migrate/sqlite/0107_create_table_cde_gateways.up.sql @@ -0,0 +1,26 @@ +CREATE TABLE IF NOT EXISTS cde_gateways +( + cgate_id INTEGER PRIMARY KEY AUTOINCREMENT, + cgate_name TEXT NOT NULL, + cgate_group_name TEXT NOT NULL, + cgate_region TEXT NOT NULL, + cgate_zone TEXT NOT NULL, + cgate_version TEXT NOT NULL, + cgate_health TEXT NOT NULL, + cgate_space_id BIGINT NOT NULL, + cgate_infra_provider_config_id BIGINT NOT NULL, + cgate_envoy_health TEXT NOT NULL, + cgate_created BIGINT NOT NULL, + cgate_updated BIGINT NOT NULL, + CONSTRAINT fk_cgate_space_id FOREIGN KEY (cgate_space_id) + REFERENCES spaces (space_id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE NO ACTION, + CONSTRAINT fk_cgate_infra_provider_config_id FOREIGN KEY (cgate_infra_provider_config_id) + REFERENCES infra_provider_configs (ipconf_id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE NO ACTION +); + +CREATE UNIQUE INDEX IF NOT EXISTS cde_gateways_space_id_infra_config_id_region_group_name_name + ON cde_gateways (cgate_space_id, cgate_infra_provider_config_id, cgate_region, cgate_group_name, cgate_name); \ No newline at end of file diff --git a/app/store/database/wire.go b/app/store/database/wire.go index 3f7b30815..33371ca3b 100644 --- a/app/store/database/wire.go +++ b/app/store/database/wire.go @@ -74,6 +74,7 @@ var WireSet = wire.NewSet( ProvideInfraProviderTemplateStore, ProvideInfraProvisionedStore, ProvideUsageMetricStore, + ProvideCDEGatewayStore, ) // migrator is helper function to set up the database by performing automated @@ -355,3 +356,7 @@ func ProvideInfraProvisionedStore(db *sqlx.DB) store.InfraProvisionedStore { func ProvideUsageMetricStore(db *sqlx.DB) store.UsageMetricStore { return NewUsageMetricsStore(db) } + +func ProvideCDEGatewayStore(db *sqlx.DB) store.CDEGatewayStore { + return NewCDEGatewayStore(db) +} diff --git a/cmd/gitness/wire_gen.go b/cmd/gitness/wire_gen.go index 926984aa8..b3798929a 100644 --- a/cmd/gitness/wire_gen.go +++ b/cmd/gitness/wire_gen.go @@ -347,7 +347,8 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro } dockerProvider := infraprovider.ProvideDockerProvider(dockerConfig, dockerClientFactory, reporter4) factory := infraprovider.ProvideFactory(dockerProvider) - infraproviderService := infraprovider2.ProvideInfraProvider(transactor, gitspaceConfigStore, infraProviderResourceStore, infraProviderConfigStore, infraProviderTemplateStore, factory, spaceFinder) + cdeGatewayStore := database.ProvideCDEGatewayStore(db) + infraproviderService := infraprovider2.ProvideInfraProvider(transactor, gitspaceConfigStore, infraProviderResourceStore, infraProviderConfigStore, infraProviderTemplateStore, factory, spaceFinder, cdeGatewayStore) gitnessSCM := scm.ProvideGitnessSCM(repoStore, repoFinder, gitInterface, tokenStore, principalStore, provider) genericSCM := scm.ProvideGenericSCM() scmFactory := scm.ProvideFactory(gitnessSCM, genericSCM) diff --git a/types/cde_gateway.go b/types/cde_gateway.go new file mode 100644 index 000000000..2dec61e2d --- /dev/null +++ b/types/cde_gateway.go @@ -0,0 +1,45 @@ +// Copyright 2023 Harness, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +const GatewayHealthHealthy = "healthy" +const GatewayHealthUnhealthy = "unhealthy" + +type CDEGatewayStats struct { + Name string `json:"name"` + GroupName string `json:"group_name"` + Region string `json:"region"` + Zone string `json:"zone"` + Health string `json:"health"` + EnvoyHealth string `json:"envoy_health"` + Version string `json:"version"` +} + +type CDEGateway struct { + CDEGatewayStats + SpaceID int64 `json:"space_id,omitempty"` + SpacePath string `json:"space_path"` + InfraProviderConfigID int64 `json:"infra_provider_config_id,omitempty"` + InfraProviderConfigIdentifier string `json:"infra_provider_config_identifier"` + OverallHealth string `json:"overall_health,omitempty"` + Created int64 `json:"created"` + Updated int64 `json:"updated"` +} + +type CDEGatewayFilter struct { + Health string `json:"health,omitempty"` + HealthReportValidityInMins int `json:"health_report_validity_in_mins,omitempty"` + InfraProviderConfigIDs []int64 `json:"infra_provider_config_ids,omitempty"` +}