Complete Groovy to Java 17 migration and modernize with Lombok and Records - #541
Complete Groovy to Java 17 migration and modernize with Lombok and Records#541DerDaehne wants to merge 73 commits into
Conversation
Migrate 'ReturnCode' enum and 'MapUtils' helper class from Groovy to Java. This is the first step of the Groovy-to-Java migration, proving the joint compilation setup works perfectly. Co-authored-by: Gemini <gemini@google.com>
Migrate 'DockerImageParser' and its nested 'Image' class from Groovy to Java. Use modern Java Records for intermediate Tuple representation. Co-authored-by: Gemini <gemini@google.com>
Migrate 'NetworkingUtils' class from Groovy to Java. Implement method overloading to replace Groovy default parameters, and replace dynamic property accesses with standard Java getters. Co-authored-by: Gemini <gemini@google.com>
…er to Java Migrate 'CommandExecutor' and 'InsecureCredentialProvider' from Groovy to Java. Implement necessary Groovy-interoperable method overloads for process-execution and environmental variable mapping. Co-authored-by: Gemini <gemini@google.com>
Migrate 'AirGappedUtils' class from Groovy to Java. Adjust visibility of GitRepo.NAMESPACE_3RD_PARTY_DEPENDENCIES constant to public so it is exposed to the Java compiler in joint compilation. Co-authored-by: Gemini <gemini@google.com>
Migrate 'ClusterResourcesCopyFilter' utility from Groovy to Java. Implement streams and lambdas to replace Groovy collections and closures. Co-authored-by: Gemini <gemini@google.com>
Migrate 'AllowListFreemarkerObjectWrapper' from Groovy to Java. Use standard Java anonymous classes to represent the filtered TemplateHashModel. Co-authored-by: Gemini <gemini@google.com>
Migrate 'TemplatingEngine' from Groovy to Java. Implement overloads to replace Groovy default parameters and use try-with-resources to safely close Files.walk streams. Co-authored-by: Gemini <gemini@google.com>
Migrate 'FileSystemUtils' from Groovy to Java. Use Files.readString and Files.writeString instead of Groovy extensions. Implement try-with-resources for file walks to prevent stream resource leaks. Co-authored-by: Gemini <gemini@google.com>
Migrate 'ScmProviderType' enum and 'ConfigConstants' interface from Groovy to Java. Co-authored-by: Gemini <gemini@google.com>
Migrate 'Credentials' configuration model class from Groovy to Java. Implement standard Java getters and setters and override toString. Co-authored-by: Gemini <gemini@google.com>
Migrate 'GitlabConfig' and 'ScmManagerConfig' interfaces from Groovy to Java. Co-authored-by: Gemini <gemini@google.com>
Migrate 'JsonSchemaGenerator' and 'JsonSchemaValidator' from Groovy to Java. Use standard streams and list representation for schema validation messages. Co-authored-by: Gemini <gemini@google.com>
Migrate 'MultiTenantSchema', 'ScmCentralSchema', and 'ScmTenantSchema' from Groovy to Java. Use standard Java nested static classes and bean properties for Picocli option parsing. Co-authored-by: Gemini <gemini@google.com>
Fix GString cast issue in CommandExecutorForTest by using standard java String list. Let TemplatingEngine propagate raw Freemarker exceptions so that AllowlistFreemarkerObjectWrapperTest asserts the correct exception type. Co-authored-by: Gemini <gemini@google.com>
Migrate the central 'Config' class from Groovy to Java. Implement nested static configuration schemas and explicit bean getters/setters. Integrate modern Java SecureRandom password generator and lambda-based Jackson serialization modifiers. Co-authored-by: Gemini <gemini@google.com>
Migrate 'Role', 'RoleBinding', and 'ServiceAccountRef' from Groovy to Java. Implement nested enum Variant in Role and standard constructor logic. Co-authored-by: Gemini <gemini@google.com>
Migrate 'RbacDefinition' logic from Groovy to Java. Co-authored-by: Gemini <gemini@google.com>
Migrate 'HelmClient' utility from Groovy to Java. Implement method overloads to replace Groovy default parameter values. Delete empty 'HelmClientTest.groovy' placeholder. Co-authored-by: Gemini <gemini@google.com>
Migrate the central 'K8sClient' from Groovy to Java. Implement composition and delegation by splitting off private stateless helpers into a package-private 'K8sClientHelper' class. Expose mutable 'client' and 'gopConfig' fields for mock test injections. Co-authored-by: Gemini <gemini@google.com>
Migrate 'GitRepo' and 'GitRepoFactory' from Groovy to Java. Adjust AirGappedUtils.java to properly wrap checked JGit GitAPIExceptions/IOExceptions in RuntimeExceptions. Co-authored-by: Gemini <gemini@google.com>
Migrate 'ScmManagerApiClient', 'ScmManagerApi', 'RepositoryApi', 'UsersApi', and 'PluginApi' from Groovy to Java. Adjust ScmManagerSetupTest Mockito stubbing for getGitProvider() to support Java getters. Co-authored-by: Gemini <gemini@google.com>
Migrate 'JenkinsApiClient', 'UserManager', 'JobManager', and 'GlobalPropertyManager' from Groovy to Java. Use Java Text Blocks and precise string placeholders/replacements to match multiline Groovy string test assertions exactly. Use LinkedHashMap to preserve exact JSON map insertion order in credential serialization. Co-authored-by: Gemini <gemini@google.com>
Migrate 'Tool', 'CommonToolConfig', and 'ImagePullSecretCreator' from Groovy to Java. Use private logger visibility in Tool.java to prevent name collisions with Groovy subclasses annotated with @slf4j. Implement robust Java reflection fallback to support subclass dynamic 'namespace' property lookups. Co-authored-by: Gemini <gemini@google.com>
Migrate 'Ingress', 'Registry', 'CertManager', and 'ExternalSecretsOperator' from Groovy to Java. All migrated classes inherit from the new Java 'Tool' base class. Co-authored-by: Gemini <gemini@google.com>
Migrate 'ArgoCD' and 'ScmManager' from Groovy to Java. Keep standard annotations, DI wiring and orders intact. Co-authored-by: Gemini <gemini@google.com>
Migrate 'Jenkins', 'Vault', and 'Monitoring' from Groovy to Java. Wrap checked IOException and TemplateException thrown by TemplatingEngine.replaceTemplate in Vault.java and convert etc/group gid lookup to use pure Java parsing. Co-authored-by: Gemini <gemini@google.com>
Migrate all core Application components, Workspace classes, ContentLoader and CLI classes from Groovy to Java 17. Ensure proper type checking for nested RepoCoordinate in ContentLoaderTest. Co-authored-by: Gemini <gemini@google.com>
- Wrap JGit checked exceptions in Tool.java and ArgoCD.java. - Implement robust raw Map type check and Groovy-compatible map printing in ArgoCD.java's postConfigInit. - Propagate raw RuntimeExceptions in AirGappedUtils.java. - Use a mutable HashMap for service registry helm values to support deep merging. Co-authored-by: Gemini <gemini@google.com>
Three version pins were controllable by Renovate but not actually tracked: - scm-manager helm chart version had no renovate annotation and its file wasn't in the custom manager's fileMatch - kube-prometheus-stack's renovate comment was split across two lines, which the custom manager's regex can't match - Dockerfile's HELM_VERSION arg was only used in curl download URLs, invisible to Renovate's default dockerfile manager Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
|
||
| // Ownership of clusterResourcesRepository is handed off to the returned RepositoryWorkspace, | ||
| // which closes it in RepositoryWorkspace#close(). Sonar can't trace that across the boundary. | ||
| @SuppressWarnings("java:S2095") |
There was a problem hiding this comment.
Supressing warnings without comment
mdroll
left a comment
There was a problem hiding this comment.
Need a bunch of rework here and there. Not explicitly looked on general sonar hints.
Still we need to test also the others profiles. And also test on openshift / OKD.
| @Slf4j | ||
| public class ApplicationConfigurator { | ||
|
|
||
| private final FileSystemUtils fileSystemUtils; |
| } | ||
|
|
||
| private void storeGopInformationInSecret(DeploymentContext context) { | ||
| String namespace = DEFAULT_GOP_NAMESPACE; |
There was a problem hiding this comment.
This method is suited for to be in a own method
| } | ||
|
|
||
| public void createLocalDirectories() { | ||
| Path.of(clusterResourcesRootDir()).toFile().mkdirs(); |
There was a problem hiding this comment.
Next pitch switch to more modern API-Way:
Stream.of(
clusterResourcesRootDir(),
clusterResourcesAppsDir(),
clusterResourcesArgoCdDir(),
clusterResourcesApplicationsDir(),
clusterResourcesProjectsDir()
).forEach(this::createDirectorySafely);
| * @param name name of the secret | ||
| * @return the base64-encoded {@code namespaces} value of the secret | ||
| */ | ||
| public String getArgoCDNamespacesSecret(String name) { |
| * @param usernameKey data key holding the username | ||
| * @return the decoded credentials | ||
| */ | ||
| public Credentials getCredentialsFromSecret( |
| * @param name name of the ConfigMap | ||
| * @param filePath path of the file whose content becomes the ConfigMap data | ||
| */ | ||
| public void createConfigMapFromFile(String name, String filePath) { |
| } | ||
|
|
||
| private List<HasMetadata> loadYamlItems(InputStream stream, String sourceDescription) { | ||
| try { |
|
|
||
| /** Kubernetes client using Fabric8 Kubernetes Client. */ | ||
| @Singleton | ||
| @SuppressWarnings("java:S3776") |
There was a problem hiding this comment.
Supressing here
Lombok may can be added here so a few setter/getter methods are not needed
There was a problem hiding this comment.
This @SuppressWarnings("java:S3776") is applied to the entire class, which hides complexity warnings for all methods. Consider narrowing it to only the specific methods that truly need it (e.g. applyYaml), or reduce complexity there instead. A class-level suppression makes it easy to miss new problems added later.
| () -> { | ||
| // type is NonNamespaceOperation<Secret, SecretList, Resource<Secret>>; kept as `var` | ||
| // deliberately, spelling it out would hurt readability more than it helps. | ||
| var secretsClient = client.secrets().inNamespace(resolveNamespace(namespace)); |
There was a problem hiding this comment.
This delete-then-create pattern is non-atomic: if the delete succeeds but the subsequent create fails, the secret is left absent. The createImagePullSecret method uses createOr(NonDeletingOperation::update) which is idempotent in a single API call. Consider using the same pattern here:
secretsClient.resource(secret).createOr(NonDeletingOperation::update);
| log.debug("Namespace {} already exists.", namespace); | ||
| return true; | ||
| } | ||
| } catch (Exception e) { |
There was a problem hiding this comment.
Catching Exception broadly here can silently swallow real connectivity or authorization errors (e.g. 403 Forbidden would be treated as "namespace does not exist", causing createNamespace to attempt creation and fail with a confusing error). Consider catching only KubernetesClientException and rethrowing for non-404 status codes.
| String auth = | ||
| Base64.getEncoder() | ||
| .encodeToString((user + ":" + password).getBytes(StandardCharsets.UTF_8)); | ||
| String dockerConfig = |
There was a problem hiding this comment.
The dockerConfig JSON string is assembled by hand via string concatenation. A username or password containing " would break the JSON silently. Use a proper serializer instead:
Map<String, Object> dockerConfigMap = Map.of( "auths", Map.of(host, Map.of("username", user, "password", password, "auth", auth))); String dockerConfig = Serialization.asJson(dockerConfigMap);
| * @param selectors label key-value pairs the resources must match | ||
| */ | ||
| public void delete(String resource, String namespace, Tuple<?, ?>... selectors) { | ||
| if (selectors == null || selectors.length == 0) { |
There was a problem hiding this comment.
The two convenience overloads delete(resource) and delete(resource, namespace) both delegate to this varargs method with an empty array, which immediately throws IllegalArgumentException("Missing selectors"). This makes those overloads permanently broken. Either remove the empty-array guard for "delete all" semantics, or remove/document the no-selector overloads.
| patchType = PatchType.JSON; | ||
| break; | ||
| default: | ||
| patchType = PatchType.STRATEGIC_MERGE; |
There was a problem hiding this comment.
Undocumented fallthrough to STRATEGIC_MERGE — An unrecognised type string silently falls through to STRATEGIC_MERGE. Consider throwing IllegalArgumentException so misconfigurations surface immediately rather than silently using the wrong patch type.
| Map<String, Object> match = findApiResourceViaDiscovery(client, normalized, resourceType); | ||
|
|
||
| if (match.isEmpty()) { | ||
| throw new K8sClient.KubernetesApiResourceNotFoundException(resourceType); |
There was a problem hiding this comment.
Inconsistent not-found contract — getCustomResourceClient throws KubernetesApiResourceNotFoundException when the match is empty, but findApiResourceViaDiscovery returns an empty map. Callers must know which convention applies. Consider making both throw, or both return Optional<Map<...>>.
| log.debug("Actual config: {}", config.toYaml(true)); | ||
| runHook(app, "postConfigInit", AbstractTool::postConfigInit, config); | ||
|
|
||
| context.close(); |
There was a problem hiding this comment.
app bean used after its originating ApplicationContext is closed — app is fetched from the first context (line 83), which is then closed on line 97. app is reused on line 122 from the new context but the same object reference from the old closed context is passed to runHook. Fetch app from the second context after register() for consistency, or document why cross-context reuse is safe here.
| private void addScmConfig(Config newConfig) { | ||
| log.debug("Adding additional config for SCM"); | ||
|
|
||
| if (hasText(newConfig.getScm().getScmManager().getUrl())) { |
There was a problem hiding this comment.
Potential NPE when scmManager is null — newConfig.getScm().getScmManager().getUrl() is called unconditionally. In GitLab-only mode, GitHandler.validate() explicitly sets scmManager to null. Add a null guard:
if (newConfig.getScm().getScmManager() != null && hasText(newConfig.getScm().getScmManager().getUrl())) {| "Successfully set features.argocd.resourceInclusionsCluster via Kubernetes ENV to: {}", | ||
| internalClusterUrl); | ||
| } catch (MalformedURLException e) { | ||
| throw new UncheckedIOException(errorMessage, e); |
There was a problem hiding this comment.
MalformedURLException wrapped as UncheckedIOException — MalformedURLException is not an I/O error. Wrapping it as UncheckedIOException misrepresents the failure to callers and catch blocks. Use IllegalArgumentException (consistent with how line 328 handles the same exception type in isUrlSetAndValid).
|
|
||
| @JsonIgnore | ||
| public String getHost() { | ||
| return URI.create(url).getHost(); |
There was a problem hiding this comment.
URI.create(url) throws IllegalArgumentException on empty or invalid URL — getHost() and getProtocol() call URI.create(url) unconditionally. If url is empty (its default value is ""), URI.create("").getHost() returns null rather than throwing, but callers expecting a non-null host may behave unexpectedly. A blank-url guard or Optional return type would make the contract explicit.
| } | ||
| } | ||
|
|
||
| public void listDirectories(String parentDir) { |
There was a problem hiding this comment.
listDirectories method has no return value and unclear purpose — This method (lines 196–202) calls getAllFilesFromDirectoryWithEnding(parentDir, "") which matches all files (not just directories), then logs each path at DEBUG. The method name says "list directories" but lists all files. Either fix the predicate to filter for directories only, or rename/document the method's actual behaviour.
| try { | ||
| TemplateModel statics = | ||
| new DefaultObjectWrapperBuilder(freemarker.template.Configuration.VERSION_2_3_32) | ||
| .build() | ||
| .getStaticModels(); | ||
| values.put("statics", statics); | ||
| } catch (Exception e) { | ||
| throw new RuntimeException("Failed to expose freemarker statics model", e); |
There was a problem hiding this comment.
Freemarker getStaticModels() failure wrapped as RuntimeException — The catch block on line 200 wraps a Exception as RuntimeException. This is the same pattern flagged elsewhere in the codebase. DefaultObjectWrapperBuilder.build() does not declare checked exceptions; if it throws, it's a programming error (misconfiguration), not an expected runtime failure. Let it propagate unchecked or use a more specific message.
| : new HashMap<>(); | ||
|
|
||
| Map<String, Object> mergedMap = MapUtils.deepMerge(values, templatedMap); | ||
| tempValuesPath = new FileSystemUtils().writeTempFile(mergedMap); |
There was a problem hiding this comment.
new FileSystemUtils() bypasses DI — prepareHelmValues() calls new FileSystemUtils() directly instead of using a constructor-injected instance. This makes the class harder to test and breaks Micronaut's singleton scope for FileSystemUtils. Add FileSystemUtils fileSystemUtils as a constructor parameter and use it here.
| while (System.currentTimeMillis() - startTime < timeoutMillis) { | ||
| try { | ||
| retrofit2.Call<Void> call = scmManager.getApiClient().generalApi().checkScmmAvailable(); | ||
| retrofit2.Response<Void> response = call.execute(); | ||
|
|
||
| if (response.isSuccessful()) { | ||
| log.debug("SCM-Manager is available."); | ||
| return; | ||
| } | ||
| } catch (Exception e) { | ||
| log.debug("Waiting for SCM-Manager... Error: {}", e.getMessage()); | ||
| } | ||
|
|
||
| try { | ||
| Thread.sleep(intervalMillis); | ||
| } catch (InterruptedException e) { | ||
| Thread.currentThread().interrupt(); | ||
| } | ||
| } | ||
|
|
||
| throw new IllegalStateException( | ||
| "Timeout: SCM-Manager did not respond with 200 OK within " + timeoutSeconds + " seconds"); | ||
| } |
There was a problem hiding this comment.
waitForScmmAvailable swallows InterruptedException in the timeout loop — Line 209 catches InterruptedException, restores the interrupt flag with Thread.currentThread().interrupt(), but then continues the loop rather than returning or throwing. This means an interrupted thread will busy-spin until the timeout expires instead of stopping promptly. Add a return or break after Thread.currentThread().interrupt().
… later usage in place.
…-java # Conflicts: # src/main/groovy/com/cloudogu/gitops/config/Config.groovy # src/main/groovy/com/cloudogu/gitops/tools/core/Jenkins.groovy
…est parallelization
Overview
This pull request completes the migration of the remaining core application components from Groovy to Java 17. The changes focus on reducing boilerplate code and improving
readability by implementing Lombok, Java 17 Records, and modern Java syntax features.
Key Changes
in a net reduction of 1,352 lines of code (~63% reduction). All custom validation logic and constructors initializing helm defaults were preserved.
ensuring sensitive passwords are never written to application logs.
validation.
parsing issues caused by the custom AllowListFreemarkerObjectWrapper requiring standard JavaBean getter methods (i.e. getRegistry()).
ADMIN ->).
ScmTenantSchema.java.