go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/deploy/service/rpcs/assets.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 rpcs 16 17 import ( 18 "context" 19 "sort" 20 21 "google.golang.org/grpc/codes" 22 "google.golang.org/grpc/status" 23 24 "go.chromium.org/luci/common/errors" 25 "go.chromium.org/luci/gae/service/datastore" 26 "go.chromium.org/luci/grpc/grpcutil" 27 28 "go.chromium.org/luci/deploy/api/modelpb" 29 "go.chromium.org/luci/deploy/api/rpcpb" 30 "go.chromium.org/luci/deploy/service/model" 31 ) 32 33 // Assets is the implementation of deploy.service.Assets service. 34 type Assets struct { 35 rpcpb.UnimplementedAssetsServer 36 } 37 38 // GetAsset implements the corresponding RPC method. 39 func (*Assets) GetAsset(ctx context.Context, req *rpcpb.GetAssetRequest) (resp *modelpb.Asset, err error) { 40 defer func() { err = grpcutil.GRPCifyAndLogErr(ctx, err) }() 41 42 asset, err := fetchAssetEntity(ctx, req.AssetId) 43 if err != nil { 44 return nil, err 45 } 46 return asset.Asset, nil 47 } 48 49 // ListAssets implements the corresponding RPC method. 50 func (*Assets) ListAssets(ctx context.Context, req *rpcpb.ListAssetsRequest) (resp *rpcpb.ListAssetsResponse, err error) { 51 defer func() { err = grpcutil.GRPCifyAndLogErr(ctx, err) }() 52 53 q := datastore.NewQuery("Asset") 54 55 var entities []*model.Asset 56 if err = datastore.GetAll(ctx, q, &entities); err != nil { 57 return nil, status.Errorf(codes.Internal, "datastore query to list assets failed: %s", err) 58 } 59 60 assets, err := sortedProtoList(entities) 61 if err != nil { 62 return nil, err 63 } 64 65 return &rpcpb.ListAssetsResponse{Assets: assets}, nil 66 } 67 68 // ListAssetHistory implements the corresponding RPC method. 69 func (*Assets) ListAssetHistory(ctx context.Context, req *rpcpb.ListAssetHistoryRequest) (resp *rpcpb.ListAssetHistoryResponse, err error) { 70 defer func() { err = grpcutil.GRPCifyAndLogErr(ctx, err) }() 71 72 asset, err := fetchAssetEntity(ctx, req.AssetId) 73 if err != nil { 74 return nil, err 75 } 76 77 latestID := req.LatestHistoryId 78 if latestID == 0 || latestID > asset.LastHistoryID { 79 latestID = asset.LastHistoryID 80 } 81 82 if req.Limit == 0 { 83 req.Limit = 20 84 } else if req.Limit > 200 { 85 req.Limit = 200 86 } else if req.Limit < 0 { 87 return nil, status.Errorf(codes.InvalidArgument, "limit can't be negative") 88 } 89 90 assetKey := datastore.KeyForObj(ctx, asset) 91 q := datastore.NewQuery("AssetHistory"). 92 Ancestor(assetKey). 93 Lte("__key__", datastore.NewKey(ctx, "AssetHistory", "", latestID, assetKey)). 94 Order("-__key__"). 95 Limit(req.Limit) 96 97 var entries []*model.AssetHistory 98 if err := datastore.GetAll(ctx, q, &entries); err != nil { 99 return nil, errors.Annotate(err, "querying AssetHistory").Tag(grpcutil.InternalTag).Err() 100 } 101 102 resp = &rpcpb.ListAssetHistoryResponse{Asset: asset.Asset} 103 if asset.IsRecordingHistoryEntry() { 104 resp.Current = asset.HistoryEntry 105 } 106 resp.LastRecordedHistoryId = asset.LastHistoryID 107 resp.History = make([]*modelpb.AssetHistory, len(entries)) 108 for i, e := range entries { 109 resp.History[i] = e.Entry 110 } 111 return resp, nil 112 } 113 114 // fetchAssetEntity fetches Asset entity returning gRPC errors on failures. 115 func fetchAssetEntity(ctx context.Context, assetID string) (*model.Asset, error) { 116 entity := &model.Asset{ID: assetID} 117 switch err := datastore.Get(ctx, entity); { 118 case err == datastore.ErrNoSuchEntity: 119 return nil, status.Errorf(codes.NotFound, "no such asset") 120 case err != nil: 121 return nil, status.Errorf(codes.Internal, "datastore error when fetching the asset: %s", err) 122 default: 123 if _, err := checkAssetEntity(entity); err != nil { 124 return nil, err 125 } 126 return entity, nil 127 } 128 } 129 130 // checkAssetEntity checks the proto payload of the asset entity is correct. 131 // 132 // Returns gRPC errors. 133 func checkAssetEntity(e *model.Asset) (*modelpb.Asset, error) { 134 if e.Asset.GetId() != e.ID { 135 return nil, status.Errorf(codes.Internal, "asset entity %q has bad proto payload %v", e.ID, e.Asset) 136 } 137 return e.Asset, nil 138 } 139 140 // sortedProtoList extracts Asset protos and sorts them by ID. 141 // 142 // Returns gRPC errors. 143 func sortedProtoList(entities []*model.Asset) ([]*modelpb.Asset, error) { 144 out := make([]*modelpb.Asset, len(entities)) 145 for i, ent := range entities { 146 var err error 147 if out[i], err = checkAssetEntity(ent); err != nil { 148 return nil, err 149 } 150 } 151 sort.Slice(out, func(i, j int) bool { 152 return out[i].Id < out[j].Id 153 }) 154 return out, nil 155 }