Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cli): migrations for team validation #1124

Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions cmd/cli/from_2.13_to_2.14.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package main

import "github.com/moira-alert/moira"

func updateFrom213(logger moira.Logger, database moira.Database) error {
logger.Info().Msg("Update 2.13 -> 2.14 started")

err := fillTeamNamesHash(logger, database)
if err != nil {
return err
}

logger.Info().Msg("Update 2.13 -> 2.14 was finished")
return nil
}

func downgradeTo213(logger moira.Logger, database moira.Database) error {
logger.Info().Msg("Downgrade 2.14 -> 2.13 started")

err := removeTeamNamesHash(logger, database)
if err != nil {
return err
}

logger.Info().Msg("Downgrade 2.14 -> 2.13 was finished")
return nil
}
24 changes: 23 additions & 1 deletion cmd/cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,15 @@ var (
GoVersion = "unknown"
)

var moiraValidVersions = []string{"2.3", "2.6", "2.7", "2.9", "2.11", "2.12"}
var moiraValidVersions = []string{
"2.3",
"2.6",
"2.7",
"2.9",
"2.11",
"2.12",
"2.13",
}

var (
configFileName = flag.String("config", "/etc/moira/cli.yml", "Path to configuration file")
Expand Down Expand Up @@ -125,6 +133,13 @@ func main() { //nolint
Error(err).
Msg("Fail to update from version 2.12")
}
case "2.13":
err := updateFrom213(logger, database)
if err != nil {
logger.Fatal().
Error(err).
Msg("Fail to update from version 2.13")
}
}
}

Expand Down Expand Up @@ -173,6 +188,13 @@ func main() { //nolint
Error(err).
Msg("Fail to update to version 2.12")
}
case "2.13":
err := downgradeTo213(logger, database)
if err != nil {
logger.Fatal().
Error(err).
Msg("Fail to update to version 2.13")
kissken marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

Expand Down
219 changes: 219 additions & 0 deletions cmd/cli/teams_names.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
package main

import (
"context"
"encoding/json"
"errors"
"fmt"
"strconv"
"strings"

goredis "github.com/go-redis/redis/v8"
"github.com/moira-alert/moira"
"github.com/moira-alert/moira/database/redis"
)

const (
teamsKey = "moira-teams"
teamsByNamesKey = "moira-teams-by-names"
)

var errTeamsCountAndUniqueNamesCountMismatch = errors.New(
"count of teams does not match count of unique names after transformation")

// fillTeamNamesHash does the following
// 1. Get all teams from DB.
// 2. Group teams with same names.
// 3. For teams with same names change name (example: ["name", "name", "name"] -> ["name", "name1", "name2"]).
// 4. Update teams with name changed.
// 5. Save pairs teamName:team.ID to "moira-teams-by-names" redis hash.
func fillTeamNamesHash(logger moira.Logger, database moira.Database) error {
logger.Info().Msg("Start filling \"moira-teams-by-names\" hash")

switch db := database.(type) {
case *redis.DbConnector:
logger.Info().Msg("collecting teams from redis node...")

teamsMap, err := db.Client().HGetAll(db.Context(), teamsKey).Result()
if err != nil {
return fmt.Errorf("failed to fetch teams from redis node: %w", err)
}

logger.Info().
Int("total_teams_count", len(teamsMap)).
Msg("fetched teams")

teamsByNameMap, err := groupTeamsByNames(logger, teamsMap)
if err != nil {
return fmt.Errorf("failed to group teams by names: %w", err)
}

teamByUniqueName := transformTeamsByNameMap(teamsByNameMap)

if len(teamByUniqueName) != len(teamsMap) {
return errTeamsCountAndUniqueNamesCountMismatch
}

client := db.Client()
ctx := db.Context()

_, pipeErr := client.TxPipelined(
ctx,
func(pipe goredis.Pipeliner) error {
return updateTeamsInPipe(ctx, logger, pipe, teamByUniqueName)
})
if pipeErr != nil {
return pipeErr
}

default:
return makeUnknownDBError(database)
}
return nil
}

func groupTeamsByNames(logger moira.Logger, teamsMap map[string]string) (map[string][]teamWithID, error) {
teamsByNameMap := make(map[string][]teamWithID, len(teamsMap))

for teamID, marshaledTeam := range teamsMap {
team, err := unmarshalTeam(teamID, []byte(marshaledTeam))
if err != nil {
return nil, err
}

lowercaseTeamName := strings.ToLower(team.Name)

teamWithNameList, exists := teamsByNameMap[lowercaseTeamName]
if exists {
teamWithNameList = append(teamWithNameList, team)
teamsByNameMap[lowercaseTeamName] = teamWithNameList
} else {
teamsByNameMap[lowercaseTeamName] = []teamWithID{team}
}
}

logger.Info().
kissken marked this conversation as resolved.
Show resolved Hide resolved
Int("unique_team_names_count", len(teamsByNameMap)).
Msg("grouped teams with same names")

return teamsByNameMap, nil
}

func transformTeamsByNameMap(teamsByNameMap map[string][]teamWithID) map[string]teamWithID {
teamByUniqueName := make(map[string]teamWithID, len(teamsByNameMap))

for _, teams := range teamsByNameMap {
for i, team := range teams {
iStr := strconv.FormatInt(int64(i), 10)

if i > 0 {
team.Name += iStr
}

for {
// sometimes we have the following situation in db (IDs and team names):
// moira-teams: {
// team1: "team name",
// team2: "team Name",
// team3: "Team name1"
// }
// so we can't just add 1 to one of [team1, team2]
lowercasedTeamName := strings.ToLower(team.Name)

_, exists := teamByUniqueName[lowercasedTeamName]
if exists {
team.Name += "_" + iStr
} else {
teamByUniqueName[lowercasedTeamName] = team
break
}
}
}
}

return teamByUniqueName
}

func updateTeamsInPipe(ctx context.Context, logger moira.Logger, pipe goredis.Pipeliner, teamsByUniqueName map[string]teamWithID) error {
for _, team := range teamsByUniqueName {
teamBytes, err := getTeamBytes(team)
if err != nil {
return err
}

err = pipe.HSet(ctx, teamsKey, team.ID, teamBytes).Err()
if err != nil {
logger.Error().
Error(err).
String("team_id", team.ID).
String("new_team_name", team.Name).
Msg("failed to update team name")

return fmt.Errorf("failed to update team name: %w", err)
}

err = pipe.HSet(ctx, teamsByNamesKey, strings.ToLower(team.Name), team.ID).Err()
if err != nil {
logger.Error().
Error(err).
String("team_id", team.ID).
String("new_team_name", team.Name).
Msg("failed to add team name to redis hash")

return fmt.Errorf("failed to add team name to redis hash: %w", err)
}
}

return nil
}

// removeTeamNamesHash remove "moira-teams-by-names" redis hash.
// Note that if fillTeamNamesHash have been called, then team names would not be changed back.
func removeTeamNamesHash(logger moira.Logger, database moira.Database) error {
logger.Info().Msg("Start removing \"moira-teams-by-names\" redis hash")

switch db := database.(type) {
case *redis.DbConnector:
_, err := db.Client().Del(db.Context(), teamsByNamesKey).Result()
if err != nil {
return fmt.Errorf("failed to delete teamsByNameKey: %w", err)
}

default:
return makeUnknownDBError(database)
}

return nil
}

type teamStorageElement struct {
Tetrergeru marked this conversation as resolved.
Show resolved Hide resolved
Name string `json:"name"`
Description string `json:"description"`
}

type teamWithID struct {
teamStorageElement
ID string
}

func unmarshalTeam(teamID string, teamBytes []byte) (teamWithID, error) {
var storedTeam teamStorageElement
err := json.Unmarshal(teamBytes, &storedTeam)
if err != nil {
return teamWithID{}, fmt.Errorf("failed to deserialize team: %w", err)
}

return teamWithID{
teamStorageElement: storedTeam,
ID: teamID,
}, nil
}

func getTeamBytes(team teamWithID) ([]byte, error) {
bytes, err := json.Marshal(team.teamStorageElement)
if err != nil {
return nil, fmt.Errorf("failed to marshal team: %w", err)
}

return bytes, nil
}
Loading
Loading