INFRA-2701 | Dhruv | use github client sdk

This commit is contained in:
dhruvjoshi
2024-05-30 09:39:01 +05:30
parent 24e82c8490
commit e2cc24a208
8 changed files with 86 additions and 185 deletions

10
pom.xml
View File

@@ -138,6 +138,16 @@
<artifactId>httpclient</artifactId>
<version>4.5.13</version>
</dependency>
<dependency>
<artifactId>kotlin-stdlib</artifactId>
<groupId>org.jetbrains.kotlin</groupId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>com.spotify</groupId>
<artifactId>github-client</artifactId>
<version>0.2.17</version>
</dependency>
<dependency>
<artifactId>httpmime</artifactId>
<groupId>org.apache.httpcomponents</groupId>

View File

@@ -0,0 +1,16 @@
package com.navi.infra.portal.configuration;
import com.spotify.github.v3.clients.GitHubClient;
import java.net.URI;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class GitHubClientConfig {
@Bean
public GitHubClient githubClient(@Value("${github.token}") String authToken) {
return GitHubClient.create(URI.create("https://api.github.com/"), authToken);
}
}

View File

@@ -18,11 +18,13 @@ import com.navi.infra.portal.util.KubernetesManifestGenerator;
import com.navi.infra.portal.util.MapDiffUtil;
import com.navi.infra.portal.util.gocd.GocdConfigValidatorUtil;
import com.navi.infra.portal.util.gocd.PipelineValidatorUtil;
import com.navi.infra.portal.v2.client.github.GitHubApiRequest;
import com.navi.infra.portal.v2.client.github.GitHubApiResponse;
import com.navi.infra.portal.v2.client.github.GitHubClient;
import com.navi.infra.portal.v2.exception.GocdValidationException;
import com.navi.infra.portal.v2.exception.PipelineDifferenceException;
import com.spotify.github.v3.clients.GitHubClient;
import com.spotify.github.v3.clients.RepositoryClient;
import com.spotify.github.v3.repos.Content;
import com.spotify.github.v3.repos.requests.FileCreate;
import com.spotify.github.v3.repos.requests.ImmutableFileCreate;
import com.sun.jdi.request.InvalidRequestStateException;
import java.io.ByteArrayOutputStream;
import java.io.File;
@@ -34,7 +36,6 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.Optional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
@@ -49,9 +50,11 @@ public class PipelineManifestService {
private final String PIPELINE_PATH = "pipelines";
private final String PIPELINE_MANIFEST_FILE_NAME = "pipeline_manifest.json";
private final String PIPELINE_FILE_NAME = "pipelines.gocd.yaml";
private final String orgName;
private final String repository = "test-github-action-deployment-portal-backend";
private final ObjectMapper objectMapper;
private final ObjectMapper yamlMapper;
private final RepositoryClient repositoryClient;
private final PipelineManifestRepository pipelineManifestRepository;
private final KubernetesManifestGenerator kubernetesManifestGenerator;
@@ -60,16 +63,15 @@ public class PipelineManifestService {
private final GocdConfigValidatorUtil gocdConfigValidatorUtil;
private final PipelineValidatorUtil pipelineValidatorUtil;
private final MapDiffUtil mapDiffUtil;
private final GitHubClient gitHubClient;
PipelineManifestService(
ObjectMapper objectMapper,
@Qualifier("yamlMapper") ObjectMapper yamlMapper,
GitHubClient githubClient,
PipelineManifestRepository pipelineManifestRepository,
KubernetesManifestGenerator kubernetesManifestGenerator,
ManifestService manifestService,
AuthorizationService authorizationFilter,
GitHubClient gitHubClient,
PipelineValidatorUtil pipelineValidatorUtil,
GocdConfigValidatorUtil gocdConfigValidatorUtil,
@Value("${github.org.name}") String orgName,
@@ -82,11 +84,10 @@ public class PipelineManifestService {
this.kubernetesManifestGenerator = kubernetesManifestGenerator;
this.manifestService = manifestService;
this.authorizationFilter = authorizationFilter;
this.gitHubClient = gitHubClient;
this.pipelineValidatorUtil = pipelineValidatorUtil;
this.gocdConfigValidatorUtil = gocdConfigValidatorUtil;
this.orgName = orgName;
this.mapDiffUtil = mapDiffUtil;
this.repositoryClient = githubClient.createRepositoryClient(orgName, repository);
}
@@ -147,7 +148,7 @@ public class PipelineManifestService {
private void validatePipelineDifference(
PipelineManifest pipelineManifestRequest,
GitHubApiResponse fetchedPipeline
Content fetchedPipeline
) {
PipelineManifest existingPipelineManifest = getPipelineManifest(
pipelineManifestRequest.getName(), Gocd.MERGED.name());
@@ -155,30 +156,30 @@ public class PipelineManifestService {
return;
}
if ((existingPipelineManifest == null ^ fetchedPipeline == null)
|| manualChangesInPipeline(existingPipelineManifest, fetchedPipeline.getContent())) {
|| manualChangesInPipeline(existingPipelineManifest, fetchedPipeline.content())) {
throw new PipelineDifferenceException(String.format(
"Manual changes detected in pipeline %s. Please update the pipeline through github.",
pipelineManifestRequest.getName()));
}
}
private String getPipeLineCreationMessage(GitHubApiResponse response) {
private String getPipeLineCreationMessage(Content response) {
return String.format("Pipeline %s by %s at %s", response == null ? "created" : "updated",
getUserEmail(), LocalDateTime.now());
}
private void createOrUpdatePipelineInGitHub(
String filePath, GitHubApiResponse content, Map<String, Object> pipelineMap
String filePath, Content content, Map<String, Object> pipelineMap
) {
try {
String encodedContent = base64Encode(yamlMapper.writeValueAsString(pipelineMap));
String message = getPipeLineCreationMessage(content);
String sha = Optional.ofNullable(content)
.map(GitHubApiResponse::getSha)
.orElse(null);
GitHubApiRequest githubRequest = new GitHubApiRequest(sha, message,
encodedContent, "master");
gitHubClient.createOrUpdateFileContent(orgName, filePath, githubRequest);
FileCreate fileCreate = ImmutableFileCreate.builder()
.message(message)
.content(encodedContent)
.branch("master")
.build();
repositoryClient.createFileContent(filePath, fileCreate);
} catch (Exception e) {
throw new RuntimeException(e.getMessage());
}
@@ -203,7 +204,12 @@ public class PipelineManifestService {
private Map<String, Object> generateGocdPipeline(PipelineManifest pipelineManifest) {
String filePath = String.format("%s.gocd.yaml", pipelineManifest.getName());
GitHubApiResponse fetchedPipeline = gitHubClient.getFileContent(orgName, filePath);
Content fetchedPipeline;
try {
fetchedPipeline = repositoryClient.getFileContent(filePath).get();
} catch (Exception e) {
fetchedPipeline = null;
}
var createdPipelineManifestRequest = pipelineManifestRepository.save(pipelineManifest);
validatePipelineDifference(pipelineManifest, fetchedPipeline);
Map<String, Object> pipelineMap = generatePipelineTemplate(pipelineManifest);

View File

@@ -31,6 +31,7 @@ public class GocdConfigValidatorUtil {
}
public boolean validate(String filePath) throws Exception {
validateFilePath(filePath);
Path path = Paths.get(filePath);
if (!Files.exists(path)) {
throw new FileNotFoundException("File not found: " + filePath);
@@ -42,4 +43,11 @@ public class GocdConfigValidatorUtil {
}
return true;
}
private void validateFilePath(String filePath) {
Path path = Paths.get(filePath).normalize();
if (path.isAbsolute() || path.toString().contains("..")) {
throw new IllegalArgumentException("Invalid file path: " + filePath);
}
}
}

View File

@@ -1,21 +0,0 @@
package com.navi.infra.portal.v2.client.github;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class GitHubApiRequest {
private String sha;
private String message;
private String content;
private String branch;
}

View File

@@ -1,26 +0,0 @@
package com.navi.infra.portal.v2.client.github;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import java.io.Serializable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class GitHubApiResponse implements Serializable {
private static final long serialVersionUID = 8769281411503539181L;
private String name;
private String path;
private String sha;
private String content;
private String encoding;
}

View File

@@ -1,99 +0,0 @@
package com.navi.infra.portal.v2.client.github;
import static java.lang.String.format;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class GitHubClient {
private final String authToken;
private final ObjectMapper objectMapper;
private final HttpClient httpClient;
// TODO change to gocd-pipelines
private final String repository = "test-github-action-deployment-portal-backend";
public GitHubClient(
@Value("${github.token}") String authToken,
@Qualifier("jsonMapper") ObjectMapper objectMapper
) {
this.authToken = authToken;
this.objectMapper = objectMapper;
this.httpClient = HttpClient.newHttpClient();
}
private HttpRequest.Builder createRequestBuilder(String url) {
return HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Authorization", "token " + this.authToken)
.header("Accept", "application/vnd.github+json")
.header("X-GitHub-Api-Version", "2022-11-28")
.header("Content-Type", "application/json");
}
String filePathUrl(String org, String repository, String filePath) {
return format("https://api.github.com/repos/%s/%s/contents/%s", org, repository, filePath);
}
public GitHubApiResponse getFileContent(String org, String filePath) {
String url = filePathUrl(org, repository, filePath);
try {
HttpRequest request = createRequestBuilder(url).GET().build();
HttpResponse<String> response = httpClient.send(request,
HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == HttpStatus.OK.value()) {
log.info("Successfully retrieved file content from GitHub for {}", filePath);
return objectMapper.readValue(response.body(), GitHubApiResponse.class);
}
if (response.statusCode() == HttpStatus.NOT_FOUND.value()) {
log.error("File not found in GitHub : {}", response.body());
return null;
}
throw new RuntimeException("Failed to access content from GitHub.");
} catch (Exception e) {
log.error("Error occurred while getting file from filePath {} : {}",
filePath, e.getMessage());
throw new RuntimeException("Failed to get file from GitHub", e);
}
}
public void createOrUpdateFileContent(
String org,
String filePath,
GitHubApiRequest githubRequest
) {
String url = filePathUrl(org, repository, filePath);
try {
String payload = objectMapper.writeValueAsString(githubRequest);
HttpRequest request = createRequestBuilder(url).PUT(
HttpRequest.BodyPublishers.ofString(payload)).build();
HttpResponse<String> response = httpClient.send(request,
HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == HttpStatus.OK.value()) {
log.info("Successfully updated file in GitHub.");
return;
}
if (response.statusCode() == HttpStatus.CREATED.value()) {
log.info("Successfully created file in GitHub.");
return;
}
throw new RuntimeException(response.body());
} catch (Exception e) {
log.error("Error occurred while creating/updating file in {} org with path {} : {}",
org, filePath, e.getMessage());
throw new RuntimeException("Failed to commit changes to GitHub", e);
}
}
}

View File

@@ -6,7 +6,7 @@ import static com.navi.infra.portal.v2.privilege.ResourceType.MANIFEST;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doNothing;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;
import com.fasterxml.jackson.core.JsonProcessingException;
@@ -24,15 +24,17 @@ import com.navi.infra.portal.util.KutegenClient;
import com.navi.infra.portal.util.MapDiffUtil;
import com.navi.infra.portal.util.gocd.GocdConfigValidatorUtil;
import com.navi.infra.portal.util.gocd.PipelineValidatorUtil;
import com.navi.infra.portal.v2.client.github.GitHubApiResponse;
import com.navi.infra.portal.v2.client.github.GitHubClient;
import com.navi.infra.portal.v2.client.gocd.GocdClient;
import com.navi.infra.portal.v2.exception.PipelineDifferenceException;
import com.spotify.github.v3.clients.GitHubClient;
import com.spotify.github.v3.clients.RepositoryClient;
import com.spotify.github.v3.repos.ImmutableContent;
import com.sun.jdi.request.InvalidRequestStateException;
import io.kubernetes.client.openapi.ApiException;
import java.io.IOException;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import org.apache.commons.io.FileUtils;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
@@ -64,9 +66,12 @@ public class PipelineManifestServiceTest extends ExternalIntegrationProvider {
PipelineManifest oldPipelineManifestWithId;
PipelineManifest newPipelineManifestWithId;
Manifest manifest;
@Mock
GitHubClient gitHubClient;
@Mock
RepositoryClient repositoryClient;
@Mock
GocdClient gocdClient;
private PipelineManifestService pipelineManifestService;
@Mock
@@ -92,14 +97,16 @@ public class PipelineManifestServiceTest extends ExternalIntegrationProvider {
@BeforeEach
public void init() throws IOException {
when(gitHubClient.createRepositoryClient(anyString(), anyString())).thenReturn(
repositoryClient);
pipelineManifestService = new PipelineManifestService(
objectMapper,
yamlMapper,
gitHubClient,
pipelineManifestRepository,
kutegenClient,
manifestService,
authorizationService,
gitHubClient,
pipelineValidatorUtil,
gocdConfigValidatorUtil,
"navi-infra",
@@ -190,16 +197,15 @@ public class PipelineManifestServiceTest extends ExternalIntegrationProvider {
Manifest manifest = new Manifest();
// base64 encoded string which isn't a valid yaml
var gitHubApiResponse = new GitHubApiResponse(null, null,
"4827ca65e48152eb304c4fe39d7baea3e13856c2",
"some-invalid-content-from-github-that-isn't-base-64", "base64");
var content = ImmutableContent.builder().content("some-invalid-content-from-github")
.sha("4827ca65e48152eb304c4fe39d7baea3e13856c2").encoding("base64").build();
when(manifestService.fetchById(manifestId)).thenReturn(manifest);
when(authorizationService.hasPermissions(MANIFEST, manifest, MANIFEST_WRITE)).thenReturn(
true);
when(pipelineValidatorUtil.isValidPipeline(any())).thenReturn(true);
when(gitHubClient.getFileContent(any(), any())).thenReturn(gitHubApiResponse);
when(repositoryClient.getFileContent(any())).thenReturn(
CompletableFuture.completedFuture(content));
when(pipelineManifestRepository.save(newPipelineManifest)).thenReturn(
newPipelineManifestWithId);
when(pipelineManifestRepository.findLatestByNameAndStatus(any(), any()))
@@ -215,16 +221,15 @@ public class PipelineManifestServiceTest extends ExternalIntegrationProvider {
Manifest manifest = new Manifest();
// base64 encoded string which isn't same as the pipeline manifest yaml
var gitHubApiResponse = new GitHubApiResponse(null, null,
"4827ca65e48152eb304c4fe39d7baea3e13856c2",
inconsistentBase64EncodedYaml, "base64");
var content = ImmutableContent.builder().content(inconsistentBase64EncodedYaml)
.sha("4827ca65e48152eb304c4fe39d7baea3e13856c2").encoding("base64").build();
when(manifestService.fetchById(manifestId)).thenReturn(manifest);
when(authorizationService.hasPermissions(MANIFEST, manifest, MANIFEST_WRITE)).thenReturn(
true);
when(pipelineValidatorUtil.isValidPipeline(any())).thenReturn(true);
when(gitHubClient.getFileContent(any(), any())).thenReturn(gitHubApiResponse);
when(repositoryClient.getFileContent(any())).thenReturn(
CompletableFuture.completedFuture(content));
when(pipelineManifestRepository.save(newPipelineManifest)).thenReturn(
newPipelineManifestWithId);
when(pipelineManifestRepository.findLatestByNameAndStatus(any(), any()))
@@ -242,16 +247,17 @@ public class PipelineManifestServiceTest extends ExternalIntegrationProvider {
Manifest manifest = new Manifest();
// base64 encoded string which is same as the old pipeline manifest yaml
var gitHubApiResponse = new GitHubApiResponse(null, null,
"4827ca65e48152eb304c4fe39d7baea3e13856c2",
oldBase64EncodedPipelineYaml, "base64");
var content = ImmutableContent.builder().content(oldBase64EncodedPipelineYaml)
.sha("4827ca65e48152eb304c4fe39d7baea3e13856c2").encoding("base64").build();
when(manifestService.fetchById(manifestId)).thenReturn(manifest);
when(authorizationService.hasPermissions(MANIFEST, manifest, MANIFEST_WRITE)).thenReturn(
true);
when(pipelineValidatorUtil.isValidPipeline(any())).thenReturn(true);
when(gitHubClient.getFileContent(any(), any())).thenReturn(gitHubApiResponse);
when(repositoryClient.getFileContent(any())).thenReturn(
CompletableFuture.completedFuture(content));
when(pipelineManifestRepository.findLatestByNameAndStatus(any(), any()))
.thenReturn(Optional.of(oldPipelineManifestWithId));
try {
@@ -259,7 +265,8 @@ public class PipelineManifestServiceTest extends ExternalIntegrationProvider {
} catch (Exception e) {
e.printStackTrace();
}
doNothing().when(gitHubClient).createOrUpdateFileContent(any(), any(), any());
when(repositoryClient.createFileContent(any(), any())).thenReturn(
CompletableFuture.completedFuture(any()));
newPipelineManifest.setStatus(Gocd.MERGED.name());
when(pipelineManifestRepository.save(newPipelineManifest)).thenReturn(
newPipelineManifestWithId);