Repository: provectus/kafka-ui
Issue: Air Gap: Enhancement: Opt out of version check
Labels: type/enhancement
scope/backend
scope/frontend
status/pending-frontend

Hello,

i'm using your great UI for kafka cluster instance and it's perfect : functionality, rapidity, installating in k8s world with helm.
But i try to install this UI in isolated network without internet access so github.com is not allow.

Is it possible to disable call from the ui to https://api.github.com/repos/provectus/kafka-ui/releases/latest and load the font inside the image ?
Like that the UI could be use in isolated network.



Issue comments:
Hello there LaurentDanti! 👋

Thank you and congratulations 🎉 for opening your very first issue in this project! 💖

In case you want to claim this issue, please comment down below! We will try to get back to you as soon as we can. 👀
@LaurentDanti thank you for the issue!

@Haarolean I think we already have issue created for fonts, maybe we add github call handling to that issue? 
hey @LaurentDanti, thanks for reaching out.
I know already at least a few cases in which people successfully use the app in the "air gap" environment (e.g. #1390).
We already have the fonts issue in progress (#2728), we'll keep this open for considering opting out of the version check.
@Haarolean  hello, I also have the fonts issue, UI doesn't work in isolated network, would you give  workaround ?
@Fuou we're working on it (#2372)
 @Haarolean what's the status here?
@cassanellicarlo it's up for grabs for the 3rd-party contributors since we don't have enough capacity to address this issue.
Hey 🙏 what is the status woth the opt out option?
Frontend implementation is invalid.

Tagged versions before:
<img width="361" alt="image" src="https://github.com/provectus/kafka-ui/assets/1494347/c44b0863-63dd-446f-a25a-a5d816843a60">
Tagged versions now are missing the tag and have a commit displayed anyway:
<img width="408" alt="image" src="https://github.com/provectus/kafka-ui/assets/1494347/ed5fb888-52fa-4f8a-aa18-9d0d768d1402">

if version == versionTag display version rather than commitId
<img width="766" alt="image" src="https://github.com/provectus/kafka-ui/assets/1494347/66a9a277-d452-46df-ac17-2696fa8bba87">

@David-DB88 



ok


Pull request review comments:
I suggest a rename as it's not kafka-related

Patch that resolved the issue:
diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/ApplicationConfigController.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/ApplicationConfigController.java
index b21ef10c619..571250ba947 100644
--- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/ApplicationConfigController.java
+++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/ApplicationConfigController.java
@@ -13,12 +13,12 @@
 import com.provectus.kafka.ui.model.RestartRequestDTO;
 import com.provectus.kafka.ui.model.UploadedFileInfoDTO;
 import com.provectus.kafka.ui.model.rbac.AccessContext;
+import com.provectus.kafka.ui.service.ApplicationInfoService;
 import com.provectus.kafka.ui.service.KafkaClusterFactory;
 import com.provectus.kafka.ui.service.rbac.AccessControlService;
 import com.provectus.kafka.ui.util.ApplicationRestarter;
 import com.provectus.kafka.ui.util.DynamicConfigOperations;
 import com.provectus.kafka.ui.util.DynamicConfigOperations.PropertiesStructure;
-import java.util.List;
 import java.util.Map;
 import javax.annotation.Nullable;
 import lombok.RequiredArgsConstructor;
@@ -53,18 +53,11 @@ interface PropertiesMapper {
   private final DynamicConfigOperations dynamicConfigOperations;
   private final ApplicationRestarter restarter;
   private final KafkaClusterFactory kafkaClusterFactory;
-
+  private final ApplicationInfoService applicationInfoService;
 
   @Override
   public Mono<ResponseEntity<ApplicationInfoDTO>> getApplicationInfo(ServerWebExchange exchange) {
-    return Mono.just(
-        new ApplicationInfoDTO()
-            .enabledFeatures(
-                dynamicConfigOperations.dynamicConfigEnabled()
-                    ? List.of(ApplicationInfoDTO.EnabledFeaturesEnum.DYNAMIC_CONFIG)
-                    : List.of()
-            )
-    ).map(ResponseEntity::ok);
+    return Mono.just(applicationInfoService.getApplicationInfo()).map(ResponseEntity::ok);
   }
 
   @Override
diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ApplicationInfoService.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ApplicationInfoService.java
new file mode 100644
index 00000000000..750a7179fb8
--- /dev/null
+++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ApplicationInfoService.java
@@ -0,0 +1,76 @@
+package com.provectus.kafka.ui.service;
+
+import static com.provectus.kafka.ui.model.ApplicationInfoDTO.EnabledFeaturesEnum;
+
+import com.provectus.kafka.ui.model.ApplicationInfoBuildDTO;
+import com.provectus.kafka.ui.model.ApplicationInfoDTO;
+import com.provectus.kafka.ui.model.ApplicationInfoLatestReleaseDTO;
+import com.provectus.kafka.ui.util.DynamicConfigOperations;
+import com.provectus.kafka.ui.util.GithubReleaseInfo;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.Properties;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.info.BuildProperties;
+import org.springframework.boot.info.GitProperties;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Service;
+
+@Service
+public class ApplicationInfoService {
+
+  private final GithubReleaseInfo githubReleaseInfo = new GithubReleaseInfo();
+
+  private final DynamicConfigOperations dynamicConfigOperations;
+  private final BuildProperties buildProperties;
+  private final GitProperties gitProperties;
+
+  public ApplicationInfoService(DynamicConfigOperations dynamicConfigOperations,
+                                @Autowired(required = false) BuildProperties buildProperties,
+                                @Autowired(required = false) GitProperties gitProperties) {
+    this.dynamicConfigOperations = dynamicConfigOperations;
+    this.buildProperties = Optional.ofNullable(buildProperties).orElse(new BuildProperties(new Properties()));
+    this.gitProperties = Optional.ofNullable(gitProperties).orElse(new GitProperties(new Properties()));
+  }
+
+  public ApplicationInfoDTO getApplicationInfo() {
+    var releaseInfo = githubReleaseInfo.get();
+    return new ApplicationInfoDTO()
+        .build(getBuildInfo(releaseInfo))
+        .enabledFeatures(getEnabledFeatures())
+        .latestRelease(convert(releaseInfo));
+  }
+
+  private ApplicationInfoLatestReleaseDTO convert(GithubReleaseInfo.GithubReleaseDto releaseInfo) {
+    return new ApplicationInfoLatestReleaseDTO()
+        .htmlUrl(releaseInfo.html_url())
+        .publishedAt(releaseInfo.published_at())
+        .versionTag(releaseInfo.tag_name());
+  }
+
+  private ApplicationInfoBuildDTO getBuildInfo(GithubReleaseInfo.GithubReleaseDto release) {
+    return new ApplicationInfoBuildDTO()
+        .isLatestRelease(release.tag_name() != null && release.tag_name().equals(buildProperties.getVersion()))
+        .commitId(gitProperties.getShortCommitId())
+        .version(buildProperties.getVersion())
+        .buildTime(buildProperties.getTime() != null
+            ? DateTimeFormatter.ISO_INSTANT.format(buildProperties.getTime()) : null);
+  }
+
+  private List<EnabledFeaturesEnum> getEnabledFeatures() {
+    var enabledFeatures = new ArrayList<EnabledFeaturesEnum>();
+    if (dynamicConfigOperations.dynamicConfigEnabled()) {
+      enabledFeatures.add(EnabledFeaturesEnum.DYNAMIC_CONFIG);
+    }
+    return enabledFeatures;
+  }
+
+  // updating on startup and every hour
+  @Scheduled(fixedRateString = "${github-release-info-update-rate:3600000}")
+  public void updateGithubReleaseInfo() {
+    githubReleaseInfo.refresh().block();
+  }
+
+}
diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/GithubReleaseInfo.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/GithubReleaseInfo.java
new file mode 100644
index 00000000000..2ad0c9c399b
--- /dev/null
+++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/GithubReleaseInfo.java
@@ -0,0 +1,53 @@
+package com.provectus.kafka.ui.util;
+
+import com.google.common.annotations.VisibleForTesting;
+import java.time.Duration;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.reactive.function.client.WebClient;
+import reactor.core.publisher.Mono;
+
+@Slf4j
+public class GithubReleaseInfo {
+
+  private static final String GITHUB_LATEST_RELEASE_RETRIEVAL_URL =
+      "https://api.github.com/repos/provectus/kafka-ui/releases/latest";
+
+  private static final Duration GITHUB_API_MAX_WAIT_TIME = Duration.ofSeconds(2);
+
+  public record GithubReleaseDto(String html_url, String tag_name, String published_at) {
+
+    static GithubReleaseDto empty() {
+      return new GithubReleaseDto(null, null, null);
+    }
+  }
+
+  private volatile GithubReleaseDto release = GithubReleaseDto.empty();
+
+  private final Mono<Void> refreshMono;
+
+  public GithubReleaseInfo() {
+    this(GITHUB_LATEST_RELEASE_RETRIEVAL_URL);
+  }
+
+  @VisibleForTesting
+  GithubReleaseInfo(String url) {
+    this.refreshMono = WebClient.create()
+        .get()
+        .uri(url)
+        .exchangeToMono(resp -> resp.bodyToMono(GithubReleaseDto.class))
+        .timeout(GITHUB_API_MAX_WAIT_TIME)
+        .doOnError(th -> log.trace("Error getting latest github release info", th))
+        .onErrorResume(th -> true, th -> Mono.just(GithubReleaseDto.empty()))
+        .doOnNext(release -> this.release = release)
+        .then();
+  }
+
+  public GithubReleaseDto get() {
+    return release;
+  }
+
+  public Mono<Void> refresh() {
+    return refreshMono;
+  }
+
+}
diff --git a/kafka-ui-contract/src/main/resources/swagger/kafka-ui-api.yaml b/kafka-ui-contract/src/main/resources/swagger/kafka-ui-api.yaml
index d3b9f331366..7b6fd3c1131 100644
--- a/kafka-ui-contract/src/main/resources/swagger/kafka-ui-api.yaml
+++ b/kafka-ui-contract/src/main/resources/swagger/kafka-ui-api.yaml
@@ -1917,6 +1917,26 @@ components:
             type: string
             enum:
               - DYNAMIC_CONFIG
+        build:
+          type: object
+          properties:
+            commitId:
+              type: string
+            version:
+              type: string
+            buildTime:
+              type: string
+            isLatestRelease:
+              type: boolean
+        latestRelease:
+          type: object
+          properties:
+            versionTag:
+              type: string
+            publishedAt:
+              type: string
+            htmlUrl:
+              type: string
 
     Cluster:
       type: object


Test patch:
diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/GithubReleaseInfoTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/GithubReleaseInfoTest.java
new file mode 100644
index 00000000000..6ec4bb78638
--- /dev/null
+++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/GithubReleaseInfoTest.java
@@ -0,0 +1,54 @@
+package com.provectus.kafka.ui.util;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.IOException;
+import java.time.Duration;
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import reactor.test.StepVerifier;
+
+class GithubReleaseInfoTest {
+
+  private final MockWebServer mockWebServer = new MockWebServer();
+
+  @BeforeEach
+  void startMockServer() throws IOException {
+    mockWebServer.start();
+  }
+
+  @AfterEach
+  void stopMockServer() throws IOException {
+    mockWebServer.close();
+  }
+
+  @Test
+  void test() {
+    mockWebServer.enqueue(new MockResponse()
+        .addHeader("content-type: application/json")
+        .setBody("""
+            {
+              "published_at": "2023-03-09T16:11:31Z",
+              "tag_name": "v0.6.0",
+              "html_url": "https://github.com/provectus/kafka-ui/releases/tag/v0.6.0",
+              "some_unused_prop": "ololo"
+            }
+            """));
+    var url = mockWebServer.url("repos/provectus/kafka-ui/releases/latest").toString();
+
+    var infoHolder = new GithubReleaseInfo(url);
+    infoHolder.refresh().block();
+
+    var i = infoHolder.get();
+    assertThat(i.html_url())
+        .isEqualTo("https://github.com/provectus/kafka-ui/releases/tag/v0.6.0");
+    assertThat(i.published_at())
+        .isEqualTo("2023-03-09T16:11:31Z");
+    assertThat(i.tag_name())
+        .isEqualTo("v0.6.0");
+  }
+
+}