yuqi1129 commented on code in PR #13724:
URL: https://github.com/apache/gravitino/pull/13724#discussion_r4245517429


##########
plugins/k8s-job-executor/src/main/java/org/apache/gravitino/job/k8s/K8sKubeconfigUtils.java:
##########
@@ -0,0 +1,107 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.gravitino.job.k8s;
+
+import com.google.common.base.Preconditions;
+import io.fabric8.kubernetes.api.model.Cluster;
+import io.fabric8.kubernetes.api.model.Config;
+import io.fabric8.kubernetes.api.model.Context;
+import io.fabric8.kubernetes.api.model.NamedCluster;
+import io.fabric8.kubernetes.api.model.NamedContext;
+import io.fabric8.kubernetes.client.internal.KubeConfigUtils;

Review Comment:
   [Nit] `io.fabric8.kubernetes.client.internal.KubeConfigUtils` is in 
fabric8's `internal` package, which has no compatibility promise. The comment 
in `libs.versions.toml` already expects a move to 7.3.x. A comment here saying 
it depends on an internal API, or a note on the version pin listing it as 
something to recheck on upgrade, would keep that from turning into a surprise 
compile error or a silent behavior change.
   
   If the public `Serialization.unmarshal(file, 
io.fabric8.kubernetes.api.model.Config.class)` covers what `parseConfig` does 
here, that avoids the dependency altogether. I have not checked whether 
`parseConfig` does more than unmarshal in 7.2.0.



##########
plugins/k8s-job-executor/src/main/java/org/apache/gravitino/job/k8s/K8sClientUtils.java:
##########
@@ -0,0 +1,212 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.gravitino.job.k8s;
+
+import com.google.common.base.Preconditions;
+import io.fabric8.kubernetes.client.Config;
+import io.fabric8.kubernetes.client.ConfigBuilder;
+import io.fabric8.kubernetes.client.KubernetesClient;
+import io.fabric8.kubernetes.client.KubernetesClientBuilder;
+import io.fabric8.kubernetes.client.OAuthTokenProvider;
+import java.io.File;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.attribute.FileTime;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Pattern;
+import javax.annotation.Nullable;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Creates the Kubernetes client of the job cluster from the job executor 
configurations. */
+public final class K8sClientUtils {
+
+  /**
+   * An {@link OAuthTokenProvider} that reads the token from a file, and reads 
it again whenever the
+   * file changes, so that a rotated token is picked up without restarting the 
server.
+   */
+  static final class FileTokenProvider implements OAuthTokenProvider {
+
+    // How often a token file that stays empty or unreadable is reported.
+    private static final long FAILURE_LOG_INTERVAL_NANOS = 
TimeUnit.MINUTES.toNanos(5);
+
+    private final Path tokenFile;
+
+    private FileTime lastModified;
+
+    private String token;
+
+    private boolean failing;
+
+    private long failureLoggedAtNanos;
+
+    FileTokenProvider(Path tokenFile) {
+      this.tokenFile = tokenFile;
+    }
+
+    @Override
+    public synchronized String getToken() {
+      try {
+        FileTime modified = Files.getLastModifiedTime(tokenFile);
+        if (token == null || !modified.equals(lastModified)) {
+          String content = new String(Files.readAllBytes(tokenFile), 
StandardCharsets.UTF_8).trim();
+          if (StringUtils.isBlank(content)) {
+            if (token == null) {
+              throw new IllegalStateException("The token file " + tokenFile + 
" is empty");
+            }
+            // The file is being rewritten. Its modification time isn't 
remembered: the new token
+            // may be written within the same tick of the file system's clock, 
and would then
+            // never be read.
+            return keepToken("is empty", null);
+          }
+          token = content;
+          lastModified = modified;
+        }
+      } catch (IOException e) {
+        if (token == null) {
+          throw new UncheckedIOException("Failed to read the token file " + 
tokenFile, e);
+        }
+        // For example, the file is being replaced.
+        return keepToken("can't be read", e);
+      }
+      failing = false;
+      return token;
+    }
+
+    /**
+     * Keeps the token read before while the file has no token, checking the 
file again on the next
+     * request. The problem is reported when it starts, and then at an 
interval rather than on every
+     * request, so that a file that stays broken remains visible without 
flooding the log.
+     */
+    private String keepToken(String problem, @Nullable IOException cause) {
+      long now = System.nanoTime();
+      if (!failing || now - failureLoggedAtNanos >= 
FAILURE_LOG_INTERVAL_NANOS) {
+        LOG.warn(
+            "The token file {} {}, keeping the token read before. Requests 
will fail once that"
+                + " token expires",
+            tokenFile,
+            problem,
+            cause);
+        failing = true;
+        failureLoggedAtNanos = now;
+      }
+      return token;
+    }
+  }
+
+  private static final Logger LOG = 
LoggerFactory.getLogger(K8sClientUtils.class);
+
+  private static final Pattern SERVER_SCHEME =
+      Pattern.compile("^https?://", Pattern.CASE_INSENSITIVE);
+
+  private K8sClientUtils() {}
+
+  /**
+   * Creates the Kubernetes client of the job cluster.
+   *
+   * @param configs the job executor configurations
+   * @return the Kubernetes client
+   */
+  public static KubernetesClient createClient(K8sJobExecutorConfigs configs) {
+    return new 
KubernetesClientBuilder().withConfig(createClientConfig(configs)).build();
+  }
+
+  /**
+   * Creates the Kubernetes client configuration of the job cluster. It is, by 
precedence:
+   *
+   * <ul>
+   *   <li>the API server URL, with its CA certificate and token file, if the 
master URL is set;
+   *   <li>the given kubeconfig file and context, if the kubeconfig is set;
+   *   <li>fabric8's default lookup otherwise, that is the {@code KUBECONFIG} 
environment variable
+   *       or {@code ~/.kube/config} with the given context, then the 
in-cluster service account.
+   * </ul>
+   *
+   * <p>In every case the default namespace of the client is the namespace of 
the job executor
+   * configurations, rather than the one of the kubeconfig context or of the 
pod Gravitino runs in.
+   *
+   * @param configs the job executor configurations
+   * @return the Kubernetes client configuration
+   * @throws IllegalArgumentException if a configured file doesn't exist, or 
the kubeconfig has no
+   *     usable context
+   */
+  static Config createClientConfig(K8sJobExecutorConfigs configs) {
+    if (configs.masterUrl() != null) {
+      ConfigBuilder builder = new 
ConfigBuilder(Config.empty()).withMasterUrl(configs.masterUrl());
+      if (configs.caCertFile() != null) {
+        checkFileExists(configs.caCertFile(), 
K8sJobExecutorConfigs.CA_CERT_FILE);
+        builder.withCaCertFile(configs.caCertFile());
+      }
+      if (configs.tokenFile() != null) {
+        checkFileExists(configs.tokenFile(), K8sJobExecutorConfigs.TOKEN_FILE);
+        builder.withOauthTokenProvider(new 
FileTokenProvider(Paths.get(configs.tokenFile())));
+      }
+      return builder.withNamespace(configs.namespace()).build();
+    }
+
+    Config config;
+    if (configs.kubeconfig() != null) {
+      checkFileExists(configs.kubeconfig(), K8sJobExecutorConfigs.KUBECONFIG);
+      File kubeconfig = new File(configs.kubeconfig());
+      String server = K8sKubeconfigUtils.getSelectedServer(kubeconfig, 
configs.context());
+      config = Config.fromKubeconfig(configs.context(), kubeconfig);
+      // The client reads the file itself, so make sure it ended up with the 
server found above,
+      // whatever it does with a kubeconfig that is malformed or changed in 
between.
+      Preconditions.checkArgument(
+          getHostAndPath(server).equals(getHostAndPath(config.getMasterUrl())),

Review Comment:
   [Nit] No test pins this server check: with the condition replaced by one 
that always holds, all 11 `TestK8sClientUtils` cases still pass.
   
   I also could not find an input that reaches it. `K8sKubeconfigUtils` already 
rejects every shape in `testKubeconfigWithoutUsableCluster`, and a context with 
no `user`, or with a `user` that is not defined, gives the same server from 
both parsers. So the check only guards against fabric8's parser diverging from 
ours, or the file changing between the two reads.
   
   Either is fine with me:
   - say in the comment above that this is a defensive check with no known 
triggering input, or
   - add a test that makes the two disagree, for example by statically mocking 
`Config.fromKubeconfig` to return a different master.
   
   Verified by: a mutation run of `TestK8sClientUtils` on 67022ada24, plus a 
probe against the shaded jar with the two kubeconfig shapes above.



##########
plugins/k8s-job-executor/src/main/java/org/apache/gravitino/job/k8s/K8sClientUtils.java:
##########
@@ -0,0 +1,212 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.gravitino.job.k8s;
+
+import com.google.common.base.Preconditions;
+import io.fabric8.kubernetes.client.Config;
+import io.fabric8.kubernetes.client.ConfigBuilder;
+import io.fabric8.kubernetes.client.KubernetesClient;
+import io.fabric8.kubernetes.client.KubernetesClientBuilder;
+import io.fabric8.kubernetes.client.OAuthTokenProvider;
+import java.io.File;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.attribute.FileTime;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Pattern;
+import javax.annotation.Nullable;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Creates the Kubernetes client of the job cluster from the job executor 
configurations. */
+public final class K8sClientUtils {
+
+  /**
+   * An {@link OAuthTokenProvider} that reads the token from a file, and reads 
it again whenever the
+   * file changes, so that a rotated token is picked up without restarting the 
server.
+   */
+  static final class FileTokenProvider implements OAuthTokenProvider {

Review Comment:
   [Nit] The nested `FileTokenProvider` comes before the `LOG` and 
`SERVER_SCHEME` constants at lines 118-121. The project's member order puts 
static constants first, so moving the nested class below them, or to the end of 
the class, would match it.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to