From 5b0991a24c891ff2c65e16f4ae79bb1d9bfe6b5f Mon Sep 17 00:00:00 2001 From: Susan Odom Date: Tue, 24 Mar 2020 11:56:15 -0500 Subject: [PATCH 1/5] KAM support initial code --- .../application/rest/v1/ActionsEndpoint.java | 614 +++++++++--------- .../rest/v1/ComponentsEndpoint.java | 26 +- .../application/rest/v1/KAppNavEndpoint.java | 56 +- .../rest/v1/actions/ResourceResolver.java | 4 +- .../rest/v1/configmaps/ConfigMapCache.java | 57 +- .../v1/configmaps/ConfigMapProcessor.java | 195 ++++-- .../KindActionMappingProcessor.java | 462 +++++++++++++ 7 files changed, 1020 insertions(+), 394 deletions(-) create mode 100644 src/main/java/application/rest/v1/configmaps/KindActionMappingProcessor.java diff --git a/src/main/java/application/rest/v1/ActionsEndpoint.java b/src/main/java/application/rest/v1/ActionsEndpoint.java index 94760db..7a66b95 100644 --- a/src/main/java/application/rest/v1/ActionsEndpoint.java +++ b/src/main/java/application/rest/v1/ActionsEndpoint.java @@ -30,7 +30,6 @@ import java.util.Date; import java.sql.Timestamp; -import java.text.ParseException; import java.text.SimpleDateFormat; import javax.inject.Inject; @@ -88,9 +87,7 @@ public class ActionsEndpoint extends KAppNavEndpoint { private static final String className = ActionsEndpoint.class.getName(); - - private static final String CMD_NOT_FOUND = "Command Not Found"; - + private static final String APPLICATION_KIND = "Application"; private static final String JOB_KIND = "Job"; private static final String KAPPNAV_PREVIX = "kappnav"; @@ -151,145 +148,146 @@ public class ActionsEndpoint extends KAppNavEndpoint { summary = "Resolves an action config map action pattern.", description = "Returns the resolved action pattern string for a kAppNav action config map action pattern." ) - @APIResponses({@APIResponse(responseCode = "200", description = "OK"), + @APIResponses({@APIResponse(responseCode = "200", description = "OK"), @APIResponse(responseCode = "207", description = "Multi-Status (Error from Kubernetes API)"), @APIResponse(responseCode = "400", description = "Bad Request (Malformed input)"), @APIResponse(responseCode = "422", description = "Unprocessable Entity (User input is invalid)"), @APIResponse(responseCode = "500", description = "Internal Server Error")}) - public Response resolve(@Pattern(regexp = NAME_PATTERN_ONE_OR_MORE) @PathParam("resource-name") @Parameter(description = "The name of the resource") String name, - @PathParam("resource-kind") @Parameter(description = "The Kubernetes resource kind for the resource") String kind, - @Pattern(regexp = NAME_PATTERN_ZERO_OR_MORE) @DefaultValue("default") @QueryParam("namespace") @Parameter(description = "The namespace of the resource") String namespace, - @DefaultValue("") @QueryParam("action-pattern") @Parameter(description = "The action pattern to resolve") String pattern) { + public Response resolve(@Pattern(regexp = NAME_PATTERN_ONE_OR_MORE) @PathParam("resource-name") @Parameter(description = "The name of the resource") final String name, + @PathParam("resource-kind") @Parameter(description = "The Kubernetes resource kind for the resource") final String kind, + @Pattern(regexp = API_VERSION_PATTERN_ZERO_OR_MORE) @DefaultValue("") @QueryParam("apiVersion") @Parameter(description = "The apiVersion of the resource") final String apiVersion, + @Pattern(regexp = NAME_PATTERN_ZERO_OR_MORE) @DefaultValue("default") @QueryParam("namespace") @Parameter(description = "The namespace of the resource") final String namespace, + @DefaultValue("") @QueryParam("action-pattern") @Parameter(description = "The action pattern to resolve") final String pattern) { try { final ApiClient client = getApiClient(); - ResponseBuilder builder = Response.ok(new ActionSubstitutionResolverResponse(resolve(client, name, kind, "", namespace, pattern)).getJSON()); + final ResponseBuilder builder = Response + .ok(new ActionSubstitutionResolverResponse(resolve(client, name, kind, apiVersion, namespace, pattern)) + .getJSON()); return builder.build(); - } - catch (IOException | ApiException | PatternException e) { + } catch (IOException | ApiException | PatternException e) { String msg = null; if (e instanceof PatternException) { msg = "pattern-error: " + e.getMessage(); if (Logger.isErrorEnabled()) { - Logger.log(className, "resolve", Logger.LogType.ERROR, "Caught PatternException returning status: " + getResponseCode(e) + " " + msg); + Logger.log(className, "resolve", Logger.LogType.ERROR, + "Caught PatternException returning status: " + getResponseCode(e) + " " + msg); } } else if (e instanceof ApiException) { - msg = "input-error: " + e.getMessage(); + msg = "input-error: " + e.getMessage(); if (Logger.isErrorEnabled()) { - Logger.log(className, "resolve", Logger.LogType.ERROR, "Caught ApiException returning status: " + getResponseCode(e) + " " + msg); + Logger.log(className, "resolve", Logger.LogType.ERROR, + "Caught ApiException returning status: " + getResponseCode(e) + " " + msg); } } else { - msg = "internal-error: An internal error occurred in resolving an action config map pattern. error: " + e.getMessage(); + msg = "internal-error: An internal error occurred in resolving an action config map pattern. error: " + + e.getMessage(); if (Logger.isErrorEnabled()) { - Logger.log(className, "resolve", Logger.LogType.ERROR, "Caught IOException returning status: " + getResponseCode(e) + " " + msg); + Logger.log(className, "resolve", Logger.LogType.ERROR, + "Caught IOException returning status: " + getResponseCode(e) + " " + msg); } - } + } return Response.status(getResponseCode(e)).entity(getStatusMessageAsJSON(msg)).build(); } } - + @POST @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) @Path("/{application-name}/execute/command/{command-action-name}") - @Operation( - summary = "Resolves a command action pattern from the action config map and creates a job in Kubernetes from the resolved action.", - description = "Returns the Kubernetes job created from the resolved command action pattern." - ) - @APIResponses({@APIResponse(responseCode = "200", description = "OK"), - @APIResponse(responseCode = "207", description = "Multi-Status (Error from Kubernetes API)"), - @APIResponse(responseCode = "400", description = "Bad Request (Malformed input)"), - @APIResponse(responseCode = "404", description = "Not Found (Command not found in Config Map)"), - @APIResponse(responseCode = "422", description = "Unprocessable Entity (User input is invalid)"), - @APIResponse(responseCode = "500", description = "Internal Server Error")}) + @Operation(summary = "Resolves a command action pattern from the action config map and creates a job in Kubernetes from the resolved action.", description = "Returns the Kubernetes job created from the resolved command action pattern.") + @APIResponses({ @APIResponse(responseCode = "200", description = "OK"), + @APIResponse(responseCode = "207", description = "Multi-Status (Error from Kubernetes API)"), + @APIResponse(responseCode = "400", description = "Bad Request (Malformed input)"), + @APIResponse(responseCode = "404", description = "Not Found (Command not found in Config Map)"), + @APIResponse(responseCode = "422", description = "Unprocessable Entity (User input is invalid)"), + @APIResponse(responseCode = "500", description = "Internal Server Error") }) @RequestBody(description = "User Input Map") - public Response executeApplicationCommand(String jsonstr, @Pattern(regexp = NAME_PATTERN_ONE_OR_MORE) @PathParam("application-name") @Parameter(description = "The name of the application") String name, - @Pattern(regexp = NAME_PATTERN_ZERO_OR_MORE) @DefaultValue("default") @QueryParam("namespace") @Parameter(description = "The namespace of the application") String namespace, - @PathParam("command-action-name") @Parameter(description = "The name of the command action") String commandName, - @CookieParam("kappnav-user") @DefaultValue("") @Parameter(description = "The user that submitted the command action") String user) { - return executeCommand(jsonstr, name, APPLICATION_KIND, "", namespace, commandName, null, null, user); + public Response executeApplicationCommand(final String jsonstr, + @Pattern(regexp = NAME_PATTERN_ONE_OR_MORE) @PathParam("application-name") @Parameter(description = "The name of the application") final String name, + @Pattern(regexp = API_VERSION_PATTERN_ZERO_OR_MORE) @DefaultValue("default") @QueryParam("apiVersion") @Parameter(description = "The apiVersion of the resource") final String apiVersion, + @Pattern(regexp = NAME_PATTERN_ZERO_OR_MORE) @DefaultValue("default") @QueryParam("namespace") @Parameter(description = "The namespace of the application") final String namespace, + @PathParam("command-action-name") @Parameter(description = "The name of the command action") final String commandName, + @CookieParam("kappnav-user") @DefaultValue("") @Parameter(description = "The user that submitted the command action") final String user) { + return executeCommand(jsonstr, name, APPLICATION_KIND, apiVersion, namespace, commandName, null, null, user); } - + @POST @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) @Path("/{application-name}/{component-name}/{component-kind}/execute/command/{command-action-name}") - @Operation( - summary = "Resolves a command action pattern from the action config map and creates a job in Kubernetes from the resolved action.", - description = "Returns the Kubernetes job created from the resolved command action pattern." - ) - @APIResponses({@APIResponse(responseCode = "200", description = "OK"), - @APIResponse(responseCode = "207", description = "Multi-Status (Error from Kubernetes API)"), - @APIResponse(responseCode = "400", description = "Bad Request (Malformed input)"), - @APIResponse(responseCode = "404", description = "Not Found (Command not found in Config Map)"), - @APIResponse(responseCode = "422", description = "Unprocessable Entity (User input is invalid)"), - @APIResponse(responseCode = "500", description = "Internal Server Error")}) + @Operation(summary = "Resolves a command action pattern from the action config map and creates a job in Kubernetes from the resolved action.", description = "Returns the Kubernetes job created from the resolved command action pattern.") + @APIResponses({ @APIResponse(responseCode = "200", description = "OK"), + @APIResponse(responseCode = "207", description = "Multi-Status (Error from Kubernetes API)"), + @APIResponse(responseCode = "400", description = "Bad Request (Malformed input)"), + @APIResponse(responseCode = "404", description = "Not Found (Command not found in Config Map)"), + @APIResponse(responseCode = "422", description = "Unprocessable Entity (User input is invalid)"), + @APIResponse(responseCode = "500", description = "Internal Server Error") }) @RequestBody(description = "User Input Map") - public Response executeComponentCommand(String jsonstr, @Pattern(regexp = NAME_PATTERN_ONE_OR_MORE) @PathParam("application-name") @Parameter(description = "The name of the application") String appName, - @Pattern(regexp = NAME_PATTERN_ZERO_OR_MORE) @DefaultValue("default") @QueryParam("application-namespace") @Parameter(description = "The namespace of the application") String appNamespace, - @Pattern(regexp = NAME_PATTERN_ONE_OR_MORE) @PathParam("component-name") @Parameter(description = "The name of the component") String name, - @PathParam("component-kind") @Parameter(description = "The Kubernetes resource kind for the component") String kind, - @Pattern(regexp = NAME_PATTERN_ZERO_OR_MORE) @DefaultValue("default") @QueryParam("namespace") @Parameter(description = "The namespace of the component") String namespace, - @PathParam("command-action-name") @Parameter(description = "The name of the command action") String commandName, - @CookieParam("kappnav-user") @DefaultValue("") @Parameter(description = "The user that submitted the command action") String user) { - return executeCommand(jsonstr, name, kind, "", namespace, commandName, appName, appNamespace, user); + public Response executeComponentCommand(final String jsonstr, + @Pattern(regexp = NAME_PATTERN_ONE_OR_MORE) @PathParam("application-name") @Parameter(description = "The name of the application") final String appName, + @Pattern(regexp = NAME_PATTERN_ZERO_OR_MORE) @DefaultValue("default") @QueryParam("application-namespace") @Parameter(description = "The namespace of the application") final String appNamespace, + @Pattern(regexp = NAME_PATTERN_ONE_OR_MORE) @PathParam("component-name") @Parameter(description = "The name of the component") final String name, + @PathParam("component-kind") @Parameter(description = "The Kubernetes resource kind for the component") final String kind, + @Pattern(regexp = API_VERSION_PATTERN_ZERO_OR_MORE) @DefaultValue("default") @QueryParam("apiVersion") @Parameter(description = "The apiVersion of the resource") final String apiVersion, + @Pattern(regexp = NAME_PATTERN_ZERO_OR_MORE) @DefaultValue("default") @QueryParam("namespace") @Parameter(description = "The namespace of the component") final String namespace, + @PathParam("command-action-name") @Parameter(description = "The name of the command action") final String commandName, + @CookieParam("kappnav-user") @DefaultValue("") @Parameter(description = "The user that submitted the command action") final String user) { + return executeCommand(jsonstr, name, kind, apiVersion, namespace, commandName, appName, appNamespace, user); } @GET @Produces(MediaType.APPLICATION_JSON) @Path("/command-time") - @Operation( - summary = "Retrieve current time to specify as query value for retrieving Kubernetes jobs using /commands api. Time returned in yyyy-MM-dd'T'HH:mm:ss.000Z format", - description = "Retrieve current time for retrieving jobs." - ) - @APIResponses({@APIResponse(responseCode = "200", description = "OK"), - @APIResponse(responseCode = "207", description = "Multi-Status (Error from Kubernetes API)"), - @APIResponse(responseCode = "500", description = "Internal Server Error")}) - public Response getCommandTime() { + @Operation(summary = "Retrieve current time to specify as query value for retrieving Kubernetes jobs using /commands api. Time returned in yyyy-MM-dd'T'HH:mm:ss.000Z format", description = "Retrieve current time for retrieving jobs.") + @APIResponses({ @APIResponse(responseCode = "200", description = "OK"), + @APIResponse(responseCode = "207", description = "Multi-Status (Error from Kubernetes API)"), + @APIResponse(responseCode = "500", description = "Internal Server Error") }) + public Response getCommandTime() { final String methodName = "getCommandTime"; if (Logger.isEntryEnabled()) { Logger.log(className, methodName, Logger.LogType.ENTRY, ""); - } + } - // convert current time to {time: value} JSON where value is yyyy-MM-dd'T'HH:mm:ss.000Z format - // This format matches the format returned in the completionTime field for jobs. Kubernetes - // stores timestamps only to the second, the 000 (millisecond) is just to unify timestamp format - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.'000Z'"); + // convert current time to {time: value} JSON where value is + // yyyy-MM-dd'T'HH:mm:ss.000Z format + // This format matches the format returned in the completionTime field for jobs. + // Kubernetes + // stores timestamps only to the second, the 000 (millisecond) is just to unify + // timestamp format + final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.'000Z'"); dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); String formattedTimestamp = null; final Date date = new Date(); - try { - formattedTimestamp = dateFormat.format( date ); - } - catch(Exception e) { - String msg = "Internal error parsing date: " + date; + try { + formattedTimestamp = dateFormat.format(date); + } catch (final Exception e) { + final String msg = "Internal error parsing date: " + date; if (Logger.isErrorEnabled()) { - Logger.log(className, methodName, Logger.LogType.ERROR, msg); + Logger.log(className, methodName, Logger.LogType.ERROR, msg); } return Response.status(getResponseCode(e)).entity(getStatusMessageAsJSON(msg)).build(); } - JsonObject timeJSON = new JsonObject(); - JsonElement timeElement = new JsonPrimitive(formattedTimestamp); + final JsonObject timeJSON = new JsonObject(); + final JsonElement timeElement = new JsonPrimitive(formattedTimestamp); timeJSON.add(TIME_PROPERTY_NAME, timeElement); if (Logger.isExitEnabled()) { - Logger.log(className, methodName, Logger.LogType.EXIT, "timeJSON="+timeJSON); + Logger.log(className, methodName, Logger.LogType.EXIT, "timeJSON=" + timeJSON); } - return Response.ok(timeJSON.toString()).build(); + return Response.ok(timeJSON.toString()).build(); } - + @GET @Produces(MediaType.APPLICATION_JSON) @Path("/commands") - @Operation( - summary = "Retrieve the list of Kubernetes jobs for command actions, optionally filtered by user and time and only jobs with completion timestamp newer than specified timestamp are returned", - description = "Returns two lists: the list of Kubernetes jobs for command actions; the list of job actions for the jobs." - ) - @APIResponses({@APIResponse(responseCode = "200", description = "OK"), - @APIResponse(responseCode = "207", description = "Multi-Status (Error from Kubernetes API)"), - @APIResponse(responseCode = "500", description = "Internal Server Error")}) - public Response getCommands(@CookieParam("kappnav-user") @DefaultValue("") @Parameter(description = "The user that submitted the command action") String user, - @QueryParam("time") @DefaultValue("") @Parameter(description = "The job completion time stamp in yyyy-MM-dd'T'HH:mm:sss format") String time) { + @Operation(summary = "Retrieve the list of Kubernetes jobs for command actions, optionally filtered by user and time and only jobs with completion timestamp newer than specified timestamp are returned", description = "Returns two lists: the list of Kubernetes jobs for command actions; the list of job actions for the jobs.") + @APIResponses({ @APIResponse(responseCode = "200", description = "OK"), + @APIResponse(responseCode = "207", description = "Multi-Status (Error from Kubernetes API)"), + @APIResponse(responseCode = "500", description = "Internal Server Error") }) + public Response getCommands( + @CookieParam("kappnav-user") @DefaultValue("") @Parameter(description = "The user that submitted the command action") final String user, + @QueryParam("time") @DefaultValue("") @Parameter(description = "The job completion time stamp in yyyy-MM-dd'T'HH:mm:sss format") final String time) { final String methodName = "getCommands"; if (Logger.isEntryEnabled()) { @@ -297,74 +295,78 @@ public Response getCommands(@CookieParam("kappnav-user") @DefaultValue("") @Para } try { final ApiClient client = getApiClient(); - + // Build the selector for the query. final Selector s = new Selector().addMatchLabel(KAPPNAV_JOB_TYPE, KAPPNAV_JOB_COMMAND_TYPE); - + // convert time to timestamp in yyyy-MM-dd'T'HH:mm:sss format - Timestamp timelaterTimestamp = convertTimeStringToTimestamp(time); - - final String labelSelector = s.toString(); + final Timestamp timelaterTimestamp = convertTimeStringToTimestamp(time); + + final String labelSelector = s.toString(); // Query the list of jobs from Kubernetes and return the list to the caller. final BatchV1Api batch = new BatchV1Api(); batch.setApiClient(client); if (Logger.isDebugEnabled()) { - Logger.log(className, methodName, Logger.LogType.DEBUG, - "GLOBAL_NAMESPACE=" + GLOBAL_NAMESPACE + " ,labelSelector=" + labelSelector); + Logger.log(className, methodName, Logger.LogType.DEBUG, + "GLOBAL_NAMESPACE=" + GLOBAL_NAMESPACE + " ,labelSelector=" + labelSelector); } - List commands = getItemsAsList(client, batch.listNamespacedJob(GLOBAL_NAMESPACE, null, null, null, null, labelSelector, null, null, null, null)); - final CommandsResponse response = new CommandsResponse(); - + final List commands = getItemsAsList(client, batch.listNamespacedJob(GLOBAL_NAMESPACE, null, + null, null, null, labelSelector, null, null, null, null)); + final CommandsResponse response = new CommandsResponse(); + commands.forEach(v -> { if (v.get(KIND_PROPERTY_NAME) == null) { v.addProperty(KIND_PROPERTY_NAME, JOB_KIND); } - if(user != null && !user.isEmpty()) { + if (user != null && !user.isEmpty()) { // Only return jobs belonging to the user final String job_username = getCommandActionUserName(v); - if(! user.equals(job_username)) { + if (!user.equals(job_username)) { // Completely skip this command because it does // not belong to the user requested by the API caller return; } } - if (time == null || time.isEmpty()) { - //no time specified, return all jobs + if (time == null || time.isEmpty()) { + // no time specified, return all jobs response.add(v); - } else { - JsonObject status = v.getAsJsonObject(STATUS_PROPERTY_NAME); + } else { + final JsonObject status = v.getAsJsonObject(STATUS_PROPERTY_NAME); if (status != null) { - JsonElement e = status.get(COMPLETION_TIME_PROPERTY_NAME); + final JsonElement e = status.get(COMPLETION_TIME_PROPERTY_NAME); if (e != null && e.isJsonPrimitive()) { - String completionTime = e.getAsString(); + final String completionTime = e.getAsString(); if (completionTime != null) { // convert job completion time to timestamp in yyyy-MM-dd'T'HH:mm:sss format try { - Timestamp completionTimestamp = convertTimeStringToTimestamp(completionTime); - // Only jobs with completion time stamp newer than specified time stamp are returned or no timestamp specified. - if (completionTimestamp.after(timelaterTimestamp)) { + final Timestamp completionTimestamp = convertTimeStringToTimestamp(completionTime); + // Only jobs with completion time stamp newer than specified time stamp are + // returned or no timestamp specified. + if (completionTimestamp.after(timelaterTimestamp)) { response.add(v); - } - } catch (Exception ex) { + } + } catch (final Exception ex) { if (Logger.isDebugEnabled()) { - Logger.log(className, methodName, Logger.LogType.DEBUG, "Caught an exception " + ex.toString()); + Logger.log(className, methodName, Logger.LogType.DEBUG, + "Caught an exception " + ex.toString()); } - } + } } } } } }); - // If there are jobs, get actions available for jobs and add to response - if ( ! commands.isEmpty() && response.size() > 0) { - JsonObject job= commands.get(0); // get first job, any job, so we can retrieve actions + // If there are jobs, get actions available for jobs and add to response + if (!commands.isEmpty() && response.size() > 0) { + final JsonObject job = commands.get(0); // get first job, any job, so we can retrieve actions final ConfigMapProcessor processor = new ConfigMapProcessor(KAPPNAV_JOB_RESOURCE_KIND); - JsonObject actionsMap = processor.getConfigMap(client, job, ConfigMapProcessor.ConfigMapType.ACTION); + final JsonObject actionsMap = processor.getConfigMap(client, job, + ConfigMapProcessor.ConfigMapType.ACTION); response.addActions(actionsMap); } @@ -372,176 +374,183 @@ public Response getCommands(@CookieParam("kappnav-user") @DefaultValue("") @Para if (Logger.isExitEnabled()) { Logger.log(className, methodName, Logger.LogType.EXIT, "responseJSON=" + responseJSON); } - return Response.ok(responseJSON).build(); - } - catch (IOException | ApiException e ) { - String msg = null; - if (e instanceof ApiException) { - msg = "input-error: " + e.getMessage(); + return Response.ok(responseJSON).build(); + } catch (IOException | ApiException e) { + String msg = null; + if (e instanceof ApiException) { + msg = "input-error: " + e.getMessage(); if (Logger.isErrorEnabled()) { - Logger.log(className, methodName, Logger.LogType.ERROR, "Caught ApiException returning status: " + getResponseCode(e) + " " + msg); + Logger.log(className, methodName, Logger.LogType.ERROR, + "Caught ApiException returning status: " + getResponseCode(e) + " " + msg); } } else { - msg = "internal-error: An internal error occurred in retrieving list of kubernetes jobs. error: " + e.getMessage(); + msg = "internal-error: An internal error occurred in retrieving list of kubernetes jobs. error: " + + e.getMessage(); if (Logger.isErrorEnabled()) { - Logger.log(className, methodName, Logger.LogType.ERROR, "Caught IOException returning status: " + getResponseCode(e) + " " + msg); - } + Logger.log(className, methodName, Logger.LogType.ERROR, + "Caught IOException returning status: " + getResponseCode(e) + " " + msg); + } } return Response.status(getResponseCode(e)).entity(getStatusMessageAsJSON(msg)).build(); - } + } } - + @DELETE @Produces(MediaType.APPLICATION_JSON) @Path("/command/{job-name}") - @Operation( - summary = "Deletes a command action job.", - description = "Delete the specified command action job. The namespace of the job is assumed to be 'kappnav'." - ) - @APIResponses({@APIResponse(responseCode = "200", description = "OK"), - @APIResponse(responseCode = "207", description = "Multi-Status (Error from Kubernetes API)"), - @APIResponse(responseCode = "500", description = "Internal Server Error")}) - public Response deleteCommand(@Pattern(regexp = NAME_PATTERN_ONE_OR_MORE) @PathParam("job-name") @Parameter(description = "The name of the command action job") String jobName) { + @Operation(summary = "Deletes a command action job.", description = "Delete the specified command action job. The namespace of the job is assumed to be 'kappnav'.") + @APIResponses({ @APIResponse(responseCode = "200", description = "OK"), + @APIResponse(responseCode = "207", description = "Multi-Status (Error from Kubernetes API)"), + @APIResponse(responseCode = "500", description = "Internal Server Error") }) + public Response deleteCommand( + @Pattern(regexp = NAME_PATTERN_ONE_OR_MORE) @PathParam("job-name") @Parameter(description = "The name of the command action job") final String jobName) { try { - final ApiClient client = getApiClient(); + final ApiClient client = getApiClient(); final BatchV1Api batch = new BatchV1Api(); batch.setApiClient(client); - + // Check that the specified job exists and is a command action. final Selector s = new Selector().addMatchLabel(KAPPNAV_JOB_TYPE, KAPPNAV_JOB_COMMAND_TYPE); - JsonObject job = getItemAsObject(client, batch.readNamespacedJob(jobName, GLOBAL_NAMESPACE, null, null, null)); + final JsonObject job = getItemAsObject(client, + batch.readNamespacedJob(jobName, GLOBAL_NAMESPACE, null, null, null)); if (!s.matches(job)) { - String msg = "input-error: Job " + jobName + " is not found in command action."; + final String msg = "input-error: Job " + jobName + " is not found in command action."; if (Logger.isErrorEnabled()) { - Logger.log(className, "deleteCommand", Logger.LogType.ERROR, msg); + Logger.log(className, "deleteCommand", Logger.LogType.ERROR, msg); } throw new ApiException(404, msg); } - + // Delete the specified job. final V1DeleteOptions options = new V1DeleteOptions(); batch.deleteNamespacedJob(jobName, GLOBAL_NAMESPACE, options, null, 0, true, ""); return Response.ok(getStatusMessageAsJSON("OK")).build(); - } - catch (JsonSyntaxException e) { + } catch (final JsonSyntaxException e) { final Throwable cause = e.getCause(); if (cause instanceof IllegalStateException) { final IllegalStateException _cause = (IllegalStateException) e.getCause(); final String message = _cause.getMessage(); if (message != null && message.contains("Expected a string but was BEGIN_OBJECT")) { // Workaround for an issue in the Kubernetes API client. The job was deleted, - // but due to a defect in the client an error occurs in constructing the V1Status - // object return value. See "https://github.com/kubernetes-client/java/issues/86" + // but due to a defect in the client an error occurs in constructing the + // V1Status + // object return value. See + // "https://github.com/kubernetes-client/java/issues/86" // for more details. return Response.ok(getStatusMessageAsJSON("OK")).build(); } } - String msg = "syntax-error: " + e.getMessage(); + final String msg = "syntax-error: " + e.getMessage(); if (Logger.isErrorEnabled()) { - Logger.log(className, "deleteCommand", Logger.LogType.ERROR, "Caught JsonSyntaxException returning status: " + getResponseCode(e) + " " + msg); + Logger.log(className, "deleteCommand", Logger.LogType.ERROR, + "Caught JsonSyntaxException returning status: " + getResponseCode(e) + " " + msg); } return Response.status(getResponseCode(e)).entity(getStatusMessageAsJSON(msg)).build(); - } - catch (IOException | ApiException e) { - String msg = null; + } catch (IOException | ApiException e) { + String msg = null; if (e instanceof ApiException) { - msg = "input-error: " + e.getMessage(); + msg = "input-error: " + e.getMessage(); if (Logger.isErrorEnabled()) { - Logger.log(className, "deleteCommand", Logger.LogType.ERROR, "Caught ApiException returning status: " + getResponseCode(e) + " " + msg); + Logger.log(className, "deleteCommand", Logger.LogType.ERROR, + "Caught ApiException returning status: " + getResponseCode(e) + " " + msg); } - } - else { - msg = "internal-error: " + "An internal error occurred in deleting a command action job. error:" + e.getMessage(); + } else { + msg = "internal-error: " + "An internal error occurred in deleting a command action job. error:" + + e.getMessage(); if (Logger.isErrorEnabled()) { - Logger.log(className, "deleteCommand", Logger.LogType.ERROR, "Caught IOException returning status: " + getResponseCode(e) + " " + msg); + Logger.log(className, "deleteCommand", Logger.LogType.ERROR, + "Caught IOException returning status: " + getResponseCode(e) + " " + msg); } } return Response.status(getResponseCode(e)).entity(getStatusMessageAsJSON(msg)).build(); - } + } } - + @GET @Produces(MediaType.APPLICATION_JSON) @Path("/{resource-name}/{resource-kind}/actions") - @Operation( - summary = "Retrieves an action config map for the specified Kubernetes resource.", - description = "Returns the action config map for the specified Kubernetes resource." - ) - @APIResponses({@APIResponse(responseCode = "200", description = "OK"), - @APIResponse(responseCode = "207", description = "Multi-Status (Error from Kubernetes API)"), - @APIResponse(responseCode = "400", description = "Bad Request (Malformed input)"), - @APIResponse(responseCode = "500", description = "Internal Server Error")}) - public Response getActionMap(@Pattern(regexp = NAME_PATTERN_ONE_OR_MORE) @PathParam("resource-name") @Parameter(description = "The name of the resource") String name, - @PathParam("resource-kind") @Parameter(description = "The Kubernetes resource kind for the resource") String kind, - @Pattern(regexp = NAME_PATTERN_ZERO_OR_MORE) @DefaultValue("default") @QueryParam("namespace") @Parameter(description = "The namespace of the resource") String namespace) { + @Operation(summary = "Retrieves an action config map for the specified Kubernetes resource.", description = "Returns the action config map for the specified Kubernetes resource.") + @APIResponses({ @APIResponse(responseCode = "200", description = "OK"), + @APIResponse(responseCode = "207", description = "Multi-Status (Error from Kubernetes API)"), + @APIResponse(responseCode = "400", description = "Bad Request (Malformed input)"), + @APIResponse(responseCode = "500", description = "Internal Server Error") }) + public Response getActionMap( + @Pattern(regexp = NAME_PATTERN_ONE_OR_MORE) @PathParam("resource-name") @Parameter(description = "The name of the resource") final String name, + @PathParam("resource-kind") @Parameter(description = "The Kubernetes resource kind for the resource") final String kind, + @Pattern(regexp = API_VERSION_PATTERN_ZERO_OR_MORE) @DefaultValue("") @QueryParam("apiVersion") @Parameter(description = "The apiVersion of the resource") final String apiVersion, + @Pattern(regexp = NAME_PATTERN_ZERO_OR_MORE) @DefaultValue("default") @QueryParam("namespace") @Parameter(description = "The namespace of the resource") final String namespace) { try { final ApiClient client = getApiClient(); - final JsonObject resource = getResource(client, name, kind, "", namespace); + final JsonObject resource = getResource(client, name, kind, apiVersion, namespace); final JsonObject map; - + if (resource != null) { - ConfigMapProcessor processor = new ConfigMapProcessor(kind); + final ConfigMapProcessor processor = new ConfigMapProcessor(kind); map = processor.getConfigMap(client, resource, ConfigMapProcessor.ConfigMapType.ACTION); - } - else { + } else { map = new JsonObject(); } return Response.ok(map.toString()).build(); - } - catch (IOException | ApiException e) { - String msg = null; + } catch (IOException | ApiException e) { + String msg = null; if (e instanceof ApiException) { - msg = "input-error: " + e.getMessage(); + msg = "input-error: " + e.getMessage(); if (Logger.isErrorEnabled()) { - Logger.log(className, "getActionMap", Logger.LogType.ERROR, "Caught ApiException returning status: " + getResponseCode(e) + " " + msg); + Logger.log(className, "getActionMap", Logger.LogType.ERROR, + "Caught ApiException returning status: " + getResponseCode(e) + " " + msg); } - } else { - msg = "internal-error: An internal error occurred in retrieving an action config map. error: " + e.getMessage(); + } else { + msg = "internal-error: An internal error occurred in retrieving an action config map. error: " + + e.getMessage(); if (Logger.isErrorEnabled()) { - Logger.log(className, "getActionMap", Logger.LogType.ERROR, "Caught IOException returning status: " + getResponseCode(e) + " " + msg); + Logger.log(className, "getActionMap", Logger.LogType.ERROR, + "Caught IOException returning status: " + getResponseCode(e) + " " + msg); } } return Response.status(getResponseCode(e)).entity(getStatusMessageAsJSON(msg)).build(); - } + } } - - private String resolve(ApiClient client, String name, String kind, String apiVersion, String namespace, String pattern) throws ApiException { - String methodName = "resolve"; + + private String resolve(final ApiClient client, final String name, final String kind, final String apiVersion, + final String namespace, final String pattern) throws ApiException { + final String methodName = "resolve"; if (Logger.isEntryEnabled()) { - Logger.log(className, methodName, Logger.LogType.ENTRY, "Name=" + name + ", kind="+ kind + ", apiVersion="+apiVersion + ", namespace="+namespace + ", pattern="+pattern); + Logger.log(className, methodName, Logger.LogType.ENTRY, "Name=" + name + ", kind=" + kind + ", apiVersion=" + + apiVersion + ", namespace=" + namespace + ", pattern=" + pattern); } - final JsonObject resource; + final JsonObject resource; final ResolvedValue resolvedValue; - try { - resource = getResource(client, name, kind, apiVersion, namespace); + try { + resource = getResource(client, name, kind, apiVersion, namespace); // Add a 'kind' property to the resource if it is missing. if (resource.get(KIND_PROPERTY_NAME) == null) { resource.addProperty(KIND_PROPERTY_NAME, kind); - } + } final ResolutionContext context = new ResolutionContext(client, registry, resource, kind); resolvedValue = context.resolve(pattern); - } catch (ApiException e) { - String msg = e.getMessage(); + } catch (final ApiException e) { + final String msg = e.getMessage(); if (Logger.isErrorEnabled()) { Logger.log(className, methodName, Logger.LogType.ERROR, "Caught ApiException " + msg); } - throw new ApiException(404, msg); - } catch (PatternException e) { - String msg = e.getMessage(); + throw new ApiException(404, msg); + } catch (final PatternException e) { + final String msg = e.getMessage(); if (Logger.isErrorEnabled()) { Logger.log(className, methodName, Logger.LogType.ERROR, "Caught PatternException " + msg); } throw new ApiException(207, msg); - } + } if (Logger.isExitEnabled()) { - Logger.log(className, methodName, Logger.LogType.EXIT, "ResolvedValue="+resolvedValue.getValue()); + Logger.log(className, methodName, Logger.LogType.EXIT, "ResolvedValue=" + resolvedValue.getValue()); } - return resolvedValue.getValue(); + return resolvedValue.getValue(); } - - private Response executeCommand(String jsonstr, String name, String kind, String apiVersion, String namespace, - String commandName, String appName, String appNamespace, String user) { + + private Response executeCommand(final String jsonstr, final String name, final String kind, final String apiVersion, + final String namespace, final String commandName, final String appName, final String appNamespace, + final String user) { final String methodName = "executeCommand"; if (Logger.isErrorEnabled()) { @@ -554,67 +563,68 @@ private Response executeCommand(String jsonstr, String name, String kind, String try { final ApiClient client = getApiClient(); final JsonObject resource; - try { + try { resource = getResource(client, name, kind, apiVersion, namespace); - } catch (ApiException e) { - String msg = e.getMessage(); + } catch (final ApiException e) { + final String msg = e.getMessage(); if (Logger.isErrorEnabled()) { Logger.log(className, methodName, Logger.LogType.ERROR, "Caught ApiException " + msg); } - throw new ApiException (404, msg); + throw new ApiException(404, msg); } - + // Add a 'kind' property to the resource if it is missing. if (resource.get(KIND_PROPERTY_NAME) == null) { resource.addProperty(KIND_PROPERTY_NAME, kind); } final ResolutionContext context = new ResolutionContext(client, registry, resource, kind); - - // Retrieve the command action from the config map. - final JsonObject cmdAction = context.getCommandAction(commandName); + + // Retrieve the command action from the config map. + final JsonObject cmdAction = context.getCommandAction(commandName); if (cmdAction == null) { - String msg = "Command action " + commandName + " is not found in config map."; + final String msg = "Command action " + commandName + " is not found in config map."; if (Logger.isErrorEnabled()) { Logger.log(className, methodName, Logger.LogType.ERROR, msg); } throw new ApiException(404, msg); } - + // Process user input if required. processUserInput(jsonstr, cmdAction, context); - + // Retrieve the image name. final JsonElement imageProp = cmdAction.get(IMAGE_PROPERTY_NAME); if (imageProp == null || !imageProp.isJsonPrimitive()) { - String msg = "Image was not specified in the command action."; + final String msg = "Image was not specified in the command action."; if (Logger.isErrorEnabled()) { Logger.log(className, methodName, Logger.LogType.ERROR, msg); } throw new ApiException(404, msg); } final String imageName = imageProp.getAsString(); - + // Retrieve the command pattern. final JsonElement cmdPatternProp = cmdAction.get(CMD_PATTERN_PROPERTY_NAME); if (cmdPatternProp == null || !cmdPatternProp.isJsonPrimitive()) { - String msg = "cmd-pattern was not specified in the command action."; + final String msg = "cmd-pattern was not specified in the command action."; if (Logger.isErrorEnabled()) { Logger.log(className, methodName, Logger.LogType.ERROR, msg); } throw new ApiException(404, msg); } final String cmdPattern = cmdPatternProp.getAsString(); - + // Retrieve the display text. final JsonElement textProp = cmdAction.get(TEXT_PROPERTY_NAME); final String text = (textProp != null && textProp.isJsonPrimitive()) ? textProp.getAsString() : null; - + // Resolve the command pattern. final ResolvedValue resolvedValue = context.resolve(cmdPattern); if (!resolvedValue.isFullyResolved()) { // If the resolution failed we should stop here instead of generating a bad job. // REVISIT: Message translation required. - String msg = "An internal error occurred in the resolution of the command action pattern" + cmdPattern; + final String msg = "An internal error occurred in the resolution of the command action pattern" + + cmdPattern; if (Logger.isErrorEnabled()) { Logger.log(className, methodName, Logger.LogType.ERROR, msg); } @@ -622,7 +632,7 @@ private Response executeCommand(String jsonstr, String name, String kind, String } final String resolvedPattern = resolvedValue.getValue(); final CommandLineTokenizer tokenizer = new CommandLineTokenizer(resolvedPattern); - + // Construct the pod template / container from the request. final V1Container container = new V1Container(); container.setName(KAPPNAV_PREVIX + "-" + UUID.randomUUID().toString()); @@ -632,38 +642,38 @@ private Response executeCommand(String jsonstr, String name, String kind, String command.add(parameter); }); container.setCommand(command); - + final V1PodSpec spec = new V1PodSpec(); spec.setContainers(Collections.singletonList(container)); spec.setRestartPolicy("Never"); setSecurityContextAndServiceAccountName(client, spec); - + final V1PodTemplateSpec podTemplate = new V1PodTemplateSpec(); podTemplate.setSpec(spec); - + // Create and populate the job object. final V1Job job = new V1Job(); job.setApiVersion("batch/v1"); job.setKind("Job"); final V1ObjectMeta meta = new V1ObjectMeta(); meta.setName(KAPPNAV_PREVIX + "-" + UUID.randomUUID().toString()); - + // Add context labels to the job, allowing for queries using selectors. - final Map labels = createJobLabels(client, resource, name, kind, - namespace, appName, appNamespace, commandName); + final Map labels = createJobLabels(client, resource, name, kind, namespace, appName, + appNamespace, commandName); meta.setLabels(labels); - - final Map annotations = createJobAnnotations(text, user); + + final Map annotations = createJobAnnotations(text, user); if (annotations != null) { meta.setAnnotations(annotations); - } - + } + job.setMetadata(meta); final V1JobSpec jobSpec = new V1JobSpec(); job.setSpec(jobSpec); jobSpec.setBackoffLimit(4); jobSpec.setTemplate(podTemplate); - + // Submit the job to Kubernetes and return the job object to the caller. final BatchV1Api batch = new BatchV1Api(); batch.setApiClient(client); @@ -672,32 +682,33 @@ private Response executeCommand(String jsonstr, String name, String kind, String Logger.log(className, methodName, Logger.LogType.EXIT, response.toString()); } return Response.ok(response.toString()).build(); - } - catch (IOException | JsonSyntaxException | ApiException | KAppNavException | ValidationException | PatternException e) { + } catch (IOException | JsonSyntaxException | ApiException | KAppNavException | ValidationException + | PatternException e) { String msg = null; if (e instanceof JsonSyntaxException) msg = "syntax-error: "; - if (e instanceof ApiException) + if (e instanceof ApiException) msg = "input-error: "; if (e instanceof IOException || e instanceof KAppNavException) msg = "internal-error: "; - if (e instanceof ValidationException) + if (e instanceof ValidationException) msg = "validation-error: "; - if (e instanceof PatternException) - msg = "pattern-error: "; - msg = msg + e.getMessage(); + if (e instanceof PatternException) + msg = "pattern-error: "; + msg = msg + e.getMessage(); if (Logger.isErrorEnabled()) { Logger.log(className, methodName, Logger.LogType.ERROR, "Caught Exception " + msg); } return Response.status(getResponseCode(e)).entity(getStatusMessageAsJSON(msg)).build(); - } + } } - - private void processUserInput(String jsonstr, JsonObject action, ResolutionContext context) throws JsonSyntaxException, - ValidationException, KAppNavException { - String methodName = "processUserInput"; + + private void processUserInput(final String jsonstr, final JsonObject action, final ResolutionContext context) + throws JsonSyntaxException, ValidationException, KAppNavException { + final String methodName = "processUserInput"; if (Logger.isEntryEnabled()) { - Logger.log(className, methodName, Logger.LogType.ENTRY, "JsonStr=" + jsonstr + ", action= " + action.toString() + ", context=" + context.toString()); + Logger.log(className, methodName, Logger.LogType.ENTRY, + "JsonStr=" + jsonstr + ", action= " + action.toString() + ", context=" + context.toString()); } final JsonElement requiresInputProp = action.get(REQUIRES_INPUT_PROPERTY_NAME); @@ -707,31 +718,30 @@ private void processUserInput(String jsonstr, JsonObject action, ResolutionConte if (fields != null) { final JsonObject userInput; if (jsonstr != null && !jsonstr.trim().isEmpty()) { - JsonParser parser = new JsonParser(); - JsonElement element = parser.parse(jsonstr); + final JsonParser parser = new JsonParser(); + final JsonElement element = parser.parse(jsonstr); if (element.isJsonObject()) { userInput = element.getAsJsonObject(); - } - else { + } else { // REVISIT: Message translation required. - String msg = "The inputs " + jsonstr + " specified was in the wrong format. It is expected to be a map."; + final String msg = "The inputs " + jsonstr + + " specified was in the wrong format. It is expected to be a map."; if (Logger.isErrorEnabled()) { Logger.log(className, methodName, Logger.LogType.ERROR, msg); } throw new ValidationException(msg); } - } - else { + } else { userInput = null; } if (Logger.isExitEnabled()) { - Logger.log(className, methodName, Logger.LogType.EXIT, "userInput="+userInput); + Logger.log(className, methodName, Logger.LogType.EXIT, "userInput=" + userInput); } context.setUserInput(userInput, fields); - } - else { + } else { // REVISIT: Message translation required. - String msg = "An error occurred in the resolution of the field definitions for input \"" + requiresInput + "\"." ; + final String msg = "An error occurred in the resolution of the field definitions for input \"" + + requiresInput + "\"."; if (Logger.isErrorEnabled()) { Logger.log(className, methodName, Logger.LogType.ERROR, msg); } @@ -739,9 +749,9 @@ private void processUserInput(String jsonstr, JsonObject action, ResolutionConte } } } - + // REVISIT: Should re-think how we set this for user provided command actions. - private void setSecurityContextAndServiceAccountName(ApiClient client, V1PodSpec spec) { + private void setSecurityContextAndServiceAccountName(final ApiClient client, final V1PodSpec spec) { final V1PodSecurityContext podSecurityContext = new V1PodSecurityContext(); podSecurityContext.setRunAsNonRoot(true); podSecurityContext.setRunAsUser(1001L); @@ -750,43 +760,46 @@ private void setSecurityContextAndServiceAccountName(ApiClient client, V1PodSpec // Initialize the config here if CDI failed to do it. config = new KAppNavConfig(client); } - spec.setServiceAccountName(config.getkAppNavServiceAccountName()); + spec.setServiceAccountName(config.getkAppNavServiceAccountName()); } - - private Map createJobAnnotations(String text, String user) { - Map annotations = new HashMap(); + + private Map createJobAnnotations(final String text, final String user) { + final Map annotations = new HashMap(); if (text != null && !text.isEmpty()) { annotations.put(KAPPNAV_JOB_ACTION_TEXT, text); } - if(user != null && !user.isEmpty()) { + if (user != null && !user.isEmpty()) { annotations.put(KAPPNAV_JOB_USER_ID, user); } - if(! annotations.isEmpty()) { + if (!annotations.isEmpty()) { return Collections.unmodifiableMap(annotations); } return null; } - - private Map createJobLabels(ApiClient client, JsonObject resource, String name, String kind, - String namespace, String appName, String appNamespace, String actionName) { - String methodName = "createJobLabels"; + + private Map createJobLabels(final ApiClient client, final JsonObject resource, final String name, + final String kind, final String namespace, final String appName, final String appNamespace, + final String actionName) { + final String methodName = "createJobLabels"; if (Logger.isEntryEnabled()) { - Logger.log(className, methodName, Logger.LogType.ENTRY, "Resource=" + resource.toString() + ", name="+ name + ", kind=" + kind + ", namespace="+namespace + ", appName=" + appName + ", appNamespace=" + appNamespace + ", actionName=" + actionName); + Logger.log(className, methodName, Logger.LogType.ENTRY, + "Resource=" + resource.toString() + ", name=" + name + ", kind=" + kind + ", namespace=" + namespace + + ", appName=" + appName + ", appNamespace=" + appNamespace + ", actionName=" + actionName); } - final Map labels = new HashMap<>(); + final Map labels = new HashMap<>(); labels.put(KAPPNAV_JOB_TYPE, KAPPNAV_JOB_COMMAND_TYPE); labels.put(KAPPNAV_JOB_ACTION_NAME, actionName); - // If appName is not set this resource is an application. Only set the application labels. + // If appName is not set this resource is an application. Only set the + // application labels. if (appName == null || appName.isEmpty()) { labels.put(KAPPNAV_JOB_APPLICATION_NAME, name); labels.put(KAPPNAV_JOB_APPLICATION_NAMESPACE, namespace); labels.put(KAPPNAV_JOB_APPLICATION_SCOPE, "true"); - } - else { + } else { // Set application labels. labels.put(KAPPNAV_JOB_APPLICATION_NAME, appName); labels.put(KAPPNAV_JOB_APPLICATION_NAMESPACE, appNamespace); @@ -805,18 +818,20 @@ private Map createJobLabels(ApiClient client, JsonObject resource } return labels; } - - private JsonObject getResource(ApiClient client, String name, String kind, String apiVersion, String namespace) throws ApiException { - String methodName = "resolve"; + + private JsonObject getResource(final ApiClient client, final String name, final String kind, String apiVersion, + final String namespace) throws ApiException { + final String methodName = "getResource"; if (Logger.isEntryEnabled()) { - Logger.log(className, methodName, Logger.LogType.ENTRY, "Name=" + name + ", kind=" + kind + ", apiVersion=" + apiVersion + ", namespace=" + namespace); + Logger.log(className, methodName, Logger.LogType.ENTRY, + "Name=" + name + ", kind=" + kind + ", apiVersion=" + apiVersion + ", namespace=" + namespace); } if (registry == null) { // Initialize the registry here if CDI failed to do it. registry = new ComponentInfoRegistry(client); } - + if (apiVersion == null || apiVersion.trim().length() == 0) { apiVersion = ComponentInfoRegistry.CORE_KIND_TO_API_VERSION_MAP.get(kind); if (apiVersion == null) { @@ -832,68 +847,76 @@ private JsonObject getResource(ApiClient client, String name, String kind, Strin } return getItemAsObject(client, o); } - + static final class ActionSubstitutionResolverResponse { private final JsonObject o; - public ActionSubstitutionResolverResponse(String resolvedAction) { + + public ActionSubstitutionResolverResponse(final String resolvedAction) { o = new JsonObject(); o.addProperty(ACTION_PROPERTY_NAME, resolvedAction); } + public String getJSON() { return o.toString(); } } - + static final class CommandsResponse { private final JsonObject o; private final JsonArray commands; + // Constructs: // { - // commands: [ {...}, {...}, ... ] + // commands: [ {...}, {...}, ... ] // } public CommandsResponse() { o = new JsonObject(); o.add(COMMANDS_PROPERTY_NAME, commands = new JsonArray()); } + public void add(final JsonObject command) { commands.add(command); } - public void addActions(final JsonObject actions) { - o.add(ACTION_MAP_PROPERTY_NAME, actions); - } + + public void addActions(final JsonObject actions) { + o.add(ACTION_MAP_PROPERTY_NAME, actions); + } + public int size() { return commands.size(); } + public String getJSON() { return o.toString(); } } - private Timestamp convertTimeStringToTimestamp(String time) throws ApiException { + private Timestamp convertTimeStringToTimestamp(final String time) throws ApiException { Timestamp timestamp = null; try { if (time != null && !time.isEmpty()) { - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:sss"); - Date parsedDate = dateFormat.parse(time); + final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:sss"); + final Date parsedDate = dateFormat.parse(time); timestamp = new java.sql.Timestamp(parsedDate.getTime()); } - } catch (Exception e) { - String msg = e.getMessage() + ". The correct format is yyyy-MM-dd'T'HH:mm:sss"; + } catch (final Exception e) { + final String msg = e.getMessage() + ". The correct format is yyyy-MM-dd'T'HH:mm:sss"; if (Logger.isErrorEnabled()) { Logger.log(className, "convertTimeStringToTimestamp", Logger.LogType.ERROR, "Caught exception " + msg); } throw new ApiException(msg); - } + } return timestamp; } - + // Decodes a URL encoded string using `UTF-8` - private static String urlDecode(String value) { + private static String urlDecode(final String value) { try { return URLDecoder.decode(value, StandardCharsets.UTF_8.toString()); - } catch (UnsupportedEncodingException ex) { + } catch (final UnsupportedEncodingException ex) { if (Logger.isErrorEnabled()) { - Logger.log(className, "urlDecode", Logger.LogType.ERROR, "Caught UnsupportedEncodingException " + ex.toString()); + Logger.log(className, "urlDecode", Logger.LogType.ERROR, + "Caught UnsupportedEncodingException " + ex.toString()); } throw new RuntimeException(ex.getCause()); } @@ -901,10 +924,11 @@ private static String urlDecode(String value) { /** * Return the username associated with the command action + * * @param commandAction - JsonObject of the command action details * @return String - username who created the command action, else empty String */ - private String getCommandActionUserName(JsonObject commandAction) { + private String getCommandActionUserName(final JsonObject commandAction) { final String methodName = "getCommandUserName"; if (Logger.isEntryEnabled()) { Logger.log(className, methodName, Logger.LogType.ENTRY, "job=" + commandAction); diff --git a/src/main/java/application/rest/v1/ComponentsEndpoint.java b/src/main/java/application/rest/v1/ComponentsEndpoint.java index 0b6fcef..3e29d26 100644 --- a/src/main/java/application/rest/v1/ComponentsEndpoint.java +++ b/src/main/java/application/rest/v1/ComponentsEndpoint.java @@ -58,6 +58,7 @@ public class ComponentsEndpoint extends KAppNavEndpoint { private static final String ACTION_MAP_PROPERTY_NAME = "action-map"; private static final String SECTION_MAP_PROPERTY_NAME = "section-map"; private static final String KIND_PROPERTY_NAME = "kind"; + private static final String APIVERSION_PROPERTY_NAME = "apiVersion"; @Inject private ComponentInfoRegistry registry; @@ -110,14 +111,14 @@ private Response processComponentKinds(ApiClient client, List com System.out.println("processComponentKinds using apiVersion: " + apiVersion + " for Application: " + appName +" componentKind group: " + v.group + " kind: " + v.kind); if (!registry.isNamespaced(client, v.kind, apiVersion)) { Object o = registry.listClusterObject(client, v.kind, apiVersion, null, labelSelector, null, null); - processComponents(client, response, v, getItemsAsList(client, o)); + processComponents(client, response, v, apiVersion, getItemsAsList(client, o)); } else { // If the component kind is namespaced, query components for each of the specified namespaces. final String apiVersion1 = apiVersion; // to avoid compiler error namespaces.forEach(n -> { try { Object o = registry.listNamespacedObject(client, v.kind, apiVersion1, n, null, labelSelector, null, null); - processComponents(client, response, v, getItemsAsList(client, o), appNamespace, appName); + processComponents(client, response, v, apiVersion, getItemsAsList(client, o), appNamespace, appName); } catch (ApiException e) { } }); @@ -139,19 +140,27 @@ private Response processComponentKinds(ApiClient client, List com return Response.ok(response.getJSON()).build(); } - private void processComponents(ApiClient client, ComponentResponse response, ComponentKind componentKind, List components) { + private void processComponents(ApiClient client, ComponentResponse response, ComponentKind componentKind, + String apiVersion, List components) { final ConfigMapProcessor processor = new ConfigMapProcessor(componentKind.kind); final SectionConfigMapProcessor sectionProcessor = new SectionConfigMapProcessor(componentKind.kind); components.forEach(v -> { // Add 'kind' property to components that are missing it. if (v.get(KIND_PROPERTY_NAME) == null) { v.addProperty(KIND_PROPERTY_NAME, componentKind.kind); - } + } + + // Add 'apiVersion' property to components that are missing it. + if (v.get(APIVERSION_PROPERTY_NAME) == null) { + v.addProperty(APIVERSION_PROPERTY_NAME, apiVersion); + } + response.add(v, processor.getConfigMap(client, v, ConfigMapProcessor.ConfigMapType.ACTION), sectionProcessor.processSectionMap(client, v)); }); } - private void processComponents(ApiClient client, ComponentResponse response, ComponentKind componentKind, List components, String appNamespace, String appName) { + private void processComponents(ApiClient client, ComponentResponse response, ComponentKind componentKind, + String apiVersion, List components, String appNamespace, String appName) { final ConfigMapProcessor processor = new ConfigMapProcessor(componentKind.kind); final SectionConfigMapProcessor sectionProcessor = new SectionConfigMapProcessor(componentKind.kind); components.forEach(v -> { @@ -159,7 +168,12 @@ private void processComponents(ApiClient client, ComponentResponse response, Com if (v.get(KIND_PROPERTY_NAME) == null) { v.addProperty(KIND_PROPERTY_NAME, componentKind.kind); } - + + // Add 'apiVersion' property to components that are missing it. + if (v.get(APIVERSION_PROPERTY_NAME) == null) { + v.addProperty(APIVERSION_PROPERTY_NAME, apiVersion); + } + // filter out recursive app from component list if (!(componentKind.kind.equals("Application") && getComponentName(v).equals(appName) && diff --git a/src/main/java/application/rest/v1/KAppNavEndpoint.java b/src/main/java/application/rest/v1/KAppNavEndpoint.java index 387cfc6..9b5e0a8 100644 --- a/src/main/java/application/rest/v1/KAppNavEndpoint.java +++ b/src/main/java/application/rest/v1/KAppNavEndpoint.java @@ -87,7 +87,11 @@ public String run() { private static final String KAPPNAV_STATUS_PROPERTY_NAME = "kappnav.status"; private static final String KAPPNAV_SUB_KIND_PROPERTY_NAME = "kappnav.subkind"; - // Status object properties. + // Kind actions mapping properties + private static final String API_VERSION_PROPERTY_NAME = "apiVersion"; + private static final String KIND_PROPRETY_NAME = "kind"; + + // Status object properties. private static final String VALUE_PROPERTY_NAME = "value"; private static final String FLYOVER_PROPERTY_NAME = "flyover"; private static final String FLYOVER_NLS_PROPERTY_NAME = "flyover.nls"; @@ -163,8 +167,45 @@ public static JsonObject getItemAsObject(ApiClient client, Object resource) { return null; } + public static String getComponentApiVersion(JsonObject component, String kind) { + if (Logger.isEntryEnabled()) + Logger.log(className, "getComponentApiVersion", Logger.LogType.ENTRY,"kind = " + kind); + + final JsonElement apiv_e = component.get(API_VERSION_PROPERTY_NAME); + String apiVersion = null; + if (apiv_e != null) + apiVersion = apiv_e.getAsString(); + + if (apiVersion == null || apiVersion.length() == 0) { + if (Logger.isEntryEnabled()) + Logger.log(className, "getComponentApiVersion", Logger.LogType.ENTRY, + "apiVersion is null or empty and get it from ComponentInfoRegistry"); + apiVersion = ComponentInfoRegistry.CORE_KIND_TO_API_VERSION_MAP.get(kind); + } + + if (Logger.isExitEnabled()) + Logger.log(className, "getComponentApiVersion", Logger.LogType.EXIT,"apiVersion = " + + apiVersion); + return apiVersion; + } + + public static String getComponentKind(JsonObject component) { + if (Logger.isEntryEnabled()) + Logger.log(className, "getComponentKind", Logger.LogType.ENTRY,""); + + final JsonElement kind_e = component.get(KIND_PROPRETY_NAME); + String kind = null; + if (kind_e != null) + kind = kind_e.getAsString(); + + if (Logger.isExitEnabled()) + Logger.log(className, "getComponentKind", Logger.LogType.EXIT,"kind = " + + kind); + return kind; + } + public static String getComponentSubKind(JsonObject component) { - if (Logger.isEntryEnabled()) { + if (Logger.isEntryEnabled()) { Logger.log(className, "getComponentSubKind", Logger.LogType.ENTRY,""); } final JsonObject metadata = component.getAsJsonObject(METADATA_PROPERTY_NAME); @@ -195,17 +236,11 @@ public static String getComponentSubKind(JsonObject component) { } public static String getComponentName(JsonObject component) { - if (Logger.isEntryEnabled()) { - Logger.log(className, "getComponentName", Logger.LogType.ENTRY,""); - } final JsonObject metadata = component.getAsJsonObject(METADATA_PROPERTY_NAME); if (metadata != null) { JsonElement e = metadata.get(NAME_PROPERTY_NAME); if (e != null && e.isJsonPrimitive()) { String componentName = e.getAsString(); - if (Logger.isExitEnabled()) { - Logger.log(className, "getComponentName", Logger.LogType.EXIT, componentName); - } return componentName; } } else { @@ -213,9 +248,6 @@ public static String getComponentName(JsonObject component) { Logger.log(className, "getComponentName", Logger.LogType.DEBUG, "Metadata is null."); } } - if (Logger.isExitEnabled()) { - Logger.log(className, "getComponentName", Logger.LogType.EXIT, "Return null."); - } return null; } @@ -479,7 +511,7 @@ public void checkServerTrusted(X509Certificate[] certs, String authType) {} } } - protected static String encodeURLParameter(String s) { + public static String encodeURLParameter(String s) { try { return URLEncoder.encode(s, "UTF-8"); } diff --git a/src/main/java/application/rest/v1/actions/ResourceResolver.java b/src/main/java/application/rest/v1/actions/ResourceResolver.java index 7b03e93..c3d67aa 100644 --- a/src/main/java/application/rest/v1/actions/ResourceResolver.java +++ b/src/main/java/application/rest/v1/actions/ResourceResolver.java @@ -31,8 +31,8 @@ public String getName() { @Override public String resolve(ResolutionContext context, String suffix) throws PatternException { - if (Logger.isErrorEnabled()) { - Logger.log(ResourceResolver.class.getName(), "resolve", Logger.LogType.ERROR, "For suffix=" + suffix); + if (Logger.isDebugEnabled()) { + Logger.log(ResourceResolver.class.getName(), "resolve", Logger.LogType.DEBUG, "For suffix=" + suffix); } final JSONPathParser parser = new JSONPathParser(); final JSONPath path = parser.parse(suffix); diff --git a/src/main/java/application/rest/v1/configmaps/ConfigMapCache.java b/src/main/java/application/rest/v1/configmaps/ConfigMapCache.java index d2dc9e6..2d94d1b 100644 --- a/src/main/java/application/rest/v1/configmaps/ConfigMapCache.java +++ b/src/main/java/application/rest/v1/configmaps/ConfigMapCache.java @@ -18,6 +18,7 @@ import java.lang.ref.SoftReference; import java.lang.reflect.Type; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -27,6 +28,7 @@ import com.google.common.reflect.TypeToken; import com.google.gson.JsonElement; +import com.google.gson.JsonObject; import com.ibm.kappnav.logging.Logger; import com.squareup.okhttp.Call; @@ -122,6 +124,29 @@ private Selector getSelector() { }); } + public static ArrayList getConfigMapsAsJSON(ApiClient client, ArrayList configMapsList) { + ArrayList configMaps = new ArrayList(); + for (int i=0; i(map)); - if (Logger.isDebugEnabled()) { + if (Logger.isDebugEnabled()) Logger.log(CLASS_NAME, "getConfigMap", Logger.LogType.DEBUG, "Found ConfigMap, Name: " + name + ", Namespace: " + namespace + ". Storing it in the cache."); - } } + if (Logger.isExitEnabled()) + Logger.log(CLASS_NAME, "getConfigMap", Logger.LogType.EXIT, + "name = " + name + " is found in namespace " + namespace); + return map; } catch (ApiException e) { - if (Logger.isDebugEnabled()) { + if (Logger.isDebugEnabled()) Logger.log(CLASS_NAME, "getConfigMap", Logger.LogType.DEBUG, "Caught ApiException: " + e.toString()); - } } // No ConfigMap. Store null reference in the cache. if (mapCache != null) { mapCache.put(tuple, NULL_REFERENCE); - if (Logger.isDebugEnabled()) { + if (Logger.isDebugEnabled()) Logger.log(CLASS_NAME, "getConfigMap", Logger.LogType.DEBUG, "ConfigMap, Name: " + name + ", Namespace: " - + namespace + " not found. Storing a null reference in the cache."); - } - } + + namespace + " not found. Storing a null reference in the cache."); + } + if (Logger.isExitEnabled()) + Logger.log(CLASS_NAME, "getConfigMap", Logger.LogType.EXIT, + "Return null as the confignmap " + name + " is not found in " + namespace); + return null; } } diff --git a/src/main/java/application/rest/v1/configmaps/ConfigMapProcessor.java b/src/main/java/application/rest/v1/configmaps/ConfigMapProcessor.java index 23972ef..5b3c70c 100644 --- a/src/main/java/application/rest/v1/configmaps/ConfigMapProcessor.java +++ b/src/main/java/application/rest/v1/configmaps/ConfigMapProcessor.java @@ -16,6 +16,7 @@ package application.rest.v1.configmaps; +import java.util.ArrayList; import java.util.HashMap; import java.util.Locale; import java.util.Map; @@ -59,74 +60,61 @@ public ConfigMapProcessor(String kind) { } public JsonObject getConfigMap(ApiClient client, JsonObject component, ConfigMapType type) { - final ConfigMapBuilder builder = type == ConfigMapType.ACTION ? new ActionConfigMapBuilder() : new StatusMappingConfigMapBuilder(); + final ConfigMapBuilder builder = type == ConfigMapType.ACTION ? new ActionConfigMapBuilder() : new StatusMappingConfigMapBuilder(); + final String namespace = KAppNavEndpoint.getComponentNamespace(component); + final String kind = KAppNavEndpoint.getComponentKind(component); + final String apiVersion = KAppNavEndpoint.getComponentApiVersion(component, kind); final String subkind = KAppNavEndpoint.getComponentSubKind(component); final String name = KAppNavEndpoint.getComponentName(component); - - if (Logger.isDebugEnabled()) { - Logger.log(className, "getConfigMap", Logger.LogType.DEBUG, "For component subkind=" + subkind + ", component name=" + name); - } - - // Map: kappnav.actions.{kind}[-{subkind}].{name} - if (name != null && !name.isEmpty()) { - final String namespace = KAppNavEndpoint.getComponentNamespace(component); - JsonObject map = getConfigMap(client, namespace, getConfigMapName(type, - (subkind != null && !subkind.isEmpty()) ? '-' + subkind + '.' + name : '.' + name)); - if (map != null) { - builder.merge(map); - // Stop here if the action is replace. - if (getConflictAction(map) == ConflictAction.REPLACE) { - return builder.getConfigMap(); - } - } else { - if (Logger.isDebugEnabled()) { - Logger.log(className, "getConfigMap", Logger.LogType.DEBUG, "Map-1 is null."); - } - } - } - - // Map: kappnav.actions.{kind}-{subkind} - if (subkind != null && !subkind.isEmpty()) { - JsonObject map = getConfigMap(client, GLOBAL_NAMESPACE, getConfigMapName(type, '-' + subkind)); - if (map != null) { - builder.merge(map); - // Stop here if the action is replace. - if (getConflictAction(map) == ConflictAction.REPLACE) { - return builder.getConfigMap(); - } - } else { - if (Logger.isDebugEnabled()) { - Logger.log(className, "getConfigMap", Logger.LogType.DEBUG, "Map-2 is null."); - } - } - } + JsonObject map = null; - // Map: kappnav.actions.{kind} - JsonObject map = getConfigMap(client, GLOBAL_NAMESPACE, getConfigMapName(type, "")); - if (map != null) { - builder.merge(map); - } else { - if (Logger.isDebugEnabled()) { - Logger.log(className, "getConfigMap", Logger.LogType.DEBUG, "Map-3 is null."); - } + if (Logger.isDebugEnabled()) { + Logger.log(className, "getConfigMap", Logger.LogType.DEBUG, + "\n type = " + type + + "\n component namespace = " + namespace + + "\n component apiVersion = " + apiVersion + + "\n component name= " + name + + "\n component subkind= " + subkind + + "\n component kind = " + kind); } - if (type == ConfigMapType.STATUS_MAPPING && builder.getConfigMap().entrySet().size() == 0) { - // unregistered, try the unregistered configmap - map = getConfigMap(client, GLOBAL_NAMESPACE, getUnregisteredConfigMapName()); - if (map != null) { - builder.merge(map); - } else { - if (Logger.isDebugEnabled()) { - Logger.log(className, "getConfigMap", Logger.LogType.DEBUG, "Map-4 is null."); + if ((apiVersion == null) || (apiVersion.isEmpty())) { + if (Logger.isDebugEnabled()) + Logger.log(className, "getConfigMap", Logger.LogType.DEBUG, "component apiVersion is null or empty."); + } else { + if (type == ConfigMapType.ACTION) { // using KAM CRs for action configmaps + KindActionMappingProcessor kam = + new KindActionMappingProcessor(namespace, apiVersion, name, subkind, kind); + String configMapName = getConfigMapName(type, name, subkind, kind); + map = getConfigMap(client, kam, namespace, configMapName, builder); + } else { // status mapping configmap + if (type == ConfigMapType.STATUS_MAPPING && builder.getConfigMap().entrySet().size() == 0) { + // unregistered, try the unregistered configmap + map = getConfigMap(client, null, GLOBAL_NAMESPACE, getUnregisteredConfigMapName(), builder); + if (map != null) { + builder.merge(map); + } else { + if (Logger.isDebugEnabled()) + Logger.log(className, "getConfigMap", Logger.LogType.DEBUG, "configmap is null."); + } } } } + System.out.println("EXIT: builder.getConfigMap()" + builder.getConfigMap()); return builder.getConfigMap(); } - private String getConfigMapName(ConfigMapType type, String suffix) { + private String getConfigMapName(ConfigMapType type, String name, String subkind, String kind) { + String suffix; + if (name != null && !name.isEmpty()) { + suffix = (subkind != null && !subkind.isEmpty()) ? '-' + subkind + '.' + name : '.' + name; + } else if (subkind != null && !subkind.isEmpty()) { + suffix = '-' + subkind; + } else { + suffix = ""; + } + return (type == ConfigMapType.ACTION ? actionNameWithKind : statusMappingNameWithKind) + suffix; } @@ -134,10 +122,12 @@ private String getUnregisteredConfigMapName() { return UNREGISTERED; } - private JsonObject getConfigMap(ApiClient client, String namespace, String configMapName) { + private JsonObject getConfigMap(ApiClient client, KindActionMappingProcessor kam, + String namespace, String configMapName, ConfigMapBuilder builder) { // Return the map from the local cache if it's been previously loaded. - if (Logger.isDebugEnabled()) { - Logger.log(className, "getConfigMap", Logger.LogType.DEBUG, "For namespace=" + namespace + ", configMapName=" + configMapName); + if (Logger.isEntryEnabled()) { + Logger.log(className, "getConfigMap", Logger.LogType.ENTRY, "For namespace=" + namespace + + ", configMapName=" + configMapName); } final boolean isGlobalNS = GLOBAL_NAMESPACE.equals(namespace); @@ -145,27 +135,62 @@ private JsonObject getConfigMap(ApiClient client, String namespace, String confi return kappnavNSMapCache.get(configMapName); } - final JsonElement element = ConfigMapCache.getConfigMapAsJSON(client, namespace, configMapName); - if (element != null && element.isJsonObject()) { - final JsonObject m = element.getAsJsonObject(); - if (isGlobalNS) { - // Store the map in the local cache. - kappnavNSMapCache.put(configMapName, m); + if (kam != null) { + // get Configmaps declared in the KindActionMapping custom resources + ArrayList configMapsList = kam.getConfigMapsFromKAMs(client); + + if (configMapsList != null) { + // look up the configmaps in a cluster + final ArrayList configMapsFound = ConfigMapCache.getConfigMapsAsJSON(client, configMapsList); + + // merge configmaps found + mergeConfigMaps(configMapsFound, builder); + JsonObject map = builder.getConfigMap(); + if (map != null) { + if (isGlobalNS) { + // Store the map in the local cache. + kappnavNSMapCache.put(configMapName, map); + } + if (Logger.isExitEnabled()) + Logger.log(className, "getConfigMap", Logger.LogType.EXIT, "Merged configmap returned = " + map); + return map; + } else { + if (Logger.isDebugEnabled()) + Logger.log(className, "getConfigMap", Logger.LogType.DEBUG, "map to be merged is null"); + } + } else { + if (Logger.isDebugEnabled()) + Logger.log(className, "getConfigMap", Logger.LogType.DEBUG, "no configmap with given kam is found"); + } + } else { + final JsonElement element = ConfigMapCache.getConfigMapAsJSON(client, namespace, configMapName); + if (element != null && element.isJsonObject()) { + final JsonObject map = element.getAsJsonObject(); + if (isGlobalNS) { + // Store the map in the local cache. + kappnavNSMapCache.put(configMapName, map); + } + if (Logger.isExitEnabled()) + Logger.log(className, "getConfigMap", Logger.LogType.EXIT, "Status mapping configmap returned = " + map); + return map; } - return m; } - + if (isGlobalNS) { if (Logger.isDebugEnabled()) { - Logger.log(className, "getConfigMap", Logger.LogType.DEBUG, "Global namespace, store null in the local cache"); + Logger.log(className, "getConfigMap", Logger.LogType.DEBUG, + "Global namespace, store null in the local cache"); } // No map. Store null in the local cache. kappnavNSMapCache.put(configMapName, null); } + + if (Logger.isExitEnabled()) + Logger.log(className, "getConfigMap", Logger.LogType.EXIT, "returns null"); return null; } - private ConflictAction getConflictAction(JsonObject map) { + private static ConflictAction getConflictAction(JsonObject map) { final JsonObject metadata = map.getAsJsonObject(METADATA_PROPERTY_NAME); if (metadata != null) { final JsonObject annotations = metadata.getAsJsonObject(ANNOTATIONS_PROPERTY_NAME); @@ -174,15 +199,22 @@ private ConflictAction getConflictAction(JsonObject map) { if (e != null && e.isJsonPrimitive()) { String s = e.getAsString(); if ("merge".equals(s)) { + if (Logger.isDebugEnabled()) + Logger.log(className, "getConflictAction", Logger.LogType.DEBUG, "MERGE"); return ConflictAction.MERGE; } else if ("replace".equals(s)) { + if (Logger.isDebugEnabled()) + Logger.log(className, "getConflictAction", Logger.LogType.DEBUG, "REPLACE"); return ConflictAction.REPLACE; } } } } + // Default value + if (Logger.isDebugEnabled()) + Logger.log(className, "getConflictAction", Logger.LogType.DEBUG, "MERGE"); return ConflictAction.MERGE; } @@ -190,4 +222,29 @@ enum ConflictAction { MERGE, REPLACE } + + private static void mergeConfigMaps(ArrayList configMapsFound, ConfigMapBuilder builder) { + for (int cIdx=0; cIdx getConfigMapsFromKAMs(ApiClient client) { + + if (Logger.isEntryEnabled()) + Logger.log(className, "getConfigMapsFromKAMs", Logger.LogType.ENTRY,""); + + String[][] mapNamesFound = new String[MAX_PRECEDENCE][TOTAL_INDIVIDUAL_MAPPINGS]; + ArrayList configMapsList = null; + List kamList = null; + + try { + kamList = listKAMCustomResources(client); + kamList.forEach (v -> { + JsonElement items = v.get(ITEMS_PROPERTY_NAME); + if ((items != null) && (items.isJsonArray())) { + JsonArray itemsArray = items.getAsJsonArray(); + + // go though all kams to get the qualified configmaps defined in those kams + // Sort the configmaps found in order of hierarchy & precedence + itemsArray.forEach(item-> { + if ( (item != null) && (item.isJsonObject()) ) { + if (Logger.isDebugEnabled()) + Logger.log(className, "getConfigMapsFromKAMs", Logger.LogType.DEBUG, + "\nKindActionMapping found: " + item); + + if (item != null) { + JsonElement element = item.getAsJsonObject().get(SPEC_PROPERTY_NAME); + String kamNamespace = getKAMNamespace(item); + if (element != null) { + JsonObject spec = element.getAsJsonObject(); + int precedenceIndex = spec.get(PRECEDENCE_PROPERTY_NAME).getAsInt()-1; + JsonArray mappings = spec.getAsJsonArray(MAPPINGS_PROPERTY_NAME); + + // iterate through each mapping within a KAM resource + mappings.forEach(mapItem-> { + if (mapItem != null) { + JsonObject props = mapItem.getAsJsonObject(); + if (props != null) { + JsonElement prop = props.get(APIVERSION_PROPERTY_NAME); + String apiVersion = (prop != null) ? prop.getAsString():null; + prop = props.get(NAME_PROPERTY_NAME); + String name = (prop != null) ? prop.getAsString():null; + prop = props.get(SUBKIND_PROPERTY_NAME); + String subkind = (prop != null) ? prop.getAsString():null; + prop = props.get(KIND_PROPERTY_NAME); + String kind = (prop != null) ? prop.getAsString():null; + prop = props.get(MAPNAME_PROPERTY_NAME); + String mapname = (prop != null) ? prop.getAsString():null; + Logger.log(className, "getConfigMapsFromKAMs", Logger.LogType.DEBUG, + "\nmapping info: " + + "\napiVersion = " + apiVersion + + "\nname = " + name + + "\nsubkind = " + subkind + + "\nkind = " + kind + + "\nmapname = " + mapname); + + if (isApiVersionMatch(apiVersion, compApiVersion)) { + int compPropsIdx = exemineMappingProperties(compName, compSubkind, compKind); + int kamMappingPropIdx = exemineMappingProperties(name, subkind, kind); + boolean found = foundMatchedConfigMap(compPropsIdx, kamMappingPropIdx, + compName, compSubkind, compKind, + name, subkind, kind); + + if (found){ + if (Logger.isDebugEnabled()) + Logger.log(className, "getQualifiedKindActionMappings", Logger.LogType.DEBUG, + "mapName " + mapname + " is stored in configMapsFound["+precedenceIndex+"][" + + kamMappingPropIdx+"]"); + // each mapname found is stored in the 2D array as "mapname@namespace" + if ((kamMappingPropIdx == KSN) || (kamMappingPropIdx == KN)) + mapNamesFound[precedenceIndex][kamMappingPropIdx] = mapname+"@"+compNamespace; + else + mapNamesFound[precedenceIndex][kamMappingPropIdx] = mapname+"@"+kamNamespace; + } else { + if (Logger.isDebugEnabled()) + Logger.log(className, "getQualifiedKindActionMappings", Logger.LogType.DEBUG, + "no match!!!"); + } + } else { + if (apiVersion == null) { + if (Logger.isErrorEnabled()) + Logger.log(className, "getQualifiedKindActionMappings", Logger.LogType.ERROR, + "apiVersion of the KindActionMapping resource " + name + " is null."); + } + } + } + } + }); // mappings.forEach + } + } + } + }); // itemsArray.forEach + } + }); // kamList.forEach + + // process candidate mapnames including a string substitution as needed and then store + // them to a list according the configmap hierarchy and (high to low) precedence + configMapsList = processCandidateMapnames(mapNamesFound, compNamespace, + compName, compSubkind, compKind); + } catch (ApiException e) { + if (Logger.isErrorEnabled()) { + Logger.log(className, "getQualifiedKindActionMappings", Logger.LogType.ERROR, + "Caught PatternException returning status: " + e.toString());} + } + + if (Logger.isExitEnabled()) + Logger.log(className, "getConfigMapsFromKAMs", Logger.LogType.EXIT,""); + return configMapsList; + } + + /** + * Get all "KindActionMapping" custom resources in a cluster + * + * @param client + * @return + * @throws ApiException + */ + protected List listKAMCustomResources(ApiClient client) + throws ApiException { + final CustomObjectsApi coa = new CustomObjectsApi(); + coa.setApiClient(client); + if (Logger.isDebugEnabled()) { + Logger.log(className, "listKAMCustomResources", Logger.LogType.DEBUG, + "\n List KAM Custom Resources for all namespaces with" + + "\n group = " + "actions.kappnav.io" + + "\n namespace = kappnav and name = default"); + } + + Object kamResource = coa.listClusterCustomObject(KAM_GROUP, KAM_VERSION, KAM_PLURAL, null, + null, null, null); + return KAppNavEndpoint.getItemAsList(client, kamResource); + } + + /** + * Get the namespace for the given KindActionMapping resource + * + * @param kam + * @return the namespace for the given kam resource + */ + private String getKAMNamespace(JsonElement kam) { + if (Logger.isEntryEnabled()) + Logger.log(className, "getKAMNamespace", Logger.LogType.ENTRY,""); + + String namespaceStr = null; + JsonElement metadata = (JsonObject) kam.getAsJsonObject().get(METADATA_PROPERTY_NAME); + if (metadata != null) { + JsonObject namespace= metadata.getAsJsonObject(); + if (namespace != null) + namespaceStr = namespace.get(NAMESPACE_PROPERTY_NAME).getAsString(); + } else { + if (Logger.isDebugEnabled()) + Logger.log(className, "getKAMNamespace", Logger.LogType.DEBUG, "kam metadata is null."); + } + + if (Logger.isExitEnabled()) { + Logger.log(className, "getKAMNamespace", Logger.LogType.EXIT, "kam namespace = " + namespaceStr); + } + return namespaceStr; + } + + /** + * Check to see if the apiVersions of a component and a mapping are matching + * + * @param apiVersion + * @param compApiVersion + * @return + */ + private boolean isApiVersionMatch(String apiVersion, String compApiVersion) { + if (Logger.isEntryEnabled()) + Logger.log(className, "isApiVersionMatch", Logger.LogType.ENTRY, "apiVerion = " + apiVersion + + ", compApiVersion = " + compApiVersion); + + boolean match = false; + if ( (apiVersion != null) && (!apiVersion.isEmpty()) && + (compApiVersion != null) && (!compApiVersion.isEmpty()) ) { + if (apiVersion.equals(compApiVersion)) { // group/version - exact match + match = true; + } else { + String grp_version[] = apiVersion.split("/"); + String comp_grp_version[] = compApiVersion.split("/"); + + if ( (grp_version.length == 2) && (comp_grp_version.length == 2) ){ // group/version - wildcard match + if (grp_version[0].equals(comp_grp_version[0]) || (grp_version[0].equals(WILDCARD)) ) + if (grp_version[1].equals(comp_grp_version[1]) || (grp_version[1].equals(WILDCARD)) ) { + match = true; + } + } else if ( (grp_version.length == 1) && (comp_grp_version.length == 1) ) { // verion only + if (grp_version[0].equals(comp_grp_version[0]) || (grp_version[0].equals(WILDCARD)) ) { + match = true; + } + } + } + } + + if (Logger.isExitEnabled()) + Logger.log(className, "isApiVersionMatch", Logger.LogType.EXIT, "match = " + match); + return match; + } + + /** + * Examine the mapping properites passed in + * + * @param name + * @param subkind + * @param kind + * @return KSN, KS, KN, or K + */ + private int exemineMappingProperties(String name, String subkind, String kind) { + if (Logger.isEntryEnabled()) + Logger.log(className, "exemineMappingProperties", Logger.LogType.ENTRY, + "(name, subkind, kind) = (" + name +", " + subkind + ", " + kind + ")"); + + int retVal = -1; + if ((kind != null) && !kind.isEmpty()) { + if ((subkind != null) && !subkind.isEmpty()) { + if ((name != null) && !name.isEmpty()) { + retVal = KSN; + } else { + retVal = KS; + } + } else if ((name != null) && !name.isEmpty()) { + retVal = KN; + } else { + retVal = K; + } + } else { + if (Logger.isErrorEnabled()) + Logger.log(className, "exemineMappingProperties", Logger.LogType.ERROR, + "kind given is null or empty string"); + } + + if (Logger.isExitEnabled()) + Logger.log(className, "exemineMappingProperties", Logger.LogType.EXIT, + "retVal = " + retVal); + + return retVal; + } + + /** + * Find a matched configmap defined in a given KAM + * + * @param cNumFields + * @param kNumFields + * @param cName + * @param cSubkind + * @param cKind + * @param gName + * @param gSubkind + * @param gKind + * @return true if found; otherwise false + */ + private boolean foundMatchedConfigMap(int cNumFields, int kNumFields, + String cName, String cSubkind, String cKind, + String gName, String gSubkind, String gKind) { + + if (Logger.isEntryEnabled()) + Logger.log(className, "foundMatchedConfigMap", Logger.LogType.ENTRY, + "cNumField = " + cNumFields + " kNumField = " + kNumFields); + + if ( (cNumFields == -1) || (kNumFields == -1)) { + if (Logger.isExitEnabled()) + Logger.log(className, "isMatched", Logger.LogType.EXIT, "return false"); + return false; + } + + boolean match = false; + if (cNumFields == kNumFields) { + switch (kNumFields) { + case KSN: + if ((gName.equals(cName)) || (gName.equals(WILDCARD))) + if ((gSubkind.equals(cSubkind)) || (gSubkind.equals(WILDCARD))) + if ((gKind.equals(cKind)) || (gKind.equals(WILDCARD))) + match = true; + break; + case KS: + if ((gSubkind.equals(cSubkind)) || (gSubkind.equals(WILDCARD))) + if ((gKind.equals(cKind)) || (gKind.equals(WILDCARD))) + match = true; + break; + case KN: + if ((gName.equals(cName)) || (gName.equals(WILDCARD))) + if ((gKind.equals(cKind)) || (gKind.equals(WILDCARD))) + match = true; + break; + case K: + if (cNumFields == K) + if ((gKind.equals(cKind)) || (gKind.equals(WILDCARD))) + match = true; + } + } else if (kNumFields == K) { + if ((gKind.equals(cKind)) || (gKind.equals(WILDCARD))) + match = true; + } else if ((kNumFields == KS) && (cNumFields == KSN)) { + if ( (gSubkind.equals(cSubkind)) || (gSubkind.equals(WILDCARD)) ) + if ( (gKind.equals(cKind)) || (gKind.equals(WILDCARD)) ) + match = true; + } + + if (Logger.isExitEnabled()) + Logger.log(className, "foundMatchedConfigMap", Logger.LogType.EXIT, "return = " + match); + return match; + } + + /** + * Process mapnames with variable substitution if applies and store the mapnames along with their + * namespaces in a list according to the configmap hierarchy and precedence in decedending order + * + * @param configMapsFound + * @param namespace + * @param name + * @param subkind + * @param kind + * @return processed configmap list + */ + private ArrayList processCandidateMapnames(String[][] configMapsFound, String namespace, + String name, String subkind, String kind) { + ArrayList configMapList = new ArrayList (); + + for (int ksnIdx=KSN; ksnIdx>=0; ksnIdx--) { + for (int precedenceIdx=MAX_PRECEDENCE-1; precedenceIdx>=0; precedenceIdx--) { + String rawMapName = (configMapsFound[precedenceIdx][ksnIdx]); + if (rawMapName != null) { + String[] mapname_namespace = rawMapName.split("@"); + rawMapName = mapname_namespace[0]; // take off @namespace + if (Logger.isDebugEnabled()) + Logger.log(className, "processCandidateMapnames", Logger.LogType.DEBUG, + "rawMapName = " + rawMapName); + String actualMapName = mapNameSubstitute(rawMapName, namespace, name, subkind, kind); + if (Logger.isDebugEnabled()) + Logger.log(className, "processCandidateMapnames", Logger.LogType.DEBUG, + "actualMapName = " + actualMapName); + configMapList.add(actualMapName+"@"+mapname_namespace[1]); + } + } + } + return configMapList; + } + + /** + * Substitute the string variables like namespace, name, subkind, kind with actual values passed in + * + * @param rawMapName + * @param namespace + * @param name + * @param subkind + * @param kind + * @return substituted mapName + */ + private String mapNameSubstitute(String rawMapName, String namespace, String name, String subkind, String kind) { + String actualMapName = new String(""); + String[] parts = rawMapName.split("\\."); + for (int i=0; i Date: Thu, 26 Mar 2020 09:52:39 -0500 Subject: [PATCH 2/5] more clean up with KAM impl --- .../java/application/rest/v1/ComponentsEndpoint.java | 7 +++++-- .../rest/v1/configmaps/ConfigMapProcessor.java | 10 +++++++--- .../rest/v1/configmaps/KindActionMappingProcessor.java | 4 ++-- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/main/java/application/rest/v1/ComponentsEndpoint.java b/src/main/java/application/rest/v1/ComponentsEndpoint.java index 3e29d26..a9aac1a 100644 --- a/src/main/java/application/rest/v1/ComponentsEndpoint.java +++ b/src/main/java/application/rest/v1/ComponentsEndpoint.java @@ -108,7 +108,10 @@ private Response processComponentKinds(ApiClient client, List com Set apiVersions = registry.getComponentGroupApiVersions(v); for (String apiVersion : apiVersions) { if (apiVersion != null) { - System.out.println("processComponentKinds using apiVersion: " + apiVersion + " for Application: " + appName +" componentKind group: " + v.group + " kind: " + v.kind); + if (Logger.isDebugEnabled()) { + Logger.log(className, "processComponentKinds", Logger.LogType.DEBUG, "Using apiVersion: " + apiVersion + + " for Application: " + appName +" componentKind group: " + v.group + " kind: " + v.kind); + } if (!registry.isNamespaced(client, v.kind, apiVersion)) { Object o = registry.listClusterObject(client, v.kind, apiVersion, null, labelSelector, null, null); processComponents(client, response, v, apiVersion, getItemsAsList(client, o)); @@ -125,7 +128,7 @@ private Response processComponentKinds(ApiClient client, List com } } else { if (Logger.isWarningEnabled()) { - Logger.log(className, "processComponentKinds", Logger.LogType.WARNING, " Application: " + appName +" componentKind group: " + v.group + " kind: " + v.kind + " not recognized. skipping"); + Logger.log(className, "processComponentKinds", Logger.LogType.WARNING, "Application: " + appName +" componentKind group: " + v.group + " kind: " + v.kind + " not recognized. skipping"); } } } diff --git a/src/main/java/application/rest/v1/configmaps/ConfigMapProcessor.java b/src/main/java/application/rest/v1/configmaps/ConfigMapProcessor.java index 5b3c70c..891552b 100644 --- a/src/main/java/application/rest/v1/configmaps/ConfigMapProcessor.java +++ b/src/main/java/application/rest/v1/configmaps/ConfigMapProcessor.java @@ -60,6 +60,9 @@ public ConfigMapProcessor(String kind) { } public JsonObject getConfigMap(ApiClient client, JsonObject component, ConfigMapType type) { + if (Logger.isEntryEnabled()) + Logger.log(className, "getConfigMap", Logger.LogType.ENTRY, "ConfigMapType = " + type.toString()); + final ConfigMapBuilder builder = type == ConfigMapType.ACTION ? new ActionConfigMapBuilder() : new StatusMappingConfigMapBuilder(); final String namespace = KAppNavEndpoint.getComponentNamespace(component); final String kind = KAppNavEndpoint.getComponentKind(component); @@ -70,7 +73,6 @@ public JsonObject getConfigMap(ApiClient client, JsonObject component, ConfigMap if (Logger.isDebugEnabled()) { Logger.log(className, "getConfigMap", Logger.LogType.DEBUG, - "\n type = " + type + "\n component namespace = " + namespace + "\n component apiVersion = " + apiVersion + "\n component name= " + name + @@ -101,8 +103,10 @@ public JsonObject getConfigMap(ApiClient client, JsonObject component, ConfigMap } } - System.out.println("EXIT: builder.getConfigMap()" + builder.getConfigMap()); - return builder.getConfigMap(); + JsonObject configMapFound = builder.getConfigMap(); + if (Logger.isExitEnabled()) + Logger.log(className, "getConfigMap", Logger.LogType.EXIT, "configMap found = " + configMapFound); + return configMapFound; } private String getConfigMapName(ConfigMapType type, String name, String subkind, String kind) { diff --git a/src/main/java/application/rest/v1/configmaps/KindActionMappingProcessor.java b/src/main/java/application/rest/v1/configmaps/KindActionMappingProcessor.java index 8669da0..c0cf490 100644 --- a/src/main/java/application/rest/v1/configmaps/KindActionMappingProcessor.java +++ b/src/main/java/application/rest/v1/configmaps/KindActionMappingProcessor.java @@ -42,7 +42,7 @@ public class KindActionMappingProcessor { // KindActionMapping/KAM definitions private static final String KAM_PLURAL = "kindactionmappings"; private static final String KAM_GROUP = "actions.kappnav.io"; - private static final String KAM_VERSION = "v1beta1"; + private static final String KAM_VERSION = "v1"; private static final String WILDCARD = "*"; private static final int MAX_PRECEDENCE = 9; @@ -191,7 +191,7 @@ protected ArrayList getConfigMapsFromKAMs(ApiClient client) { } catch (ApiException e) { if (Logger.isErrorEnabled()) { Logger.log(className, "getQualifiedKindActionMappings", Logger.LogType.ERROR, - "Caught PatternException returning status: " + e.toString());} + "Caught ApiException: " + e.toString());} } if (Logger.isExitEnabled()) From 4786b5d7e5ce89e6085a010c7df7bff595194f00 Mon Sep 17 00:00:00 2001 From: Kin Ueng Date: Wed, 1 Apr 2020 13:06:32 -0500 Subject: [PATCH 3/5] apiVersion: Add suppose for the percentage symbol - The apiVersion value will be url encoded, which means the `%` can exist. Add the `%` to the regular expression for the apiVersion query parameter. --- src/main/java/application/rest/v1/KAppNavEndpoint.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/application/rest/v1/KAppNavEndpoint.java b/src/main/java/application/rest/v1/KAppNavEndpoint.java index 9b5e0a8..5df3121 100644 --- a/src/main/java/application/rest/v1/KAppNavEndpoint.java +++ b/src/main/java/application/rest/v1/KAppNavEndpoint.java @@ -53,7 +53,7 @@ public abstract class KAppNavEndpoint { protected static final String NAME_PATTERN_ONE_OR_MORE = "^[a-z0-9-.:]+$"; protected static final String NAME_PATTERN_ZERO_OR_MORE = "^[a-z0-9-.:]*$"; - protected static final String API_VERSION_PATTERN_ZERO_OR_MORE = "^[a-z0-9-.:/]*$"; + protected static final String API_VERSION_PATTERN_ZERO_OR_MORE = "^[a-z0-9-.:/%]*$"; private static final boolean DISABLE_TRUST_ALL_CERTS; private static final String DISABLE_TRUST_ALL_CERTS_PROPERTY = "kappnav.disable.trust.all.certs"; From 5f86c078bc6267ab28cbe0bf92cd4cb77e298d92 Mon Sep 17 00:00:00 2001 From: Susan Odom Date: Thu, 2 Apr 2020 09:08:06 -0500 Subject: [PATCH 4/5] Code review updates --- .../rest/v1/configmaps/ConfigMapCache.java | 9 +- .../v1/configmaps/ConfigMapProcessor.java | 62 ++- .../KindActionMappingProcessor.java | 392 +++++++++++------- 3 files changed, 291 insertions(+), 172 deletions(-) diff --git a/src/main/java/application/rest/v1/configmaps/ConfigMapCache.java b/src/main/java/application/rest/v1/configmaps/ConfigMapCache.java index 2d94d1b..8acc8d0 100644 --- a/src/main/java/application/rest/v1/configmaps/ConfigMapCache.java +++ b/src/main/java/application/rest/v1/configmaps/ConfigMapCache.java @@ -124,18 +124,18 @@ private Selector getSelector() { }); } - public static ArrayList getConfigMapsAsJSON(ApiClient client, ArrayList configMapsList) { + public static ArrayList getConfigMapsAsJSON(ApiClient client, ArrayList configMapsList) { ArrayList configMaps = new ArrayList(); for (int i=0; i configMapsList = kam.getConfigMapsFromKAMs(client); + ArrayList configMapsList = kam.getConfigMapsFromKAMs(client); if (configMapsList != null) { // look up the configmaps in a cluster @@ -203,13 +253,9 @@ private static ConflictAction getConflictAction(JsonObject map) { if (e != null && e.isJsonPrimitive()) { String s = e.getAsString(); if ("merge".equals(s)) { - if (Logger.isDebugEnabled()) - Logger.log(className, "getConflictAction", Logger.LogType.DEBUG, "MERGE"); return ConflictAction.MERGE; } else if ("replace".equals(s)) { - if (Logger.isDebugEnabled()) - Logger.log(className, "getConflictAction", Logger.LogType.DEBUG, "REPLACE"); return ConflictAction.REPLACE; } } @@ -217,8 +263,6 @@ else if ("replace".equals(s)) { } // Default value - if (Logger.isDebugEnabled()) - Logger.log(className, "getConflictAction", Logger.LogType.DEBUG, "MERGE"); return ConflictAction.MERGE; } @@ -240,7 +284,7 @@ private static void mergeConfigMaps(ArrayList configMapsFound, Confi if (getConflictAction(cMap) == ConflictAction.REPLACE) { if (Logger.isDebugEnabled()) Logger.log(className, "getConflictAction", Logger.LogType.DEBUG, - "Stop merging with a REPLACE conflicit action"); + "Stop merging with a REPLACE conflict action"); return; } } else { diff --git a/src/main/java/application/rest/v1/configmaps/KindActionMappingProcessor.java b/src/main/java/application/rest/v1/configmaps/KindActionMappingProcessor.java index c0cf490..8ff913a 100644 --- a/src/main/java/application/rest/v1/configmaps/KindActionMappingProcessor.java +++ b/src/main/java/application/rest/v1/configmaps/KindActionMappingProcessor.java @@ -18,6 +18,9 @@ import java.util.ArrayList; import java.util.List; +import java.util.Locale; + +import javax.xml.namespace.QName; import com.google.gson.JsonArray; import com.google.gson.JsonElement; @@ -31,9 +34,11 @@ import com.ibm.kappnav.logging.Logger; /** - * KindActionMappings provide mapping rules that map a resource to a set of action configmaps. - * This class provides the methods that facilitates a process of getting a configmap for a given + * KindActionMapping provides mapping rules that map a resource to a set of action configmaps. + * This class provides the methods that facilitate a process of getting a configmap for a given * resource following the mapping rules and configmap hierarchy & precedence. + * + * Desing document link: https://github.com/kappnav/design/blob/master/kind-action-mapping.md */ public class KindActionMappingProcessor { @@ -47,11 +52,13 @@ public class KindActionMappingProcessor { private static final int MAX_PRECEDENCE = 9; private static final int TOTAL_INDIVIDUAL_MAPPINGS = 4; + private static final int KAM_N = 10; private static final int KSN = 3; // Kind, Subkind, name - instance specific private static final int KN = 2; // Kind, name - instance specific private static final int KS = 1; // Kind, Subkind - subkind specific private static final int K = 0; // Kind - kind specific + private static final int NO_KSN = -1; // Null or Empty KSN private static final String METADATA_PROPERTY_NAME = "metadata"; private static final String NAMESPACE_PROPERTY_NAME = "namespace"; @@ -82,21 +89,49 @@ public KindActionMappingProcessor(String namespace, String apiVersion, String na } /** - * Get configmaps defined in the mappings in the KindActionMapping custom resources - * for a given resource. One or more action configmaps may exist to which the same - * resource maps. + * This method is to get configmaps defined in the mappings and precedences of the + * KindActionMapping custom resources for a given resource. + * + * The KindActionMapping CRD defines which config maps contain the action definitions for + * which resource kinds. The mappings are based on the following resource fields: + * + * - apiVersion is the group/version identifier of the resource. Note Kubernetes resources + * with no group value (e.g. Service) specify apiVersion as version only. E.g. apiVersion: v1. + * - kind is the resource's kind field + * - subkind is the resource's metadata.annotations.kappnav.subkind annotation. + * - name is the resource's metadata.name field + * + * KindActionMappings provide mapping rules that map a resource to a set of action config maps. + * These action config maps are then combined to form the set of actions applicable to the resource. + * + * One or more action configmaps may exist to which the same resource maps. Multiple mapping rules + * may exist to which a resource maps; mapping rules are searched for in this order, using the match + * values from the from resource, searching for a matching rule from the most specific to least specific: + * + * For a resource kind qualified by the subkind annotation: + * - kind-subkind.name - instance specific + * - kind-subkind - subkind specific + * - kind - kind specific + * + * For a resource without subkind qualification: + * - kind.name - instance specific + * - kind - kind specific + * + * Multiple KindActionMapping resources may specify mappings rules for the same resource kind. + * When this happens, additional action configmap mappings are inserted into the configmap + * hierarchy, based on the KindActionMapping instance's precedence value. * - * @param client + * @param client ApiClient * @return configmaps matched the defined action configmap mappings in KAM in order of * configmap hierarchy & precedence */ - protected ArrayList getConfigMapsFromKAMs(ApiClient client) { - + protected ArrayList getConfigMapsFromKAMs(ApiClient client) { + String methodName = "getConfigMapsFromKAMs"; if (Logger.isEntryEnabled()) - Logger.log(className, "getConfigMapsFromKAMs", Logger.LogType.ENTRY,""); + Logger.log(className, methodName, Logger.LogType.ENTRY,""); - String[][] mapNamesFound = new String[MAX_PRECEDENCE][TOTAL_INDIVIDUAL_MAPPINGS]; - ArrayList configMapsList = null; + QName[][][] mapNamesFound = new QName[MAX_PRECEDENCE][KAM_N][TOTAL_INDIVIDUAL_MAPPINGS]; + ArrayList configMapsList = null; List kamList = null; try { @@ -111,74 +146,102 @@ protected ArrayList getConfigMapsFromKAMs(ApiClient client) { itemsArray.forEach(item-> { if ( (item != null) && (item.isJsonObject()) ) { if (Logger.isDebugEnabled()) - Logger.log(className, "getConfigMapsFromKAMs", Logger.LogType.DEBUG, + Logger.log(className, methodName, Logger.LogType.DEBUG, "\nKindActionMapping found: " + item); - if (item != null) { - JsonElement element = item.getAsJsonObject().get(SPEC_PROPERTY_NAME); - String kamNamespace = getKAMNamespace(item); - if (element != null) { - JsonObject spec = element.getAsJsonObject(); - int precedenceIndex = spec.get(PRECEDENCE_PROPERTY_NAME).getAsInt()-1; - JsonArray mappings = spec.getAsJsonArray(MAPPINGS_PROPERTY_NAME); - - // iterate through each mapping within a KAM resource - mappings.forEach(mapItem-> { - if (mapItem != null) { - JsonObject props = mapItem.getAsJsonObject(); - if (props != null) { - JsonElement prop = props.get(APIVERSION_PROPERTY_NAME); - String apiVersion = (prop != null) ? prop.getAsString():null; - prop = props.get(NAME_PROPERTY_NAME); - String name = (prop != null) ? prop.getAsString():null; - prop = props.get(SUBKIND_PROPERTY_NAME); - String subkind = (prop != null) ? prop.getAsString():null; - prop = props.get(KIND_PROPERTY_NAME); - String kind = (prop != null) ? prop.getAsString():null; - prop = props.get(MAPNAME_PROPERTY_NAME); - String mapname = (prop != null) ? prop.getAsString():null; - Logger.log(className, "getConfigMapsFromKAMs", Logger.LogType.DEBUG, - "\nmapping info: " + - "\napiVersion = " + apiVersion + - "\nname = " + name + - "\nsubkind = " + subkind + - "\nkind = " + kind + - "\nmapname = " + mapname); - - if (isApiVersionMatch(apiVersion, compApiVersion)) { - int compPropsIdx = exemineMappingProperties(compName, compSubkind, compKind); - int kamMappingPropIdx = exemineMappingProperties(name, subkind, kind); - boolean found = foundMatchedConfigMap(compPropsIdx, kamMappingPropIdx, - compName, compSubkind, compKind, - name, subkind, kind); - - if (found){ - if (Logger.isDebugEnabled()) - Logger.log(className, "getQualifiedKindActionMappings", Logger.LogType.DEBUG, - "mapName " + mapname + " is stored in configMapsFound["+precedenceIndex+"][" + - kamMappingPropIdx+"]"); - // each mapname found is stored in the 2D array as "mapname@namespace" + JsonElement element = item.getAsJsonObject().get(SPEC_PROPERTY_NAME); + String kamNamespace = getKAMNamespace(item); + if (element != null) { + JsonObject spec = element.getAsJsonObject(); + JsonElement precedence = spec.get(PRECEDENCE_PROPERTY_NAME); + int precedenceIndex; + if (precedence != null) + precedenceIndex = spec.get(PRECEDENCE_PROPERTY_NAME).getAsInt()-1; + else + precedenceIndex = 0; // No precedence specified: set default precedenceIndex as 0 + // (The default for precedence is 1) + JsonArray mappings = spec.getAsJsonArray(MAPPINGS_PROPERTY_NAME); + + // iterate through each mapping within a KAM custom resource + mappings.forEach(mapItem-> { + if (mapItem != null) { + JsonObject props = mapItem.getAsJsonObject(); + if (props != null) { + JsonElement prop = props.get(APIVERSION_PROPERTY_NAME); + String apiVersion = (prop != null) ? prop.getAsString():null; + prop = props.get(NAME_PROPERTY_NAME); + String name = (prop != null) ? prop.getAsString():null; + prop = props.get(SUBKIND_PROPERTY_NAME); + String subkind = (prop != null) ? prop.getAsString():null; + prop = props.get(KIND_PROPERTY_NAME); + String kind = (prop != null) ? prop.getAsString():null; + prop = props.get(MAPNAME_PROPERTY_NAME); + String mapname = (prop != null) ? prop.getAsString():null; + Logger.log(className, methodName, Logger.LogType.DEBUG, + "\nmapping info: " + + "\napiVersion = " + apiVersion + + "\nname = " + name + + "\nsubkind = " + subkind + + "\nkind = " + kind + + "\nmapname = " + mapname); + + if (isApiVersionMatch(apiVersion, compApiVersion)) { + int compPropsIdx = examineMappingProperties(compName, compSubkind, compKind); + int kamMappingPropIdx = examineMappingProperties(name, subkind, kind); + + // if the resource given matches the kind action mapping rules? + boolean matches = isResourceMatchesRule(compPropsIdx, kamMappingPropIdx, + name, subkind, kind); + + if (matches){ + // find next available slot for a kam in same precedence + // currently, we allow 10 kams with the same precedence with this impl. + // The kam found after all slots are used are being ignored with a warning. + int kamNIndex=-1; + for (int i=0; i getConfigMapsFromKAMs(ApiClient client) { // process candidate mapnames including a string substitution as needed and then store // them to a list according the configmap hierarchy and (high to low) precedence - configMapsList = processCandidateMapnames(mapNamesFound, compNamespace, - compName, compSubkind, compKind); + configMapsList = processCandidateMapnames(mapNamesFound, compNamespace); } catch (ApiException e) { if (Logger.isErrorEnabled()) { - Logger.log(className, "getQualifiedKindActionMappings", Logger.LogType.ERROR, + Logger.log(className, methodName, Logger.LogType.ERROR, "Caught ApiException: " + e.toString());} } if (Logger.isExitEnabled()) - Logger.log(className, "getConfigMapsFromKAMs", Logger.LogType.EXIT,""); + Logger.log(className, methodName, Logger.LogType.EXIT,""); return configMapsList; } /** * Get all "KindActionMapping" custom resources in a cluster * - * @param client - * @return + * @param client apiVersion + * @return a list of KAM CR instances in a cluster * @throws ApiException */ protected List listKAMCustomResources(ApiClient client) throws ApiException { + String methodName = "listKAMCustomResources"; final CustomObjectsApi coa = new CustomObjectsApi(); coa.setApiClient(client); if (Logger.isDebugEnabled()) { - Logger.log(className, "listKAMCustomResources", Logger.LogType.DEBUG, + Logger.log(className, methodName, Logger.LogType.DEBUG, "\n List KAM Custom Resources for all namespaces with" + "\n group = " + "actions.kappnav.io" + "\n namespace = kappnav and name = default"); @@ -229,8 +292,9 @@ protected List listKAMCustomResources(ApiClient client) * @return the namespace for the given kam resource */ private String getKAMNamespace(JsonElement kam) { + String methodName = "getKAMNamespace"; if (Logger.isEntryEnabled()) - Logger.log(className, "getKAMNamespace", Logger.LogType.ENTRY,""); + Logger.log(className, methodName, Logger.LogType.ENTRY,""); String namespaceStr = null; JsonElement metadata = (JsonObject) kam.getAsJsonObject().get(METADATA_PROPERTY_NAME); @@ -240,25 +304,29 @@ private String getKAMNamespace(JsonElement kam) { namespaceStr = namespace.get(NAMESPACE_PROPERTY_NAME).getAsString(); } else { if (Logger.isDebugEnabled()) - Logger.log(className, "getKAMNamespace", Logger.LogType.DEBUG, "kam metadata is null."); + Logger.log(className, methodName, Logger.LogType.DEBUG, "kam metadata is null."); } if (Logger.isExitEnabled()) { - Logger.log(className, "getKAMNamespace", Logger.LogType.EXIT, "kam namespace = " + namespaceStr); + Logger.log(className, methodName, Logger.LogType.EXIT, "kam namespace = " + namespaceStr); } return namespaceStr; } + private static final int GROUP = 0; + private static final int VERSION = 1; + private static final int GROUPLESS_VERSION = 0; /** * Check to see if the apiVersions of a component and a mapping are matching * * @param apiVersion * @param compApiVersion - * @return + * @return true when the apiVersion is matched; false otherwise */ private boolean isApiVersionMatch(String apiVersion, String compApiVersion) { + String methodName = "isApiVersionMatch"; if (Logger.isEntryEnabled()) - Logger.log(className, "isApiVersionMatch", Logger.LogType.ENTRY, "apiVerion = " + apiVersion + + Logger.log(className, methodName, Logger.LogType.ENTRY, "apiVerion = " + apiVersion + ", compApiVersion = " + compApiVersion); boolean match = false; @@ -271,37 +339,49 @@ private boolean isApiVersionMatch(String apiVersion, String compApiVersion) { String comp_grp_version[] = compApiVersion.split("/"); if ( (grp_version.length == 2) && (comp_grp_version.length == 2) ){ // group/version - wildcard match - if (grp_version[0].equals(comp_grp_version[0]) || (grp_version[0].equals(WILDCARD)) ) - if (grp_version[1].equals(comp_grp_version[1]) || (grp_version[1].equals(WILDCARD)) ) { + if (grp_version[GROUP].equals(comp_grp_version[GROUP]) || (grp_version[GROUP].equals(WILDCARD)) ) + if (grp_version[VERSION].equals(comp_grp_version[VERSION]) || (grp_version[VERSION].equals(WILDCARD)) ) { match = true; } } else if ( (grp_version.length == 1) && (comp_grp_version.length == 1) ) { // verion only - if (grp_version[0].equals(comp_grp_version[0]) || (grp_version[0].equals(WILDCARD)) ) { + if (grp_version[GROUPLESS_VERSION].equals(comp_grp_version[GROUPLESS_VERSION]) || + (grp_version[GROUPLESS_VERSION].equals(WILDCARD)) ) { match = true; } + } else { + if (Logger.isDebugEnabled()) + Logger.log(className, methodName, Logger.LogType.DEBUG, "No match: group_version = " + grp_version + + ", component group_version = " + comp_grp_version); } } } if (Logger.isExitEnabled()) - Logger.log(className, "isApiVersionMatch", Logger.LogType.EXIT, "match = " + match); + Logger.log(className, methodName, Logger.LogType.EXIT, "match = " + match); return match; } /** - * Examine the mapping properites passed in + * Examine the mapping properites passed in and return one of the four combinations of + * the mapping properties as follows: + * + * - KSN (Kind-Subkind-Name), + * - KS (Kind-Subkind), + * - KN (Kind-Name), + * - K (Kind) * * @param name * @param subkind * @param kind * @return KSN, KS, KN, or K */ - private int exemineMappingProperties(String name, String subkind, String kind) { + private int examineMappingProperties(String name, String subkind, String kind) { + String methodName = "examineMappingProperties"; if (Logger.isEntryEnabled()) - Logger.log(className, "exemineMappingProperties", Logger.LogType.ENTRY, + Logger.log(className, methodName, Logger.LogType.ENTRY, "(name, subkind, kind) = (" + name +", " + subkind + ", " + kind + ")"); - int retVal = -1; + int retVal = NO_KSN; if ((kind != null) && !kind.isEmpty()) { if ((subkind != null) && !subkind.isEmpty()) { if ((name != null) && !name.isEmpty()) { @@ -316,79 +396,77 @@ private int exemineMappingProperties(String name, String subkind, String kind) { } } else { if (Logger.isErrorEnabled()) - Logger.log(className, "exemineMappingProperties", Logger.LogType.ERROR, + Logger.log(className, methodName, Logger.LogType.ERROR, "kind given is null or empty string"); } if (Logger.isExitEnabled()) - Logger.log(className, "exemineMappingProperties", Logger.LogType.EXIT, + Logger.log(className, methodName, Logger.LogType.EXIT, "retVal = " + retVal); return retVal; } /** - * Find a matched configmap defined in a given KAM + * Test to see if the given component resource KSN value matches exactly or matches via wildcard + * to the kam mapping KSN value * - * @param cNumFields - * @param kNumFields - * @param cName - * @param cSubkind - * @param cKind - * @param gName - * @param gSubkind - * @param gKind - * @return true if found; otherwise false + * @param compKSNValue can be KSN, KS, KN, K + * @param kamKSNValue can be KSN, KS, KN, K + * @param kamName kam name property + * @param kamSubkind kam subkind property + * @param kamKind kam kind property + * @return true if kam KSN value matches component KSN value; otherwise false */ - private boolean foundMatchedConfigMap(int cNumFields, int kNumFields, - String cName, String cSubkind, String cKind, - String gName, String gSubkind, String gKind) { + private boolean isResourceMatchesRule(int compKSNValue, int kamKSNValue, + String kamName, String kamSubkind, String kamKind) { + String methodName = "isResourceMatchesRule"; if (Logger.isEntryEnabled()) - Logger.log(className, "foundMatchedConfigMap", Logger.LogType.ENTRY, - "cNumField = " + cNumFields + " kNumField = " + kNumFields); + Logger.log(className, methodName, Logger.LogType.ENTRY, + "cNumField = " + compKSNValue + " kNumField = " + kamKSNValue); - if ( (cNumFields == -1) || (kNumFields == -1)) { - if (Logger.isExitEnabled()) - Logger.log(className, "isMatched", Logger.LogType.EXIT, "return false"); + if ( (compKSNValue == NO_KSN) || (kamKSNValue == NO_KSN)) { + if (Logger.isErrorEnabled()) + Logger.log(className, methodName, Logger.LogType.ERROR, "return false"); return false; } boolean match = false; - if (cNumFields == kNumFields) { - switch (kNumFields) { + if (compKSNValue == kamKSNValue) { + switch (kamKSNValue) { case KSN: - if ((gName.equals(cName)) || (gName.equals(WILDCARD))) - if ((gSubkind.equals(cSubkind)) || (gSubkind.equals(WILDCARD))) - if ((gKind.equals(cKind)) || (gKind.equals(WILDCARD))) + if ((kamName.equals(this.compName)) || (kamName.equals(WILDCARD))) + if ((kamSubkind.equals(this.compSubkind)) || (kamSubkind.equals(WILDCARD))) + if ((kamKind.equals(this.compKind)) || (kamKind.equals(WILDCARD))) match = true; break; case KS: - if ((gSubkind.equals(cSubkind)) || (gSubkind.equals(WILDCARD))) - if ((gKind.equals(cKind)) || (gKind.equals(WILDCARD))) + if ((kamSubkind.equals(this.compSubkind)) || (kamSubkind.equals(WILDCARD))) + if ((kamKind.equals(this.compKind)) || (kamKind.equals(WILDCARD))) match = true; break; case KN: - if ((gName.equals(cName)) || (gName.equals(WILDCARD))) - if ((gKind.equals(cKind)) || (gKind.equals(WILDCARD))) + if ((kamName.equals(this.compName)) || (kamName.equals(WILDCARD))) + if ((kamKind.equals(this.compKind)) || (kamKind.equals(WILDCARD))) match = true; break; case K: - if (cNumFields == K) - if ((gKind.equals(cKind)) || (gKind.equals(WILDCARD))) + if (compKSNValue == K) + if ((kamKind.equals(this.compKind)) || (kamKind.equals(WILDCARD))) match = true; } - } else if (kNumFields == K) { - if ((gKind.equals(cKind)) || (gKind.equals(WILDCARD))) - match = true; - } else if ((kNumFields == KS) && (cNumFields == KSN)) { - if ( (gSubkind.equals(cSubkind)) || (gSubkind.equals(WILDCARD)) ) - if ( (gKind.equals(cKind)) || (gKind.equals(WILDCARD)) ) + } else if ((compKSNValue == KSN) && (kamKSNValue == KS)) { + if ( (kamSubkind.equals(this.compSubkind)) || (kamSubkind.equals(WILDCARD)) ) + if ( (kamKind.equals(this.compKind)) || (kamKind.equals(WILDCARD)) ) match = true; + } else if (kamKSNValue == K) { + if ((kamKind.equals(this.compKind)) || (kamKind.equals(WILDCARD))) + match = true; } if (Logger.isExitEnabled()) - Logger.log(className, "foundMatchedConfigMap", Logger.LogType.EXIT, "return = " + match); + Logger.log(className, methodName, Logger.LogType.EXIT, "return = " + match); return match; } @@ -396,31 +474,30 @@ private boolean foundMatchedConfigMap(int cNumFields, int kNumFields, * Process mapnames with variable substitution if applies and store the mapnames along with their * namespaces in a list according to the configmap hierarchy and precedence in decedending order * - * @param configMapsFound - * @param namespace - * @param name - * @param subkind - * @param kind + * @param configMapsFound configmaps found + * @param namespace resource's namespace if this is a instance specific configmaps; + * kam's namespace for all other configmaps. * @return processed configmap list */ - private ArrayList processCandidateMapnames(String[][] configMapsFound, String namespace, - String name, String subkind, String kind) { - ArrayList configMapList = new ArrayList (); + private ArrayList processCandidateMapnames(QName[][][] configMapsFound, String namespace) { + String methodName = "processCandidateMapnames"; + ArrayList configMapList = new ArrayList (); for (int ksnIdx=KSN; ksnIdx>=0; ksnIdx--) { for (int precedenceIdx=MAX_PRECEDENCE-1; precedenceIdx>=0; precedenceIdx--) { - String rawMapName = (configMapsFound[precedenceIdx][ksnIdx]); - if (rawMapName != null) { - String[] mapname_namespace = rawMapName.split("@"); - rawMapName = mapname_namespace[0]; // take off @namespace - if (Logger.isDebugEnabled()) - Logger.log(className, "processCandidateMapnames", Logger.LogType.DEBUG, - "rawMapName = " + rawMapName); - String actualMapName = mapNameSubstitute(rawMapName, namespace, name, subkind, kind); - if (Logger.isDebugEnabled()) - Logger.log(className, "processCandidateMapnames", Logger.LogType.DEBUG, - "actualMapName = " + actualMapName); - configMapList.add(actualMapName+"@"+mapname_namespace[1]); + for (int kamNIdx=0; kamNIdx Date: Fri, 3 Apr 2020 10:20:17 -0500 Subject: [PATCH 5/5] fix bad spelling --- src/main/java/application/rest/v1/KAppNavEndpoint.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/application/rest/v1/KAppNavEndpoint.java b/src/main/java/application/rest/v1/KAppNavEndpoint.java index 5df3121..1f4a6de 100644 --- a/src/main/java/application/rest/v1/KAppNavEndpoint.java +++ b/src/main/java/application/rest/v1/KAppNavEndpoint.java @@ -89,7 +89,7 @@ public String run() { // Kind actions mapping properties private static final String API_VERSION_PROPERTY_NAME = "apiVersion"; - private static final String KIND_PROPRETY_NAME = "kind"; + private static final String KIND_PROPERTY_NAME = "kind"; // Status object properties. private static final String VALUE_PROPERTY_NAME = "value"; @@ -193,7 +193,7 @@ public static String getComponentKind(JsonObject component) { if (Logger.isEntryEnabled()) Logger.log(className, "getComponentKind", Logger.LogType.ENTRY,""); - final JsonElement kind_e = component.get(KIND_PROPRETY_NAME); + final JsonElement kind_e = component.get(KIND_PROPERTY_NAME); String kind = null; if (kind_e != null) kind = kind_e.getAsString();