Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(kustomize): support git/repo artifact in kustomize bake manifest #449

Merged
merged 12 commits into from
Oct 29, 2019
Merged
1 change: 1 addition & 0 deletions rosco-manifests/rosco-manifests.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ dependencies {
implementation "com.netflix.spinnaker.kork:kork-exceptions"
implementation "com.netflix.spinnaker.kork:kork-security"
implementation "commons-io:commons-io"
implementation "org.apache.commons:commons-compress:1.14"
implementation "org.yaml:snakeyaml:1.25"

implementation "com.squareup.retrofit:retrofit"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@

import com.netflix.spinnaker.kork.artifacts.model.Artifact;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;

public interface ArtifactDownloader {
void downloadArtifact(Artifact artifact, Path targetFile) throws IOException;
InputStream downloadArtifact(Artifact artifact) throws IOException;

void downloadArtifactToFile(Artifact artifact, Path targetFile) throws IOException;
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,24 @@ public ArtifactDownloaderImpl(ClouddriverService clouddriverService) {
this.clouddriverService = clouddriverService;
}

public void downloadArtifact(Artifact artifact, Path targetFile) throws IOException {
public InputStream downloadArtifact(Artifact artifact) throws IOException {
Response response =
retrySupport.retry(() -> clouddriverService.fetchArtifact(artifact), 5, 1000, true);
if (response.getBody() == null) {
throw new IOException("Failure to fetch artifact: empty response");
}
return response.getBody().in();
}

public void downloadArtifactToFile(Artifact artifact, Path targetFile) throws IOException {
KathrynLewis marked this conversation as resolved.
Show resolved Hide resolved
try (OutputStream outputStream = Files.newOutputStream(targetFile)) {
Response response =
retrySupport.retry(() -> clouddriverService.fetchArtifact(artifact), 5, 1000, true);
if (response.getBody() != null) {
try (InputStream inputStream = response.getBody().in()) {
IOUtils.copy(inputStream, outputStream);
} catch (IOException e) {
throw new IOException(
String.format(
"Failed to read input stream of downloaded artifact: %s. Error: %s",
artifact, e.getMessage()));
}
try (InputStream inputStream = downloadArtifact(artifact)) {
IOUtils.copy(inputStream, outputStream);
} catch (IOException e) {
throw new IOException(
String.format(
"Failed to read input stream of downloaded artifact: %s. Error: %s",
artifact, e.getMessage()));
}
} catch (RetrofitError e) {
throw new IOException(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ private Path downloadArtifactToTmpFile(BakeManifestEnvironment env, Artifact art
throws IOException {
String fileName = UUID.randomUUID().toString();
Path targetPath = env.resolvePath(fileName);
artifactDownloader.downloadArtifact(artifact, targetPath);
artifactDownloader.downloadArtifactToFile(artifact, targetPath);
return targetPath;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,5 @@
@EqualsAndHashCode(callSuper = true)
public class KustomizeBakeManifestRequest extends BakeManifestRequest {
private Artifact inputArtifact;
private String kustomizeFilePath;
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,24 @@
import com.netflix.spinnaker.rosco.manifests.ArtifactDownloader;
import com.netflix.spinnaker.rosco.manifests.BakeManifestEnvironment;
import com.netflix.spinnaker.rosco.manifests.kustomize.mapping.Kustomization;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
import org.apache.commons.io.FilenameUtils;
import org.springframework.stereotype.Component;

Expand All @@ -50,13 +56,27 @@ public KustomizeTemplateUtils(
}

public BakeRecipe buildBakeRecipe(
BakeManifestEnvironment env, KustomizeBakeManifestRequest request) {
BakeManifestEnvironment env, KustomizeBakeManifestRequest request) throws IOException {
BakeRecipe result = new BakeRecipe();
result.setName(request.getOutputName());
Artifact artifact = request.getInputArtifact();
if (artifact == null) {
throw new IllegalArgumentException("Exactly one input artifact must be provided to bake.");
}

String artifactType = Optional.of(artifact.getType()).orElse("");
if ("git/repo".equals(artifactType)) {
return buildBakeRecipeFromGitRepo(env, request, artifact);
} else {
return oldBuildBakeRecipe(env, request, artifact);
}
}

// Keep the old logic for now. This will be removed as soon as the rest of the git/repo artifact
// PRs are merged
private BakeRecipe oldBuildBakeRecipe(
BakeManifestEnvironment env, KustomizeBakeManifestRequest request, Artifact artifact) {

String kustomizationfilename = FilenameUtils.getName(artifact.getReference());
if (kustomizationfilename == null
|| (kustomizationfilename != null
Expand All @@ -79,11 +99,78 @@ public BakeRecipe buildBakeRecipe(
command.add("kustomize");
command.add("build");
command.add(templatePath.getParent().toString());

BakeRecipe result = new BakeRecipe();
result.setCommand(command);
return result;
}

private BakeRecipe buildBakeRecipeFromGitRepo(
BakeManifestEnvironment env, KustomizeBakeManifestRequest request, Artifact artifact)
throws IOException {
// This is a redundant check for now, but it's here for when we soon remove the old logic of
// building from a github/file artifact type and instead, only support the git/repo artifact
// type
if (!"git/repo".equals(artifact.getType())) {
throw new IllegalArgumentException("The inputArtifact should be of type \"git/repo\".");
}

String kustomizeFilePath = request.getKustomizeFilePath();
if (kustomizeFilePath == null) {
throw new IllegalArgumentException("The bake request should contain a kustomize file path.");
}

InputStream inputStream;
try {
inputStream = artifactDownloader.downloadArtifact(artifact);
} catch (IOException e) {
throw new IOException("Failed to download git/repo artifact: " + e.getMessage(), e);
}

try {
extractArtifact(inputStream, env.resolvePath(""));
} catch (IOException e) {
throw new IOException("Failed to extract git/repo artifact: " + e.getMessage(), e);
}

List<String> command = new ArrayList<>();
command.add("kustomize");
command.add("build");
command.add(env.resolvePath(kustomizeFilePath).getParent().toString());

BakeRecipe result = new BakeRecipe();
result.setCommand(command);
return result;
}

// This being here is temporary until we find a better way to abstract it
private static void extractArtifact(InputStream inputStream, Path outputPath) throws IOException {
try (TarArchiveInputStream tarArchiveInputStream =
new TarArchiveInputStream(
new GzipCompressorInputStream(new BufferedInputStream(inputStream)))) {

ArchiveEntry archiveEntry;
while ((archiveEntry = tarArchiveInputStream.getNextEntry()) != null) {
Path archiveEntryOutput = validateArchiveEntry(archiveEntry.getName(), outputPath);
if (archiveEntry.isDirectory()) {
if (!Files.exists(archiveEntryOutput)) {
Files.createDirectory(archiveEntryOutput);
}
} else {
Files.copy(tarArchiveInputStream, archiveEntryOutput);
}
}
}
}

private static Path validateArchiveEntry(String archiveEntryName, Path outputPath) {
Path entryPath = outputPath.resolve(archiveEntryName);
if (!entryPath.normalize().startsWith(outputPath)) {
throw new IllegalStateException("Attempting to create a file outside of the staging path.");
}
return entryPath;
}

protected void downloadArtifactToTmpFileStructure(
BakeManifestEnvironment env, Artifact artifact, String referenceBaseURL) throws IOException {
if (artifact.getReference() == null) {
Expand All @@ -93,7 +180,7 @@ protected void downloadArtifactToTmpFileStructure(
Path artifactFilePath = env.resolvePath(artifactFileName);
Path artifactParentDirectory = artifactFilePath.getParent();
Files.createDirectories(artifactParentDirectory);
artifactDownloader.downloadArtifact(artifact, artifactFilePath);
artifactDownloader.downloadArtifactToFile(artifact, artifactFilePath);
}

private List<Artifact> getArtifacts(Artifact artifact) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public void downloadsArtifactContent() throws IOException {
try (ArtifactDownloaderImplTest.AutoDeletingFile file = new AutoDeletingFile()) {
when(clouddriverService.fetchArtifact(testArtifact))
.thenReturn(successfulResponse(testContent));
artifactDownloader.downloadArtifact(testArtifact, file.path);
artifactDownloader.downloadArtifactToFile(testArtifact, file.path);

Assertions.assertThat(file.path).hasContent(testContent);
}
Expand All @@ -63,7 +63,7 @@ public void retries() throws IOException {
when(clouddriverService.fetchArtifact(testArtifact))
.thenThrow(RetrofitError.networkError("", new IOException("timeout")))
.thenReturn(successfulResponse(testContent));
artifactDownloader.downloadArtifact(testArtifact, file.path);
artifactDownloader.downloadArtifactToFile(testArtifact, file.path);

Assertions.assertThat(file.path).hasContent(testContent);
}
Expand Down