go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/deploy/service/rpcs/actuations.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/common/logging" 26 "go.chromium.org/luci/common/retry/transient" 27 "go.chromium.org/luci/gae/service/datastore" 28 "go.chromium.org/luci/grpc/grpcutil" 29 "go.chromium.org/luci/server/auth" 30 31 "go.chromium.org/luci/deploy/api/modelpb" 32 "go.chromium.org/luci/deploy/api/rpcpb" 33 "go.chromium.org/luci/deploy/service/model" 34 ) 35 36 // Actuations is an implementation of deploy.service.Actuations service. 37 type Actuations struct { 38 rpcpb.UnimplementedActuationsServer 39 } 40 41 // BeginActuation implements the corresponding RPC method. 42 func (srv *Actuations) BeginActuation(ctx context.Context, req *rpcpb.BeginActuationRequest) (resp *rpcpb.BeginActuationResponse, err error) { 43 defer func() { err = grpcutil.GRPCifyAndLogErr(ctx, err) }() 44 45 assetIDs, err := validateBeginActuation(req) 46 if err != nil { 47 return nil, err 48 } 49 50 // Fill in the server enforced fields. 51 req.Actuation.Actuator.Identity = string(auth.CurrentIdentity(ctx)) 52 53 // TODO: Store all new artifacts from req.Artifacts. 54 55 err = model.Txn(ctx, func(ctx context.Context) error { 56 // Check if there's already such actuation. This RPC may be a retry. 57 act := model.Actuation{ID: req.Actuation.Id} 58 switch err := datastore.Get(ctx, &act); { 59 case err == nil: 60 existing := act.Actuation.GetActuator().GetIdentity() 61 if existing == req.Actuation.Actuator.Identity { 62 // The same caller: most likely a retry, return the previous response. 63 logging.Warningf(ctx, "The actuation %q already exists, assuming it is a retry", req.Actuation.Id) 64 resp = &rpcpb.BeginActuationResponse{Decisions: act.Decisions.Decisions} 65 return nil 66 } 67 // A different caller: some kind of a conflict. 68 return status.Errorf(codes.FailedPrecondition, "actuation %q already exists and was created by %q", req.Actuation.Id, existing) 69 case err != datastore.ErrNoSuchEntity: 70 return errors.Annotate(err, "fetching Actuation").Tag(transient.Tag).Err() 71 } 72 73 // Start a new operation that would update Asset and Actuation entities. 74 // Copy only subset of Actuation fields allowed to be set by the actuator. 75 op, err := model.NewActuationBeginOp(ctx, assetIDs, &modelpb.Actuation{ 76 Id: req.Actuation.Id, 77 Deployment: req.Actuation.Deployment, 78 Actuator: req.Actuation.Actuator, 79 Triggers: req.Actuation.Triggers, 80 LogUrl: req.Actuation.LogUrl, 81 }) 82 if err != nil { 83 return errors.Annotate(err, "creating Actuation").Err() 84 } 85 for assetID, assetToActuate := range req.Assets { 86 op.MakeDecision(ctx, assetID, assetToActuate) 87 } 88 decisions, err := op.Apply(ctx) 89 if err != nil { 90 return errors.Annotate(err, "applying changes").Err() 91 } 92 resp = &rpcpb.BeginActuationResponse{Decisions: decisions} 93 return nil 94 }) 95 96 return resp, err 97 } 98 99 // EndActuation implements the corresponding RPC method. 100 func (srv *Actuations) EndActuation(ctx context.Context, req *rpcpb.EndActuationRequest) (resp *rpcpb.EndActuationResponse, err error) { 101 defer func() { err = grpcutil.GRPCifyAndLogErr(ctx, err) }() 102 103 assetIDs, err := validateEndActuation(req) 104 if err != nil { 105 return nil, err 106 } 107 108 err = model.Txn(ctx, func(ctx context.Context) error { 109 // The actuation must exist already. 110 act := &model.Actuation{ID: req.ActuationId} 111 switch err := datastore.Get(ctx, act); { 112 case err == datastore.ErrNoSuchEntity: 113 return status.Errorf(codes.NotFound, "actuation %q doesn't exists", req.ActuationId) 114 case err != nil: 115 return errors.Annotate(err, "fetching Actuation").Tag(transient.Tag).Err() 116 } 117 118 // The actuation must have been started by the same identity. 119 startedBy := act.Actuation.GetActuator().GetIdentity() 120 if startedBy != string(auth.CurrentIdentity(ctx)) { 121 return status.Errorf(codes.FailedPrecondition, "actuation %q was created by %q", req.ActuationId, startedBy) 122 } 123 124 // If the actuation is already in the terminal state, assume this RPC is 125 // a retry and just ignore it. 126 if act.Actuation.State != modelpb.Actuation_EXECUTING { 127 logging.Warningf(ctx, "The actuation is in the terminal state %q already, skipping the call", act.Actuation.State) 128 return nil 129 } 130 131 // The set of reported assets must match exactly the set of actively 132 // actuated assets as reported by the previous BeginActuation. 133 if expected := act.ActuatedAssetIDs(); !model.EqualStrSlice(assetIDs, expected) { 134 return status.Errorf(codes.InvalidArgument, 135 "the reported set of actuated assets doesn't match the set previously returned by BeginActuation: %q != %q", 136 assetIDs, expected) 137 } 138 139 // Mutate the state of all actuated assets. 140 op, err := model.NewActuationEndOp(ctx, act) 141 if err != nil { 142 return errors.Annotate(err, "finalizing Actuation").Err() 143 } 144 op.UpdateActuationStatus(ctx, req.Status, req.LogUrl) 145 for assetID, actuatedAsset := range req.Assets { 146 op.HandleActuatedState(ctx, assetID, actuatedAsset) 147 } 148 if err := op.Apply(ctx); err != nil { 149 return errors.Annotate(err, "applying changes").Err() 150 } 151 return nil 152 }) 153 154 return &rpcpb.EndActuationResponse{}, err 155 } 156 157 //////////////////////////////////////////////////////////////////////////////// 158 159 // validateBeginActuation checks the format of the RPC request. 160 // 161 // Returns the sorted list of asset IDs on success or gRPC error on failure. 162 func validateBeginActuation(req *rpcpb.BeginActuationRequest) ([]string, error) { 163 actuationPb := req.Actuation 164 if actuationPb.GetId() == "" { 165 return nil, status.Errorf(codes.InvalidArgument, "`id` is required") 166 } 167 if actuationPb.GetDeployment() == nil { 168 return nil, status.Errorf(codes.InvalidArgument, "`deployment` is required") 169 } 170 if actuationPb.GetActuator() == nil { 171 return nil, status.Errorf(codes.InvalidArgument, "`actuator` is required") 172 } 173 174 // Validate AssetState has correct fields populated. 175 if len(req.Assets) == 0 { 176 return nil, status.Errorf(codes.InvalidArgument, "`assets` is required") 177 } 178 assetIDs := make([]string, 0, len(req.Assets)) 179 for assetID, assetToActuate := range req.Assets { 180 if err := model.ValidateIntendedState(assetID, assetToActuate.IntendedState); err != nil { 181 return nil, status.Errorf(codes.InvalidArgument, "asset %q: bad intended state - %s", assetID, err) 182 } 183 if err := model.ValidateReportedState(assetID, assetToActuate.ReportedState); err != nil { 184 return nil, status.Errorf(codes.InvalidArgument, "asset %q: bad reported state - %s", assetID, err) 185 } 186 assetIDs = append(assetIDs, assetID) 187 } 188 sort.Strings(assetIDs) 189 190 return assetIDs, nil 191 } 192 193 // validateEndActuation checks the format of the RPC request. 194 // 195 // Returns the sorted list of asset IDs on success or gRPC error on failure. 196 func validateEndActuation(req *rpcpb.EndActuationRequest) ([]string, error) { 197 // Validate the request. 198 if req.ActuationId == "" { 199 return nil, status.Errorf(codes.InvalidArgument, "`actuation_id` is required") 200 } 201 202 // Validate AssetState has correct fields populated. 203 if len(req.Assets) == 0 { 204 return nil, status.Errorf(codes.InvalidArgument, "`assets` is required") 205 } 206 assetIDs := make([]string, 0, len(req.Assets)) 207 for assetID, actuatedAsset := range req.Assets { 208 if err := model.ValidateReportedState(assetID, actuatedAsset.State); err != nil { 209 return nil, status.Errorf(codes.InvalidArgument, "asset %q: bad reported state - %s", assetID, err) 210 } 211 assetIDs = append(assetIDs, assetID) 212 } 213 sort.Strings(assetIDs) 214 215 return assetIDs, nil 216 }