Migrate pap startup & controllers to spring boot
[policy/pap.git] / main / src / main / java / org / onap / policy / pap / main / rest / PdpGroupDeployProvider.java
1 /*
2  * ============LICENSE_START=======================================================
3  * ONAP PAP
4  * ================================================================================
5  * Copyright (C) 2019, 2021 AT&T Intellectual Property. All rights reserved.
6  * Modifications Copyright (C) 2020-2021 Nordix Foundation.
7  * Modifications Copyright (C) 2021 Bell Canada. All rights reserved.
8  * ================================================================================
9  * Licensed under the Apache License, Version 2.0 (the "License");
10  * you may not use this file except in compliance with the License.
11  * You may obtain a copy of the License at
12  *
13  *      http://www.apache.org/licenses/LICENSE-2.0
14  *
15  * Unless required by applicable law or agreed to in writing, software
16  * distributed under the License is distributed on an "AS IS" BASIS,
17  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18  * See the License for the specific language governing permissions and
19  * limitations under the License.
20  * ============LICENSE_END=========================================================
21  */
22
23 package org.onap.policy.pap.main.rest;
24
25 import com.google.gson.annotations.SerializedName;
26 import java.util.ArrayList;
27 import java.util.HashMap;
28 import java.util.HashSet;
29 import java.util.LinkedHashSet;
30 import java.util.List;
31 import java.util.Map;
32 import java.util.Set;
33 import java.util.stream.Collectors;
34 import javax.ws.rs.core.Response.Status;
35 import lombok.Getter;
36 import org.onap.policy.common.parameters.BeanValidationResult;
37 import org.onap.policy.common.parameters.BeanValidator;
38 import org.onap.policy.common.parameters.ValidationResult;
39 import org.onap.policy.common.parameters.ValidationStatus;
40 import org.onap.policy.common.parameters.annotations.NotNull;
41 import org.onap.policy.common.parameters.annotations.Pattern;
42 import org.onap.policy.common.parameters.annotations.Valid;
43 import org.onap.policy.common.utils.coder.CoderException;
44 import org.onap.policy.common.utils.coder.StandardCoder;
45 import org.onap.policy.common.utils.services.Registry;
46 import org.onap.policy.models.base.PfKey;
47 import org.onap.policy.models.base.PfModelException;
48 import org.onap.policy.models.base.PfModelRuntimeException;
49 import org.onap.policy.models.pap.concepts.PdpDeployPolicies;
50 import org.onap.policy.models.pdp.concepts.DeploymentGroup;
51 import org.onap.policy.models.pdp.concepts.DeploymentGroups;
52 import org.onap.policy.models.pdp.concepts.DeploymentSubGroup;
53 import org.onap.policy.models.pdp.concepts.Pdp;
54 import org.onap.policy.models.pdp.concepts.PdpGroup;
55 import org.onap.policy.models.pdp.concepts.PdpSubGroup;
56 import org.onap.policy.models.tosca.authorative.concepts.ToscaConceptIdentifier;
57 import org.onap.policy.models.tosca.authorative.concepts.ToscaConceptIdentifierOptVersion;
58 import org.onap.policy.models.tosca.authorative.concepts.ToscaPolicy;
59 import org.slf4j.Logger;
60 import org.slf4j.LoggerFactory;
61 import org.springframework.stereotype.Service;
62
63 /**
64  * Provider for PAP component to deploy PDP groups. The following items must be in the
65  * {@link Registry}:
66  * <ul>
67  * <li>PDP Modification Lock</li>
68  * <li>PDP Modify Request Map</li>
69  * <li>PAP DAO Factory</li>
70  * </ul>
71  */
72 @Service
73 public class PdpGroupDeployProvider extends ProviderBase {
74     private static final Logger logger = LoggerFactory.getLogger(PdpGroupDeployProvider.class);
75     private static final StandardCoder coder = new StandardCoder();
76
77     private static final String POLICY_RESULT_NAME = "policy";
78
79     /**
80      * Updates policies in specific PDP groups.
81      *
82      * @param groups PDP group deployments to be updated
83      * @param user user triggering deployment
84      * @throws PfModelException if an error occurred
85      */
86     public void updateGroupPolicies(DeploymentGroups groups, String user) throws PfModelException {
87         ValidationResult result = groups.validatePapRest();
88         if (!result.isValid()) {
89             String msg = result.getResult().trim();
90             throw new PfModelException(Status.BAD_REQUEST, msg);
91         }
92
93         process(user, groups, this::updateGroups);
94     }
95
96     /**
97      * Updates policies in specific PDP groups. This is the method that does the actual work.
98      *
99      * @param data session data
100      * @param groups PDP group deployments
101      * @throws PfModelException if an error occurred
102      */
103     private void updateGroups(SessionData data, DeploymentGroups groups) throws PfModelException {
104         var result = new BeanValidationResult("groups", groups);
105
106         for (DeploymentGroup group : groups.getGroups()) {
107             PdpGroup dbgroup = data.getGroup(group.getName());
108
109             if (dbgroup == null) {
110                 result.addResult(group.getName(), group, ValidationStatus.INVALID, "unknown group");
111
112             } else {
113                 result.addResult(updateGroup(data, dbgroup, group));
114             }
115         }
116
117         if (!result.isValid()) {
118             throw new PfModelException(Status.BAD_REQUEST, result.getResult().trim());
119         }
120     }
121
122     /**
123      * Updates an existing group.
124      *
125      * @param data session data
126      * @param dbgroup the group, as it appears within the DB
127      * @param group the group to be added
128      * @return the validation result
129      * @throws PfModelException if an error occurred
130      */
131     private ValidationResult updateGroup(SessionData data, PdpGroup dbgroup, DeploymentGroup group)
132                     throws PfModelException {
133
134         var result = new BeanValidationResult(group.getName(), group);
135
136         boolean updated = updateSubGroups(data, dbgroup, group, result);
137
138         if (result.isValid() && updated) {
139             data.update(dbgroup);
140         }
141
142         return result;
143     }
144
145     /**
146      * Adds or updates subgroups within the group.
147      *
148      * @param data session data
149      * @param dbgroup the group, as it appears within the DB
150      * @param group the group to be added
151      * @param result the validation result
152      * @return {@code true} if the DB group was modified, {@code false} otherwise
153      * @throws PfModelException if an error occurred
154      */
155     private boolean updateSubGroups(SessionData data, PdpGroup dbgroup, DeploymentGroup group,
156                     BeanValidationResult result) throws PfModelException {
157
158         // create a map of existing subgroups
159         Map<String, PdpSubGroup> type2sub = new HashMap<>();
160         dbgroup.getPdpSubgroups().forEach(subgrp -> type2sub.put(subgrp.getPdpType(), subgrp));
161
162         var updated = false;
163
164         for (DeploymentSubGroup subgrp : group.getDeploymentSubgroups()) {
165             PdpSubGroup dbsub = type2sub.get(subgrp.getPdpType());
166             var subResult = new BeanValidationResult(subgrp.getPdpType(), subgrp);
167
168             if (dbsub == null) {
169                 subResult.addResult(subgrp.getPdpType(), subgrp, ValidationStatus.INVALID, "unknown subgroup");
170
171             } else {
172                 updated = updateSubGroup(data, dbgroup, dbsub, subgrp, subResult) || updated;
173             }
174
175             result.addResult(subResult);
176         }
177
178         return updated;
179     }
180
181     /**
182      * Updates an existing subgroup.
183      *
184      * @param data session data
185      * @param dbgroup the group, from the DB, containing the subgroup
186      * @param dbsub the subgroup, from the DB
187      * @param subgrp the subgroup to be updated, updated to fully qualified versions upon
188      *        return
189      * @param container container for additional validation results
190      * @return {@code true} if the subgroup content was changed, {@code false} if there
191      *         were no changes
192      * @throws PfModelException if an error occurred
193      */
194     private boolean updateSubGroup(SessionData data, PdpGroup dbgroup, PdpSubGroup dbsub, DeploymentSubGroup subgrp,
195                     BeanValidationResult container) throws PfModelException {
196
197         // perform additional validations first
198         if (!validateSubGroup(data, dbsub, subgrp, container)) {
199             return false;
200         }
201
202         var updated = false;
203
204         switch (subgrp.getAction()) {
205             case POST:
206                 updated = addPolicies(data, dbgroup.getName(), dbsub, subgrp);
207                 break;
208             case DELETE:
209                 updated = deletePolicies(data, dbgroup.getName(), dbsub, subgrp);
210                 break;
211             case PATCH:
212             default:
213                 updated = updatePolicies(data, dbgroup.getName(), dbsub, subgrp);
214                 break;
215         }
216
217         if (updated) {
218             // publish any changes to the PDPs
219             makeUpdates(data, dbgroup, dbsub);
220             return true;
221         }
222
223         return false;
224     }
225
226     private boolean addPolicies(SessionData data, String pdpGroup, PdpSubGroup dbsub, DeploymentSubGroup subgrp)
227                     throws PfModelException {
228
229         Set<ToscaConceptIdentifier> policies = new LinkedHashSet<>(dbsub.getPolicies());
230         policies.addAll(subgrp.getPolicies());
231
232         var subgrp2 = new DeploymentSubGroup(subgrp);
233         subgrp2.getPolicies().clear();
234         subgrp2.getPolicies().addAll(policies);
235
236         return updatePolicies(data, pdpGroup, dbsub, subgrp2);
237     }
238
239     private boolean deletePolicies(SessionData data, String pdpGroup, PdpSubGroup dbsub, DeploymentSubGroup subgrp)
240                     throws PfModelException {
241
242         Set<ToscaConceptIdentifier> policies = new LinkedHashSet<>(dbsub.getPolicies());
243         policies.removeAll(subgrp.getPolicies());
244
245         var subgrp2 = new DeploymentSubGroup(subgrp);
246         subgrp2.getPolicies().clear();
247         subgrp2.getPolicies().addAll(policies);
248
249         return updatePolicies(data, pdpGroup, dbsub, subgrp2);
250     }
251
252     private boolean updatePolicies(SessionData data, String pdpGroup, PdpSubGroup dbsub, DeploymentSubGroup subgrp)
253                     throws PfModelException {
254
255         Set<ToscaConceptIdentifier> undeployed = new HashSet<>(dbsub.getPolicies());
256         undeployed.removeAll(subgrp.getPolicies());
257
258         Set<ToscaConceptIdentifier> deployed = new HashSet<>(subgrp.getPolicies());
259         deployed.removeAll(dbsub.getPolicies());
260
261         if (deployed.isEmpty() && undeployed.isEmpty()) {
262             // lists are identical
263             return false;
264         }
265
266         Set<String> pdps = dbsub.getPdpInstances().stream().map(Pdp::getInstanceId).collect(Collectors.toSet());
267
268         for (ToscaConceptIdentifier policyId : deployed) {
269             ToscaPolicy policyToBeDeployed = data.getPolicy(new ToscaConceptIdentifierOptVersion(policyId));
270             data.trackDeploy(policyToBeDeployed, pdps, pdpGroup, dbsub.getPdpType());
271         }
272
273         for (ToscaConceptIdentifier policyId : undeployed) {
274             data.trackUndeploy(policyId, pdps, pdpGroup, dbsub.getPdpType());
275         }
276
277         dbsub.setPolicies(new ArrayList<>(subgrp.getPolicies()));
278         return true;
279     }
280
281     /**
282      * Performs additional validations of a subgroup.
283      *
284      * @param data session data
285      * @param dbsub the subgroup, from the DB
286      * @param subgrp the subgroup to be validated, updated to fully qualified versions
287      *        upon return
288      * @param container container for additional validation results
289      * @return {@code true} if the subgroup is valid, {@code false} otherwise
290      * @throws PfModelException if an error occurred
291      */
292     private boolean validateSubGroup(SessionData data, PdpSubGroup dbsub, DeploymentSubGroup subgrp,
293                     BeanValidationResult container) throws PfModelException {
294
295         var result = new BeanValidationResult(subgrp.getPdpType(), subgrp);
296
297         result.addResult(validatePolicies(data, dbsub, subgrp));
298         container.addResult(result);
299
300         return result.isValid();
301     }
302
303     /**
304      * Performs additional validations of the policies within a subgroup.
305      *
306      * @param data session data
307      * @param dbsub subgroup from the DB, or {@code null} if this is a new subgroup
308      * @param subgrp the subgroup whose policies are to be validated, updated to fully
309      *        qualified versions upon return
310      * @return the validation result
311      * @throws PfModelException if an error occurred
312      */
313     private ValidationResult validatePolicies(SessionData data, PdpSubGroup dbsub, DeploymentSubGroup subgrp)
314                     throws PfModelException {
315
316         // build a map of the DB data, from policy name to (fully qualified) policy
317         // version
318         Map<String, String> dbname2vers = new HashMap<>();
319         dbsub.getPolicies().forEach(ident -> dbname2vers.put(ident.getName(), ident.getVersion()));
320
321         var result = new BeanValidationResult(subgrp.getPdpType(), subgrp);
322
323         for (ToscaConceptIdentifier ident : subgrp.getPolicies()) {
324             // note: "ident" may not have a fully qualified version
325
326             String expectedVersion = dbname2vers.get(ident.getName());
327             if (expectedVersion != null) {
328                 // policy exists in the DB list - compare the versions
329                 validateVersion(expectedVersion, ident, result);
330                 ident.setVersion(expectedVersion);
331                 continue;
332             }
333
334             // policy doesn't appear in the DB's policy list - look it up
335
336             ToscaPolicy policy = data.getPolicy(new ToscaConceptIdentifierOptVersion(ident));
337             if (policy == null) {
338                 result.addResult(POLICY_RESULT_NAME, ident, ValidationStatus.INVALID, "unknown policy");
339
340             } else if (!isPolicySupported(dbsub.getSupportedPolicyTypes(), policy.getTypeIdentifier())) {
341                 result.addResult(POLICY_RESULT_NAME, ident, ValidationStatus.INVALID,
342                                 "not a supported policy for the subgroup");
343
344             } else {
345                 // replace version with the fully qualified version from the policy
346                 ident.setVersion(policy.getVersion());
347             }
348         }
349
350         return result;
351     }
352
353     /**
354      * Determines if the new version matches the version in the DB.
355      *
356      * @param dbvers fully qualified version from the DB
357      * @param ident identifier whose version is to be validated; the version need not be
358      *        fully qualified
359      * @param result the validation result
360      */
361     private void validateVersion(String dbvers, ToscaConceptIdentifier ident, BeanValidationResult result) {
362         String idvers = ident.getVersion();
363         if (dbvers.equals(idvers)) {
364             return;
365         }
366
367         // did not match - see if it's a prefix
368
369         if (SessionData.isVersionPrefix(idvers) && dbvers.startsWith(idvers + ".")) {
370             // ident has a prefix of this version
371             return;
372         }
373
374         result.addResult(POLICY_RESULT_NAME, ident, ValidationStatus.INVALID,
375                         "different version already deployed: " + dbvers);
376     }
377
378     /**
379      * Deploys or updates PDP policies using the simple API.
380      *
381      * @param policies PDP policies
382      * @param user user triggering deployment
383      * @throws PfModelException if an error occurred
384      */
385     public void deployPolicies(PdpDeployPolicies policies, String user) throws PfModelException {
386         try {
387             MyPdpDeployPolicies checked = coder.convert(policies, MyPdpDeployPolicies.class);
388             ValidationResult result = new BeanValidator().validateTop(PdpDeployPolicies.class.getSimpleName(), checked);
389             if (!result.isValid()) {
390                 String msg = result.getResult().trim();
391                 throw new PfModelException(Status.BAD_REQUEST, msg);
392             }
393         } catch (CoderException e) {
394             throw new PfModelException(Status.INTERNAL_SERVER_ERROR, "cannot decode request", e);
395         }
396
397         process(user, policies, this::deploySimplePolicies);
398     }
399
400     /**
401      * Deploys or updates PDP policies using the simple API. This is the method that does
402      * the actual work.
403      *
404      * @param data session data
405      * @param policies external PDP policies
406      * @throws PfModelException if an error occurred
407      */
408     private void deploySimplePolicies(SessionData data, PdpDeployPolicies policies) throws PfModelException {
409
410         for (ToscaConceptIdentifierOptVersion desiredPolicy : policies.getPolicies()) {
411             try {
412                 processPolicy(data, desiredPolicy);
413
414             } catch (PfModelException | RuntimeException e) {
415                 // no need to log the error here, as it will be logged by the invoker
416                 logger.warn("failed to deploy policy: {}", desiredPolicy);
417                 throw e;
418             }
419         }
420     }
421
422     /**
423      * Adds a policy to a subgroup, if it isn't there already.
424      */
425     @Override
426     protected Updater makeUpdater(SessionData data, ToscaPolicy policy,
427                     ToscaConceptIdentifierOptVersion requestedIdent) {
428
429         ToscaConceptIdentifier desiredIdent = policy.getIdentifier();
430         ToscaConceptIdentifier desiredType = policy.getTypeIdentifier();
431
432         return (group, subgroup) -> {
433
434             if (!isPolicySupported(subgroup.getSupportedPolicyTypes(), desiredType)) {
435                 // doesn't support the desired policy type
436                 return false;
437             }
438
439             if (containsPolicy(group, subgroup, desiredIdent)) {
440                 return false;
441             }
442
443             if (subgroup.getPdpInstances().isEmpty()) {
444                 throw new PfModelRuntimeException(Status.BAD_REQUEST, "group " + group.getName() + " subgroup "
445                                 + subgroup.getPdpType() + " has no active PDPs");
446             }
447
448
449             // add the policy to the subgroup
450             subgroup.getPolicies().add(desiredIdent);
451
452             logger.info("add policy {} to subgroup {} {} count={}", desiredIdent, group.getName(),
453                             subgroup.getPdpType(), subgroup.getPolicies().size());
454
455             Set<String> pdps = subgroup.getPdpInstances().stream().map(Pdp::getInstanceId).collect(Collectors.toSet());
456             ToscaPolicy policyToBeDeployed = data.getPolicy(new ToscaConceptIdentifierOptVersion(desiredIdent));
457             data.trackDeploy(policyToBeDeployed, pdps, group.getName(), subgroup.getPdpType());
458
459             return true;
460         };
461     }
462
463     /**
464      * Determines if a policy type is supported.
465      *
466      * @param supportedTypes supported policy types, any of which may end with ".*"
467      * @param desiredType policy type of interest
468      * @return {@code true} if the policy type is supported, {@code false} otherwise
469      */
470     private boolean isPolicySupported(List<ToscaConceptIdentifier> supportedTypes,
471                     ToscaConceptIdentifier desiredType) {
472
473         if (supportedTypes.contains(desiredType)) {
474             return true;
475         }
476
477         String desiredTypeName = desiredType.getName();
478         for (ToscaConceptIdentifier type : supportedTypes) {
479             String supType = type.getName();
480             if (supType.endsWith(".*") && desiredTypeName.startsWith(supType.substring(0, supType.length() - 1))) {
481                 // matches everything up to, AND INCLUDING, the "."
482                 return true;
483             }
484         }
485
486         return false;
487     }
488
489     /**
490      * Determines if a subgroup already contains the desired policy.
491      *
492      * @param group group that contains the subgroup
493      * @param subgroup subgroup of interest
494      * @param desiredIdent identifier of the desired policy
495      * @return {@code true} if the subgroup contains the desired policy, {@code false}
496      *         otherwise
497      * @throws PfModelRuntimeException if the subgroup contains a different version of the
498      *         desired policy
499      */
500     private boolean containsPolicy(PdpGroup group, PdpSubGroup subgroup, ToscaConceptIdentifier desiredIdent) {
501         String desnm = desiredIdent.getName();
502         String desvers = desiredIdent.getVersion();
503
504         for (ToscaConceptIdentifier actualIdent : subgroup.getPolicies()) {
505             if (!actualIdent.getName().equals(desnm)) {
506                 continue;
507             }
508
509             // found the policy - ensure the version matches
510             if (!actualIdent.getVersion().equals(desvers)) {
511                 throw new PfModelRuntimeException(Status.BAD_REQUEST,
512                                 "group " + group.getName() + " subgroup " + subgroup.getPdpType() + " policy " + desnm
513                                                 + " " + desvers + " different version already deployed: "
514                                                 + actualIdent.getVersion());
515             }
516
517             // already has the desired policy & version
518             logger.info("subgroup {} {} already contains policy {}", group.getName(), subgroup.getPdpType(),
519                         desiredIdent);
520             return true;
521         }
522
523         return false;
524     }
525
526     /*
527      * These are only used to validate the incoming request.
528      */
529
530     @Getter
531     public static class MyPdpDeployPolicies {
532         @NotNull
533         private List<@NotNull @Valid PolicyIdent> policies;
534     }
535
536     @Getter
537     public static class PolicyIdent {
538         @SerializedName("policy-id")
539         @NotNull
540         @Pattern(regexp = PfKey.NAME_REGEXP)
541         private String name;
542
543         @SerializedName("policy-version")
544         @Pattern(regexp = "\\d+([.]\\d+[.]\\d+)?")
545         private String version;
546     }
547 }