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

CLI: ability to check health of all projects for support users #3725

14 changes: 14 additions & 0 deletions admin/database/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ type DB interface {
DeleteProject(ctx context.Context, id string) error
UpdateProject(ctx context.Context, id string, opts *UpdateProjectOptions) (*Project, error)
CountProjectsForOrganization(ctx context.Context, orgID string) (int, error)
FindProjectsHealth(ctx context.Context, afterProject string, limit int) ([]*ProjectHealth, error)
FindProjectsHealthForOrganization(ctx context.Context, orgID string, afterProject string, limit int) ([]*ProjectHealth, error)
FindProjectsHealthForUser(ctx context.Context, userID string, afterProject string, limit int) ([]*ProjectHealth, error)
FindProjectsHealthForDomain(ctx context.Context, domain string, afterProject string, limit int) ([]*ProjectHealth, error)

FindExpiredDeployments(ctx context.Context) ([]*Deployment, error)
FindDeploymentsForProject(ctx context.Context, projectID string) ([]*Deployment, error)
Expand Down Expand Up @@ -269,6 +273,16 @@ type Project struct {
UpdatedOn time.Time `db:"updated_on"`
}

type ProjectHealth struct {
ProjectID string `db:"id"`
ProjectName string `db:"name"`
OrgID string `db:"org_id"`
ProdDeploymentID *string `db:"prod_deployment_id"`
Status DeploymentStatus `db:"status"`
StatusMessage string `db:"status_message"`
UpdatedOn time.Time `db:"updated_on"`
}

// Variables implements JSON SQL encoding of variables in Project.
type Variables map[string]string

Expand Down
69 changes: 69 additions & 0 deletions admin/database/postgres/postgres.go
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,75 @@ func (c *connection) FindProjectByName(ctx context.Context, orgName, name string
return res, nil
}

// FindProjectsHealth returns all the projects along with its deployment status and status message
func (c *connection) FindProjectsHealth(ctx context.Context, afterName string, limit int) ([]*database.ProjectHealth, error) {
var res []*database.ProjectHealth
err := c.getDB(ctx).SelectContext(ctx, &res, `
SELECT p.id, p.name, p.org_id, p.prod_deployment_id, d.status, d.status_message, d.updated_on FROM projects p
LEFT JOIN deployments d ON p.prod_deployment_id = d.id
WHERE lower(p.name) > lower($1)
ORDER BY lower(p.name) LIMIT $2
`, afterName, limit)
if err != nil {
return nil, parseErr("projects", err)
}
return res, nil
}

func (c *connection) FindProjectsHealthForOrganization(ctx context.Context, orgID, afterName string, limit int) ([]*database.ProjectHealth, error) {
var res []*database.ProjectHealth
err := c.getDB(ctx).SelectContext(ctx, &res, `
SELECT p.id, p.name, p.org_id, p.prod_deployment_id, d.status, d.status_message, d.updated_on FROM projects p
LEFT JOIN deployments d ON p.prod_deployment_id = d.id
WHERE p.org_id = $1 AND lower(p.name) > lower($2)
ORDER BY lower(p.name) LIMIT $3
`, orgID, afterName, limit)
if err != nil {
return nil, parseErr("projects", err)
}
return res, nil
}

func (c *connection) FindProjectsHealthForUser(ctx context.Context, userID, afterName string, limit int) ([]*database.ProjectHealth, error) {
var res []*database.ProjectHealth
err := c.getDB(ctx).SelectContext(ctx, &res, `
SELECT p.id, p.name, p.org_id, p.prod_deployment_id, d.status, d.status_message, d.updated_on FROM projects p
LEFT JOIN deployments d ON p.prod_deployment_id = d.id
WHERE p.id IN (
SELECT upr.project_id FROM users_projects_roles upr WHERE upr.user_id = $1
UNION
SELECT ugpr.project_id FROM usergroups_projects_roles ugpr JOIN usergroups_users uug ON ugpr.usergroup_id = uug.usergroup_id WHERE uug.user_id = $1
) AND lower(p.name) > lower($2)
ORDER BY lower(p.name) LIMIT $3
`, userID, afterName, limit)
if err != nil {
return nil, parseErr("projects", err)
}
return res, nil
}

func (c *connection) FindProjectsHealthForDomain(ctx context.Context, domain, afterName string, limit int) ([]*database.ProjectHealth, error) {
var res []*database.ProjectHealth
err := c.getDB(ctx).SelectContext(ctx, &res, `
SELECT p.id, p.name, p.org_id, p.prod_deployment_id, d.status, d.status_message, d.updated_on FROM projects p
LEFT JOIN deployments d ON p.prod_deployment_id = d.id
WHERE p.id IN (
SELECT upr.project_id FROM users_projects_roles upr WHERE upr.user_id IN (
SELECT u.id FROM users u WHERE lower(u.email) LIKE lower($1)
)
UNION
SELECT ugpr.project_id FROM usergroups_projects_roles ugpr JOIN usergroups_users uug ON ugpr.usergroup_id = uug.usergroup_id WHERE uug.user_id IN (
SELECT u.id FROM users u WHERE lower(u.email) LIKE lower($1)
)
) AND lower(p.name) > lower($2)
ORDER BY lower(p.name) LIMIT $3
`, "%@"+domain, afterName, limit)
if err != nil {
return nil, parseErr("projects", err)
}
return res, nil
}

func (c *connection) InsertProject(ctx context.Context, opts *database.InsertProjectOptions) (*database.Project, error) {
if err := database.Validate(opts); err != nil {
return nil, err
Expand Down
172 changes: 172 additions & 0 deletions admin/server/projects.go
Original file line number Diff line number Diff line change
Expand Up @@ -742,6 +742,178 @@ func (s *Server) SetProjectMemberRole(ctx context.Context, req *adminv1.SetProje
return &adminv1.SetProjectMemberRoleResponse{}, nil
}

// SudoListProjectsHealth returns the health of all projects in the organization
func (s *Server) SudoListProjectsHealth(ctx context.Context, req *adminv1.SudoListProjectsHealthRequest) (*adminv1.SudoListProjectsHealthResponse, error) {
// Check the request is made by a superuser
claims := auth.GetClaims(ctx)
if !claims.Superuser(ctx) {
return nil, status.Error(codes.Unauthenticated, "not authenticated")
}

token, err := unmarshalPageToken(req.PageToken)
if err != nil {
return nil, err
}
pageSize := validPageSize(req.PageSize)

// Find projects health
projectsHealth, err := s.admin.DB.FindProjectsHealth(ctx, token.Val, pageSize)
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}

nextToken := ""
if len(projectsHealth) >= pageSize {
nextToken = marshalPageToken(projectsHealth[len(projectsHealth)-1].ProjectName)
}

// Convert to proto
projectsHealthDtos := make([]*adminv1.ProjectHealth, len(projectsHealth))
for i, projectHealth := range projectsHealth {
projectsHealthDtos[i] = projectHealthToPB(projectHealth)
}

return &adminv1.SudoListProjectsHealthResponse{
Projects: projectsHealthDtos,
NextPageToken: nextToken,
}, nil
}

// SudoListProjectsHealthForOrganization returns the health of all projects in the organization
func (s *Server) SudoListProjectsHealthForOrganization(ctx context.Context, req *adminv1.SudoListProjectsHealthForOrganizationRequest) (*adminv1.SudoListProjectsHealthForOrganizationResponse, error) {
// Check the request is made by a superuser
claims := auth.GetClaims(ctx)
if !claims.Superuser(ctx) {
return nil, status.Error(codes.Unauthenticated, "not authenticated")
}

// Find org
org, err := s.admin.DB.FindOrganizationByName(ctx, req.Organization)
if err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}

token, err := unmarshalPageToken(req.PageToken)
if err != nil {
return nil, err
}
pageSize := validPageSize(req.PageSize)

// Find projects health
projectsHealth, err := s.admin.DB.FindProjectsHealthForOrganization(ctx, org.ID, token.Val, pageSize)
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}

nextToken := ""
if len(projectsHealth) >= pageSize {
nextToken = marshalPageToken(projectsHealth[len(projectsHealth)-1].ProjectName)
}

// Convert to proto
projectsHealthDtos := make([]*adminv1.ProjectHealth, len(projectsHealth))
for i, projectHealth := range projectsHealth {
projectsHealthDtos[i] = projectHealthToPB(projectHealth)
}

return &adminv1.SudoListProjectsHealthForOrganizationResponse{
Projects: projectsHealthDtos,
NextPageToken: nextToken,
}, nil
}

// SudoListProjectsHealthForUser returns the health of all projects in the organization
func (s *Server) SudoListProjectsHealthForUser(ctx context.Context, req *adminv1.SudoListProjectsHealthForUserRequest) (*adminv1.SudoListProjectsHealthForUserResponse, error) {
// Check the request is made by a superuser
claims := auth.GetClaims(ctx)
if !claims.Superuser(ctx) {
return nil, status.Error(codes.Unauthenticated, "not authenticated")
}

// Find user
user, err := s.admin.DB.FindUserByEmail(ctx, req.Email)
if err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}

token, err := unmarshalPageToken(req.PageToken)
if err != nil {
return nil, err
}
pageSize := validPageSize(req.PageSize)

// Find projects health
projectsHealth, err := s.admin.DB.FindProjectsHealthForUser(ctx, user.ID, token.Val, pageSize)
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}

nextToken := ""
if len(projectsHealth) >= pageSize {
nextToken = marshalPageToken(projectsHealth[len(projectsHealth)-1].ProjectName)
}

// Convert to proto
projectsHealthDtos := make([]*adminv1.ProjectHealth, len(projectsHealth))
for i, projectHealth := range projectsHealth {
projectsHealthDtos[i] = projectHealthToPB(projectHealth)
}

return &adminv1.SudoListProjectsHealthForUserResponse{
Projects: projectsHealthDtos,
NextPageToken: nextToken,
}, nil
}

// SudoListProjectsHealthForDomain returns the health of all projects for a domain
func (s *Server) SudoListProjectsHealthForDomain(ctx context.Context, req *adminv1.SudoListProjectsHealthForDomainRequest) (*adminv1.SudoListProjectsHealthForDomainResponse, error) {
// Check the request is made by a superuser
claims := auth.GetClaims(ctx)
if !claims.Superuser(ctx) {
return nil, status.Error(codes.Unauthenticated, "not authenticated")
}

token, err := unmarshalPageToken(req.PageToken)
if err != nil {
return nil, err
}
pageSize := validPageSize(req.PageSize)

// Find projects health
projectsHealth, err := s.admin.DB.FindProjectsHealthForDomain(ctx, req.Domain, token.Val, pageSize)
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}

nextToken := ""
if len(projectsHealth) >= pageSize {
nextToken = marshalPageToken(projectsHealth[len(projectsHealth)-1].ProjectName)
}

// Convert to proto
projectsHealthDtos := make([]*adminv1.ProjectHealth, len(projectsHealth))
for i, projectHealth := range projectsHealth {
projectsHealthDtos[i] = projectHealthToPB(projectHealth)
}

return &adminv1.SudoListProjectsHealthForDomainResponse{
Projects: projectsHealthDtos,
NextPageToken: nextToken,
}, nil
}

func projectHealthToPB(projectHealth *database.ProjectHealth) *adminv1.ProjectHealth {
return &adminv1.ProjectHealth{
ProjectId: projectHealth.ProjectID,
ProjectName: projectHealth.ProjectName,
OrgId: projectHealth.OrgID,
DeploymentId: *projectHealth.ProdDeploymentID,
Status: adminv1.DeploymentStatus(projectHealth.Status),
StatusMessage: projectHealth.StatusMessage,
DeploymentStatusTimestamp: timestamppb.New(projectHealth.UpdatedOn),
}
}

// getAndCheckGithubInstallationID returns a valid installation ID iff app is installed and user is a collaborator of the repo
func (s *Server) getAndCheckGithubInstallationID(ctx context.Context, githubURL, userID string) (int64, error) {
// Get Github installation ID for the repo
Expand Down
Loading
Loading