go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/rpc/projects.go (about) 1 // Copyright 2022 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package rpc 16 17 import ( 18 "context" 19 "fmt" 20 "sort" 21 "strings" 22 23 "go.chromium.org/luci/common/errors" 24 "go.chromium.org/luci/server/auth" 25 "go.chromium.org/luci/server/auth/realms" 26 27 "go.chromium.org/luci/analysis/internal/perms" 28 configpb "go.chromium.org/luci/analysis/proto/config" 29 pb "go.chromium.org/luci/analysis/proto/v1" 30 ) 31 32 type projectServer struct{} 33 34 func NewProjectsServer() *pb.DecoratedProjects { 35 return &pb.DecoratedProjects{ 36 Prelude: checkAllowedPrelude, 37 Service: &projectServer{}, 38 Postlude: gRPCifyAndLogPostlude, 39 } 40 } 41 42 func (*projectServer) GetConfig(ctx context.Context, req *pb.GetProjectConfigRequest) (*pb.ProjectConfig, error) { 43 project, err := parseProjectConfigName(req.Name) 44 if err != nil { 45 return nil, invalidArgumentError(errors.Annotate(err, "name").Err()) 46 } 47 48 if err := perms.VerifyProjectPermissions(ctx, project, perms.PermGetConfig); err != nil { 49 return nil, err 50 } 51 52 // Fetch a recent project configuration. 53 // (May be a recent value that was cached.) 54 cfg, err := readProjectConfig(ctx, project) 55 if err != nil { 56 return nil, err 57 } 58 59 response := createProjectConfigPB(project, cfg.Config) 60 return response, nil 61 } 62 63 func createProjectConfigPB(project string, cfg *configpb.ProjectConfig) *pb.ProjectConfig { 64 result := &pb.ProjectConfig{ 65 Name: fmt.Sprintf("projects/%s/config", project), 66 BugManagement: toBugManagementPB(cfg.BugManagement), 67 } 68 return result 69 } 70 71 func toBugManagementPB(bm *configpb.BugManagement) *pb.BugManagement { 72 // Always set BugManagement struct to avoid clients needing to 73 // do pointless nullness checks. Having an empty BugManagement struct 74 // is semantically identical to having an unset BugManagement struct. 75 result := &pb.BugManagement{} 76 77 if bm != nil { 78 policies := make([]*pb.BugManagementPolicy, 0, len(bm.Policies)) 79 for _, policy := range bm.Policies { 80 policies = append(policies, toBugManagementPolicyPB(policy)) 81 } 82 result.Policies = policies 83 result.Monorail = toMonorailProjectPB(bm.Monorail) 84 } 85 return result 86 } 87 88 func toBugManagementPolicyPB(policy *configpb.BugManagementPolicy) *pb.BugManagementPolicy { 89 metrics := make([]*pb.BugManagementPolicy_Metric, 0, len(policy.Metrics)) 90 for _, m := range policy.Metrics { 91 metrics = append(metrics, &pb.BugManagementPolicy_Metric{ 92 MetricId: m.MetricId, 93 ActivationThreshold: toMetricThresholdPB(m.ActivationThreshold), 94 DeactivationThreshold: toMetricThresholdPB(m.DeactivationThreshold), 95 }) 96 } 97 98 return &pb.BugManagementPolicy{ 99 Id: policy.Id, 100 Owners: policy.Owners, 101 HumanReadableName: policy.HumanReadableName, 102 Priority: pb.BuganizerPriority(policy.Priority), 103 Metrics: metrics, 104 Explanation: &pb.BugManagementPolicy_Explanation{ 105 // Explanation should never be nil. Config validation enforces this. 106 ProblemHtml: policy.Explanation.ProblemHtml, 107 ActionHtml: policy.Explanation.ActionHtml, 108 }, 109 } 110 } 111 112 func toMonorailProjectPB(cfg *configpb.MonorailProject) *pb.MonorailProject { 113 if cfg == nil { 114 return nil 115 } 116 return &pb.MonorailProject{ 117 Project: cfg.Project, 118 DisplayPrefix: cfg.DisplayPrefix, 119 } 120 } 121 122 func toMetricThresholdPB(threshold *configpb.MetricThreshold) *pb.MetricThreshold { 123 if threshold == nil { 124 return nil 125 } 126 return &pb.MetricThreshold{ 127 OneDay: threshold.OneDay, 128 ThreeDay: threshold.ThreeDay, 129 SevenDay: threshold.SevenDay, 130 } 131 } 132 133 func (*projectServer) List(ctx context.Context, request *pb.ListProjectsRequest) (*pb.ListProjectsResponse, error) { 134 readableRealms, err := auth.QueryRealms(ctx, perms.PermGetConfig, "", nil) 135 if err != nil { 136 return nil, err 137 } 138 readableProjects := make([]string, 0) 139 for _, r := range readableRealms { 140 project, realm := realms.Split(r) 141 if realm == realms.RootRealm || realm == realms.ProjectRealm { 142 readableProjects = append(readableProjects, project) 143 } 144 } 145 // Return projects in a stable order. 146 sort.Strings(readableProjects) 147 148 return &pb.ListProjectsResponse{ 149 Projects: createProjectPBs(readableProjects), 150 }, nil 151 } 152 153 func createProjectPBs(projects []string) []*pb.Project { 154 projectsPbs := make([]*pb.Project, 0, len(projects)) 155 for _, project := range projects { 156 projectsPbs = append(projectsPbs, &pb.Project{ 157 Name: fmt.Sprintf("projects/%s", project), 158 DisplayName: strings.Title(project), 159 Project: project, 160 }) 161 } 162 return projectsPbs 163 }