Skip to content

Commit

Permalink
Ability to specify updates using regular expressions
Browse files Browse the repository at this point in the history
  • Loading branch information
srozange committed Sep 9, 2023
1 parent 561b832 commit 6dc75c8
Show file tree
Hide file tree
Showing 22 changed files with 289 additions and 95 deletions.
66 changes: 56 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@

**Update YAML files the GitOps way**

yupd is a command-line tool that allows updating a YAML file in a remote GitHub or GitLab gitRepository.
Yupd is a command-line tool that allows updating a YAML file in a remote GitHub or GitLab repository.

## Usage

Assuming we have this file in a Git gitRepository:
Assuming we have this file in a git repository:

```yaml
apiVersion: apps/v1
Expand Down Expand Up @@ -39,35 +39,81 @@ Let's use yupd to update the nginx image version as well as the last-updated ann
- For GitHub :
```bash
```shell
yupd --repo-type github --token <updateme> --project srozange/playground --path k8s/deployment.yml --branch yupd-it --set *.containers[0].image=nginx:newversion --set metadata.annotations.last-updated="$(date)"
```

- For GitLab :

```bash
```shell
yupd --repo-type gitlab --token <updateme> --project 48677990 --path k8s/deployment.yml --branch yupd-it --set *.containers[0].image=nginx:newversion --set metadata.annotations.last-updated="$(date)"
```

Voilà!

Additionally, instead of making direct updates to the target branch, yupd can create either a merge request or a pull request (based on the Git provider context) by simply adding the **--merge-request** or **--pull-request** flag.

## YAML path expressions
## Updating files

You can specify how the file is updated using the repeatable `--set` option:

```shell
--set [type:]expression=value
```

Available types are : ```ypath``` and ```regex```.

We will use the following file for the subsequent examples:

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: oldname
```
### YAML path expressions
Yupd uses the [YamlPath](https://github.com/yaml-path/YamlPath) library.
You can check their [readme page](https://github.com/yaml-path/YamlPath) for the syntax.
Here's an example where we change the name field to 'newname':
```shell
--set ypath:metadata.name=newname
```

Or you can use the shorthand:

```shell
--set metadata.name=newname
```

For more detailed syntax information, you can refer to the [YamlPath readme page](https://github.com/yaml-path/YamlPath).

### Regular Expressions

Yupd can also update files using regular expressions, which can be especially useful when you need to modify a file that isn't in YAML format.

Here's an example where we change the name field to 'newname':

```shell
--set "regex:name: (.*)=newname"
```

The text matched within the parentheses will be replaced with the right part.

For more information on the syntax, you can refer to the [Javadoc of class 'Pattern'](https://docs.oracle.com/javase/8/docs/api/java/util/regex/Pattern.html).

## Manual
```bash
```shell
Usage: yupd [-hV] [--dry-run] [--insecure] [--pull-request] [--verbose] -b=<branch> [-f=<sourceFile>] [-m=<commitMessage>] -p=<path> --project=<project>
[-r=<url>] --repo-type=<repoType> -t=<token> --set=<String=String> [--set=<String=String>]...
-b, --branch=<branch> Specifies the branch name of the target file to update (env: YUPD_BRANCH)
--dry-run If set to true, no write operation is done (env: YUPD_DRY_RUN)
-f, --template=<sourceFile>
Points to a local YAML file to be used as the source, instead of the remote one (env: YUPD_TEMPLATE)
-h, --help Show this help message and exit.
--insecure If set to true, disable SSL certificate validation (applicable to GitLab only)(env: YUPD_INSECURE)
--insecure If set to true, disable SSL certificate validation (applicable to GitLab only) (env: YUPD_INSECURE)
-m, --commit-msg=<commitMessage>
Provides a custom commit message for the update (env: YUPD_COMMIT_MSG)
-p, --path=<path> Specifies the path of the target file to update (env: YUPD_PATH)
Expand All @@ -77,7 +123,7 @@ Usage: yupd [-hV] [--dry-run] [--insecure] [--pull-request] [--verbose] -b=<bran
-r, --repo=<url> Specifies the URL of the Git repository (env: YUPD_REPO)
--repo-type=<repoType>
Specifies the repository type; valid values: 'gitlab' or 'github' (env: YUPD_REPO_TYPE)
--set=<String=String> Allows setting YAML path expressions (e.g., metadata.name=new_name) (env: YUPD_SET)
--set=<String=String> Allows setting YAML path expressions (e.g., metadata.name=new_name) or regular expressions (env: YUPD_SET)
-t, --token=<token> Provides the authentication token (env: YUPD_TOKEN)
-V, --version Print version information and exit.
--verbose If set to true, sets the log level to debug (env: YUPD_VERBOSE)
Expand All @@ -89,7 +135,7 @@ You can grab the latest binaries from the [releases page](https://github.com/sro

## Docker Image

Docker images are available on [Docker Hub](https://hub.docker.com/gitRepository/docker/srozange/yupd/general).
Docker images are available on [Docker Hub](https://hub.docker.com/repository/docker/srozange/yupd).

To use the image, you can run the following command:

Expand Down
16 changes: 8 additions & 8 deletions src/main/java/io/github/yupd/business/YamlRepoUpdater.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
import io.github.yupd.infrastructure.git.GitConnectorFactory;
import io.github.yupd.infrastructure.git.GitConnector;
import io.github.yupd.infrastructure.git.model.GitFile;
import io.github.yupd.infrastructure.update.ContentUpdateService;
import io.github.yupd.infrastructure.utils.LogUtils;
import io.github.yupd.infrastructure.utils.StringUtils;
import io.github.yupd.infrastructure.utils.UniqueIdGenerator;
import io.github.yupd.infrastructure.yaml.YamlPathUpdator;
import io.github.yupd.infrastructure.utils.IOUtils;
import io.github.yupd.infrastructure.yaml.model.YamlPathEntry;
import io.github.yupd.infrastructure.update.model.ContentUpdateCriteria;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.jboss.logging.Logger;
Expand All @@ -24,7 +24,7 @@ public class YamlRepoUpdater {
GitConnectorFactory gitConnectorFactory;

@Inject
YamlPathUpdator yamlPathUpdator;
ContentUpdateService updateService;

@Inject
UniqueIdGenerator uniqueIdGenerator;
Expand Down Expand Up @@ -75,23 +75,23 @@ private String computeNewContent(YamlRepoUpdaterParameter parameter, String oldC
String newContent;
if (parameter.getSourceFile().isPresent()) {
LOGGER.info("Applying YAML path expressions on the template file");
newContent = yamlPathUpdator.update(IOUtils.readFile(parameter.getSourceFile().get()), parameter.getYamlPathUpdates());
newContent = updateService.update(IOUtils.readFile(parameter.getSourceFile().get()), parameter.getContentUpdates());
} else {
LOGGER.info("Applying YAML path expressions");
newContent = yamlPathUpdator.update(oldContent, parameter.getYamlPathUpdates());
newContent = updateService.update(oldContent, parameter.getContentUpdates());
}
return newContent;
}

private String computeMergeRequestBody(YamlRepoUpdaterParameter parameter) {
return parameter.getYamlPathUpdates()
return parameter.getContentUpdates()
.stream()
.map(YamlRepoUpdater::computePathEntryDescription)
.collect(Collectors.joining("\n", "Proposed update in " + parameter.getGitFile().getPath() + ":\n", "\n"));
}

private static String computePathEntryDescription(YamlPathEntry entry) {
return "- [yamlpath] " + entry.getPath() + "=" + entry.getReplacement();
private static String computePathEntryDescription(ContentUpdateCriteria entry) {
return "- [" + entry.type().getDisplayName() + "] " + entry.key() + "=" + entry.value();
}

public record YamlUpdateResult(boolean updated, String originalContent, String newContent) {
Expand Down
22 changes: 11 additions & 11 deletions src/main/java/io/github/yupd/business/YamlRepoUpdaterParameter.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import io.github.yupd.infrastructure.git.model.GitFile;
import io.github.yupd.infrastructure.utils.StringUtils;
import io.github.yupd.infrastructure.yaml.model.YamlPathEntry;
import io.github.yupd.infrastructure.update.model.ContentUpdateCriteria;

import java.nio.file.Path;
import java.util.List;
Expand All @@ -18,7 +18,7 @@ public class YamlRepoUpdaterParameter {

private String message;

private List<YamlPathEntry> yamlPathEntries;
private List<ContentUpdateCriteria> contentUpdates;

private boolean mergeRequest;

Expand All @@ -33,11 +33,11 @@ public Optional<Path> getSourceFile() {
}

public String getMessage() {
return StringUtils.isNullOrEmpty(message) ? "Udpate values in " + getGitFile().getPath() : message;
return StringUtils.isNullOrEmpty(message) ? "Update values in " + getGitFile().getPath() : message;
}

public List<YamlPathEntry> getYamlPathUpdates() {
return yamlPathEntries;
public List<ContentUpdateCriteria> getContentUpdates() {
return contentUpdates;
}

public GitFile getGitFile() {
Expand All @@ -56,7 +56,7 @@ public static final class Builder {
private Path sourceFile;
private GitFile gitFile;
private String message;
private List<YamlPathEntry> yamlPathEntries;
private List<ContentUpdateCriteria> contentUpdates;
private boolean dryRun;
private boolean mergeRequest;

Expand All @@ -78,13 +78,13 @@ public Builder withMessage(String message) {
return this;
}

public Builder withYamlPathEntries(List<YamlPathEntry> yamlPathEntries) {
this.yamlPathEntries = yamlPathEntries;
public Builder withContentUpdates(List<ContentUpdateCriteria> contentUpdates) {
this.contentUpdates = contentUpdates;
return this;
}

public Builder withYamlPathEntries(Map<String, String> yamlPathMap) {
withYamlPathEntries(yamlPathMap.entrySet().stream().map(YamlPathEntry::new).collect(Collectors.toList()));
public Builder withContentUpdates(Map<String, String> yamlPathMap) {
withContentUpdates(yamlPathMap.entrySet().stream().map(ContentUpdateCriteria::new).collect(Collectors.toList()));
return this;
}

Expand All @@ -101,7 +101,7 @@ public Builder withMergeRequest(boolean mergeRequest) {
public YamlRepoUpdaterParameter build() {
YamlRepoUpdaterParameter yamlRepoUpdaterParameter = new YamlRepoUpdaterParameter();
yamlRepoUpdaterParameter.dryRun = this.dryRun;
yamlRepoUpdaterParameter.yamlPathEntries = this.yamlPathEntries;
yamlRepoUpdaterParameter.contentUpdates = this.contentUpdates;
yamlRepoUpdaterParameter.message = this.message;
yamlRepoUpdaterParameter.gitFile = this.gitFile;
yamlRepoUpdaterParameter.sourceFile = this.sourceFile;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ YamlRepoUpdaterParameter create() {
.withGitFile(buildGitFile())
.withMessage(cmd.commitMessage)
.withSourceFile(cmd.sourceFile)
.withYamlPathEntries(cmd.yamlPathMap)
.withContentUpdates(cmd.contentUpdates)
.withDryRun(cmd.dryRun)
.withMergeRequest(cmd.mergeRequest)
.build();
Expand Down
6 changes: 3 additions & 3 deletions src/main/java/io/github/yupd/command/YupdCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public class YupdCommand implements Callable<Integer> {
@CommandLine.Option(names = {"-p", "--path"}, required = true, defaultValue = "${YUPD_PATH}", description = "Specifies the path of the target file to update (env: YUPD_PATH)")
String path;

@CommandLine.Option(names = {"--insecure"}, defaultValue = "${YUPD_INSECURE:-false}", description = "If set to true, disable SSL certificate validation (applicable to GitLab only)(env: YUPD_INSECURE)")
@CommandLine.Option(names = {"--insecure"}, defaultValue = "${YUPD_INSECURE:-false}", description = "If set to true, disable SSL certificate validation (applicable to GitLab only) (env: YUPD_INSECURE)")
boolean insecure;

@CommandLine.Option(names = {"-f", "--template"}, defaultValue = "${YUPD_TEMPLATE}", description = "Points to a local YAML file to be used as the source, instead of the remote one (env: YUPD_TEMPLATE)")
Expand All @@ -42,8 +42,8 @@ public class YupdCommand implements Callable<Integer> {
@CommandLine.Option(names = {"-m", "--commit-msg"}, defaultValue = "${YUPD_COMMIT_MSG}", description = "Provides a custom commit message for the update (env: YUPD_COMMIT_MSG)")
String commitMessage;

@CommandLine.Option(names = {"--set"}, required = true, defaultValue = "${YUPD_SET}", description = "Allows setting YAML path expressions (e.g., metadata.name=new_name) (env: YUPD_SET)")
Map<String, String> yamlPathMap = new LinkedHashMap<>();
@CommandLine.Option(names = {"--set"}, required = true, description = "Allows setting YAML path expressions (e.g., metadata.name=new_name) or regular expressions (env: YUPD_SET)")
Map<String, String> contentUpdates = new LinkedHashMap<>();

@CommandLine.Option(names = {"--merge-request", "--pull-request"}, defaultValue = "${YUPD_MERGE_REQUEST:-false}", description = "If set to true, open either a pull request or a merge request based on the Git provider context (env: YUPD_MERGE_REQUEST)")
boolean mergeRequest;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package io.github.yupd.infrastructure.update;

import io.github.yupd.infrastructure.update.model.ContentUpdateCriteria;
import io.github.yupd.infrastructure.update.model.ContentUpdateType;
import io.github.yupd.infrastructure.update.updator.RegexUpdator;
import io.github.yupd.infrastructure.update.updator.YamlPathUpdator;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;

import java.util.List;
import java.util.stream.Collectors;

@ApplicationScoped
public class ContentUpdateService {

@Inject
RegexUpdator regexUpdator;

@Inject
YamlPathUpdator yamlPathUpdator;

public String update(String content, List<ContentUpdateCriteria> updates) {
return computeRegexUpdates(computeYamlPathUpdates(content, updates), updates);
}

private String computeYamlPathUpdates(String content, List<ContentUpdateCriteria> updates) {
List<ContentUpdateCriteria> yamlPathUpdates = filterUpdates(updates, ContentUpdateType.YAMLPATH);
if (yamlPathUpdates.isEmpty()) {
return content;
}
return yamlPathUpdator.update(content, yamlPathUpdates);
}

private String computeRegexUpdates(String content, List<ContentUpdateCriteria> updates) {
List<ContentUpdateCriteria> regexUpdates = filterUpdates(updates, ContentUpdateType.REGEX);
if (regexUpdates.isEmpty()) {
return content;
}
return regexUpdator.update(content, regexUpdates);
}

private static List<ContentUpdateCriteria> filterUpdates(List<ContentUpdateCriteria> updates, ContentUpdateType type) {
return updates.stream().filter(update -> update.type() == type).collect(Collectors.toList());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.github.yupd.infrastructure.update.model;

import java.util.Map;

public record ContentUpdateCriteria(ContentUpdateType type, String key, String value) {

public ContentUpdateCriteria(String key, String value) {
this(ContentUpdateType.computeType(key),
key.replace(ContentUpdateType.computeType(key).getPrefix(), ""),
value);
}

public ContentUpdateCriteria(Map.Entry<String, String> entry) {
this(entry.getKey(), entry.getValue());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package io.github.yupd.infrastructure.update.model;

import java.util.Arrays;

public enum ContentUpdateType {
YAMLPATH("ypath:", "yamlpath"),
REGEX("regex:", "regex");

private String prefix;
private String displayName;

ContentUpdateType(String prefix, String displayName) {
this.prefix = prefix;
this.displayName = displayName;
}

public String getDisplayName() {
return displayName;
}

public String getPrefix() {
return prefix;
}

public static ContentUpdateType computeType(String key) {
return Arrays.stream(values()).filter(v -> key.startsWith(v.prefix)).findFirst().orElse(YAMLPATH);
}
}
Loading

0 comments on commit 6dc75c8

Please sign in to comment.