support CA certificates for incoming replication Replication from hosts using non-publicly-trusted certificates (e.g. internal GitHub Enterprise instances) failed with SSL verification errors. If the secret referenced by secretRef contains a ca.crt key, the operator now mounts it at /home/gerrit/ca.crt, same as the existing .netrc mount. The fetch-job script checks for this file at runtime and sets GIT_SSL_CAINFO. Change-Id: I8e81f45ad674fe0c9b539b5fd578809431d2b31b
diff --git a/container-images/fetch-job/README.md b/container-images/fetch-job/README.md index 9d217fc..55a4939 100644 --- a/container-images/fetch-job/README.md +++ b/container-images/fetch-job/README.md
@@ -35,5 +35,22 @@ - ^refs/heads/excluded-* ``` -You will need to mount the credentials used to authenticate with remote servers -to the container, e.g. by providing a .netrc file at `home/gerrit/.netrc`. +## Authentication + +Mount a `.netrc` file at `/home/gerrit/.netrc` to provide credentials for +authenticating with remote servers. + +## TLS/SSL + +If the remote server uses a certificate signed by a non-publicly-trusted CA +(e.g. an internal CA), provide the CA certificate in PEM format at +`/home/gerrit/ca.crt`. The fetch-job will automatically use it for all fetch +operations if present. + +When using the Gerrit operator, both the `.netrc` and `ca.crt` files can be +provided via the `secretRef` field of the `IncomingReplicationTask` resource. +If the referenced secret contains a `.netrc` key and/or a `ca.crt` key, the +operator will mount them at the expected paths automatically. + +Adding a custom `ca.crt` will use that file for SSL verification, ignoring +existing system CA bundles. \ No newline at end of file
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/tasks/incomingrepl/dependent/IncomingReplicationTaskCronJob.java b/operator/src/main/java/com/google/gerrit/k8s/operator/tasks/incomingrepl/dependent/IncomingReplicationTaskCronJob.java index f833818..18174a7 100644 --- a/operator/src/main/java/com/google/gerrit/k8s/operator/tasks/incomingrepl/dependent/IncomingReplicationTaskCronJob.java +++ b/operator/src/main/java/com/google/gerrit/k8s/operator/tasks/incomingrepl/dependent/IncomingReplicationTaskCronJob.java
@@ -26,6 +26,7 @@ import com.google.gerrit.k8s.operator.util.CRUDReconcileAddKubernetesDependentResource; import io.fabric8.kubernetes.api.model.Container; import io.fabric8.kubernetes.api.model.ContainerBuilder; +import io.fabric8.kubernetes.api.model.EnvVarBuilder; import io.fabric8.kubernetes.api.model.Secret; import io.fabric8.kubernetes.api.model.Volume; import io.fabric8.kubernetes.api.model.VolumeBuilder; @@ -43,6 +44,7 @@ public class IncomingReplicationTaskCronJob extends CRUDReconcileAddKubernetesDependentResource<CronJob, IncomingReplicationTask> { private static final String CONFIGMAP_VOLUME_NAME = "incoming-repl-config"; + private static final String CA_MOUNT_PATH = "/home/gerrit/ca.crt"; public IncomingReplicationTaskCronJob() { super(CronJob.class); @@ -127,23 +129,40 @@ private Container buildTaskContainer( IncomingReplicationTask incomingReplTask, Context<IncomingReplicationTask> context) { + return new ContainerBuilder() + .withName("incoming-replication") + .withSecurityContext(GerritSecurityContext.forContainer()) + .withImagePullPolicy(incomingReplTask.getSpec().getContainerImages().getImagePullPolicy()) + .withImage( + incomingReplTask + .getSpec() + .getContainerImages() + .getGerritImages() + .getFullImageName("fetch-job")) + .withResources(incomingReplTask.getSpec().getResources()) + .withVolumeMounts(getVolumeMounts(incomingReplTask, context)) + .withEnv(getEnvVars(incomingReplTask, context)) + .build(); + } - ContainerBuilder taskContainerBuilder = - new ContainerBuilder() - .withName("incoming-replication") - .withSecurityContext(GerritSecurityContext.forContainer()) - .withImagePullPolicy( - incomingReplTask.getSpec().getContainerImages().getImagePullPolicy()) - .withImage( - incomingReplTask - .getSpec() - .getContainerImages() - .getGerritImages() - .getFullImageName("fetch-job")) - .withResources(incomingReplTask.getSpec().getResources()) - .withVolumeMounts(getVolumeMounts(incomingReplTask, context)); - - return taskContainerBuilder.build(); + private List<io.fabric8.kubernetes.api.model.EnvVar> getEnvVars( + IncomingReplicationTask incomingReplTask, Context<IncomingReplicationTask> context) { + List<io.fabric8.kubernetes.api.model.EnvVar> envVars = new ArrayList<>(); + String secretRef = incomingReplTask.getSpec().getSecretRef(); + if (secretRef != null && !secretRef.isBlank()) { + Secret secret = + context + .getClient() + .resources(Secret.class) + .inNamespace(incomingReplTask.getMetadata().getNamespace()) + .withName(secretRef) + .get(); + if (secret != null && secret.getData().containsKey("ca.crt")) { + envVars.add( + new EnvVarBuilder().withName("GIT_SSL_CAINFO").withValue(CA_MOUNT_PATH).build()); + } + } + return envVars; } private List<VolumeMount> getVolumeMounts( @@ -178,13 +197,23 @@ .inNamespace(incomingReplTask.getMetadata().getNamespace()) .withName(secretRef) .get(); - if (secret != null && secret.getData().containsKey(".netrc")) { - volumeMounts.add( - new VolumeMountBuilder() - .withName(secretRef) - .withSubPath(".netrc") - .withMountPath("/home/gerrit/.netrc") - .build()); + if (secret != null) { + if (secret.getData().containsKey(".netrc")) { + volumeMounts.add( + new VolumeMountBuilder() + .withName(secretRef) + .withSubPath(".netrc") + .withMountPath("/home/gerrit/.netrc") + .build()); + } + if (secret.getData().containsKey("ca.crt")) { + volumeMounts.add( + new VolumeMountBuilder() + .withName(secretRef) + .withSubPath("ca.crt") + .withMountPath(CA_MOUNT_PATH) + .build()); + } } } return volumeMounts;
diff --git a/operator/src/test/java/com/google/gerrit/k8s/operator/tasks/incomingrepl/dependent/IncomingReplicationTaskTest.java b/operator/src/test/java/com/google/gerrit/k8s/operator/tasks/incomingrepl/dependent/IncomingReplicationTaskTest.java index 4c9cf7e..7024a67 100644 --- a/operator/src/test/java/com/google/gerrit/k8s/operator/tasks/incomingrepl/dependent/IncomingReplicationTaskTest.java +++ b/operator/src/test/java/com/google/gerrit/k8s/operator/tasks/incomingrepl/dependent/IncomingReplicationTaskTest.java
@@ -38,6 +38,7 @@ import java.util.stream.Stream; import org.junit.Rule; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.TestInstance.Lifecycle; import org.junit.jupiter.params.ParameterizedTest; @@ -107,6 +108,78 @@ .isEqualTo(ReconcilerUtils.loadYaml(ConfigMap.class, this.getClass(), expectedConfigMap)); } + @Test + public void caCertMountedWhenPresentInSecret() throws Exception { + IncomingReplicationTask incomingReplTask = + ReconcilerUtils.loadYaml( + IncomingReplicationTask.class, this.getClass(), "../incomingrepltask.yaml"); + + // Use a different secret name to avoid conflicting with the parameterized test mock + String testSecretName = "test-secret-with-ca-cert"; + incomingReplTask.getSpec().setSecretRef(testSecretName); + + Secret testSecret = + new SecretBuilder() + .withNewMetadata() + .withName(testSecretName) + .withNamespace(incomingReplTask.getMetadata().getNamespace()) + .endMetadata() + .withData( + Map.of( + ".netrc", "c2VjcmV0Cg==", + "ca.crt", "Y2VydGlmaWNhdGUK")) + .build(); + + kubernetesServer + .expect() + .get() + .withPath( + String.format( + "/api/v1/namespaces/%s/secrets/%s", + incomingReplTask.getMetadata().getNamespace(), testSecretName)) + .andReturn(HttpURLConnection.HTTP_OK, testSecret) + .always(); + + Context<IncomingReplicationTask> context = + getContext(new IncomingReplicationTaskReconciler(), incomingReplTask); + CronJob cronJob = new IncomingReplicationTaskCronJob().desired(incomingReplTask, context); + + boolean hasCaCertMount = + cronJob + .getSpec() + .getJobTemplate() + .getSpec() + .getTemplate() + .getSpec() + .getContainers() + .get(0) + .getVolumeMounts() + .stream() + .anyMatch( + vm -> + vm.getMountPath().equals("/home/gerrit/ca.crt") + && vm.getSubPath().equals("ca.crt")); + + boolean hasGitSslCaInfo = + cronJob + .getSpec() + .getJobTemplate() + .getSpec() + .getTemplate() + .getSpec() + .getContainers() + .get(0) + .getEnv() + .stream() + .anyMatch( + env -> + env.getName().equals("GIT_SSL_CAINFO") + && env.getValue().equals("/home/gerrit/ca.crt")); + + assertThat(hasGitSslCaInfo).isTrue(); + assertThat(hasCaCertMount).isTrue(); + } + private Context<IncomingReplicationTask> getContext( Reconciler<IncomingReplicationTask> reconciler, IncomingReplicationTask primary) { Controller<IncomingReplicationTask> controller =