From 42e178d1a8174cdee0c6822c656cae438c7c7327 Mon Sep 17 00:00:00 2001 From: Marc Nuri Date: Sun, 21 Apr 2024 14:31:08 +0200 Subject: [PATCH] feat: support for helm search repo command (Go side) --- README.md | 32 ++++- .../src/main/java/com/marcnuri/helm/Helm.java | 10 ++ .../java/com/marcnuri/helm/SearchCommand.java | 111 ++++++++++++++++++ .../java/com/marcnuri/helm/SearchResult.java | 70 +++++++++++ .../com/marcnuri/helm/HelmSearchTest.java | 77 ++++++++++++ .../java/com/marcnuri/helm/jni/HelmLib.java | 2 + .../com/marcnuri/helm/jni/SearchOptions.java | 21 ++++ native/internal/helm/search.go | 6 +- native/main_test.go | 8 +- 9 files changed, 329 insertions(+), 8 deletions(-) create mode 100644 helm-java/src/main/java/com/marcnuri/helm/SearchCommand.java create mode 100644 helm-java/src/main/java/com/marcnuri/helm/SearchResult.java create mode 100644 helm-java/src/test/java/com/marcnuri/helm/HelmSearchTest.java create mode 100644 lib/api/src/main/java/com/marcnuri/helm/jni/SearchOptions.java diff --git a/README.md b/README.md index f08183c3..d95bbb4c 100644 --- a/README.md +++ b/README.md @@ -181,7 +181,7 @@ Equivalent of [`helm list`](https://helm.sh/docs/helm/helm_list/). Lists all the releases for a specified namespace (uses current namespace context if namespace not specified). ``` java -Helm.list() +List releases = Helm.list() // Optionally specify the Kubernetes namespace to list the releases from .withNamespace("namespace") // Optionally specify the path to the kubeconfig file to use for CLI requests @@ -347,7 +347,7 @@ Equivalent of [`helm repo list`](https://helm.sh/docs/helm/helm_repo_list/). List chart repositories. ``` java -Helm.repo().list() +List respositories = Helm.repo().list() // Optionally set the path to the file containing repository names and URLs // Defaults to "~/.config/helm/repositories.yaml" .withRepositoryConfig(Paths.get("path", "to", "config")) @@ -372,6 +372,34 @@ Helm.repo().remove() .call(); ``` +### Search + +Equivalent of [`helm search`](https://helm.sh/docs/helm/helm_search/). + +This command provides the ability to search for Helm charts in various places including the Artifact Hub and the repositories you have added. + +#### Repo + +Equivalent of [`helm search repo`](https://helm.sh/docs/helm/helm_search_repo/). + +Search repositories for a keyword in charts. + +``` java +List results = Helm.search().repo() + // Optionally set the path to the file containing repository names and URLs + // Defaults to "~/.config/helm/repositories.yaml" + .withRepositoryConfig(Paths.get("path", "to", "config")) + // Optionally set the keyword to match against the repo name, chart name, chart keywords, and description. + .withKeyword("keyword") + // Optionally use regular expressions for searching. + .regexp() + // Optionally search for development versions too (alpha, beta, and release candidate releases). + .devel() + // Optionally search using semantic versioning constraints + .withVersion(">=1.0.0") + .call(); +``` + ### Show Equivalent of [`helm show`](https://helm.sh/docs/helm/helm_show/). diff --git a/helm-java/src/main/java/com/marcnuri/helm/Helm.java b/helm-java/src/main/java/com/marcnuri/helm/Helm.java index b226b4f2..1cb7aa92 100644 --- a/helm-java/src/main/java/com/marcnuri/helm/Helm.java +++ b/helm-java/src/main/java/com/marcnuri/helm/Helm.java @@ -113,6 +113,16 @@ public static RepoCommand repo() { return new RepoCommand(HelmLibHolder.INSTANCE); } + /** + * This command provides the ability to search for Helm charts in various places including the Artifact Hub + * and the repositories you have added. + * + * @return the {@link SearchCommand} command. + */ + public static SearchCommand search() { + return new SearchCommand(HelmLibHolder.INSTANCE); + } + /** * This command shows information about a chart. * diff --git a/helm-java/src/main/java/com/marcnuri/helm/SearchCommand.java b/helm-java/src/main/java/com/marcnuri/helm/SearchCommand.java new file mode 100644 index 00000000..66db1bd0 --- /dev/null +++ b/helm-java/src/main/java/com/marcnuri/helm/SearchCommand.java @@ -0,0 +1,111 @@ +package com.marcnuri.helm; + +import com.marcnuri.helm.jni.HelmLib; +import com.marcnuri.helm.jni.Result; +import com.marcnuri.helm.jni.SearchOptions; + +import java.nio.file.Path; +import java.util.List; +import java.util.function.Function; + +public class SearchCommand { + + private final HelmLib helmLib; + + public SearchCommand(HelmLib helmLib) { + this.helmLib = helmLib; + } + + /** + * Search repositories for a keyword in charts. + * + * @return the {@link SearchCommand.SearchSubcommand} subcommand. + */ + public SearchCommand.SearchSubcommand> repo() { + return new SearchCommand.SearchSubcommand<>(helmLib, hl -> hl::SearchRepo, SearchResult::parse); + } + + public static final class SearchSubcommand extends HelmCommand { + + private final Function> callable; + private final Function transformer; + private Path repositoryConfig; + private String keyword; + private boolean regexp; + private boolean devel; + private String version; + + SearchSubcommand(HelmLib helmLib, Function> callable, Function transformer) { + super(helmLib); + this.callable = callable; + this.transformer = transformer; + } + + @Override + public T call() { + return transformer.apply(run(hl -> callable.apply(hl).apply(new SearchOptions( + toString(repositoryConfig), + keyword, + toInt(regexp), + toInt(devel), + version + )))); + } + + /** + * Path to the file containing repository names and URLs + * (default "~/.config/helm/repositories.yaml") + * + * @param repositoryConfig a {@link Path} to the repository configuration file. + * @return this {@link SearchCommand.SearchSubcommand} instance. + */ + public SearchCommand.SearchSubcommand withRepositoryConfig(Path repositoryConfig) { + this.repositoryConfig = repositoryConfig; + return this; + } + + /** + * The keyword(s) to match against the repo name, chart name, chart keywords, and description. + * + * @param keyword the keyword to search for. + * @return this {@link SearchCommand.SearchSubcommand} instance. + */ + public SearchCommand.SearchSubcommand withKeyword(String keyword) { + this.keyword = keyword; + return this; + } + + /** + * Use regular expressions for searching. + * + * @return this {@link SearchCommand.SearchSubcommand} instance. + */ + public SearchCommand.SearchSubcommand regexp() { + this.regexp = true; + return this; + } + + /** + * Search for development versions too (alpha, beta, and release candidate releases). + * + *

Equivalent to withVersion '>0.0.0-0'. + * + * @return this {@link SearchCommand.SearchSubcommand} instance. + */ + public SearchCommand.SearchSubcommand devel() { + this.devel = true; + return this; + } + + /** + * Search using semantic versioning constraints (e.g. >1.0.0, <2.0.0, >=1.0.0, <=2.0.0). + * + * @param version the version to search for. + * @return this {@link SearchCommand.SearchSubcommand} instance. + */ + public SearchCommand.SearchSubcommand withVersion(String version) { + this.version = version; + return this; + } + } +} diff --git a/helm-java/src/main/java/com/marcnuri/helm/SearchResult.java b/helm-java/src/main/java/com/marcnuri/helm/SearchResult.java new file mode 100644 index 00000000..ba985f2a --- /dev/null +++ b/helm-java/src/main/java/com/marcnuri/helm/SearchResult.java @@ -0,0 +1,70 @@ +package com.marcnuri.helm; + +import com.marcnuri.helm.jni.Result; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static com.marcnuri.helm.HelmCommand.parseUrlEncodedLines; + +public class SearchResult { + + private final String name; + private final int score; + private final String chartVersion; + private final String appVersion; + private final String description; + private final String keywords; + + public SearchResult(String name, int score, String chartVersion, String appVersion, String description, String keywords) { + this.name = name; + this.score = score; + this.chartVersion = chartVersion; + this.appVersion = appVersion; + this.description = description; + this.keywords = keywords; + } + + public String getName() { + return name; + } + + public int getScore() { + return score; + } + + public String getChartVersion() { + return chartVersion; + } + + public String getAppVersion() { + return appVersion; + } + + public String getDescription() { + return description; + } + + public String getKeywords() { + return keywords; + } + + public static List parse(Result result) { + if (result == null) { + throw new IllegalArgumentException("Result cannot be null"); + } + final List searchResults = new ArrayList<>(); + for (Map entries : parseUrlEncodedLines(result.out)) { + searchResults.add(new SearchResult( + entries.get("name"), + Integer.parseInt(entries.get("score")), + entries.getOrDefault("chartVersion", ""), + entries.getOrDefault("appVersion", ""), + entries.getOrDefault("description", ""), + entries.getOrDefault("keywords", "") + )); + } + return searchResults; + } +} diff --git a/helm-java/src/test/java/com/marcnuri/helm/HelmSearchTest.java b/helm-java/src/test/java/com/marcnuri/helm/HelmSearchTest.java new file mode 100644 index 00000000..4fe98151 --- /dev/null +++ b/helm-java/src/test/java/com/marcnuri/helm/HelmSearchTest.java @@ -0,0 +1,77 @@ +package com.marcnuri.helm; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.net.URI; +import java.nio.file.Path; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class HelmSearchTest { + + @Nested + class SearchRepo { + + @TempDir + Path tempDir; + + @BeforeEach + void setRepository() { + Helm.repo().add().withRepositoryConfig(tempDir.resolve("repositories.yaml")) + .withName("repo-1") + .withUrl(URI.create("https://charts.helm.sh/stable")) + .insecureSkipTlsVerify() + .call(); + } + + @Test + void withDefaults() { + final List result = Helm.search().repo() + .withRepositoryConfig(tempDir.resolve("repositories.yaml")) + .call(); + assertThat(result) + .isNotEmpty() + .first() + .extracting(SearchResult::getName, SearchResult::getScore, SearchResult::getChartVersion, SearchResult::getAppVersion, SearchResult::getDescription) + .allMatch(s -> s != null && !s.toString().isEmpty()); + } + + @Test + void withKeyword() { + final List result = Helm.search().repo() + .withRepositoryConfig(tempDir.resolve("repositories.yaml")) + .withKeyword("nginx") + .call(); + assertThat(result) + .isNotEmpty() + .allMatch(r -> r.getName().contains("nginx") || r.getDescription().contains("nginx") || r.getKeywords().contains("nginx")); + } + + @Test + void hasNoDevelVersions() { + final List result = Helm.search().repo() + .withRepositoryConfig(tempDir.resolve("repositories.yaml")) + .withKeyword("nginx") + .call(); + assertThat(result) + .extracting(SearchResult::getChartVersion) + .noneMatch(chartVersion -> chartVersion.contains("-")); + } + + @Test + void withDevelHasDevelVersions() { + final List result = Helm.search().repo() + .withRepositoryConfig(tempDir.resolve("repositories.yaml")) + .devel() + .call(); + assertThat(result) + .extracting(SearchResult::getChartVersion) + .anyMatch(chartVersion -> chartVersion.contains("-")); + } + + } +} diff --git a/lib/api/src/main/java/com/marcnuri/helm/jni/HelmLib.java b/lib/api/src/main/java/com/marcnuri/helm/jni/HelmLib.java index c0132d67..42ab649c 100644 --- a/lib/api/src/main/java/com/marcnuri/helm/jni/HelmLib.java +++ b/lib/api/src/main/java/com/marcnuri/helm/jni/HelmLib.java @@ -40,6 +40,8 @@ public interface HelmLib extends Library { Result RepoServerStopAll(); + Result SearchRepo(SearchOptions options); + Result Show(ShowOptions options); Result Test(TestOptions options); diff --git a/lib/api/src/main/java/com/marcnuri/helm/jni/SearchOptions.java b/lib/api/src/main/java/com/marcnuri/helm/jni/SearchOptions.java new file mode 100644 index 00000000..58eca535 --- /dev/null +++ b/lib/api/src/main/java/com/marcnuri/helm/jni/SearchOptions.java @@ -0,0 +1,21 @@ +package com.marcnuri.helm.jni; + +import com.sun.jna.Structure; + +@Structure.FieldOrder({"repositoryConfig", "keyword", "regexp", "devel", "version"}) +public class SearchOptions extends Structure { + + public String repositoryConfig; + public String keyword; + public int regexp; + public int devel; + public String version; + + public SearchOptions(String repositoryConfig, String keyword, int regexp, int devel, String version) { + this.repositoryConfig = repositoryConfig; + this.keyword = keyword; + this.regexp = regexp; + this.devel = devel; + this.version = version; + } +} diff --git a/native/internal/helm/search.go b/native/internal/helm/search.go index d562ac8a..055885ea 100644 --- a/native/internal/helm/search.go +++ b/native/internal/helm/search.go @@ -11,6 +11,7 @@ import ( "net/url" "path/filepath" "strconv" + "strings" ) type SearchOptions struct { @@ -73,8 +74,9 @@ func SearchRepo(options *SearchOptions) (string, error) { values.Set("name", searchResult.Name) values.Set("score", strconv.Itoa(searchResult.Score)) values.Set("chartVersion", searchResult.Chart.Version) - values.Set("chartAppVersion", searchResult.Chart.AppVersion) - values.Set("chartDescription", searchResult.Chart.Description) + values.Set("appVersion", searchResult.Chart.AppVersion) + values.Set("description", searchResult.Chart.Description) + values.Set("keywords", strings.Join(searchResult.Chart.Metadata.Keywords, ",")) _, _ = fmt.Fprintln(out, values.Encode()) } } diff --git a/native/main_test.go b/native/main_test.go index 43aa7e23..911da9e9 100644 --- a/native/main_test.go +++ b/native/main_test.go @@ -565,12 +565,12 @@ func TestSearchRepo(t *testing.T) { t.Errorf("Expected search to succeed, got %s", err) return } - if !strings.Contains(out, "chartAppVersion=") { - t.Errorf("Expected search to contain 'chartAppVersion=', got %s", out) + if !strings.Contains(out, "appVersion=") { + t.Errorf("Expected search to contain 'appVersion=', got %s", out) return } - if !strings.Contains(out, "&chartDescription=") { - t.Errorf("Expected search to contain '&chartDescription=', got %s", out) + if !strings.Contains(out, "&description=") { + t.Errorf("Expected search to contain '&description=', got %s", out) return } if !strings.Contains(out, "&name=") {