Compare commits

...
44 Commits
Author SHA1 Message Date
KUOKA Yusuke f6264bfa9d fix: do not complain on missing values files for undesired release (#617)
Fixes #616
2019-05-24 21:40:34 +09:00
KUOKA Yusuke 3b3d3092dd fix scopelint errors / possible races in hooks (#614)
Ref #612
Fixes #514
2019-05-22 18:50:38 +09:00
KUOKA Yusuke 1012256f16 feat: "base" helmfile state gotmpl is rendered with the envvals inherited from the parent (#613)
Resolves #611
2019-05-22 18:28:10 +09:00
KUOKA Yusuke 90390492a3 feat: expand glob pattern in environment values file path (#610)
This enhances helmfile's internal environment values files loader to expand glob patterns (#606)

Fixes the existing bug that helmfile was unable to load environment values file from absolute path (#549)

Resolves #606
Fixes #549
2019-05-21 16:49:57 +09:00
KUOKA Yusuke 4c9c42d3c5 fix: gotmpl layer was unable to reference env values from other layers when env!=default (#609)
Fixes #604
2019-05-21 14:37:03 +09:00
KUOKA Yusuke bd60548f10 feat: add --skip-deps for helmfile lint (#602)
Resolevs #581
2019-05-16 21:31:00 +09:00
KUOKA Yusuke 0534117b62 feat: postsync hooks (#601)
`postsync` events are triggered after each release is applied to the cluster in `helmfile sync` or `helmfile apply`.

This should be a best hook to notify only after each sync failed or succeeded. This can be used for running operations like patching K8s resources managed by helm, but that should be the last-resort. Maybe you should fork/update the chart, or submit a feature request to add `replicated/ship` integration to `helmfile` in that case :)

Resolves #599
2019-05-16 21:24:16 +09:00
KUOKA Yusuke 0104c91fce feat: support for locking the same chart for two or more versions (#600)
Resolves #598
2019-05-16 21:19:30 +09:00
Yusuke KUOKA a769d1a82a fix integration test 2019-05-15 13:21:48 +09:00
KUOKA Yusuke b0179218b9 fix: helmfile panicing on missing lock file (#596)
Fixes #595
2019-05-15 13:16:22 +09:00
KUOKA Yusuke c9a43ad9cb feat: Dependency locking (#593)
In order to maintain predictable deployments, as developer I want to generate and use "lock files" for all chart versions retrieved from a helmfile.

This change solves it by (1)enhancing `helmfile deps` to generate a lock file containing all the direct chart dependencies of each helmfile state file and
(2)making other helmfile sub-commands reads the lock file and merge the locked version numbers to the helmfile state file being processed.

The lock file is named after the helmfile state file being locked, so that you can have multiple set of the helmfile state file and the lock file pairs in a directory.

When `helmfile deps` are not explicitly run before commands like `sync`, all the helmfile behavior should remain as before.

Let's say you have `helmfile.1.yaml`:

```
repositories:
- name: stable
  url: https://kubernetes-charts.storage.googleapis.com

releases:
- name: envoy
  chart: stable/envoy
- name: envoy2
  chart: stable/envoy
```

`helmfile deps` generates `helmfile.1.lock` that looks like:

```
dependencies:
- name: envoy
  repository: https://kubernetes-charts.storage.googleapis.com
  version: 1.5.0
digest: sha256:e43b05c8528ea8ef1560f4980a519719ad2a634658abde0a98daefdb83a104e9
generated: 2019-05-14T16:45:37.78205+09:00
```

Under the hood, `helmfile deps` creates a temporary local helm chart with a dummy `Chart.yaml` and `requirements.yaml` deduced from the `helmfile.yaml` content, then runs `helm dependency update` to produce od update the corresponding `requirements.lock` file.

`helmfile` then renames it to match the name of the targeted helmfile state file and moves it,  so that it becomes adjacent to each `helmfile.yaml`.

Other `helmfile` commands like `sync`, `diiff`, `apply`, `lint` read chart version numbers from the lock file.

Resolves #483
2019-05-15 09:39:12 +09:00
KUOKA Yusuke 9eec72b318 Merge pull request #594 from sstarcher/fetch_devel
fix: Fetch devel

Fixes two issues encountered in https://sweetops.slack.com/archives/CE5NGCB9Q/p1557848927155400
2019-05-15 08:55:54 +09:00
Shane Starcher 081107bb82 error is coming back nil sometimes
this does not fix the root issue that is causing a nil error
2019-05-14 15:51:12 -07:00
Shane Starcher af69d1ba97 fetch should honor devel flag 2019-05-14 15:50:44 -07:00
KUOKA Yusuke 53ad91607a fix: log useful error message when failed walking through the file tree (#592)
Fixes #590
2019-05-14 10:02:50 +09:00
KUOKA Yusuke 255d92064e Merge pull request #587 from roboll/layering-enhancements
feat: helmfile.yaml layering enhancements

The current  [Layering](https://github.com/roboll/helmfile/blob/master/docs/writing-helmfile.md#layering) system didn't work as documented, as it relies on helmfile to template each "part" of your helmfile.yaml THEN merge them one by one.

The reality was that helmfile template all the parts of your helmfile.yaml at once, and then merge those YAML documents. In https://github.com/roboll/helmfile/issues/388#issuecomment-436186278, @sruon was making a GREAT point that we may need to change helmfile to render templates earlier - that is to evaluate a template per each helmfile.yaml part separated by `---`. Sorry I missed my expertise to follow your great idea last year @sruon  😭 

Anyways, this, in combination with the wrong documentation, has made so many people confused. To finally overcome this situation, here's a fairly large PR that introduces the 2 enhancements:

- `bases:` for easier layering without go template expressions, especially `{{ readFunc "path/to/file" }}`s. This is the first commit of this PR.
- `helmfile.yaml` is splited by the separator `---` at first. Each part is then rendered as a go template(double-render applies as before). Finally, All the results are merged in the order of occurence. I assume this as an enhanced version of @sruon's work. This is the second commit of this PR.

Resolves #388
Resolve #584
Resolves #585 (`HELMFILE_EXPERIMENTA=true -f helmfile.yaml helmfile` disables the whole-file templating, treating the helmfile.yaml as a regular YAML file as the file ext. denotes. Use `helmfile.yaml.gotmpl` or `helmfile.gotmpl` to enable)
Fixes #568 (Use `bases` or `readFile` rather than not importing implicitly with `helmfile.d`
2019-05-14 09:48:20 +09:00
Yusuke KUOKA aef366660b feat: split-render-merge helmfile.yaml parts
This splits your helmfile.yaml by the YAML document separator "---" before evaluating go template expressions as outlined in https://github.com/roboll/helmfile/issues/388#issuecomment-491710348
2019-05-13 21:49:59 +09:00
Yusuke KUOKA 1db205de48 feat: "bases" for easier layerina
This adds the new configuration key `baeses` to your helmfile.yaml files, so that you can layer them without the `readFile` template function, which was a bit unintuitive.

Please see https://github.com/roboll/helmfile/issues/388#issuecomment-491710348 for more context
2019-05-13 21:48:00 +09:00
KUOKA Yusuke 4f83e69bf6 Various U/X improvements for helmfile apply (#586)
* Various U/X improvements for `helmfile apply`

This improves the U/X of `helmfile apply`, by allowing you to selectively apply sub-helmfiles.
When you have two or more sub-helmfiles processed, typing `n` to cancel the first doesn't automatically stop the whole helmfile execution.
Instead, it proceeds by diffing the next sub-helmfile, and asks you to apply it, which should be what the user would expect.

To support this workflow, I have suppressed useless exec logs, correct exit status when diff exists in sub-helmfiles but not in the parent helmfile, and made the final error message emitted by helmfile better.

More concretely, this moves more output from `helm` to STDERR and the `debug` log-level.

The overall output from `helmfile` should be a bit more cleaner especially for `apply`, `sync`, `diff` and perhaps other `helmfile` sub-commands, too.

For example, when one of release failed, `helmfile`'s final error message now includes the error message from the failed `helm` execution, like seen in the last line:

```
List of updated releases :
RELEASE   CHART          VERSION
envoy     stable/envoy     1.5.0

List of releases in error :
RELEASE
envoy2
in ./helmfile.yaml: in .helmfiles[0]: in /Users/c-ykuoka/helmfile/helmfile.1.yaml: failed processing release envoy2: helm exited with status 1:
  Error: UPGRADE FAILED: "envoy2" has no deployed releases
```

This way you can better understand what caused helmfile to finally fail.

`helmfile` has been streaminig a lot of stdout and stderr contents from the `helm` commands regardless of the helmfile's log-level. It has been suppressed by default and moved to the `debug` log-level.
You will see that it helps you focus on what was the cause of a failure.

While working on the above, I found an another bug that made `--detailed-exitcode` useless in some case.
That is, `helmfile diff --detailed-exitcode`, when any diff existed only in sub-helmfiles, has been returning an exit code of `1`. It should return `2` when any release had diff and no release had an error, regardless of the target is a sub-helmfile or a parent helmfile. Why? Because that's what `--detailed-exitcode` meant for!

After this PR gets merged, `helmfile diff --detailed-exitcode`  propery return exit code `2` in such cases.

Fixes #543
Resolves #540
2019-05-12 16:26:11 +09:00
KUOKA Yusuke bae842f234 fix: Send log messages to STDERR (#583)
And only useful outputs from helm commands like ones from `helm template` to STDOUT.

Fixes #551
2019-05-09 14:58:15 +09:00
Raj Perera 55c275b3aa Add presync event hook (#580)
* Add presync hook

* Add note to README about new hook
2019-05-09 10:13:31 +09:00
prakharrr-sl 0fc8ac395b typo in example (#582) 2019-05-09 10:12:14 +09:00
KUOKA Yusuke 272d55e31a fix: the merge mistake in imports (#578) 2019-05-07 09:55:34 +09:00
sgandon d683ea1d7b fix(sync): make error log showing when error (#577)
In debug mode, there was an error log display even when there was no error.
This PR fixes this issue.
2019-05-06 23:28:12 +09:00
Andrey Afoninsky 0ea5960ef2 feat: add .yml extension support (#575)
Resolves #565
2019-05-06 23:26:51 +09:00
sgandon 9a820d7bf2 feat: removes dictionary key for subhelm and uses selectorsInherited (#576)
Removed the usage of subhelmfile path as map key.
I also introduced the selectorsInherited key for explicit parent selector inheritance.

Ref #344
2019-05-06 10:06:32 +09:00
sgandon 4581e004b8 feat(#344): add sub helmfiles explicit selectors (#567)
Fixes #344 by allowing explicit selectors to be specified for composed helmfiles using the following structure

```yaml
helmfiles:
- path: helmfile.d/a*.yaml
  selectors:
  - name=prometheus      
  - name!=zipkin      
- helmfile.d/b*.yaml
- path: helmfile.d/c*.yaml
  selectors: {}
```

2 modes here : 
* legacy mode when no the env var HELMFILE_EXPERIMENTAL is not set to true
  * no selector : inherit from the command line.
  * selector:  is specified then it is used (an emty means no inheritance from command line and take everything).
* experimental when the env var HELMFILE_EXPERIMENTAL=true
  * no selector : nothing is inherited from the command line so use all releases.
  * selector:  is specified then it is used (an emty means no inheritance from command line and take everything).
2019-05-05 13:38:52 +09:00
Yusuke KUOKA d2353b5b70 accept go.mod and go.sum to be updated in pristine
I have currently no way to avoid this from happening in the first place
2019-05-04 22:45:31 +09:00
Yusuke KUOKA fb7e0a360c Update go.mod after running go mod vendor
Not sure why `go mod vendor` updates go.mod but `go build .` not
2019-05-04 22:17:16 +09:00
Yusuke KUOKA 59b254ee45 fix automated releases broken due to workspace location misconfig after go mod migration 2019-05-04 22:10:37 +09:00
sgandon 950bd23d86 fix(struct): renamed Error struct field into Failed (#566) 2019-05-02 20:41:49 +09:00
KUOKA Yusuke 8f030d5eab Bump go to 1.12.4 / Switch to go modules (#564)
* Bump go to 1.12.4 / Switch to go modules

Follow-up for https://github.com/roboll/helmfile/pull/560#issuecomment-486516109
2019-05-02 20:41:36 +09:00
sgandon a31077a1c0 feat(#502): display summary of upgraded, deleted and error releases (#560)
* feat(report): display summary of upgraded, deleted and error releases

* feat(#502): adds dep target in makefile

* feat(#502): removes vendor and fixes pristine in makefile
2019-04-25 13:33:34 +09:00
Aaron Gershman 32588ae319 readme example for failing when values file is missing (#561)
* readme example for failing when values file is missing

Believe this would assist with #548

* Update README.md

Using documentation yanked from searching for the key missingFileHandler https://github.com/roboll/helmfile/search?q=missingFileHandler&unscoped_q=missingFileHandler
2019-04-25 13:12:41 +09:00
Yusuke KUOKA f74963f4b6 fix(ci,release): Use existent base image for docker builds 2019-04-13 22:02:22 +09:00
KUOKA Yusuke d93ec77ea3 fix: with environment secrets (#556)
Since tillerless support we unintentionally broke this, and there isn't a real fix to this.
We must accept a limitation that helmfile needs a tiller installed on your cluster just for decrypting environment secrets.

Fixes #550
2019-04-13 22:01:53 +09:00
KUOKA Yusuke 04a9ea1d2a fix: helmfile apply and sync should properly delete releases (#555)
Since #526, `helmfile apply` have been really able to detect deletion of the last release only, and `sync` has been unable to mark releases with `installed: false` for removal.

Fixes #554
2019-04-13 21:47:27 +09:00
KUOKA Yusuke f53c66749e Bump go to 1.12.1 (#542) 2019-04-08 20:47:36 +09:00
Facundo Guerrero 8a13999de3 Fix link to shared-configuration-across-teams (#547) 2019-04-08 20:46:39 +09:00
Patrick Valsecchi 1acd07fa7e Simple implementation of the tillerless mode (#531)
Ref #449
2019-04-05 19:02:37 +09:00
Yusuke KUOKA 72c43a2f46 fix test 2019-04-03 23:47:08 +09:00
Yusuke KUOKA 08b48a8cca fix test 2019-04-03 23:43:23 +09:00
KUOKA Yusuke 0e00cdf2a5 fix: helmfile destroy should delete releases in the specified tiller namespace (#536)
Actually, 4 helm commands including "list", "diff", "status" and "delete" were not taking tiller-rerelated flags into account in helmfile. This fixes all these commands.

Fixes #534
2019-04-03 23:41:41 +09:00
Erik Osterman f522ba320b Add slack links (#533) 2019-04-03 22:36:17 +09:00
230 changed files with 3774 additions and 31512 deletions
+30 -8
View File
@@ -4,22 +4,39 @@ jobs:
build:
docker:
- image: circleci/golang:1.11.2
working_directory: /go/src/github.com/roboll/helmfile
- image: circleci/golang:1.12.4
environment:
GOPATH: /go
working_directory: /home/circleci/workspace/helmfile
steps:
- checkout
- restore_cache:
keys:
- go-mod-cache-v1-{{ checksum "./go.sum" }}
- go-mod-cache-v1-
- run: go env
- run: make build
- save_cache:
key: go-mod-cache-v1-{{ checksum "./go.sum" }}
paths:
- /go/pkg/mod
- persist_to_workspace:
root: /go/src/github.com/roboll/helmfile
root: ./
paths:
- .
test:
environment:
GOPATH: /go
docker:
- image: circleci/golang:1.11.2
working_directory: /go/src/github.com/roboll/helmfile
- image: circleci/golang:1.12.4
working_directory: /home/circleci/workspace/helmfile
steps:
- checkout
- restore_cache:
keys:
- go-mod-cache-v1-{{ checksum "./go.sum" }}
- go-mod-cache-v1-
- run: make check
- run: make pristine
- run: make test
@@ -74,16 +91,21 @@ jobs:
release:
docker:
- image: circleci/golang:1.11.2
working_directory: /go/src/github.com/roboll/helmfile
- image: circleci/golang:1.12.4
working_directory: /home/circleci/workspace/helmfile
steps:
- checkout
- restore_cache:
keys:
- go-mod-cache-v1-{{ checksum "./go.sum" }}
- go-mod-cache-v1-
- setup_remote_docker
- run:
command: |
docker login -u="$DOCKER_USER" -p="$DOCKER_PASS" quay.io
make tools
BUILD_URL="$CIRCLE_BUILD_URL" make push release
go mod vendor
ORG=roboll BUILD_URL="$CIRCLE_BUILD_URL" make push release
workflows:
version: 2
+4 -4
View File
@@ -1,8 +1,8 @@
FROM golang:1.11.2-alpine3.8 as builder
FROM golang:1.12.4-alpine3.9 as builder
RUN apk add --no-cache make git
WORKDIR /go/src/github.com/roboll/helmfile/
COPY . /go/src/github.com/roboll/helmfile/
WORKDIR /workspace/helmfile
COPY . /workspace/helmfile
RUN make static-linux
# -----------------------------------------------------------------------------
@@ -24,6 +24,6 @@ RUN mkdir -p "$(helm home)/plugins"
RUN helm plugin install https://github.com/databus23/helm-diff && \
helm plugin install https://github.com/futuresimple/helm-secrets
COPY --from=builder /go/src/github.com/roboll/helmfile/dist/helmfile_linux_amd64 /usr/local/bin/helmfile
COPY --from=builder /workspace/helmfile/dist/helmfile_linux_amd64 /usr/local/bin/helmfile
CMD ["/usr/local/bin/helmfile"]
Generated
-121
View File
@@ -1,121 +0,0 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
digest = "1:d9141c149f722217d35834972878499a4781fe5ae46a152e8ac843ff15efc80f"
name = "github.com/Masterminds/semver"
packages = ["."]
pruneopts = "UT"
revision = "8d82589cda2d7b5b9167396841975c4535c9bec6"
version = "v1.4.1"
[[projects]]
digest = "1:46a054a232ea2b7f0a35398d682b433d26ba9975fce9197b1784824402059f5b"
name = "github.com/Masterminds/sprig"
packages = ["."]
pruneopts = "UT"
revision = "6b2a58267f6a8b1dc8e2eb5519b984008fa85e8c"
version = "v2.15.0"
[[projects]]
digest = "1:8f5416c7f59da8600725ae1ff00a99af1da8b04c211ae6f3c8f8bcab0164f650"
name = "github.com/aokoli/goutils"
packages = ["."]
pruneopts = "UT"
revision = "3391d3790d23d03408670993e957e8f408993c34"
version = "v1.0.1"
[[projects]]
digest = "1:8f8811f9be822914c3a25c6a071e93beb4c805d7b026cbf298bc577bc1cc945b"
name = "github.com/google/uuid"
packages = ["."]
pruneopts = "UT"
revision = "064e2069ce9c359c118179501254f67d7d37ba24"
version = "0.2"
[[projects]]
digest = "1:6b1e7618931d0ed9a17fec1c1d84271907aa61172c50b8272327f6cd4fafb650"
name = "github.com/huandu/xstrings"
packages = ["."]
pruneopts = "UT"
revision = "2bf18b218c51864a87384c06996e40ff9dcff8e1"
version = "v1.0.0"
[[projects]]
digest = "1:8eb1de8112c9924d59bf1d3e5c26f5eaa2bfc2a5fcbb92dc1c2e4546d695f277"
name = "github.com/imdario/mergo"
packages = ["."]
pruneopts = "UT"
revision = "9f23e2d6bd2a77f959b2bf6acdbefd708a83a4a4"
version = "v0.3.6"
[[projects]]
digest = "1:55d0f7127962b0657acfd69650634057b3a08958226feee1b64a379666bb915e"
name = "github.com/urfave/cli"
packages = ["."]
pruneopts = "UT"
revision = "6011f165dc288c72abd8acd7722f837c5c64198d"
[[projects]]
digest = "1:3c1a69cdae3501bf75e76d0d86dc6f2b0a7421bc205c0cb7b96b19eed464a34d"
name = "go.uber.org/atomic"
packages = ["."]
pruneopts = "UT"
revision = "1ea20fb1cbb1cc08cbd0d913a96dead89aa18289"
version = "v1.3.2"
[[projects]]
digest = "1:60bf2a5e347af463c42ed31a493d817f8a72f102543060ed992754e689805d1a"
name = "go.uber.org/multierr"
packages = ["."]
pruneopts = "UT"
revision = "3c4937480c32f4c13a875a1829af76c98ca3d40a"
version = "v1.1.0"
[[projects]]
digest = "1:9580b1b079114140ade8cec957685344d14f00119e0241f6b369633cb346eeb3"
name = "go.uber.org/zap"
packages = [
".",
"buffer",
"internal/bufferpool",
"internal/color",
"internal/exit",
"zapcore",
]
pruneopts = "UT"
revision = "eeedf312bc6c57391d84767a4cd413f02a917974"
version = "v1.8.0"
[[projects]]
branch = "master"
digest = "1:7b1c378a71f617ec07229a1fcbb90b6a5344295823e1d732398f50fd9ae4bbec"
name = "golang.org/x/crypto"
packages = [
"pbkdf2",
"scrypt",
]
pruneopts = "UT"
revision = "b2aa35443fbc700ab74c586ae79b81c171851023"
[[projects]]
branch = "v2"
digest = "1:342378ac4dcb378a5448dd723f0784ae519383532f5e70ade24132c4c8693202"
name = "gopkg.in/yaml.v2"
packages = ["."]
pruneopts = "UT"
revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
input-imports = [
"github.com/Masterminds/sprig",
"github.com/imdario/mergo",
"github.com/urfave/cli",
"go.uber.org/zap",
"go.uber.org/zap/zapcore",
"gopkg.in/yaml.v2",
]
solver-name = "gps-cdcl"
solver-version = 1
-15
View File
@@ -1,15 +0,0 @@
[[constraint]]
name = "github.com/Masterminds/sprig"
version = "2.15.0"
[[constraint]]
branch = "v2"
name = "gopkg.in/yaml.v2"
[prune]
go-tests = true
unused-packages = true
[[constraint]]
name = "github.com/imdario/mergo"
version = "0.3.6"
+2 -1
View File
@@ -42,7 +42,8 @@ clean:
.PHONY: clean
pristine: generate fmt
git ls-files --exclude-standard --modified --deleted --others | diff /dev/null -
git diff | cat
git ls-files --exclude-standard --modified --deleted --others -x vendor | grep -v '^go.' | diff /dev/null -
.PHONY: pristine
release: pristine cross
+71 -4
View File
@@ -3,6 +3,7 @@
Deploy Kubernetes Helm Charts
[![Docker Repository on Quay](https://quay.io/repository/roboll/helmfile/status "Docker Repository on Quay")](https://quay.io/repository/roboll/helmfile)
[![Slack Community #helmfile](https://slack.sweetops.com/badge.svg)](https://slack.sweetops.com)
## status
@@ -42,6 +43,7 @@ repositories:
#default values to set for args along with dedicated keys that can be set by contributers, cli args take precedence over these
helmDefaults:
tillerNamespace: tiller-namespace #dedicated default key for tiller-namespace
tillerless: false #dedicated default key for tillerless
kubeContext: kube-context #dedicated default key for kube-context (--kube-context)
# additional and global args passed to helm
args:
@@ -69,6 +71,7 @@ releases:
foo: bar
chart: roboll/vault-secret-manager # the chart being installed to create this release, referenced by `repository/chart` syntax
version: ~1.24.1 # the semver of the chart. range constraint is supported
missingFileHandler: warn # set to either "Error" or "Warn". "Error" instructs helmfile to fail when unable to find a values or secrets file. When "Warn", it prints the file and continues.
values:
# value files passed via --values
- vault.yaml
@@ -109,6 +112,10 @@ releases:
installed: true
# restores previous state in case of failed release
atomic: true
# name of the tiller namespace
tillerNamespace: vault
# if true, will use the helm-tiller plugin
tillerless: false
# enable TLS for request to Tiller
tls: true
# path to TLS CA certificate file (default "$HELM_HOME/ca.pem")
@@ -144,7 +151,7 @@ Environment variables can be used in most places for templating the helmfile. Cu
Examples:
```yaml
respositories:
repositories:
- name: your-private-git-repo-hosted-charts
url: https://{{ requiredEnv "GITHUB_TOKEN"}}@raw.githubusercontent.com/kmzfs/helm-repo-in-github/master/
```
@@ -254,6 +261,20 @@ dependencies of any referenced local charts.
For Helm 2.9+ you can use a username and password to authenticate to a remote repository.
### deps
The `helmfile deps` sub-command locks your helmfile state and local charts dependencies.
It basically runs `helm dependency update` on your helmfile state file and all the referenced local charts, so that you get a "lock" file per each helmfile state or local chart.
All the other `helmfile` sub-commands like `sync` use chart versions recorded in the lock files, so that e.g. untested chart versions won't suddenly get deployed to the production environment.
For example, the lock file for a helmfile state file named `helmfile.1.yaml` will be `helmfile.1.lock`. The lock file for a local chart would be `requirements.lock`, which is the same as `helm`.
It is recommended to version-control all the lock files, so that they can be used in the production deployment pipeline for extra reproducibility.
To bring in chart updates systematically, it would also be a good idea to run `helmfile deps` regularly, test it, and then update the lock files in the version-control system.
### diff
The `helmfile diff` sub-command executes the [helm-diff](https://github.com/databus23/helm-diff) plugin across all of
@@ -285,6 +306,7 @@ The `helmfile delete` sub-command deletes all the releases defined in the manife
Note that `delete` doesn't purge releases. So `helmfile delete && helmfile sync` results in sync failed due to that releases names are not deleted but preserved for future references. If you really want to remove releases for reuse, add `--purge` flag to run it like `helmfile delete --purge`.
### secrets
The `secrets` parameter in a `helmfile.yaml` causes the [helm-secrets](https://github.com/futuresimple/helm-secrets) plugin to be executed to decrypt the file.
@@ -325,7 +347,7 @@ The `selector` parameter can be specified multiple times. Each parameter is reso
`--selector tier=frontend --selector tier=backend` will select all the charts
In addition to user supplied labels the name, namespace, and chart are available to be used as selectors. The chart will just be the chart name excluding the repository (Example `stable/filebeat` would be selected using `--selector chart=filebeat`).
In addition to user supplied labels, the name, the namespace, and the chart are available to be used as selectors. The chart will just be the chart name excluding the repository (Example `stable/filebeat` would be selected using `--selector chart=filebeat`).
## Templates
@@ -556,6 +578,16 @@ Then the environment secret `foo.bar` can be referenced by the below template ex
{{ .Environment.Values.foo.bar }}
```
## Tillerless
With the [helm-tiller](https://github.com/rimusz/helm-tiller) plugin installed, you can work without tiller installed.
To enable this mode, you need to define `tillerless: true` and set the `tillerNamespace` in the `helmDefaults` section
or in the `releases` entries.
Since every commands is run with `helm tiller run ...`, you have to disable concurrency. Otherwise you'll get
mysterious errors about the tiller daemon.
## Separating helmfile.yaml into multiple independent files
Once your `helmfile.yaml` got to contain too many releases,
@@ -626,6 +658,27 @@ Just run `helmfile sync` inside `myteam/`, and you are done.
All the files are sorted alphabetically per group = array item inside `helmfiles:`, so that you have granular control over ordering, too.
#### selectors
When composing helmfiles you can use selectors from the command line as well as explicit selectors inside the parent helmfile to filter the releases to be used.
```yaml
helmfiles:
- apps/*/helmfile.yaml
- path: apps/a-helmfile.yaml
selectors: # list of selectors
- name=prometheus
- tier=frontend
- path: apps/b-helmfile.yaml # no selector, so all releases are used
selectors: []
- path: apps/c-helmfile.yaml # parent selector to be used or cli selector for the initial helmfile
selectorsInherited: true
```
* When a selector is specified, only this selector applies and the parents or CLI selectors are ignored.
* When not selector is specified there are 2 modes for the selector inheritance because we would like to change the current inheritance behavior (see [issue #344](https://github.com/roboll/helmfile/issues/344) ).
* Legacy mode, sub-helmfiles without selectors inherit selectors from their parent helmfile. The initial helmfiles inherit from the command line selectors.
* explicit mode, sub-helmfile without selectors do not inherit from their parent or the CLI selector. If you want them to inherit from their parent selector then use `selectorsInherited: true`. To enable this explicit mode you need to set the following environment variable `HELMFILE_EXPERIMENTAL=explicit-selector-inheritance` (see [experimental](#experimental-features)).
* Using `selector: []` will select all releases regardless of the parent selector or cli for the initial helmfile
* using `selectorsInherited: true` make the sub-helmfile selects releases with the parent selector or the cli for the initial helmfile. You cannot specify an explicit selector while using `selectorsInherited: true`
## Importing values from any source
The `exec` template function that is available in `values.yaml.gotmpl` is useful for importing values from any source
@@ -661,12 +714,18 @@ Once `events` are triggered, associated `hooks` are executed, by running the `co
Currently supported `events` are:
- `prepare`
- `presync`
- `postsync`
- `cleanup`
Hooks associated to `prepare` events are triggered after each release in your helmfile is loaded from YAML, before execution.
Hooks associated to `cleanup` events are triggered after each release is processed.
Hooks associated to `presync` events are triggered before each release is applied to the remote cluster. This is the ideal event to execute any commands that may mutate the cluster state as it will not be run for read-only operations like `lint`, `diff` or `template`.
Hooks associated to `postsync` events are triggered after each release is applied to the remote cluster. This is the ideal event to execute any commands that may mutate the cluster state as it will not be run for read-only operations like `lint`, `diff` or `template`.
The following is an example hook that just prints the contextual information provided to hook:
```
@@ -758,9 +817,11 @@ Use the [Helmfile Best Practices Guide](/docs/writing-helmfile.md) to write adva
- Default values
- Layering
We also have dedicated documentations for the following topics you might be interested:
We also have dedicated documentation on the following topics which might interest you:
- [Shared Configurations Across Teams](/docs/shared-configurations-across-teams.md)
- [Shared Configurations Across Teams](/docs/shared-configuration-across-teams.md)
Or join our friendly slack community in the [`#helmfile`](https://slack.sweetops.com) channel to ask questions and get help. Check out our [slack archive](https://archive.sweetops.com/helmfile/) for good examples of how others are using it.
## Using env files
@@ -786,6 +847,12 @@ Once you download all required charts into your machine, you can run `helmfile c
It basically run only `helm upgrade --install` with your already-downloaded charts, hence no Internet connection is required.
See #155 for more information on this topic.
## Experimental features
Some experimental features may be available for testing in perspective of being (or not) included in a future release.
Those features are set using the environment variable `HELMFILE_EXPERIMENTAL`. Here is the current experimental feature :
* `explicit-selector-inheritance` : remove today implicit cli selectors inheritance for composed helmfiles, see [composition selector](#selectors)
If you want to enable all experimental features set the env var to `HELMFILE_EXPERIMENTAL=true`
## Examples
+4 -10
View File
@@ -7,9 +7,7 @@ import (
"github.com/roboll/helmfile/state"
"github.com/urfave/cli"
"go.uber.org/zap"
"os/exec"
"strings"
"syscall"
)
func VisitAllDesiredStates(c *cli.Context, converge func(*state.HelmState, helmexec.Interface, app.Context) (bool, []error)) error {
@@ -27,7 +25,7 @@ func VisitAllDesiredStates(c *cli.Context, converge func(*state.HelmState, helme
return converge(st, helm, ctx)
}
err = a.VisitDesiredStates(fileOrDir, convergeWithHelmBinary)
err = a.VisitDesiredStates(fileOrDir, a.Selectors, convergeWithHelmBinary)
return toCliError(err)
}
@@ -86,14 +84,10 @@ func toCliError(err error) error {
switch e := err.(type) {
case *app.NoMatchingHelmfileError:
return cli.NewExitError(e.Error(), 2)
case *exec.ExitError:
// Propagate any non-zero exit status from the external command like `helm` that is failed under the hood
status := e.Sys().(syscall.WaitStatus)
return cli.NewExitError(e.Error(), status.ExitStatus())
case *state.DiffError:
return cli.NewExitError(e.Error(), e.Code)
case *app.Error:
return cli.NewExitError(e.Error(), e.Code())
default:
return cli.NewExitError(e.Error(), 1)
panic(fmt.Errorf("BUG: please file an github issue for this unhandled error: %T: %v", e, e))
}
}
return err
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"github.com/urfave/cli"
)
func Deps(a *app.App) cli.Command {
func Deps() cli.Command {
return cli.Command{
Name: "deps",
Usage: "update charts based on the contents of requirements.yaml",
+128 -7
View File
@@ -91,7 +91,9 @@ releases:
See the [issue 428](https://github.com/roboll/helmfile/issues/428) for more context on how this is supposed to work.
## Layering
## Layering State Files
> See **Layering State Template Files** if you're layering templates.
You may occasionally end up with many helmfiles that shares common parts like which repositories to use, and whichi release to be bundled by default.
@@ -100,10 +102,10 @@ Use Layering to extract the common parts into a dedicated *library helmfile*s, s
Let's assume that your `helmfile.yaml` looks like:
```
{ readFile "commons.yaml" }}
---
{{ readFile "environments.yaml" }}
---
bases:
- commons.yaml
- environments.yaml
releases:
- name: myapp
chart: mychart
@@ -125,23 +127,26 @@ environments:
production:
```
At run time, template expressions in your `helmfile.yaml` are executed:
At run time, `bases` in your `helmfile.yaml` are evaluated to produce:
```yaml
# commons.yaml
releases:
- name: metricbaet
chart: stable/metricbeat
---
# environments.yaml
environments:
development:
production:
---
# helmfile.yaml
releases:
- name: myapp
chart: mychart
```
Resulting YAML documents are merged in the order of occurrence,
Finally the resulting YAML documents are merged in the order of occurrence,
so that your `helmfile.yaml` becomes:
```yaml
@@ -159,3 +164,119 @@ releases:
Great!
Now, repeat the above steps for each your `helmfile.yaml`, so that all your helmfiles becomes DRY.
Please also see [the discussion in the issue 388](https://github.com/roboll/helmfile/issues/388#issuecomment-491710348) for more advanced layering examples.
## Layering State Template Files
Do you need to make your state file even more DRY?
Turned out layering state files wasn't enough for you?
Helmfile supports an advanced feature that allows you to compose state "template" files to generate the final state to be processed.
In the following example `helmfile.yaml.gotmpl`, each `---` separated part of the file is a go template.
`helmfile.yaml.gotmpl`:
```yaml
# Part 1: Reused Enviroment Values
bases:
- myenv.yaml
---
# Part 2: Reused Defaults
bases:
- mydefaults.yaml
---
# Part 3: Dynamic Releases
releases:
- name: test1
chart: mychart-{{ .Environment.Values.myname }}
values:
replicaCount: 1
image:
repository: "nginx"
tag: "latest"
```
Suppose the `myenv.yaml` and `test.env.yaml` loaded in the first part looks like:
`myenv.yaml`:
```yaml
environments:
test:
values:
- test.env.yaml
```
`test.env.yaml`:
```yaml
kubeContext: test
wait: false
cvOnly: false
myname: "dog"
```
Where the gotmpl file loaded in the second part looks like:
`mydefaults.yaml.gotmpl`:
```yaml
helmDefaults:
tillerNamespace: kube-system
kubeContext: {{ .Environment.Values.kubeContext }}
verify: false
{{ if .Environment.Values.wait }}
wait: true
{{ else }}
wait: false
{{ end }}
timeout: 600
recreatePods: false
force: true
```
Each go template is rendered in the context where `.Environment.Values` is inherited from the previous part.
So in `mydefaults.yaml.gotmpl`, both `.Environment.Values.kubeContext` and `.Environment.Values.wait` are valid as they do exist in the environment values inherited from the previous part(=the first part) of your `helmfile.yaml.gotmpl`, and therefore the template is rendered to:
```yaml
helmDefaults:
tillerNamespace: kube-system
kubeContext: test
verify: false
wait: false
timeout: 600
recreatePods: false
force: true
```
Similarly, the third part of the top-level `helmfile.yaml.gotmpl`, `.Environment.Values.myname` is valid as it is included in the environment values inherited from the previous parts:
```yaml
# Part 3: Dynamic Releases
releases:
- name: test1
chart: mychart-{{ .Environment.Values.myname }}
values:
replicaCount: 1
image:
repository: "nginx"
tag: "latest"
````
hence rendered to:
```yaml
# Part 3: Dynamic Releases
releases:
- name: test1
chart: mychart-dog
values:
replicaCount: 1
image:
repository: "nginx"
tag: "latest"
```
+1 -1
View File
@@ -88,7 +88,7 @@ func (bus *Bus) Trigger(evt string, context map[string]interface{}) (bool, error
}
}
bytes, err := bus.Runner.Execute(command, args)
bytes, err := bus.Runner.Execute(command, args, map[string]string{})
bus.Logger.Debugf("hook[%s]: %s\n", name, string(bytes))
if err != nil {
+1 -1
View File
@@ -13,7 +13,7 @@ var logger = helmexec.NewLogger(os.Stdout, "warn")
type runner struct {
}
func (r *runner) Execute(cmd string, args []string) ([]byte, error) {
func (r *runner) Execute(cmd string, args []string, env map[string]string) ([]byte, error) {
if cmd == "ng" {
return nil, fmt.Errorf("cmd failed due to invalid cmd: %s", cmd)
}
+23
View File
@@ -0,0 +1,23 @@
module github.com/roboll/helmfile
go 1.12
require (
github.com/Masterminds/semver v1.4.1
github.com/Masterminds/sprig v2.15.0+incompatible
github.com/aokoli/goutils v1.0.1 // indirect
github.com/google/go-cmp v0.3.0 // indirect
github.com/google/uuid v0.0.0-20161128191214-064e2069ce9c // indirect
github.com/huandu/xstrings v1.0.0 // indirect
github.com/imdario/mergo v0.3.6
github.com/mattn/go-runewidth v0.0.4 // indirect
github.com/pkg/errors v0.8.1 // indirect
github.com/tatsushid/go-prettytable v0.0.0-20141013043238-ed2d14c29939
github.com/urfave/cli v0.0.0-20160620154522-6011f165dc28
go.uber.org/atomic v1.3.2 // indirect
go.uber.org/multierr v1.1.0 // indirect
go.uber.org/zap v1.8.0
golang.org/x/crypto v0.0.0-20180403160946-b2aa35443fbc // indirect
gopkg.in/yaml.v2 v2.2.1
gotest.tools v2.2.0+incompatible
)
+35
View File
@@ -0,0 +1,35 @@
github.com/Masterminds/semver v1.4.1 h1:CaDA1wAoM3rj9sAFyyZP37LloExUzxFGYt+DqJ870JA=
github.com/Masterminds/semver v1.4.1/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/Masterminds/sprig v2.15.0+incompatible h1:0gSxPGWS9PAr7U2NsQ2YQg6juRDINkUyuvbb4b2Xm8w=
github.com/Masterminds/sprig v2.15.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
github.com/aokoli/goutils v1.0.1 h1:7fpzNGoJ3VA8qcrm++XEE1QUe0mIwNeLa02Nwq7RDkg=
github.com/aokoli/goutils v1.0.1/go.mod h1:SijmP0QR8LtwsmDs8Yii5Z/S4trXFGFC2oO5g9DP+DQ=
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/uuid v0.0.0-20161128191214-064e2069ce9c h1:jWtZjFEUE/Bz0IeIhqCnyZ3HG6KRXSntXe4SjtuTH7c=
github.com/google/uuid v0.0.0-20161128191214-064e2069ce9c/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/huandu/xstrings v1.0.0 h1:pO2K/gKgKaat5LdpAhxhluX2GPQMaI3W5FUz/I/UnWk=
github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo=
github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28=
github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/tatsushid/go-prettytable v0.0.0-20141013043238-ed2d14c29939 h1:BhIUXV2ySTLrKgh/Hnts+QTQlIbWtomXt3LMdzME0A0=
github.com/tatsushid/go-prettytable v0.0.0-20141013043238-ed2d14c29939/go.mod h1:omGxs4/6hNjxPKUTjmaNkPzehSnNJOJN6pMEbrlYIT4=
github.com/urfave/cli v0.0.0-20160620154522-6011f165dc28 h1:Yf7/DcGM+61oLBGXQV2Q+ztEGBcOe3EUnbKSOn4fQuE=
github.com/urfave/cli v0.0.0-20160620154522-6011f165dc28/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.8.0 h1:r6Za1Rii8+EGOYRDLvpooNOF6kP3iyDnkpzbw67gCQ8=
go.uber.org/zap v1.8.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20180403160946-b2aa35443fbc h1:Kx1Ke+iCR1aDjbWXgmEQGFxoHtNL49aRZGV7/+jJ41Y=
golang.org/x/crypto v0.0.0-20180403160946-b2aa35443fbc/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
+44
View File
@@ -0,0 +1,44 @@
package helmexec
import (
"os"
"path/filepath"
)
type HelmContext struct {
Tillerless bool
TillerNamespace string
WorkerIndex int
}
func (context *HelmContext) GetTillerlessArgs(helmBinary string) []string {
if context.Tillerless {
if context.TillerNamespace != "" {
return []string{"tiller", "run", context.TillerNamespace, "--", helmBinary}
} else {
return []string{"tiller", "run", "--", helmBinary}
}
} else {
return []string{}
}
}
func (context *HelmContext) getTillerlessEnv() map[string]string {
if context.Tillerless {
result := map[string]string{
"HELM_TILLER_SILENT": "true",
// Changing the TILLER port doesn't really work: https://github.com/helm/helm/issues/3159
// So this is not used for the moment.
// "HELM_TILLER_PORT": strconv.Itoa(44134 + context.WorkerIndex),
}
if config := os.Getenv("KUBECONFIG"); config != "" {
absConfig, err := filepath.Abs(config)
if err == nil {
result["KUBECONFIG"] = absConfig
}
}
return result
} else {
return map[string]string{}
}
}
+76 -32
View File
@@ -1,11 +1,11 @@
package helmexec
import (
"fmt"
"io"
"io/ioutil"
"os"
"strings"
"sync"
"go.uber.org/zap"
@@ -48,7 +48,9 @@ func New(logger *zap.SugaredLogger, kubeContext string) *execer {
helmBinary: command,
logger: logger,
kubeContext: kubeContext,
runner: &ShellRunner{},
runner: &ShellRunner{
logger: logger,
},
}
}
@@ -70,60 +72,68 @@ func (helm *execer) AddRepo(name, repository, certfile, keyfile, username, passw
args = append(args, "--username", username, "--password", password)
}
helm.logger.Infof("Adding repo %v %v", name, repository)
out, err := helm.exec(args...)
helm.write(out)
out, err := helm.exec(args, map[string]string{})
helm.info(out)
return err
}
func (helm *execer) UpdateRepo() error {
helm.logger.Info("Updating repo")
out, err := helm.exec("repo", "update")
helm.write(out)
out, err := helm.exec([]string{"repo", "update"}, map[string]string{})
helm.info(out)
return err
}
func (helm *execer) UpdateDeps(chart string) error {
helm.logger.Infof("Updating dependency %v", chart)
out, err := helm.exec("dependency", "update", chart)
helm.write(out)
out, err := helm.exec([]string{"dependency", "update", chart}, map[string]string{})
helm.info(out)
return err
}
func (helm *execer) BuildDeps(chart string) error {
helm.logger.Infof("Building dependency %v", chart)
out, err := helm.exec("dependency", "build", chart)
out, err := helm.exec([]string{"dependency", "build", chart}, map[string]string{})
helm.write(out)
return err
}
func (helm *execer) SyncRelease(name, chart string, flags ...string) error {
func (helm *execer) SyncRelease(context HelmContext, name, chart string, flags ...string) error {
helm.logger.Infof("Upgrading %v", chart)
out, err := helm.exec(append([]string{"upgrade", "--install", "--reset-values", name, chart}, flags...)...)
preArgs := context.GetTillerlessArgs(helm.helmBinary)
env := context.getTillerlessEnv()
out, err := helm.exec(append(append(preArgs, "upgrade", "--install", "--reset-values", name, chart), flags...), env)
helm.write(out)
return err
}
func (helm *execer) ReleaseStatus(name string) error {
func (helm *execer) ReleaseStatus(context HelmContext, name string, flags ...string) error {
helm.logger.Infof("Getting status %v", name)
out, err := helm.exec(append([]string{"status", name})...)
preArgs := context.GetTillerlessArgs(helm.helmBinary)
env := context.getTillerlessEnv()
out, err := helm.exec(append(append(preArgs, "status", name), flags...), env)
helm.write(out)
return err
}
func (helm *execer) List(filter string) (string, error) {
func (helm *execer) List(context HelmContext, filter string, flags ...string) (string, error) {
helm.logger.Infof("Listing releases matching %v", filter)
out, err := helm.exec(append([]string{"list", filter})...)
preArgs := context.GetTillerlessArgs(helm.helmBinary)
env := context.getTillerlessEnv()
out, err := helm.exec(append(append(preArgs, "list", filter), flags...), env)
helm.write(out)
return string(out), err
}
func (helm *execer) DecryptSecret(name string) (string, error) {
func (helm *execer) DecryptSecret(context HelmContext, name string, flags ...string) (string, error) {
// Prevents https://github.com/roboll/helmfile/issues/258
helm.decryptionMutex.Lock()
defer helm.decryptionMutex.Unlock()
helm.logger.Infof("Decrypting secret %v", name)
out, err := helm.exec(append([]string{"secrets", "dec", name})...)
preArgs := context.GetTillerlessArgs(helm.helmBinary)
env := context.getTillerlessEnv()
out, err := helm.exec(append(append(preArgs, "secrets", "dec", name), flags...), env)
helm.write(out)
if err != nil {
return "", err
@@ -162,47 +172,72 @@ func (helm *execer) DecryptSecret(name string) (string, error) {
}
func (helm *execer) TemplateRelease(chart string, flags ...string) error {
out, err := helm.exec(append([]string{"template", chart}, flags...)...)
out, err := helm.exec(append([]string{"template", chart}, flags...), map[string]string{})
helm.write(out)
return err
}
func (helm *execer) DiffRelease(name, chart string, flags ...string) error {
func (helm *execer) DiffRelease(context HelmContext, name, chart string, flags ...string) error {
helm.logger.Infof("Comparing %v %v", name, chart)
out, err := helm.exec(append([]string{"diff", "upgrade", "--allow-unreleased", name, chart}, flags...)...)
helm.write(out)
preArgs := context.GetTillerlessArgs(helm.helmBinary)
env := context.getTillerlessEnv()
out, err := helm.exec(append(append(preArgs, "diff", "upgrade", "--allow-unreleased", name, chart), flags...), env)
// Do our best to write STDOUT only when diff existed
// Unfortunately, this works only when you run helmfile with `--detailed-exitcode`
detailedExitcodeEnabled := false
for _, f := range flags {
if strings.Contains(f, "detailed-exitcode") {
detailedExitcodeEnabled = true
break
}
}
if detailedExitcodeEnabled {
switch e := err.(type) {
case ExitError:
if e.ExitStatus() == 2 {
helm.write(out)
return err
}
}
} else {
helm.write(out)
}
return err
}
func (helm *execer) Lint(chart string, flags ...string) error {
helm.logger.Infof("Linting %v", chart)
out, err := helm.exec(append([]string{"lint", chart}, flags...)...)
out, err := helm.exec(append([]string{"lint", chart}, flags...), map[string]string{})
helm.write(out)
return err
}
func (helm *execer) Fetch(chart string, flags ...string) error {
helm.logger.Infof("Fetching %v", chart)
out, err := helm.exec(append([]string{"fetch", chart}, flags...)...)
helm.write(out)
out, err := helm.exec(append([]string{"fetch", chart}, flags...), map[string]string{})
helm.info(out)
return err
}
func (helm *execer) DeleteRelease(name string, flags ...string) error {
func (helm *execer) DeleteRelease(context HelmContext, name string, flags ...string) error {
helm.logger.Infof("Deleting %v", name)
out, err := helm.exec(append([]string{"delete", name}, flags...)...)
preArgs := context.GetTillerlessArgs(helm.helmBinary)
env := context.getTillerlessEnv()
out, err := helm.exec(append(append(preArgs, "delete", name), flags...), env)
helm.write(out)
return err
}
func (helm *execer) TestRelease(name string, flags ...string) error {
func (helm *execer) TestRelease(context HelmContext, name string, flags ...string) error {
helm.logger.Infof("Testing %v", name)
out, err := helm.exec(append([]string{"test", name}, flags...)...)
preArgs := context.GetTillerlessArgs(helm.helmBinary)
env := context.getTillerlessEnv()
out, err := helm.exec(append(append(preArgs, "test", name), flags...), env)
helm.write(out)
return err
}
func (helm *execer) exec(args ...string) ([]byte, error) {
func (helm *execer) exec(args []string, env map[string]string) ([]byte, error) {
cmdargs := args
if len(helm.extra) > 0 {
cmdargs = append(cmdargs, helm.extra...)
@@ -210,12 +245,21 @@ func (helm *execer) exec(args ...string) ([]byte, error) {
if helm.kubeContext != "" {
cmdargs = append(cmdargs, "--kube-context", helm.kubeContext)
}
helm.logger.Debugf("exec: %s %s", helm.helmBinary, strings.Join(cmdargs, " "))
return helm.runner.Execute(helm.helmBinary, cmdargs)
cmd := fmt.Sprintf("exec: %s %s", helm.helmBinary, strings.Join(cmdargs, " "))
helm.logger.Debug(cmd)
bytes, err := helm.runner.Execute(helm.helmBinary, cmdargs, env)
helm.logger.Debugf("%s: %s", cmd, bytes)
return bytes, err
}
func (helm *execer) write(out []byte) {
func (helm *execer) info(out []byte) {
if len(out) > 0 {
helm.logger.Infof("%s", out)
}
}
func (helm *execer) write(out []byte) {
if len(out) > 0 {
fmt.Printf("%s\n", out)
}
}
+118 -22
View File
@@ -3,6 +3,7 @@ package helmexec
import (
"bytes"
"os"
"path"
"reflect"
"testing"
@@ -16,7 +17,7 @@ type mockRunner struct {
err error
}
func (mock *mockRunner) Execute(cmd string, args []string) ([]byte, error) {
func (mock *mockRunner) Execute(cmd string, args []string, env map[string]string) ([]byte, error) {
return []byte{}, nil
}
@@ -77,6 +78,7 @@ func Test_AddRepo(t *testing.T) {
helm.AddRepo("myRepo", "https://repo.example.com/", "cert.pem", "key.pem", "", "")
expected := `Adding repo myRepo https://repo.example.com/
exec: helm repo add myRepo https://repo.example.com/ --cert-file cert.pem --key-file key.pem --kube-context dev
exec: helm repo add myRepo https://repo.example.com/ --cert-file cert.pem --key-file key.pem --kube-context dev:
`
if buffer.String() != expected {
t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
@@ -86,6 +88,7 @@ exec: helm repo add myRepo https://repo.example.com/ --cert-file cert.pem --key-
helm.AddRepo("myRepo", "https://repo.example.com/", "", "", "", "")
expected = `Adding repo myRepo https://repo.example.com/
exec: helm repo add myRepo https://repo.example.com/ --kube-context dev
exec: helm repo add myRepo https://repo.example.com/ --kube-context dev:
`
if buffer.String() != expected {
t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
@@ -95,6 +98,7 @@ exec: helm repo add myRepo https://repo.example.com/ --kube-context dev
helm.AddRepo("myRepo", "https://repo.example.com/", "", "", "example_user", "example_password")
expected = `Adding repo myRepo https://repo.example.com/
exec: helm repo add myRepo https://repo.example.com/ --username example_user --password example_password --kube-context dev
exec: helm repo add myRepo https://repo.example.com/ --username example_user --password example_password --kube-context dev:
`
if buffer.String() != expected {
t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
@@ -108,6 +112,7 @@ func Test_UpdateRepo(t *testing.T) {
helm.UpdateRepo()
expected := `Updating repo
exec: helm repo update --kube-context dev
exec: helm repo update --kube-context dev:
`
if buffer.String() != expected {
t.Errorf("helmexec.UpdateRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
@@ -118,18 +123,35 @@ func Test_SyncRelease(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "dev")
helm.SyncRelease("release", "chart", "--timeout 10", "--wait")
helm.SyncRelease(HelmContext{}, "release", "chart", "--timeout 10", "--wait")
expected := `Upgrading chart
exec: helm upgrade --install --reset-values release chart --timeout 10 --wait --kube-context dev
exec: helm upgrade --install --reset-values release chart --timeout 10 --wait --kube-context dev:
`
if buffer.String() != expected {
t.Errorf("helmexec.SyncRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
buffer.Reset()
helm.SyncRelease("release", "chart")
helm.SyncRelease(HelmContext{}, "release", "chart")
expected = `Upgrading chart
exec: helm upgrade --install --reset-values release chart --kube-context dev
exec: helm upgrade --install --reset-values release chart --kube-context dev:
`
if buffer.String() != expected {
t.Errorf("helmexec.SyncRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
}
func Test_SyncReleaseTillerless(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "dev")
helm.SyncRelease(HelmContext{Tillerless: true, TillerNamespace: "foo"}, "release", "chart",
"--timeout 10", "--wait")
expected := `Upgrading chart
exec: helm tiller run foo -- helm upgrade --install --reset-values release chart --timeout 10 --wait --kube-context dev
exec: helm tiller run foo -- helm upgrade --install --reset-values release chart --timeout 10 --wait --kube-context dev:
`
if buffer.String() != expected {
t.Errorf("helmexec.SyncRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
@@ -143,6 +165,7 @@ func Test_UpdateDeps(t *testing.T) {
helm.UpdateDeps("./chart/foo")
expected := `Updating dependency ./chart/foo
exec: helm dependency update ./chart/foo --kube-context dev
exec: helm dependency update ./chart/foo --kube-context dev:
`
if buffer.String() != expected {
t.Errorf("helmexec.UpdateDeps()\nactual = %v\nexpect = %v", buffer.String(), expected)
@@ -153,6 +176,7 @@ exec: helm dependency update ./chart/foo --kube-context dev
helm.UpdateDeps("./chart/foo")
expected = `Updating dependency ./chart/foo
exec: helm dependency update ./chart/foo --verify --kube-context dev
exec: helm dependency update ./chart/foo --verify --kube-context dev:
`
if buffer.String() != expected {
t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
@@ -166,6 +190,7 @@ func Test_BuildDeps(t *testing.T) {
helm.BuildDeps("./chart/foo")
expected := `Building dependency ./chart/foo
exec: helm dependency build ./chart/foo --kube-context dev
exec: helm dependency build ./chart/foo --kube-context dev:
`
if buffer.String() != expected {
t.Errorf("helmexec.BuildDeps()\nactual = %v\nexpect = %v", buffer.String(), expected)
@@ -176,6 +201,7 @@ exec: helm dependency build ./chart/foo --kube-context dev
helm.BuildDeps("./chart/foo")
expected = `Building dependency ./chart/foo
exec: helm dependency build ./chart/foo --verify --kube-context dev
exec: helm dependency build ./chart/foo --verify --kube-context dev:
`
if buffer.String() != expected {
t.Errorf("helmexec.BuildDeps()\nactual = %v\nexpect = %v", buffer.String(), expected)
@@ -186,9 +212,10 @@ func Test_DecryptSecret(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "dev")
helm.DecryptSecret("secretName")
helm.DecryptSecret(HelmContext{}, "secretName")
expected := `Decrypting secret secretName
exec: helm secrets dec secretName --kube-context dev
exec: helm secrets dec secretName --kube-context dev:
`
if buffer.String() != expected {
t.Errorf("helmexec.DecryptSecret()\nactual = %v\nexpect = %v", buffer.String(), expected)
@@ -199,18 +226,34 @@ func Test_DiffRelease(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "dev")
helm.DiffRelease("release", "chart", "--timeout 10", "--wait")
helm.DiffRelease(HelmContext{}, "release", "chart", "--timeout 10", "--wait")
expected := `Comparing release chart
exec: helm diff upgrade --allow-unreleased release chart --timeout 10 --wait --kube-context dev
exec: helm diff upgrade --allow-unreleased release chart --timeout 10 --wait --kube-context dev:
`
if buffer.String() != expected {
t.Errorf("helmexec.DiffRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
buffer.Reset()
helm.DiffRelease("release", "chart")
helm.DiffRelease(HelmContext{}, "release", "chart")
expected = `Comparing release chart
exec: helm diff upgrade --allow-unreleased release chart --kube-context dev
exec: helm diff upgrade --allow-unreleased release chart --kube-context dev:
`
if buffer.String() != expected {
t.Errorf("helmexec.DiffRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
}
func Test_DiffReleaseTillerless(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "dev")
helm.DiffRelease(HelmContext{Tillerless: true}, "release", "chart", "--timeout 10", "--wait")
expected := `Comparing release chart
exec: helm tiller run -- helm diff upgrade --allow-unreleased release chart --timeout 10 --wait --kube-context dev
exec: helm tiller run -- helm diff upgrade --allow-unreleased release chart --timeout 10 --wait --kube-context dev:
`
if buffer.String() != expected {
t.Errorf("helmexec.DiffRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
@@ -221,9 +264,10 @@ func Test_DeleteRelease(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "dev")
helm.DeleteRelease("release")
helm.DeleteRelease(HelmContext{}, "release")
expected := `Deleting release
exec: helm delete release --kube-context dev
exec: helm delete release --kube-context dev:
`
if buffer.String() != expected {
t.Errorf("helmexec.DeleteRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
@@ -233,9 +277,10 @@ func Test_DeleteRelease_Flags(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "dev")
helm.DeleteRelease("release", "--purge")
helm.DeleteRelease(HelmContext{}, "release", "--purge")
expected := `Deleting release
exec: helm delete release --purge --kube-context dev
exec: helm delete release --purge --kube-context dev:
`
if buffer.String() != expected {
t.Errorf("helmexec.DeleteRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
@@ -246,9 +291,10 @@ func Test_TestRelease(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "dev")
helm.TestRelease("release")
helm.TestRelease(HelmContext{}, "release")
expected := `Testing release
exec: helm test release --kube-context dev
exec: helm test release --kube-context dev:
`
if buffer.String() != expected {
t.Errorf("helmexec.TestRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
@@ -258,9 +304,10 @@ func Test_TestRelease_Flags(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "dev")
helm.TestRelease("release", "--cleanup", "--timeout", "60")
helm.TestRelease(HelmContext{}, "release", "--cleanup", "--timeout", "60")
expected := `Testing release
exec: helm test release --cleanup --timeout 60 --kube-context dev
exec: helm test release --cleanup --timeout 60 --kube-context dev:
`
if buffer.String() != expected {
t.Errorf("helmexec.TestRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
@@ -271,9 +318,10 @@ func Test_ReleaseStatus(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "dev")
helm.ReleaseStatus("myRelease")
helm.ReleaseStatus(HelmContext{}, "myRelease")
expected := `Getting status myRelease
exec: helm status myRelease --kube-context dev
exec: helm status myRelease --kube-context dev:
`
if buffer.String() != expected {
t.Errorf("helmexec.ReleaseStatus()\nactual = %v\nexpect = %v", buffer.String(), expected)
@@ -284,37 +332,46 @@ func Test_exec(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "")
helm.exec("version")
expected := "exec: helm version\n"
env := map[string]string{}
helm.exec([]string{"version"}, env)
expected := `exec: helm version
exec: helm version:
`
if buffer.String() != expected {
t.Errorf("helmexec.exec()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
helm = MockExecer(logger, "dev")
ret, _ := helm.exec("diff")
ret, _ := helm.exec([]string{"diff"}, env)
if len(ret) != 0 {
t.Error("helmexec.exec() - expected empty return value")
}
buffer.Reset()
helm = MockExecer(logger, "dev")
helm.exec("diff", "release", "chart", "--timeout 10", "--wait")
expected = "exec: helm diff release chart --timeout 10 --wait --kube-context dev\n"
helm.exec([]string{"diff", "release", "chart", "--timeout 10", "--wait"}, env)
expected = `exec: helm diff release chart --timeout 10 --wait --kube-context dev
exec: helm diff release chart --timeout 10 --wait --kube-context dev:
`
if buffer.String() != expected {
t.Errorf("helmexec.exec()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
buffer.Reset()
helm.exec("version")
expected = "exec: helm version --kube-context dev\n"
helm.exec([]string{"version"}, env)
expected = `exec: helm version --kube-context dev
exec: helm version --kube-context dev:
`
if buffer.String() != expected {
t.Errorf("helmexec.exec()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
buffer.Reset()
helm.SetExtraArgs("foo")
helm.exec("version")
expected = "exec: helm version foo --kube-context dev\n"
helm.exec([]string{"version"}, env)
expected = `exec: helm version foo --kube-context dev
exec: helm version foo --kube-context dev:
`
if buffer.String() != expected {
t.Errorf("helmexec.exec()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
@@ -322,8 +379,10 @@ func Test_exec(t *testing.T) {
buffer.Reset()
helm = MockExecer(logger, "")
helm.SetHelmBinary("overwritten")
helm.exec("version")
expected = "exec: overwritten version\n"
helm.exec([]string{"version"}, env)
expected = `exec: overwritten version
exec: overwritten version:
`
if buffer.String() != expected {
t.Errorf("helmexec.exec()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
@@ -336,6 +395,7 @@ func Test_Lint(t *testing.T) {
helm.Lint("path/to/chart", "--values", "file.yml")
expected := `Linting path/to/chart
exec: helm lint path/to/chart --values file.yml --kube-context dev
exec: helm lint path/to/chart --values file.yml --kube-context dev:
`
if buffer.String() != expected {
t.Errorf("helmexec.Lint()\nactual = %v\nexpect = %v", buffer.String(), expected)
@@ -349,6 +409,7 @@ func Test_Fetch(t *testing.T) {
helm.Fetch("chart", "--version", "1.2.3", "--untar", "--untardir", "/tmp/dir")
expected := `Fetching chart
exec: helm fetch chart --version 1.2.3 --untar --untardir /tmp/dir --kube-context dev
exec: helm fetch chart --version 1.2.3 --untar --untardir /tmp/dir --kube-context dev:
`
if buffer.String() != expected {
t.Errorf("helmexec.Lint()\nactual = %v\nexpect = %v", buffer.String(), expected)
@@ -358,6 +419,7 @@ exec: helm fetch chart --version 1.2.3 --untar --untardir /tmp/dir --kube-contex
var logLevelTests = map[string]string{
"debug": `Adding repo myRepo https://repo.example.com/
exec: helm repo add myRepo https://repo.example.com/ --username example_user --password example_password
exec: helm repo add myRepo https://repo.example.com/ --username example_user --password example_password:
`,
"info": `Adding repo myRepo https://repo.example.com/
`,
@@ -376,3 +438,37 @@ func Test_LogLevels(t *testing.T) {
}
}
}
func Test_getTillerlessEnv(t *testing.T) {
context := HelmContext{Tillerless: true, TillerNamespace: "foo", WorkerIndex: 1}
os.Unsetenv("KUBECONFIG")
actual := context.getTillerlessEnv()
if val, found := actual["HELM_TILLER_SILENT"]; !found || val != "true" {
t.Errorf("getTillerlessEnv() HELM_TILLER_SILENT\nactual = %s\nexpect = true", val)
}
// This feature is disabled until it is fixed in helm
/*if val, found := actual["HELM_TILLER_PORT"]; !found || val != "44135" {
t.Errorf("getTillerlessEnv() HELM_TILLER_PORT\nactual = %s\nexpect = 44135", val)
}*/
if val, found := actual["KUBECONFIG"]; found {
t.Errorf("getTillerlessEnv() KUBECONFIG\nactual = %s\nexpect = nil", val)
}
os.Setenv("KUBECONFIG", "toto")
actual = context.getTillerlessEnv()
cwd, _ := os.Getwd()
expected := path.Join(cwd, "toto")
if val, found := actual["KUBECONFIG"]; !found || val != expected {
t.Errorf("getTillerlessEnv() KUBECONFIG\nactual = %s\nexpect = %s", val, expected)
}
os.Unsetenv("KUBECONFIG")
}
func Test_mergeEnv(t *testing.T) {
actual := env2map(mergeEnv([]string{"A=1", "B=c=d", "E=2"}, map[string]string{"B": "3", "F": "4"}))
expected := map[string]string{"A": "1", "B": "3", "E": "2", "F": "4"}
if !reflect.DeepEqual(actual, expected) {
t.Errorf("mergeEnv()\nactual = %v\nexpect = %v", actual, expected)
}
}
+36
View File
@@ -0,0 +1,36 @@
package helmexec
import (
"fmt"
"path/filepath"
"strings"
)
func newExitError(helmCmdPath string, exitStatus int, errorMessage string) ExitError {
return ExitError{
msg: fmt.Sprintf("%s exited with status %d:\n%s", filepath.Base(helmCmdPath), exitStatus, indent(strings.TrimSpace(errorMessage))),
exitStatus: exitStatus,
}
}
func indent(text string) string {
lines := strings.Split(text, "\n")
for i := range lines {
lines[i] = " " + lines[i]
}
return strings.Join(lines, "\n")
}
// ExitError is created whenever your shell command exits with a non-zero exit status
type ExitError struct {
msg string
exitStatus int
}
func (e ExitError) Error() string {
return e.msg
}
func (e ExitError) ExitStatus() int {
return e.exitStatus
}
+11 -7
View File
@@ -9,14 +9,18 @@ type Interface interface {
UpdateRepo() error
BuildDeps(chart string) error
UpdateDeps(chart string) error
SyncRelease(name, chart string, flags ...string) error
DiffRelease(name, chart string, flags ...string) error
SyncRelease(context HelmContext, name, chart string, flags ...string) error
DiffRelease(context HelmContext, name, chart string, flags ...string) error
TemplateRelease(chart string, flags ...string) error
Fetch(chart string, flags ...string) error
Lint(chart string, flags ...string) error
ReleaseStatus(name string) error
DeleteRelease(name string, flags ...string) error
TestRelease(name string, flags ...string) error
List(filter string) (string, error)
DecryptSecret(name string) (string, error)
ReleaseStatus(context HelmContext, name string, flags ...string) error
DeleteRelease(context HelmContext, name string, flags ...string) error
TestRelease(context HelmContext, name string, flags ...string) error
List(context HelmContext, filter string, flags ...string) (string, error)
DecryptSecret(context HelmContext, name string, flags ...string) (string, error)
}
type DependencyUpdater interface {
UpdateDeps(chart string) error
}
+75 -3
View File
@@ -1,7 +1,14 @@
package helmexec
import (
"bytes"
"errors"
"fmt"
"go.uber.org/zap"
"os"
"os/exec"
"strings"
"syscall"
)
const (
@@ -11,17 +18,82 @@ const (
// Runner interface for shell commands
type Runner interface {
Execute(cmd string, args []string) ([]byte, error)
Execute(cmd string, args []string, env map[string]string) ([]byte, error)
}
// ShellRunner implemention for shell commands
type ShellRunner struct {
Dir string
logger *zap.SugaredLogger
}
// Execute a shell command
func (shell ShellRunner) Execute(cmd string, args []string) ([]byte, error) {
func (shell ShellRunner) Execute(cmd string, args []string, env map[string]string) ([]byte, error) {
preparedCmd := exec.Command(cmd, args...)
preparedCmd.Dir = shell.Dir
return preparedCmd.CombinedOutput()
preparedCmd.Env = mergeEnv(os.Environ(), env)
return combinedOutput(preparedCmd, shell.logger)
}
func combinedOutput(c *exec.Cmd, logger *zap.SugaredLogger) ([]byte, error) {
if c.Stdout != nil {
return nil, errors.New("exec: Stdout already set")
}
if c.Stderr != nil {
return nil, errors.New("exec: Stderr already set")
}
var stdout bytes.Buffer
var stderr bytes.Buffer
c.Stdout = &stdout
c.Stderr = &stderr
err := c.Run()
o := stdout.Bytes()
e := stderr.Bytes()
if err != nil {
// TrimSpace is necessary, because otherwise helmfile prints the redundant new-lines after each error like:
//
// err: release "envoy2" in "helmfile.yaml" failed: exit status 1: Error: could not find a ready tiller pod
// <redundant new line!>
// err: release "envoy" in "helmfile.yaml" failed: exit status 1: Error: could not find a ready tiller pod
switch ee := err.(type) {
case *exec.ExitError:
// Propagate any non-zero exit status from the external command, rather than throwing it away,
// so that helmfile could return its own exit code accordingly
waitStatus := ee.Sys().(syscall.WaitStatus)
exitStatus := waitStatus.ExitStatus()
err = newExitError(c.Path, exitStatus, string(e))
default:
panic(fmt.Sprintf("unexpected error: %v", err))
}
}
return o, err
}
func mergeEnv(orig []string, new map[string]string) []string {
wanted := env2map(orig)
for k, v := range new {
wanted[k] = v
}
return map2env(wanted)
}
func map2env(wanted map[string]string) []string {
result := []string{}
for k, v := range wanted {
result = append(result, k+"="+v)
}
return result
}
func env2map(env []string) map[string]string {
wanted := map[string]string{}
for _, cur := range env {
pair := strings.SplitN(cur, "=", 2)
wanted[pair[0]] = pair[1]
}
return wanted
}
+56 -25
View File
@@ -30,7 +30,7 @@ func configureLogging(c *cli.Context) error {
if err != nil {
return err
}
logger = helmexec.NewLogger(os.Stdout, logLevel)
logger = helmexec.NewLogger(os.Stderr, logLevel)
if c.App.Metadata == nil {
// Auto-initialised in 1.19.0
// https://github.com/urfave/cli/blob/master/CHANGELOG.md#1190---2016-11-19
@@ -90,6 +90,7 @@ func main() {
cliApp.Before = configureLogging
cliApp.Commands = []cli.Command{
cmd.Deps(),
{
Name: "repos",
Usage: "sync repositories from state file (helm repo add && helm repo update)",
@@ -135,9 +136,12 @@ func main() {
},
},
Action: func(c *cli.Context) error {
return findAndIterateOverDesiredStatesUsingFlags(c, func(state *state.HelmState, helm helmexec.Interface, _ app.Context) []error {
return executeSyncCommand(c, state, helm)
affectedReleases := state.AffectedReleases{}
errs := findAndIterateOverDesiredStatesUsingFlags(c, func(st *state.HelmState, helm helmexec.Interface, _ app.Context) []error {
return executeSyncCommand(c, &affectedReleases, st, helm)
})
affectedReleases.DisplayAffectedReleases(c.App.Metadata["logger"].(*zap.SugaredLogger))
return errs
},
},
{
@@ -181,7 +185,7 @@ func main() {
return errs
}
}
if errs := state.PrepareRelease(helm, "diff"); errs != nil && len(errs) > 0 {
if errs := state.PrepareReleases(helm, "diff"); errs != nil && len(errs) > 0 {
return errs
}
@@ -223,7 +227,7 @@ func main() {
return errs
}
}
if errs := state.PrepareRelease(helm, "template"); errs != nil && len(errs) > 0 {
if errs := state.PrepareReleases(helm, "template"); errs != nil && len(errs) > 0 {
return errs
}
return executeTemplateCommand(c, state, helm)
@@ -248,16 +252,25 @@ func main() {
Value: 0,
Usage: "maximum number of concurrent downloads of release charts",
},
cli.BoolFlag{
Name: "skip-deps",
Usage: "skip running `helm repo update` and `helm dependency build`",
},
},
Action: func(c *cli.Context) error {
return findAndIterateOverDesiredStatesUsingFlags(c, func(state *state.HelmState, helm helmexec.Interface, ctx app.Context) []error {
values := c.StringSlice("values")
args := args.GetArgs(c.String("args"), state)
workers := c.Int("concurrency")
if errs := ctx.SyncReposOnce(state, helm); errs != nil && len(errs) > 0 {
return errs
if !c.Bool("skip-deps") {
if errs := ctx.SyncReposOnce(state, helm); errs != nil && len(errs) > 0 {
return errs
}
if errs := state.BuildDeps(helm); errs != nil && len(errs) > 0 {
return errs
}
}
if errs := state.PrepareRelease(helm, "lint"); errs != nil && len(errs) > 0 {
if errs := state.PrepareReleases(helm, "lint"); errs != nil && len(errs) > 0 {
return errs
}
return state.LintReleases(helm, values, args, workers)
@@ -288,20 +301,23 @@ func main() {
},
},
Action: func(c *cli.Context) error {
return findAndIterateOverDesiredStatesUsingFlags(c, func(state *state.HelmState, helm helmexec.Interface, ctx app.Context) []error {
affectedReleases := state.AffectedReleases{}
errs := findAndIterateOverDesiredStatesUsingFlags(c, func(st *state.HelmState, helm helmexec.Interface, ctx app.Context) []error {
if !c.Bool("skip-deps") {
if errs := ctx.SyncReposOnce(state, helm); errs != nil && len(errs) > 0 {
if errs := ctx.SyncReposOnce(st, helm); errs != nil && len(errs) > 0 {
return errs
}
if errs := state.BuildDeps(helm); errs != nil && len(errs) > 0 {
if errs := st.BuildDeps(helm); errs != nil && len(errs) > 0 {
return errs
}
}
if errs := state.PrepareRelease(helm, "sync"); errs != nil && len(errs) > 0 {
if errs := st.PrepareReleases(helm, "sync"); errs != nil && len(errs) > 0 {
return errs
}
return executeSyncCommand(c, state, helm)
return executeSyncCommand(c, &affectedReleases, st, helm)
})
affectedReleases.DisplayAffectedReleases(c.App.Metadata["logger"].(*zap.SugaredLogger))
return errs
},
},
{
@@ -332,7 +348,8 @@ func main() {
},
},
Action: func(c *cli.Context) error {
return findAndIterateOverDesiredStatesUsingFlags(c, func(st *state.HelmState, helm helmexec.Interface, ctx app.Context) []error {
affectedReleases := state.AffectedReleases{}
errs := findAndIterateOverDesiredStatesUsingFlags(c, func(st *state.HelmState, helm helmexec.Interface, ctx app.Context) []error {
if !c.Bool("skip-deps") {
if errs := ctx.SyncReposOnce(st, helm); errs != nil && len(errs) > 0 {
return errs
@@ -341,7 +358,7 @@ func main() {
return errs
}
}
if errs := st.PrepareRelease(helm, "apply"); errs != nil && len(errs) > 0 {
if errs := st.PrepareReleases(helm, "apply"); errs != nil && len(errs) > 0 {
return errs
}
@@ -352,13 +369,19 @@ func main() {
errs = append(errs, err)
}
fatalErrs := []error{}
noError := true
for _, e := range errs {
switch err := e.(type) {
case *state.DiffError:
noError = noError && err.Code == 2
case *state.ReleaseError:
if err.Code != 2 {
noError = false
fatalErrs = append(fatalErrs, e)
}
default:
noError = false
fatalErrs = append(fatalErrs, e)
}
}
@@ -396,13 +419,15 @@ Do you really want to apply?
}
st.Releases = rs
return executeSyncCommand(c, st, helm)
return executeSyncCommand(c, &affectedReleases, st, helm)
}
}
}
return errs
return fatalErrs
})
affectedReleases.DisplayAffectedReleases(c.App.Metadata["logger"].(*zap.SugaredLogger))
return errs
},
},
{
@@ -448,7 +473,8 @@ Do you really want to apply?
},
},
Action: func(c *cli.Context) error {
return cmd.FindAndIterateOverDesiredStatesUsingFlagsWithReverse(c, true, func(state *state.HelmState, helm helmexec.Interface, _ app.Context) []error {
affectedReleases := state.AffectedReleases{}
errs := cmd.FindAndIterateOverDesiredStatesUsingFlagsWithReverse(c, true, func(state *state.HelmState, helm helmexec.Interface, _ app.Context) []error {
purge := c.Bool("purge")
args := args.GetArgs(c.String("args"), state)
@@ -470,10 +496,12 @@ Do you really want to delete?
`, strings.Join(names, "\n"))
interactive := c.GlobalBool("interactive")
if !interactive || interactive && askForConfirmation(msg) {
return state.DeleteReleases(helm, purge)
return state.DeleteReleases(&affectedReleases, helm, purge)
}
return nil
})
affectedReleases.DisplayAffectedReleases(c.App.Metadata["logger"].(*zap.SugaredLogger))
return errs
},
},
{
@@ -487,7 +515,8 @@ Do you really want to delete?
},
},
Action: func(c *cli.Context) error {
return cmd.FindAndIterateOverDesiredStatesUsingFlagsWithReverse(c, true, func(state *state.HelmState, helm helmexec.Interface, _ app.Context) []error {
affectedReleases := state.AffectedReleases{}
errs := cmd.FindAndIterateOverDesiredStatesUsingFlagsWithReverse(c, true, func(state *state.HelmState, helm helmexec.Interface, _ app.Context) []error {
args := args.GetArgs(c.String("args"), state)
if len(args) > 0 {
helm.SetExtraArgs(args...)
@@ -507,10 +536,12 @@ Do you really want to delete?
`, strings.Join(names, "\n"))
interactive := c.GlobalBool("interactive")
if !interactive || interactive && askForConfirmation(msg) {
return state.DeleteReleases(helm, true)
return state.DeleteReleases(&affectedReleases, helm, true)
}
return nil
})
affectedReleases.DisplayAffectedReleases(c.App.Metadata["logger"].(*zap.SugaredLogger))
return errs
},
},
{
@@ -561,7 +592,7 @@ Do you really want to delete?
}
}
func executeSyncCommand(c *cli.Context, state *state.HelmState, helm helmexec.Interface) []error {
func executeSyncCommand(c *cli.Context, affectedReleases *state.AffectedReleases, state *state.HelmState, helm helmexec.Interface) []error {
args := args.GetArgs(c.String("args"), state)
if len(args) > 0 {
helm.SetExtraArgs(args...)
@@ -570,7 +601,7 @@ func executeSyncCommand(c *cli.Context, state *state.HelmState, helm helmexec.In
values := c.StringSlice("values")
workers := c.Int("concurrency")
return state.SyncReleases(helm, values, workers)
return state.SyncReleases(affectedReleases, helm, values, workers)
}
func executeTemplateCommand(c *cli.Context, state *state.HelmState, helm helmexec.Interface) []error {
+140 -112
View File
@@ -2,16 +2,19 @@ package app
import (
"fmt"
"github.com/roboll/helmfile/helmexec"
"github.com/roboll/helmfile/state"
"go.uber.org/zap"
"io/ioutil"
"log"
"os"
"os/signal"
"strings"
"syscall"
"github.com/roboll/helmfile/helmexec"
"github.com/roboll/helmfile/state"
"go.uber.org/zap"
"path/filepath"
"sort"
"syscall"
)
type App struct {
@@ -81,7 +84,7 @@ func (a *App) within(dir string, do func() error) error {
func (a *App) visitStateFiles(fileOrDir string, do func(string) error) error {
desiredStateFiles, err := a.findDesiredStateFiles(fileOrDir)
if err != nil {
return err
return appError("", err)
}
for _, relPath := range desiredStateFiles {
@@ -101,41 +104,51 @@ func (a *App) visitStateFiles(fileOrDir string, do func(string) error) error {
return do(file)
})
if err != nil {
return err
return appError(fmt.Sprintf("in %s/%s", dir, file), err)
}
}
return nil
}
func (a *App) VisitDesiredStates(fileOrDir string, converge func(*state.HelmState, helmexec.Interface) (bool, []error)) error {
func (a *App) loadDesiredStateFromYaml(file string) (*state.HelmState, error) {
ld := &desiredStateLoader{
readFile: a.readFile,
env: a.Env,
namespace: a.Namespace,
logger: a.Logger,
abs: a.abs,
Reverse: a.Reverse,
KubeContext: a.KubeContext,
glob: a.glob,
}
return ld.Load(file)
}
func (a *App) VisitDesiredStates(fileOrDir string, selector []string, converge func(*state.HelmState, helmexec.Interface) (bool, []error)) error {
noMatchInHelmfiles := true
err := a.visitStateFiles(fileOrDir, func(f string) error {
content, err := a.readFile(f)
if err != nil {
return err
}
// render template, in two runs
r := &twoPassRenderer{
reader: a.readFile,
env: a.Env,
namespace: a.Namespace,
filename: f,
logger: a.Logger,
abs: a.abs,
}
yamlBuf, err := r.renderTemplate(content)
if err != nil {
return fmt.Errorf("error during %s parsing: %v", f, err)
}
st, err := a.loadDesiredStateFromYaml(f)
st, err := a.loadDesiredStateFromYaml(
yamlBuf.Bytes(),
f,
a.Namespace,
a.Env,
)
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
go func() {
sig := <-sigs
errs := []error{fmt.Errorf("Received [%s] to shutdown ", sig)}
_ = context{a, st}.clean(errs)
// See http://tldp.org/LDP/abs/html/exitcodes.html
switch sig {
case syscall.SIGINT:
os.Exit(130)
case syscall.SIGTERM:
os.Exit(143)
}
}()
ctx := context{a, st}
helm := helmexec.New(a.Logger, a.KubeContext)
@@ -147,22 +160,27 @@ func (a *App) VisitDesiredStates(fileOrDir string, converge func(*state.HelmStat
case *state.UndefinedEnvError:
return nil
default:
return err
return ctx.wrapErrs(err)
}
default:
return err
return ctx.wrapErrs(err)
}
}
st.Selectors = selector
if len(st.Helmfiles) > 0 {
noMatchInSubHelmfiles := true
for _, m := range st.Helmfiles {
if err := a.VisitDesiredStates(m, converge); err != nil {
for i, m := range st.Helmfiles {
//assign parent selector to sub helm selector in legacy mode or do not inherit in experimental mode
if (m.Selectors == nil && !isExplicitSelectorInheritanceEnabled()) || m.SelectorsInherited {
m.Selectors = selector
}
if err := a.VisitDesiredStates(m.Path, m.Selectors, converge); err != nil {
switch err.(type) {
case *NoMatchingHelmfileError:
default:
return fmt.Errorf("failed processing %s: %v", m, err)
return appError(fmt.Sprintf("in .helmfiles[%d]", i), err)
}
} else {
noMatchInSubHelmfiles = false
@@ -173,11 +191,11 @@ func (a *App) VisitDesiredStates(fileOrDir string, converge func(*state.HelmStat
templated, tmplErr := st.ExecuteTemplates()
if tmplErr != nil {
return fmt.Errorf("failed executing release templates in \"%s\": %v", f, tmplErr)
return appError(fmt.Sprintf("failed executing release templates in \"%s\"", f), tmplErr)
}
processed, errs := converge(templated, helm)
noMatchInHelmfiles = noMatchInHelmfiles && !processed
return clean(templated, errs)
return context{a, templated}.clean(errs)
})
if err != nil {
@@ -192,11 +210,10 @@ func (a *App) VisitDesiredStates(fileOrDir string, converge func(*state.HelmStat
}
func (a *App) VisitDesiredStatesWithReleasesFiltered(fileOrDir string, converge func(*state.HelmState, helmexec.Interface) []error) error {
selectors := a.Selectors
err := a.VisitDesiredStates(fileOrDir, func(st *state.HelmState, helm helmexec.Interface) (bool, []error) {
if len(selectors) > 0 {
err := st.FilterReleases(selectors)
err := a.VisitDesiredStates(fileOrDir, a.Selectors, func(st *state.HelmState, helm helmexec.Interface) (bool, []error) {
if len(st.Selectors) > 0 {
err := st.FilterReleases()
if err != nil {
return false, []error{err}
}
@@ -280,11 +297,11 @@ func (a *App) findDesiredStateFiles(specifiedPath string) ([]string, error) {
} else if defaultFile != "" {
return []string{defaultFile}, nil
} else {
return []string{}, fmt.Errorf("no state file found. It must be named %s/*.yaml, %s, or %s, or otherwise specified with the --file flag", DefaultHelmfileDirectory, DefaultHelmfile, DeprecatedHelmfile)
return []string{}, fmt.Errorf("no state file found. It must be named %s/*.{yaml,yml}, %s, or %s, or otherwise specified with the --file flag", DefaultHelmfileDirectory, DefaultHelmfile, DeprecatedHelmfile)
}
}
files, err := a.glob(filepath.Join(helmfileDir, "*.yaml"))
files, err := a.glob(filepath.Join(helmfileDir, "*.y*ml"))
if err != nil {
return []string{}, err
}
@@ -304,93 +321,104 @@ func directoryExistsAt(path string) bool {
return err == nil && fileInfo.Mode().IsDir()
}
func (a *App) loadDesiredStateFromYaml(yaml []byte, file string, namespace string, env string) (*state.HelmState, error) {
c := state.NewCreator(a.Logger, a.readFile, a.abs)
st, err := c.CreateFromYaml(yaml, file, env)
if err != nil {
return nil, err
}
type Error struct {
msg string
helmfiles := []string{}
for _, globPattern := range st.Helmfiles {
var absPathPattern string
if filepath.IsAbs(globPattern) {
absPathPattern = globPattern
} else {
absPathPattern = st.JoinBase(globPattern)
}
matches, err := a.glob(absPathPattern)
if err != nil {
return nil, fmt.Errorf("failed processing %s: %v", globPattern, err)
}
sort.Strings(matches)
helmfiles = append(helmfiles, matches...)
}
st.Helmfiles = helmfiles
if a.Reverse {
rev := func(i, j int) bool {
return j < i
}
sort.Slice(st.Releases, rev)
sort.Slice(st.Helmfiles, rev)
}
if a.KubeContext != "" {
if st.HelmDefaults.KubeContext != "" {
log.Printf("err: Cannot use option --kube-context and set attribute helmDefaults.kubeContext.")
os.Exit(1)
}
st.HelmDefaults.KubeContext = a.KubeContext
}
if namespace != "" {
if st.Namespace != "" {
log.Printf("err: Cannot use option --namespace and set attribute namespace.")
os.Exit(1)
}
st.Namespace = namespace
}
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
go func() {
sig := <-sigs
errs := []error{fmt.Errorf("Received [%s] to shutdown ", sig)}
_ = clean(st, errs)
// See http://tldp.org/LDP/abs/html/exitcodes.html
switch sig {
case syscall.SIGINT:
os.Exit(130)
case syscall.SIGTERM:
os.Exit(143)
}
}()
return st, nil
Errors []error
}
func clean(st *state.HelmState, errs []error) error {
func (e *Error) Error() string {
var cause string
if e.Errors == nil {
return e.msg
}
if len(e.Errors) == 1 {
if e.Errors[0] == nil {
panic(fmt.Sprintf("[bug] assertion error: unexpected state: e.Errors: %v", e.Errors))
}
cause = e.Errors[0].Error()
} else {
msgs := []string{}
for i, err := range e.Errors {
if err == nil {
continue
}
msgs = append(msgs, fmt.Sprintf("err %d: %v", i, err.Error()))
}
cause = fmt.Sprintf("%d errors:\n%s", len(e.Errors), strings.Join(msgs, "\n"))
}
msg := ""
if e.msg != "" {
msg = fmt.Sprintf("%s: %s", e.msg, cause)
} else {
msg = cause
}
return msg
}
func (e *Error) Code() int {
allDiff := false
anyNonZero := false
for _, err := range e.Errors {
switch ee := err.(type) {
case *state.ReleaseError:
if anyNonZero {
allDiff = allDiff && ee.Code == 2
} else {
allDiff = ee.Code == 2
}
case *Error:
if anyNonZero {
allDiff = allDiff && ee.Code() == 2
} else {
allDiff = ee.Code() == 2
}
}
anyNonZero = true
}
if anyNonZero {
if allDiff {
return 2
}
return 1
}
panic(fmt.Sprintf("[bug] assertion error: unexpected state: unable to handle errors: %v", e.Errors))
}
func appError(msg string, err error) error {
return &Error{msg, []error{err}}
}
func (c context) clean(errs []error) error {
if errs == nil {
errs = []error{}
}
cleanErrs := st.Clean()
cleanErrs := c.st.Clean()
if cleanErrs != nil {
errs = append(errs, cleanErrs...)
}
return c.wrapErrs(errs...)
}
type context struct {
app *App
st *state.HelmState
}
func (c context) wrapErrs(errs ...error) error {
if errs != nil && len(errs) > 0 {
for _, err := range errs {
switch e := err.(type) {
case *state.ReleaseError:
fmt.Printf("err: release \"%s\" in \"%s\" failed: %v\n", e.Name, st.FilePath, e)
c.app.Logger.Debugf("err: release \"%s\" in \"%s\" failed: %v", e.Name, c.st.FilePath, e)
default:
fmt.Printf("err: %v\n", e)
c.app.Logger.Debugf("err: %v", e)
}
}
return errs[0]
return &Error{Errors: errs}
}
return nil
}
+764 -104
View File
@@ -2,122 +2,163 @@ package app
import (
"fmt"
"github.com/roboll/helmfile/helmexec"
"github.com/roboll/helmfile/state"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
"github.com/roboll/helmfile/helmexec"
"github.com/roboll/helmfile/state"
"gotest.tools/env"
)
type testFs struct {
wd string
dirs map[string]bool
files map[string]string
}
func appWithFs(app *App, files map[string]string) *App {
fs := newTestFs(files)
fs := state.NewTestFs(files)
return injectFs(app, fs)
}
func injectFs(app *App, fs *testFs) *App {
app.readFile = fs.readFile
app.glob = fs.glob
app.abs = fs.abs
app.getwd = fs.getwd
app.chdir = fs.chdir
app.fileExistsAt = fs.fileExistsAt
app.directoryExistsAt = fs.directoryExistsAt
func injectFs(app *App, fs *state.TestFs) *App {
app.readFile = fs.ReadFile
app.glob = fs.Glob
app.abs = fs.Abs
app.getwd = fs.Getwd
app.chdir = fs.Chdir
app.fileExistsAt = fs.FileExistsAt
app.directoryExistsAt = fs.DirectoryExistsAt
return app
}
func newTestFs(files map[string]string) *testFs {
dirs := map[string]bool{}
for abs, _ := range files {
d := filepath.Dir(abs)
dirs[d] = true
func TestVisitDesiredStatesWithReleasesFiltered_ReleaseOrder(t *testing.T) {
files := map[string]string{
"/path/to/helmfile.yaml": `
helmfiles:
- helmfile.d/a*.yaml
- helmfile.d/b*.yaml
`,
"/path/to/helmfile.d/a1.yaml": `
releases:
- name: zipkin
chart: stable/zipkin
`,
"/path/to/helmfile.d/a2.yaml": `
releases:
- name: prometheus
chart: stable/prometheus
`,
"/path/to/helmfile.d/b.yaml": `
releases:
- name: grafana
chart: stable/grafana
`,
}
return &testFs{
wd: "/path/to",
dirs: dirs,
files: files,
fs := state.NewTestFs(files)
fs.GlobFixtures["/path/to/helmfile.d/a*.yaml"] = []string{"/path/to/helmfile.d/a2.yaml", "/path/to/helmfile.d/a1.yaml"}
app := &App{
KubeContext: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
Namespace: "",
Env: "default",
}
app = injectFs(app, fs)
actualOrder := []string{}
noop := func(st *state.HelmState, helm helmexec.Interface) []error {
actualOrder = append(actualOrder, st.FilePath)
return []error{}
}
err := app.VisitDesiredStatesWithReleasesFiltered(
"helmfile.yaml", noop,
)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
expectedOrder := []string{"a1.yaml", "a2.yaml", "b.yaml", "helmfile.yaml"}
if !reflect.DeepEqual(actualOrder, expectedOrder) {
t.Errorf("unexpected order of processed state files: expected=%v, actual=%v", expectedOrder, actualOrder)
}
}
func (f *testFs) fileExistsAt(path string) bool {
var ok bool
if strings.Contains(path, "/") {
_, ok = f.files[path]
} else {
_, ok = f.files[filepath.Join(f.wd, path)]
func TestVisitDesiredStatesWithReleasesFiltered_EnvValuesFileOrder(t *testing.T) {
files := map[string]string{
"/path/to/helmfile.yaml": `
environments:
default:
values:
- env.*.yaml
releases:
- name: zipkin
chart: stable/zipkin
`,
"/path/to/env.1.yaml": `FOO: 1
BAR: 2
`,
"/path/to/env.2.yaml": `BAR: 3
BAZ: 4
`,
}
fs := state.NewTestFs(files)
fs.GlobFixtures["/path/to/env.*.yaml"] = []string{"/path/to/env.2.yaml", "/path/to/env.1.yaml"}
app := &App{
KubeContext: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
Namespace: "",
Env: "default",
}
app = injectFs(app, fs)
noop := func(st *state.HelmState, helm helmexec.Interface) []error {
return []error{}
}
err := app.VisitDesiredStatesWithReleasesFiltered(
"helmfile.yaml", noop,
)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
expectedOrder := []string{"helmfile.yaml", "/path/to/env.1.yaml", "/path/to/env.2.yaml", "/path/to/env.1.yaml", "/path/to/env.2.yaml"}
actualOrder := fs.SuccessfulReads()
if !reflect.DeepEqual(actualOrder, expectedOrder) {
t.Errorf("unexpected order of processed state files: expected=%v, actual=%v", expectedOrder, actualOrder)
}
return ok
}
func (f *testFs) directoryExistsAt(path string) bool {
var ok bool
if strings.Contains(path, "/") {
_, ok = f.dirs[path]
} else {
_, ok = f.dirs[filepath.Join(f.wd, path)]
func TestVisitDesiredStatesWithReleasesFiltered_MissingEnvValuesFile(t *testing.T) {
files := map[string]string{
"/path/to/helmfile.yaml": `
environments:
default:
values:
- env.*.yaml
releases:
- name: zipkin
chart: stable/zipkin
`,
}
fs := state.NewTestFs(files)
app := &App{
KubeContext: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
Namespace: "",
Env: "default",
}
app = injectFs(app, fs)
noop := func(st *state.HelmState, helm helmexec.Interface) []error {
return []error{}
}
return ok
}
func (f *testFs) readFile(filename string) ([]byte, error) {
var str string
var ok bool
if strings.Contains(filename, "/") {
str, ok = f.files[filename]
} else {
str, ok = f.files[filepath.Join(f.wd, filename)]
err := app.VisitDesiredStatesWithReleasesFiltered(
"helmfile.yaml", noop,
)
if err == nil {
t.Fatal("expected error did not occur")
}
if !ok {
return []byte(nil), fmt.Errorf("no file found: %s", filename)
}
return []byte(str), nil
}
func (f *testFs) glob(pattern string) ([]string, error) {
matches := []string{}
for name, _ := range f.files {
matched, err := filepath.Match(pattern, name)
if err != nil {
return nil, err
}
if matched {
matches = append(matches, name)
}
expected := "in ./helmfile.yaml: failed to read helmfile.yaml: no file matching env.*.yaml found"
if err.Error() != expected {
t.Errorf("unexpected error: expected=%s, got=%v", expected, err)
}
if len(matches) == 0 {
return []string(nil), fmt.Errorf("no file matched: %s", pattern)
}
return matches, nil
}
func (f *testFs) abs(path string) (string, error) {
var p string
if path[0] == '/' {
p = path
} else {
p = filepath.Join(f.wd, path)
}
return filepath.Clean(p), nil
}
func (f *testFs) getwd() (string, error) {
return f.wd, nil
}
func (f *testFs) chdir(dir string) error {
if dir == "/path/to" || dir == "/path/to/helmfile.d" {
f.wd = dir
return nil
}
return fmt.Errorf("unexpected chdir \"%s\"", dir)
}
// See https://github.com/roboll/helmfile/issues/193
@@ -144,10 +185,6 @@ releases:
chart: stable/grafana
`,
}
noop := func(st *state.HelmState, helm helmexec.Interface) []error {
return []error{}
}
testcases := []struct {
name string
expectErr bool
@@ -159,13 +196,20 @@ releases:
}
for _, testcase := range testcases {
app := appWithFs(&App{
fs := state.NewTestFs(files)
fs.GlobFixtures["/path/to/helmfile.d/a*.yaml"] = []string{"/path/to/helmfile.d/a2.yaml", "/path/to/helmfile.d/a1.yaml"}
app := &App{
KubeContext: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
Selectors: []string{fmt.Sprintf("name=%s", testcase.name)},
Namespace: "",
Env: "default",
}, files)
}
app = injectFs(app, fs)
noop := func(st *state.HelmState, helm helmexec.Interface) []error {
return []error{}
}
err := app.VisitDesiredStatesWithReleasesFiltered(
"helmfile.yaml", noop,
)
@@ -280,11 +324,11 @@ releases:
errMsg string
}{
{label: "name=prometheus", expectedCount: 1, expectErr: false},
{label: "name=", expectedCount: 0, expectErr: true, errMsg: "failed processing /path/to/helmfile.d/a1.yaml: Malformed label: name=. Expected label in form k=v or k!=v"},
{label: "name!=", expectedCount: 0, expectErr: true, errMsg: "failed processing /path/to/helmfile.d/a1.yaml: Malformed label: name!=. Expected label in form k=v or k!=v"},
{label: "name", expectedCount: 0, expectErr: true, errMsg: "failed processing /path/to/helmfile.d/a1.yaml: Malformed label: name. Expected label in form k=v or k!=v"},
{label: "name=", expectedCount: 0, expectErr: true, errMsg: "in ./helmfile.yaml: in .helmfiles[0]: in /path/to/helmfile.d/a1.yaml: Malformed label: name=. Expected label in form k=v or k!=v"},
{label: "name!=", expectedCount: 0, expectErr: true, errMsg: "in ./helmfile.yaml: in .helmfiles[0]: in /path/to/helmfile.d/a1.yaml: Malformed label: name!=. Expected label in form k=v or k!=v"},
{label: "name", expectedCount: 0, expectErr: true, errMsg: "in ./helmfile.yaml: in .helmfiles[0]: in /path/to/helmfile.d/a1.yaml: Malformed label: name. Expected label in form k=v or k!=v"},
// See https://github.com/roboll/helmfile/issues/193
{label: "duplicated=yes", expectedCount: 0, expectErr: true, errMsg: "failed processing /path/to/helmfile.d/b.yaml: duplicate release \"foo\" found in \"zoo\": there were 2 releases named \"foo\" matching specified selector"},
{label: "duplicated=yes", expectedCount: 0, expectErr: true, errMsg: "in ./helmfile.yaml: in .helmfiles[2]: in /path/to/helmfile.d/b.yaml: duplicate release \"foo\" found in \"zoo\": there were 2 releases named \"foo\" matching specified selector"},
{label: "duplicatedOK=yes", expectedCount: 2, expectErr: false},
}
@@ -324,6 +368,242 @@ releases:
}
}
func TestVisitDesiredStatesWithReleasesFiltered_EmbeddedSelectors(t *testing.T) {
files := map[string]string{
"/path/to/helmfile.yaml": `
helmfiles:
- path: helmfile.d/a*.yaml
selectors:
- name=prometheus
- name=zipkin
- helmfile.d/b*.yaml
- path: helmfile.d/c*.yaml
selectors: []
`,
"/path/to/helmfile.d/a1.yaml": `
releases:
- name: zipkin
chart: stable/zipkin
`,
"/path/to/helmfile.d/a2.yaml": `
releases:
- name: prometheus
chart: stable/prometheus
`,
"/path/to/helmfile.d/a3.yaml": `
releases:
- name: mongodb
chart: stable/mongodb
`,
"/path/to/helmfile.d/b.yaml": `
releases:
- name: grafana
chart: stable/grafana
- name: bar
chart: charts/foo
tillerNamespace: bar1
labels:
duplicatedOK: yes
- name: bar
chart: charts/foo
tillerNamespace: bar2
labels:
duplicatedOK: yes
`,
"/path/to/helmfile.d/c.yaml": `
releases:
- name: grafana
chart: stable/grafana
- name: postgresql
chart: charts/postgresql
labels:
whatever: yes
`,
}
//Check with legacy behavior, that is when no explicit selector then sub-helmfiles inherits from command line selector
legacyTestcases := []struct {
label string
expectedReleases []string
expectErr bool
errMsg string
}{
{label: "duplicatedOK=yes", expectedReleases: []string{"zipkin", "prometheus", "bar", "bar", "grafana", "postgresql"}, expectErr: false},
{label: "name=zipkin", expectedReleases: []string{"zipkin", "prometheus", "grafana", "postgresql"}, expectErr: false},
{label: "name=grafana", expectedReleases: []string{"zipkin", "prometheus", "grafana", "grafana", "postgresql"}, expectErr: false},
{label: "name=doesnotexists", expectedReleases: []string{"zipkin", "prometheus", "grafana", "postgresql"}, expectErr: false},
}
runFilterSubHelmFilesTests(legacyTestcases, files, t, "1st EmbeddedSelectors")
//Check with experimental behavior, that is when no explicit selector then sub-helmfiles do no inherit from any selector
desiredTestcases := []struct {
label string
expectedReleases []string
expectErr bool
errMsg string
}{
{label: "duplicatedOK=yes", expectedReleases: []string{"zipkin", "prometheus", "grafana", "bar", "bar", "grafana", "postgresql"}, expectErr: false},
{label: "name=doesnotexists", expectedReleases: []string{"zipkin", "prometheus", "grafana", "bar", "bar", "grafana", "postgresql"}, expectErr: false},
}
defer env.Patch(t, ExperimentalEnvVar, ExperimentalSelectorExplicit)()
runFilterSubHelmFilesTests(desiredTestcases, files, t, "2nd EmbeddedSelectors")
}
func TestVisitDesiredStatesWithReleasesFiltered_InheritedSelectors_3leveldeep(t *testing.T) {
files := map[string]string{
"/path/to/helmfile.yaml": `
helmfiles:
- helmfile.d/a*.yaml
releases:
- name: mongodb
chart: stable/mongodb
`,
"/path/to/helmfile.d/a.yaml": `
helmfiles:
- b*.yaml
releases:
- name: zipkin
chart: stable/zipkin
`,
"/path/to/helmfile.d/b.yaml": `
releases:
- name: grafana
chart: stable/grafana
`,
}
//Check with legacy behavior, that is when no explicit selector then sub-helmfiles inherits from command line selector
legacyTestcases := []struct {
label string
expectedReleases []string
expectErr bool
errMsg string
}{
{label: "name!=grafana", expectedReleases: []string{"zipkin", "mongodb"}, expectErr: false},
}
runFilterSubHelmFilesTests(legacyTestcases, files, t, "1st 3leveldeep")
//Check with experimental behavior, that is when no explicit selector then sub-helmfiles do no inherit from any selector
desiredTestcases := []struct {
label string
expectedReleases []string
expectErr bool
errMsg string
}{
{label: "name!=grafana", expectedReleases: []string{"grafana", "zipkin", "mongodb"}, expectErr: false},
}
defer env.Patch(t, ExperimentalEnvVar, ExperimentalSelectorExplicit)()
runFilterSubHelmFilesTests(desiredTestcases, files, t, "2nd 3leveldeep")
}
func TestVisitDesiredStatesWithReleasesFiltered_InheritedSelectors_inherits(t *testing.T) {
files := map[string]string{
"/path/to/helmfile.yaml": `
helmfiles:
- helmfile.d/a*.yaml
- path: helmfile.d/a*.yaml
selectors:
- select=foo
releases:
- name: mongodb
chart: stable/mongodb
`,
"/path/to/helmfile.d/a.yaml": `
helmfiles:
- path: b*.yaml
selectorsInherited: true
releases:
- name: zipkin
chart: stable/zipkin
labels:
select: foo
`,
"/path/to/helmfile.d/b.yaml": `
releases:
- name: grafana
chart: stable/grafana
- name: prometheus
chart: stable/prometheus
labels:
select: foo
`,
}
//Check with legacy behavior, that is when no explicit selector then sub-helmfiles inherits from command line selector
legacyTestcases := []struct {
label string
expectedReleases []string
expectErr bool
errMsg string
}{
{label: "name=grafana", expectedReleases: []string{"grafana", "prometheus", "zipkin"}, expectErr: false},
{label: "select!=foo", expectedReleases: []string{"grafana", "prometheus", "zipkin", "mongodb"}, expectErr: false},
}
runFilterSubHelmFilesTests(legacyTestcases, files, t, "1st inherits")
//Check with experimental behavior, that is when no explicit selector then sub-helmfiles do no inherit from any selector
desiredTestcases := []struct {
label string
expectedReleases []string
expectErr bool
errMsg string
}{
{label: "name=grafana", expectedReleases: []string{"grafana", "prometheus", "zipkin", "prometheus", "zipkin"}, expectErr: false},
{label: "select!=foo", expectedReleases: []string{"grafana", "prometheus", "zipkin", "prometheus", "zipkin", "mongodb"}, expectErr: false},
}
defer env.Patch(t, ExperimentalEnvVar, ExperimentalSelectorExplicit)()
runFilterSubHelmFilesTests(desiredTestcases, files, t, "2nd inherits")
}
func runFilterSubHelmFilesTests(testcases []struct {
label string
expectedReleases []string
expectErr bool
errMsg string
}, files map[string]string, t *testing.T, testName string) {
for _, testcase := range testcases {
actual := []string{}
collectReleases := func(st *state.HelmState, helm helmexec.Interface) []error {
for _, r := range st.Releases {
actual = append(actual, r.Name)
}
return []error{}
}
app := appWithFs(&App{
KubeContext: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
Namespace: "",
Selectors: []string{testcase.label},
Env: "default",
}, files)
err := app.VisitDesiredStatesWithReleasesFiltered(
"helmfile.yaml", collectReleases,
)
if testcase.expectErr {
if err == nil {
t.Errorf("[%s]error expected but not happened for selector %s", testName, testcase.label)
} else if err.Error() != testcase.errMsg {
t.Errorf("[%s]unexpected error message: expected=\"%s\", actual=\"%s\"", testName, testcase.errMsg, err.Error())
}
} else if !testcase.expectErr && err != nil {
t.Errorf("[%s]unexpected error for selector %s: %v", testName, testcase.label, err)
}
if !reflect.DeepEqual(actual, testcase.expectedReleases) {
t.Errorf("[%s]unexpected releases for selector %s: expected=%v, actual=%v", testName, testcase.label, testcase.expectedReleases, actual)
}
}
}
// See https://github.com/roboll/helmfile/issues/312
func TestVisitDesiredStatesWithReleasesFiltered_ReverseOrder(t *testing.T) {
files := map[string]string{
@@ -402,15 +682,395 @@ func TestLoadDesiredStateFromYaml_DuplicateReleaseName(t *testing.T) {
labels:
stage: post
`)
readFile := func(filename string) ([]byte, error) {
if filename != yamlFile {
return nil, fmt.Errorf("unexpected filename: %s", filename)
}
return yamlContent, nil
}
app := &App{
readFile: ioutil.ReadFile,
readFile: readFile,
glob: filepath.Glob,
abs: filepath.Abs,
KubeContext: "default",
Env: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
}
_, err := app.loadDesiredStateFromYaml(yamlContent, yamlFile, "default", "default")
_, err := app.loadDesiredStateFromYaml(yamlFile)
if err != nil {
t.Error("unexpected error")
t.Errorf("unexpected error: %v", err)
}
}
func TestLoadDesiredStateFromYaml_Bases(t *testing.T) {
yamlFile := "/path/to/yaml/file"
yamlContent := `bases:
- ../base.yaml
- ../base.gotmpl
{{ readFile "templates.yaml" }}
releases:
- name: myrelease1
chart: mychart1
labels:
stage: pre
foo: bar
- name: myrelease1
chart: mychart2
labels:
stage: post
<<: *default
`
testFs := state.NewTestFs(map[string]string{
yamlFile: yamlContent,
"/path/to/base.yaml": `environments:
default:
values:
- environments/default/1.yaml
`,
"/path/to/yaml/environments/default/1.yaml": `foo: FOO`,
"/path/to/base.gotmpl": `environments:
default:
values:
- environments/default/2.yaml
helmDefaults:
tillerNamespace: {{ .Environment.Values.tillerNs }}
`,
"/path/to/yaml/environments/default/2.yaml": `tillerNs: TILLER_NS`,
"/path/to/yaml/templates.yaml": `templates:
default: &default
missingFileHandler: Warn
values: ["` + "{{`" + `{{.Release.Name}}` + "`}}" + `/values.yaml"]
`,
})
app := &App{
readFile: testFs.ReadFile,
glob: testFs.Glob,
abs: testFs.Abs,
KubeContext: "default",
Env: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
}
st, err := app.loadDesiredStateFromYaml(yamlFile)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if st.HelmDefaults.TillerNamespace != "TILLER_NS" {
t.Errorf("unexpected helmDefaults.tillerNamespace: expected=TILLER_NS, got=%s", st.HelmDefaults.TillerNamespace)
}
if *st.Releases[1].MissingFileHandler != "Warn" {
t.Errorf("unexpected releases[0].missingFileHandler: expected=Warn, got=%s", *st.Releases[1].MissingFileHandler)
}
if st.Releases[1].Values[0] != "{{`{{.Release.Name}}`}}/values.yaml" {
t.Errorf("unexpected releases[0].missingFileHandler: expected={{`{{.Release.Name}}`}}/values.yaml, got=%s", st.Releases[1].Values[0])
}
}
func TestLoadDesiredStateFromYaml_MultiPartTemplate(t *testing.T) {
yamlFile := "/path/to/yaml/file"
yamlContent := `bases:
- ../base.yaml
---
bases:
- ../base.gotmpl
---
helmDefaults:
kubeContext: {{ .Environment.Values.foo }}
---
releases:
- name: myrelease0
chart: mychart0
---
{{ readFile "templates.yaml" }}
releases:
- name: myrelease1
chart: mychart1
labels:
stage: pre
foo: bar
- name: myrelease1
chart: mychart2
labels:
stage: post
<<: *default
`
testFs := state.NewTestFs(map[string]string{
yamlFile: yamlContent,
"/path/to/base.yaml": `environments:
default:
values:
- environments/default/1.yaml
`,
"/path/to/yaml/environments/default/1.yaml": `foo: FOO`,
"/path/to/base.gotmpl": `environments:
default:
values:
- environments/default/2.yaml
helmDefaults:
tillerNamespace: {{ .Environment.Values.tillerNs }}
`,
"/path/to/yaml/environments/default/2.yaml": `tillerNs: TILLER_NS`,
"/path/to/yaml/templates.yaml": `templates:
default: &default
missingFileHandler: Warn
values: ["` + "{{`" + `{{.Release.Name}}` + "`}}" + `/values.yaml"]
`,
})
app := &App{
readFile: testFs.ReadFile,
glob: testFs.Glob,
abs: testFs.Abs,
Env: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
}
st, err := app.loadDesiredStateFromYaml(yamlFile)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if st.HelmDefaults.TillerNamespace != "TILLER_NS" {
t.Errorf("unexpected helmDefaults.tillerNamespace: expected=TILLER_NS, got=%s", st.HelmDefaults.TillerNamespace)
}
if st.Releases[0].Name != "myrelease0" {
t.Errorf("unexpected releases[0].name: expected=myrelease0, got=%s", st.Releases[0].Name)
}
if st.Releases[1].Name != "myrelease1" {
t.Errorf("unexpected releases[1].name: expected=myrelease1, got=%s", st.Releases[1].Name)
}
if st.Releases[2].Name != "myrelease1" {
t.Errorf("unexpected releases[2].name: expected=myrelease1, got=%s", st.Releases[2].Name)
}
if st.Releases[2].Values[0] != "{{`{{.Release.Name}}`}}/values.yaml" {
t.Errorf("unexpected releases[2].missingFileHandler: expected={{`{{.Release.Name}}`}}/values.yaml, got=%s", st.Releases[1].Values[0])
}
if *st.Releases[2].MissingFileHandler != "Warn" {
t.Errorf("unexpected releases[2].missingFileHandler: expected=Warn, got=%s", *st.Releases[1].MissingFileHandler)
}
if st.Releases[2].Values[0] != "{{`{{.Release.Name}}`}}/values.yaml" {
t.Errorf("unexpected releases[2].missingFileHandler: expected={{`{{.Release.Name}}`}}/values.yaml, got=%s", st.Releases[1].Values[0])
}
if st.HelmDefaults.KubeContext != "FOO" {
t.Errorf("unexpected helmDefaults.kubeContext: expected=FOO, got=%s", st.HelmDefaults.KubeContext)
}
}
func TestLoadDesiredStateFromYaml_EnvvalsInheritanceToBaseTemplate(t *testing.T) {
yamlFile := "/path/to/yaml/file"
yamlContent := `bases:
- ../base.yaml
---
bases:
# "envvals inheritance"
# base.gotmpl should be able to reference environment values defined in the base.yaml and default/1.yaml
- ../base.gotmpl
---
releases:
- name: myrelease0
chart: mychart0
`
testFs := state.NewTestFs(map[string]string{
yamlFile: yamlContent,
"/path/to/base.yaml": `environments:
default:
values:
- environments/default/1.yaml
`,
"/path/to/base.gotmpl": `helmDefaults:
kubeContext: {{ .Environment.Values.foo }}
tillerNamespace: {{ .Environment.Values.tillerNs }}
`,
"/path/to/yaml/environments/default/1.yaml": `tillerNs: TILLER_NS
foo: FOO
`,
"/path/to/yaml/templates.yaml": `templates:
default: &default
missingFileHandler: Warn
values: ["` + "{{`" + `{{.Release.Name}}` + "`}}" + `/values.yaml"]
`,
})
app := &App{
readFile: testFs.ReadFile,
glob: testFs.Glob,
abs: testFs.Abs,
Env: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
}
st, err := app.loadDesiredStateFromYaml(yamlFile)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if st.HelmDefaults.TillerNamespace != "TILLER_NS" {
t.Errorf("unexpected helmDefaults.tillerNamespace: expected=TILLER_NS, got=%s", st.HelmDefaults.TillerNamespace)
}
if st.Releases[0].Name != "myrelease0" {
t.Errorf("unexpected releases[0].name: expected=myrelease0, got=%s", st.Releases[0].Name)
}
if st.HelmDefaults.KubeContext != "FOO" {
t.Errorf("unexpected helmDefaults.kubeContext: expected=FOO, got=%s", st.HelmDefaults.KubeContext)
}
}
func TestLoadDesiredStateFromYaml_MultiPartTemplate_WithNonDefaultEnv(t *testing.T) {
yamlFile := "/path/to/yaml/file"
yamlContent := `bases:
- ../base.yaml
---
bases:
- ../base.gotmpl
---
helmDefaults:
kubeContext: {{ .Environment.Values.foo }}
---
releases:
- name: myrelease0
chart: mychart0
---
{{ readFile "templates.yaml" }}
releases:
- name: myrelease1
chart: mychart1
labels:
stage: pre
foo: bar
- name: myrelease1
chart: mychart2
labels:
stage: post
<<: *default
`
testFs := state.NewTestFs(map[string]string{
yamlFile: yamlContent,
"/path/to/base.yaml": `environments:
test:
values:
- environments/default/1.yaml
`,
"/path/to/yaml/environments/default/1.yaml": `foo: FOO`,
"/path/to/base.gotmpl": `environments:
test:
values:
- environments/default/2.yaml
helmDefaults:
tillerNamespace: {{ .Environment.Values.tillerNs }}
`,
"/path/to/yaml/environments/default/2.yaml": `tillerNs: TILLER_NS`,
"/path/to/yaml/templates.yaml": `templates:
default: &default
missingFileHandler: Warn
values: ["` + "{{`" + `{{.Release.Name}}` + "`}}" + `/values.yaml"]
`,
})
app := &App{
readFile: testFs.ReadFile,
glob: testFs.Glob,
abs: testFs.Abs,
Env: "test",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
}
st, err := app.loadDesiredStateFromYaml(yamlFile)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if st.HelmDefaults.TillerNamespace != "TILLER_NS" {
t.Errorf("unexpected helmDefaults.tillerNamespace: expected=TILLER_NS, got=%s", st.HelmDefaults.TillerNamespace)
}
if st.Releases[0].Name != "myrelease0" {
t.Errorf("unexpected releases[0].name: expected=myrelease0, got=%s", st.Releases[0].Name)
}
if st.Releases[1].Name != "myrelease1" {
t.Errorf("unexpected releases[1].name: expected=myrelease1, got=%s", st.Releases[1].Name)
}
if st.Releases[2].Name != "myrelease1" {
t.Errorf("unexpected releases[2].name: expected=myrelease1, got=%s", st.Releases[2].Name)
}
if st.Releases[2].Values[0] != "{{`{{.Release.Name}}`}}/values.yaml" {
t.Errorf("unexpected releases[2].missingFileHandler: expected={{`{{.Release.Name}}`}}/values.yaml, got=%s", st.Releases[1].Values[0])
}
if *st.Releases[2].MissingFileHandler != "Warn" {
t.Errorf("unexpected releases[2].missingFileHandler: expected=Warn, got=%s", *st.Releases[1].MissingFileHandler)
}
if st.Releases[2].Values[0] != "{{`{{.Release.Name}}`}}/values.yaml" {
t.Errorf("unexpected releases[2].missingFileHandler: expected={{`{{.Release.Name}}`}}/values.yaml, got=%s", st.Releases[1].Values[0])
}
if st.HelmDefaults.KubeContext != "FOO" {
t.Errorf("unexpected helmDefaults.kubeContext: expected=FOO, got=%s", st.HelmDefaults.KubeContext)
}
}
func TestLoadDesiredStateFromYaml_MultiPartTemplate_WithReverse(t *testing.T) {
yamlFile := "/path/to/yaml/file"
yamlContent := `
{{ readFile "templates.yaml" }}
releases:
- name: myrelease0
chart: mychart0
- name: myrelease1
chart: mychart1
<<: *default
---
{{ readFile "templates.yaml" }}
releases:
- name: myrelease2
chart: mychart2
- name: myrelease3
chart: mychart3
<<: *default
`
testFs := state.NewTestFs(map[string]string{
yamlFile: yamlContent,
"/path/to/yaml/templates.yaml": `templates:
default: &default
missingFileHandler: Warn
values: ["` + "{{`" + `{{.Release.Name}}` + "`}}" + `/values.yaml"]
`,
})
app := &App{
readFile: testFs.ReadFile,
glob: testFs.Glob,
abs: testFs.Abs,
Env: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
Reverse: true,
}
st, err := app.loadDesiredStateFromYaml(yamlFile)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if st.Releases[0].Name != "myrelease3" {
t.Errorf("unexpected releases[0].name: expected=myrelease3, got=%s", st.Releases[0].Name)
}
if st.Releases[1].Name != "myrelease2" {
t.Errorf("unexpected releases[0].name: expected=myrelease2, got=%s", st.Releases[1].Name)
}
if st.Releases[2].Name != "myrelease1" {
t.Errorf("unexpected releases[0].name: expected=myrelease1, got=%s", st.Releases[2].Name)
}
if st.Releases[3].Name != "myrelease0" {
t.Errorf("unexpected releases[0].name: expected=myrelease0, got=%s", st.Releases[3].Name)
}
}
+19 -4
View File
@@ -1,7 +1,22 @@
package app
const (
DefaultHelmfile = "helmfile.yaml"
DeprecatedHelmfile = "charts.yaml"
DefaultHelmfileDirectory = "helmfile.d"
import (
"os"
"strings"
)
const (
DefaultHelmfile = "helmfile.yaml"
DeprecatedHelmfile = "charts.yaml"
DefaultHelmfileDirectory = "helmfile.d"
ExperimentalEnvVar = "HELMFILE_EXPERIMENTAL" // environment variable for experimental features, expecting "true" lower case
ExperimentalSelectorExplicit = "explicit-selector-inheritance" // value to remove default selector inheritance to sub-helmfiles and use the explicit one
)
func experimentalModeEnabled() bool {
return os.Getenv(ExperimentalEnvVar) == "true"
}
func isExplicitSelectorInheritanceEnabled() bool {
return experimentalModeEnabled() || strings.Contains(os.Getenv(ExperimentalEnvVar), ExperimentalSelectorExplicit)
}
+27
View File
@@ -0,0 +1,27 @@
package app
import (
"gotest.tools/assert"
is "gotest.tools/assert/cmp"
"gotest.tools/env"
"os"
"testing"
)
func TestIsExplicitSelectorInheritanceEnabled(t *testing.T) {
//env var ExperimentalEnvVar is set
assert.Assert(t, is.Equal(os.Getenv(ExperimentalEnvVar), ""))
assert.Check(t, !isExplicitSelectorInheritanceEnabled())
//check for env var ExperimentalEnvVar set to true
defer env.Patch(t, ExperimentalEnvVar, "true")()
assert.Check(t, isExplicitSelectorInheritanceEnabled())
//check for env var ExperimentalEnvVar set to anything
defer env.Patch(t, ExperimentalEnvVar, "foo")()
assert.Check(t, !isExplicitSelectorInheritanceEnabled())
//check for env var ExperimentalEnvVar set to ExperimentalSelectorExplicit
defer env.Patch(t, ExperimentalEnvVar, ExperimentalSelectorExplicit)()
assert.Check(t, isExplicitSelectorInheritanceEnabled())
}
+175
View File
@@ -0,0 +1,175 @@
package app
import (
"bytes"
"errors"
"fmt"
"github.com/imdario/mergo"
"github.com/roboll/helmfile/environment"
"github.com/roboll/helmfile/state"
"go.uber.org/zap"
"path/filepath"
"sort"
)
type desiredStateLoader struct {
KubeContext string
Reverse bool
env string
namespace string
readFile func(string) ([]byte, error)
abs func(string) (string, error)
glob func(string) ([]string, error)
logger *zap.SugaredLogger
}
func (ld *desiredStateLoader) Load(f string) (*state.HelmState, error) {
st, err := ld.loadFile(nil, filepath.Dir(f), filepath.Base(f), true)
if err != nil {
return nil, err
}
if ld.Reverse {
rev := func(i, j int) bool {
return j < i
}
sort.Slice(st.Releases, rev)
sort.Slice(st.Helmfiles, rev)
}
if ld.KubeContext != "" {
if st.HelmDefaults.KubeContext != "" {
return nil, errors.New("err: Cannot use option --kube-context and set attribute helmDefaults.kubeContext.")
}
st.HelmDefaults.KubeContext = ld.KubeContext
}
if ld.namespace != "" {
if st.Namespace != "" {
return nil, errors.New("err: Cannot use option --namespace and set attribute namespace.")
}
st.Namespace = ld.namespace
}
return st, nil
}
func (ld *desiredStateLoader) loadFile(inheritedEnv *environment.Environment, baseDir, file string, evaluateBases bool) (*state.HelmState, error) {
var f string
if filepath.IsAbs(file) {
f = file
} else {
f = filepath.Join(baseDir, file)
}
fileBytes, err := ld.readFile(f)
if err != nil {
return nil, err
}
ext := filepath.Ext(f)
var self *state.HelmState
if !experimentalModeEnabled() || ext == ".gotmpl" {
self, err = ld.renderAndLoad(
inheritedEnv,
baseDir,
f,
fileBytes,
evaluateBases,
)
} else {
self, err = ld.load(
fileBytes,
baseDir,
file,
evaluateBases,
inheritedEnv,
)
}
return self, err
}
func (a *desiredStateLoader) underlying() *state.StateCreator {
c := state.NewCreator(a.logger, a.readFile, a.abs, a.glob)
c.LoadFile = a.loadFile
return c
}
func (a *desiredStateLoader) load(yaml []byte, baseDir, file string, evaluateBases bool, env *environment.Environment) (*state.HelmState, error) {
st, err := a.underlying().ParseAndLoad(yaml, baseDir, file, a.env, evaluateBases, env)
if err != nil {
return nil, err
}
helmfiles := []state.SubHelmfileSpec{}
for _, hf := range st.Helmfiles {
matches, err := st.ExpandPaths([]string{hf.Path}, a.glob)
if err != nil {
return nil, err
}
for _, match := range matches {
newHelmfile := hf
newHelmfile.Path = match
helmfiles = append(helmfiles, newHelmfile)
}
}
st.Helmfiles = helmfiles
return st, nil
}
func (ld *desiredStateLoader) renderAndLoad(env *environment.Environment, baseDir, filename string, content []byte, evaluateBases bool) (*state.HelmState, error) {
parts := bytes.Split(content, []byte("\n---\n"))
var finalState *state.HelmState
for i, part := range parts {
var yamlBuf *bytes.Buffer
var err error
id := fmt.Sprintf("%s.part.%d", filename, i)
if env == nil {
yamlBuf, err = ld.renderTemplatesToYaml(baseDir, id, part)
if err != nil {
return nil, fmt.Errorf("error during %s parsing: %v", id, err)
}
} else {
yamlBuf, err = ld.renderTemplatesToYaml(baseDir, id, part, *env)
if err != nil {
return nil, fmt.Errorf("error during %s parsing: %v", id, err)
}
}
currentState, err := ld.load(
yamlBuf.Bytes(),
baseDir,
filename,
evaluateBases,
env,
)
if err != nil {
return nil, err
}
if finalState == nil {
finalState = currentState
} else {
if err := mergo.Merge(finalState, currentState, mergo.WithAppendSlice); err != nil {
return nil, err
}
}
env = &finalState.Env
ld.logger.Debugf("merged environment: %v", env)
}
return finalState, nil
}
+32 -23
View File
@@ -6,8 +6,6 @@ import (
"github.com/roboll/helmfile/environment"
"github.com/roboll/helmfile/state"
"github.com/roboll/helmfile/tmpl"
"go.uber.org/zap"
"path/filepath"
"strings"
)
@@ -20,61 +18,72 @@ func prependLineNumbers(text string) string {
return buf.String()
}
type twoPassRenderer struct {
reader func(string) ([]byte, error)
env string
namespace string
filename string
logger *zap.SugaredLogger
abs func(string) (string, error)
}
func (r *twoPassRenderer) renderEnvironment(content []byte) environment.Environment {
firstPassEnv := environment.Environment{Name: r.env, Values: map[string]interface{}(nil)}
func (r *desiredStateLoader) renderEnvironment(firstPassEnv environment.Environment, baseDir, filename string, content []byte) environment.Environment {
tmplData := state.EnvironmentTemplateData{Environment: firstPassEnv, Namespace: r.namespace}
firstPassRenderer := tmpl.NewFirstPassRenderer(filepath.Dir(r.filename), tmplData)
firstPassRenderer := tmpl.NewFirstPassRenderer(baseDir, tmplData)
// parse as much as we can, tolerate errors, this is a preparse
yamlBuf, err := firstPassRenderer.RenderTemplateContentToBuffer(content)
if err != nil && r.logger != nil {
r.logger.Debugf("first-pass rendering input of \"%s\":\n%s", r.filename, prependLineNumbers(string(content)))
r.logger.Debugf("first-pass rendering input of \"%s\":\n%s", filename, prependLineNumbers(string(content)))
if yamlBuf == nil { // we have a template syntax error, let the second parse report
r.logger.Debugf("template syntax error: %v", err)
return firstPassEnv
}
}
c := state.NewCreator(r.logger, r.reader, r.abs)
c := r.underlying()
c.Strict = false
// create preliminary state, as we may have an environment. Tolerate errors.
prestate, err := c.CreateFromYaml(yamlBuf.Bytes(), r.filename, r.env)
prestate, err := c.ParseAndLoad(yamlBuf.Bytes(), baseDir, filename, r.env, false, &firstPassEnv)
if err != nil && r.logger != nil {
switch err.(type) {
case *state.StateLoadError:
r.logger.Infof("could not deduce `environment:` block, configuring only .Environment.Name. error: %v", err)
}
r.logger.Debugf("error in first-pass rendering: result of \"%s\":\n%s", r.filename, prependLineNumbers(yamlBuf.String()))
r.logger.Debugf("error in first-pass rendering: result of \"%s\":\n%s", filename, prependLineNumbers(yamlBuf.String()))
}
if prestate != nil {
firstPassEnv = prestate.Env
}
return firstPassEnv
}
func (r *twoPassRenderer) renderTemplate(content []byte) (*bytes.Buffer, error) {
func (r *desiredStateLoader) renderTemplatesToYaml(baseDir, filename string, content []byte, context ...environment.Environment) (*bytes.Buffer, error) {
var env environment.Environment
if len(context) > 0 {
env = context[0]
} else {
env = environment.Environment{Name: r.env, Values: map[string]interface{}(nil)}
}
return r.twoPassRenderTemplateToYaml(env, baseDir, filename, content)
}
func (r *desiredStateLoader) twoPassRenderTemplateToYaml(initEnv environment.Environment, baseDir, filename string, content []byte) (*bytes.Buffer, error) {
// try a first pass render. This will always succeed, but can produce a limited env
firstPassEnv := r.renderEnvironment(content)
if r.logger != nil {
r.logger.Debugf("first-pass rendering input of \"%s\": %v", filename, initEnv)
}
firstPassEnv := r.renderEnvironment(initEnv, baseDir, filename, content)
if r.logger != nil {
r.logger.Debugf("first-pass rendering result of \"%s\": %v", filename, firstPassEnv)
}
tmplData := state.EnvironmentTemplateData{Environment: firstPassEnv, Namespace: r.namespace}
secondPassRenderer := tmpl.NewFileRenderer(r.reader, filepath.Dir(r.filename), tmplData)
secondPassRenderer := tmpl.NewFileRenderer(r.readFile, baseDir, tmplData)
yamlBuf, err := secondPassRenderer.RenderTemplateContentToBuffer(content)
if err != nil {
if r.logger != nil {
r.logger.Debugf("second-pass rendering failed, input of \"%s\":\n%s", r.filename, prependLineNumbers(string(content)))
r.logger.Debugf("second-pass rendering failed, input of \"%s\":\n%s", filename, prependLineNumbers(string(content)))
}
return nil, err
}
if r.logger != nil {
r.logger.Debugf("second-pass rendering result of \"%s\":\n%s", r.filename, prependLineNumbers(yamlBuf.String()))
r.logger.Debugf("second-pass rendering result of \"%s\":\n%s", filename, prependLineNumbers(yamlBuf.String()))
}
return yamlBuf, nil
}
+38 -63
View File
@@ -1,9 +1,7 @@
package app
import (
"fmt"
"os"
"path/filepath"
"strings"
"testing"
@@ -12,15 +10,16 @@ import (
"gopkg.in/yaml.v2"
)
func makeRenderer(readFile func(string) ([]byte, error), env string) *twoPassRenderer {
return &twoPassRenderer{
reader: readFile,
func makeLoader(files map[string]string, env string) (*desiredStateLoader, *state.TestFs) {
testfs := state.NewTestFs(files)
return &desiredStateLoader{
env: env,
namespace: "namespace",
filename: "",
logger: helmexec.NewLogger(os.Stdout, "debug"),
abs: filepath.Abs,
}
readFile: testfs.ReadFile,
abs: testfs.Abs,
glob: testfs.Glob,
}, testfs
}
func TestReadFromYaml_MakeEnvironmentHasNoSideEffects(t *testing.T) {
@@ -37,30 +36,21 @@ releases:
chart: mychart1
`)
fileReaderCalls := 0
// make a reader that returns a simulated context
fileReader := func(filename string) ([]byte, error) {
expectedFilename := filepath.Clean("default/values.yaml")
if !strings.HasSuffix(filename, expectedFilename) {
return nil, fmt.Errorf("unexpected filename: expected=%s, actual=%s", expectedFilename, filename)
}
fileReaderCalls++
if fileReaderCalls == 2 {
return []byte("SecondPass"), nil
}
return []byte(""), nil
files := map[string]string{
"/path/to/default/values.yaml": ``,
"/path/to/other/default/values.yaml": `SecondPass`,
}
r := makeRenderer(fileReader, "staging")
yamlBuf, err := r.renderTemplate(yamlContent)
r, testfs := makeLoader(files, "staging")
yamlBuf, err := r.renderTemplatesToYaml("", "", yamlContent)
if err != nil {
t.Errorf("unexpected error: %v", err)
t.Fatalf("unexpected error: %v", err)
}
var state state.HelmState
err = yaml.Unmarshal(yamlBuf.Bytes(), &state)
if fileReaderCalls > 2 {
if testfs.FileReaderCalls() > 2 {
t.Error("reader should be called only twice")
}
@@ -71,10 +61,10 @@ releases:
func TestReadFromYaml_RenderTemplate(t *testing.T) {
defaultValuesYaml := []byte(`
defaultValuesYaml := `
releaseName: "hello"
conditionalReleaseTag: "yes"
`)
`
yamlContent := []byte(`
environments:
@@ -93,18 +83,13 @@ releases:
`)
// make a reader that returns a simulated context
fileReader := func(filename string) ([]byte, error) {
expectedFilename := filepath.Clean("default/values.yaml")
if !strings.HasSuffix(filename, expectedFilename) {
return nil, fmt.Errorf("unexpected filename: expected=%s, actual=%s", expectedFilename, filename)
}
return defaultValuesYaml, nil
files := map[string]string{
"/path/to/default/values.yaml": defaultValuesYaml,
}
r := makeRenderer(fileReader, "staging")
r, _ := makeLoader(files, "staging")
// test the double rendering
yamlBuf, err := r.renderTemplate(yamlContent)
yamlBuf, err := r.renderTemplatesToYaml("", "", yamlContent)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
@@ -130,7 +115,7 @@ releases:
}
func TestReadFromYaml_RenderTemplateWithValuesReferenceError(t *testing.T) {
defaultValuesYaml := []byte("")
defaultValuesYaml := ``
yamlContent := []byte(`
environments:
@@ -146,14 +131,13 @@ releases:
{{ end }}
`)
// make a reader that returns a simulated context
fileReader := func(filename string) ([]byte, error) {
return defaultValuesYaml, nil
files := map[string]string{
"/path/to/default/values.yaml": defaultValuesYaml,
}
r := makeRenderer(fileReader, "staging")
r, _ := makeLoader(files, "staging")
// test the double rendering
_, err := r.renderTemplate(yamlContent)
_, err := r.renderTemplatesToYaml("", "", yamlContent)
if !strings.Contains(err.Error(), "stringTemplate:8") {
t.Fatalf("error should contain a stringTemplate error (reference to unknow key) %v", err)
@@ -165,9 +149,9 @@ releases:
// This does not apply to .gotmpl files, which is a nice side-effect.
func TestReadFromYaml_RenderTemplateWithGotmpl(t *testing.T) {
defaultValuesYamlGotmpl := []byte(`
defaultValuesYamlGotmpl := `
releaseName: {{ readFile "nonIgnoredFile" }}
`)
`
yamlContent := []byte(`
environments:
@@ -183,15 +167,13 @@ releases:
{{ end }}
`)
fileReader := func(filename string) ([]byte, error) {
if strings.HasSuffix(filename, "nonIgnoredFile") {
return []byte("release-a"), nil
}
return defaultValuesYamlGotmpl, nil
files := map[string]string{
"/path/to/nonIgnoredFile": `release-a`,
"/path/to/values.yaml.gotmpl": defaultValuesYamlGotmpl,
}
r := makeRenderer(fileReader, "staging")
rendered, _ := r.renderTemplate(yamlContent)
r, _ := makeLoader(files, "staging")
rendered, _ := r.renderTemplatesToYaml("", "", yamlContent)
var state state.HelmState
yaml.Unmarshal(rendered.Bytes(), &state)
@@ -206,19 +188,15 @@ releases:
}
func TestReadFromYaml_RenderTemplateWithNamespace(t *testing.T) {
defaultValuesYaml := []byte(``)
yamlContent := []byte(`releases:
- name: {{ .Namespace }}-myrelease
chart: mychart
`)
// make a reader that returns a simulated context
fileReader := func(filename string) ([]byte, error) {
return defaultValuesYaml, nil
}
files := map[string]string{}
r := makeRenderer(fileReader, "staging")
yamlBuf, err := r.renderTemplate(yamlContent)
r, _ := makeLoader(files, "staging")
yamlBuf, err := r.renderTemplatesToYaml("", "", yamlContent)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
@@ -231,7 +209,7 @@ func TestReadFromYaml_RenderTemplateWithNamespace(t *testing.T) {
}
}
func TestReadFromYaml_HelfileShouldBeResilentToTemplateErrors(t *testing.T) {
func TestReadFromYaml_HelmfileShouldBeResilentToTemplateErrors(t *testing.T) {
yamlContent := []byte(`environments:
staging:
production:
@@ -244,12 +222,9 @@ releases:
{{ end }}
chart: mychart
`)
fileReader := func(filename string) ([]byte, error) {
return yamlContent, nil
}
r := makeRenderer(fileReader, "staging")
_, err := r.renderTemplate(yamlContent)
r, _ := makeLoader(map[string]string{}, "staging")
_, err := r.renderTemplatesToYaml("", "", yamlContent)
if err == nil {
t.Fatalf("wanted error, none returned")
}
+381
View File
@@ -0,0 +1,381 @@
package state
import (
"fmt"
"github.com/Masterminds/semver"
"github.com/roboll/helmfile/helmexec"
"go.uber.org/zap"
"gopkg.in/yaml.v2"
"io/ioutil"
"os"
"path/filepath"
"strings"
)
type ChartMeta struct {
Name string `yaml:"name"`
}
type unresolvedChartDependency struct {
// ChartName identifies the dependant chart. In Helmfile, ChartName for `chart: stable/envoy` would be just `envoy`.
// It can't be collided with other charts referenced in the same helmfile spec.
// That is, collocating `chart: incubator/foo` and `chart: stable/foo` isn't allowed. Name them differently for a work-around.
ChartName string `yaml:"name"`
// Repository contains the URL for the helm chart repository that hosts the chart identified by ChartName
Repository string `yaml:"repository"`
// VersionConstraint is the version constraint of the dependent chart. "*" means the latest version.
VersionConstraint string `yaml:"version"`
}
type ResolvedChartDependency struct {
// ChartName identifies the dependant chart. In Helmfile, ChartName for `chart: stable/envoy` would be just `envoy`.
// It can't be collided with other charts referenced in the same helmfile spec.
// That is, collocating `chart: incubator/foo` and `chart: stable/foo` isn't allowed. Name them differently for a work-around.
ChartName string `yaml:"name"`
// Repository contains the URL for the helm chart repository that hosts the chart identified by ChartName
Repository string `yaml:"repository"`
// Version is the version number of the dependent chart.
// In the context of helmfile this can be omitted. When omitted, it is considered `*` which results helm/helmfile fetching the latest version.
Version string `yaml:"version"`
}
type UnresolvedDependencies struct {
deps map[string][]unresolvedChartDependency
}
type ChartRequirements struct {
UnresolvedDependencies []unresolvedChartDependency `yaml:"dependencies"`
}
type ChartLockedRequirements struct {
ResolvedDependencies []ResolvedChartDependency `yaml:"dependencies"`
}
func (d *UnresolvedDependencies) Add(chart, url, versionConstraint string) error {
dep := unresolvedChartDependency{
ChartName: chart,
Repository: url,
VersionConstraint: versionConstraint,
}
return d.add(dep)
}
func (d *UnresolvedDependencies) add(dep unresolvedChartDependency) error {
deps := d.deps[dep.ChartName]
if deps == nil {
deps = []unresolvedChartDependency{dep}
} else {
deps = append(deps, dep)
}
d.deps[dep.ChartName] = deps
return nil
}
func (d *UnresolvedDependencies) ToChartRequirements() *ChartRequirements {
deps := []unresolvedChartDependency{}
for _, ds := range d.deps {
for _, d := range ds {
if d.VersionConstraint == "" {
d.VersionConstraint = "*"
}
deps = append(deps, d)
}
}
return &ChartRequirements{UnresolvedDependencies: deps}
}
type ResolvedDependencies struct {
deps map[string][]ResolvedChartDependency
}
func (d *ResolvedDependencies) add(dep ResolvedChartDependency) error {
deps := d.deps[dep.ChartName]
if deps == nil {
deps = []ResolvedChartDependency{dep}
} else {
deps = append(deps, dep)
}
d.deps[dep.ChartName] = deps
return nil
}
func (d *ResolvedDependencies) Get(chart, versionConstraint string) (string, error) {
if versionConstraint == "" {
versionConstraint = "*"
}
deps, exists := d.deps[chart]
if exists {
for _, dep := range deps {
constraint, err := semver.NewConstraint(versionConstraint)
if err != nil {
return "", err
}
version, err := semver.NewVersion(dep.Version)
if err != nil {
return "", err
}
if constraint.Check(version) {
return dep.Version, nil
}
}
}
return "", fmt.Errorf("no resolved dependency found for \"%s\"", chart)
}
func resolveRemoteChart(repoAndChart string) (string, string, bool) {
parts := strings.Split(repoAndChart, "/")
if isLocalChart(repoAndChart) {
return "", "", false
}
if len(parts) != 2 {
panic(fmt.Sprintf("unsupported format of chart name: %s", repoAndChart))
}
repo := parts[0]
chart := parts[1]
return repo, chart, true
}
func (st *HelmState) mergeLockedDependencies() (*HelmState, error) {
filename, unresolved, err := getUnresolvedDependenciess(st)
if err != nil {
return nil, err
}
if len(unresolved.deps) == 0 {
return st, nil
}
depMan := NewChartDependencyManager(filename, st.logger)
if st.readFile != nil {
depMan.readFile = st.readFile
}
return resolveDependencies(st, depMan, unresolved)
}
func resolveDependencies(st *HelmState, depMan *chartDependencyManager, unresolved *UnresolvedDependencies) (*HelmState, error) {
resolved, lockfileExists, err := depMan.Resolve(unresolved)
if err != nil {
return nil, fmt.Errorf("unable to resolve %d deps: %v", len(unresolved.deps), err)
}
if !lockfileExists {
return st, nil
}
repoToURL := map[string]string{}
for _, r := range st.Repositories {
repoToURL[r.Name] = r.URL
}
updated := *st
for i, r := range updated.Releases {
repo, chart, ok := resolveRemoteChart(r.Chart)
if !ok {
continue
}
_, ok = repoToURL[repo]
// Skip this chart from dependency management, as there's no matching `repository` in the helmfile state,
// which may imply that this is a local chart within a directory, like `charts/myapp`
if !ok {
continue
}
ver, err := resolved.Get(chart, r.Version)
if err != nil {
return nil, err
}
updated.Releases[i].Version = ver
}
return &updated, nil
}
func (st *HelmState) updateDependenciesInTempDir(shell helmexec.DependencyUpdater, tempDir func(string, string) (string, error)) (*HelmState, error) {
filename, unresolved, err := getUnresolvedDependenciess(st)
if err != nil {
return nil, err
}
if len(unresolved.deps) == 0 {
return st, nil
}
d, err := tempDir("", "")
if err != nil {
return nil, fmt.Errorf("unable to create dir: %v", err)
}
defer os.RemoveAll(d)
return updateDependencies(st, shell, unresolved, filename, d)
}
func getUnresolvedDependenciess(st *HelmState) (string, *UnresolvedDependencies, error) {
repoToURL := map[string]string{}
for _, r := range st.Repositories {
repoToURL[r.Name] = r.URL
}
unresolved := &UnresolvedDependencies{deps: map[string][]unresolvedChartDependency{}}
//if err := unresolved.Add("stable/envoy", "https://kubernetes-charts.storage.googleapis.com", ""); err != nil {
// panic(err)
//}
for _, r := range st.Releases {
repo, chart, ok := resolveRemoteChart(r.Chart)
if !ok {
continue
}
url, ok := repoToURL[repo]
// Skip this chart from dependency management, as there's no matching `repository` in the helmfile state,
// which may imply that this is a local chart within a directory, like `charts/myapp`
if !ok {
continue
}
if err := unresolved.Add(chart, url, r.Version); err != nil {
return "", nil, err
}
}
filename := filepath.Base(st.FilePath)
filename = strings.TrimSuffix(filename, ".gotmpl")
filename = strings.TrimSuffix(filename, ".yaml")
filename = strings.TrimSuffix(filename, ".yml")
return filename, unresolved, nil
}
func updateDependencies(st *HelmState, shell helmexec.DependencyUpdater, unresolved *UnresolvedDependencies, filename, wd string) (*HelmState, error) {
depMan := NewChartDependencyManager(filename, st.logger)
_, err := depMan.Update(shell, wd, unresolved)
if err != nil {
return nil, fmt.Errorf("unable to resolve %d deps: %v", len(unresolved.deps), err)
}
return resolveDependencies(st, depMan, unresolved)
}
type chartDependencyManager struct {
Name string
logger *zap.SugaredLogger
readFile func(string) ([]byte, error)
writeFile func(string, []byte, os.FileMode) error
}
func NewChartDependencyManager(name string, logger *zap.SugaredLogger) *chartDependencyManager {
return &chartDependencyManager{
Name: name,
readFile: ioutil.ReadFile,
writeFile: ioutil.WriteFile,
logger: logger,
}
}
func (m *chartDependencyManager) lockFileName() string {
return fmt.Sprintf("%s.lock", m.Name)
}
func (m *chartDependencyManager) Update(shell helmexec.DependencyUpdater, wd string, unresolved *UnresolvedDependencies) (*ResolvedDependencies, error) {
// Generate `Chart.yaml` of the temporary local chart
if err := m.writeBytes(filepath.Join(wd, "Chart.yaml"), []byte(fmt.Sprintf("name: %s\n", m.Name))); err != nil {
return nil, err
}
// Generate `requirements.yaml` of the temporary local chart from the helmfile state
reqsContent, err := yaml.Marshal(unresolved.ToChartRequirements())
if err != nil {
return nil, err
}
if err := m.writeBytes(filepath.Join(wd, "requirements.yaml"), reqsContent); err != nil {
return nil, err
}
// Generate `requirements.lock` of the temporary local chart by coping `<basename>.lock`
lockFile := m.lockFileName()
lockFileContent, err := m.readBytes(lockFile)
if err != nil && !os.IsNotExist(err) {
return nil, err
}
if lockFileContent != nil {
if err := m.writeBytes(filepath.Join(wd, "requirements.lock"), lockFileContent); err != nil {
return nil, err
}
}
// Update the lock file by running `helm dependency update`
if err := shell.UpdateDeps(wd); err != nil {
return nil, err
}
updatedLockFileContent, err := m.readBytes(filepath.Join(wd, "requirements.lock"))
if err != nil {
return nil, err
}
// Commit the lock file if and only if everything looks ok
if err := m.writeBytes(lockFile, updatedLockFileContent); err != nil {
return nil, err
}
resolved, _, err := m.Resolve(unresolved)
return resolved, err
}
func (m *chartDependencyManager) Resolve(unresolved *UnresolvedDependencies) (*ResolvedDependencies, bool, error) {
updatedLockFileContent, err := m.readBytes(m.lockFileName())
if err != nil {
if os.IsNotExist(err) {
return nil, false, nil
}
return nil, false, err
}
// Load resolved dependencies into memory
lockedReqs := &ChartLockedRequirements{}
if err := yaml.Unmarshal(updatedLockFileContent, lockedReqs); err != nil {
return nil, false, err
}
resolved := &ResolvedDependencies{deps: map[string][]ResolvedChartDependency{}}
for _, d := range lockedReqs.ResolvedDependencies {
if err := resolved.add(d); err != nil {
return nil, false, err
}
}
return resolved, true, nil
}
func (m *chartDependencyManager) readBytes(filename string) ([]byte, error) {
bytes, err := m.readFile(filename)
if err != nil {
return nil, err
}
m.logger.Debugf("readBytes: read from %s:\n%s", filename, bytes)
return bytes, nil
}
func (m *chartDependencyManager) writeBytes(filename string, data []byte) error {
err := m.writeFile(filename, data, 0644)
if err != nil {
return err
}
m.logger.Debugf("writeBytes: wrote to %s:\n%s", filename, data)
return nil
}
+139 -44
View File
@@ -2,17 +2,19 @@ package state
import (
"bytes"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"sort"
"github.com/imdario/mergo"
"github.com/roboll/helmfile/environment"
"github.com/roboll/helmfile/helmexec"
"github.com/roboll/helmfile/tmpl"
"go.uber.org/zap"
"gopkg.in/yaml.v2"
"io"
"io/ioutil"
"os"
"path/filepath"
)
type StateLoadError struct {
@@ -32,42 +34,33 @@ func (e *UndefinedEnvError) Error() string {
return e.msg
}
func createFromYaml(content []byte, file string, env string, logger *zap.SugaredLogger) (*HelmState, error) {
c := &creator{
logger,
ioutil.ReadFile,
filepath.Abs,
true,
}
return c.CreateFromYaml(content, file, env)
}
type creator struct {
type StateCreator struct {
logger *zap.SugaredLogger
readFile func(string) ([]byte, error)
abs func(string) (string, error)
glob func(string) ([]string, error)
Strict bool
LoadFile func(inheritedEnv *environment.Environment, baseDir, file string, evaluateBases bool) (*HelmState, error)
}
func NewCreator(logger *zap.SugaredLogger, readFile func(string) ([]byte, error), abs func(string) (string, error)) *creator {
return &creator{
func NewCreator(logger *zap.SugaredLogger, readFile func(string) ([]byte, error), abs func(string) (string, error), glob func(string) ([]string, error)) *StateCreator {
return &StateCreator{
logger: logger,
readFile: readFile,
abs: abs,
glob: glob,
Strict: true,
}
}
func (c *creator) CreateFromYaml(content []byte, file string, env string) (*HelmState, error) {
// Parse parses YAML into HelmState
func (c *StateCreator) Parse(content []byte, baseDir, file string) (*HelmState, error) {
var state HelmState
basePath, err := c.abs(filepath.Dir(file))
if err != nil {
return nil, &StateLoadError{fmt.Sprintf("failed to read %s", file), err}
}
state.FilePath = file
state.basePath = basePath
state.basePath = baseDir
decoder := yaml.NewDecoder(bytes.NewReader(content))
if !c.Strict {
@@ -107,12 +100,6 @@ func (c *creator) CreateFromYaml(content []byte, file string, env string) (*Helm
state.logger = c.logger
e, err := state.loadEnv(env, c.readFile)
if err != nil {
return nil, &StateLoadError{fmt.Sprintf("failed to read %s", file), err}
}
state.Env = *e
state.readFile = c.readFile
state.removeFile = os.Remove
state.fileExists = func(path string) (bool, error) {
@@ -130,55 +117,163 @@ func (c *creator) CreateFromYaml(content []byte, file string, env string) (*Helm
return &state, nil
}
func (st *HelmState) loadEnv(name string, readFile func(string) ([]byte, error)) (*environment.Environment, error) {
// LoadEnvValues loads environment values files relative to the `baseDir`
func (c *StateCreator) LoadEnvValues(target *HelmState, env string, ctxEnv *environment.Environment) (*HelmState, error) {
state := *target
e, err := state.loadEnvValues(env, ctxEnv, c.readFile, c.glob)
if err != nil {
return nil, &StateLoadError{fmt.Sprintf("failed to read %s", state.FilePath), err}
}
state.Env = *e
return &state, nil
}
// Parses YAML into HelmState, while loading environment values files relative to the `baseDir`
func (c *StateCreator) ParseAndLoad(content []byte, baseDir, file string, envName string, evaluateBases bool, envValues *environment.Environment) (*HelmState, error) {
state, err := c.Parse(content, baseDir, file)
if err != nil {
return nil, err
}
if !evaluateBases {
if len(state.Bases) > 0 {
return nil, errors.New("nested `base` helmfile is unsupported. please submit a feature request if you need this!")
}
}
state, err = c.loadBases(envValues, state, baseDir)
if err != nil {
return nil, err
}
return c.LoadEnvValues(state, envName, envValues)
}
func (c *StateCreator) loadBases(envValues *environment.Environment, st *HelmState, baseDir string) (*HelmState, error) {
layers := []*HelmState{}
for _, b := range st.Bases {
base, err := c.LoadFile(envValues, baseDir, b, false)
if err != nil {
return nil, err
}
layers = append(layers, base)
}
layers = append(layers, st)
for i := 1; i < len(layers); i++ {
if err := mergo.Merge(layers[0], layers[i], mergo.WithAppendSlice); err != nil {
return nil, err
}
}
return layers[0], nil
}
func (st *HelmState) ExpandPaths(patterns []string, glob func(string) ([]string, error)) ([]string, error) {
result := []string{}
for _, globPattern := range patterns {
var absPathPattern string
if filepath.IsAbs(globPattern) {
absPathPattern = globPattern
} else {
absPathPattern = st.JoinBase(globPattern)
}
matches, err := glob(absPathPattern)
if err != nil {
return nil, fmt.Errorf("failed processing %s: %v", globPattern, err)
}
if len(matches) == 0 {
return nil, fmt.Errorf("no file matching %s found", globPattern)
}
sort.Strings(matches)
result = append(result, matches...)
}
return result, nil
}
func (st *HelmState) loadEnvValues(name string, ctxEnv *environment.Environment, readFile func(string) ([]byte, error), glob func(string) ([]string, error)) (*environment.Environment, error) {
envVals := map[string]interface{}{}
envSpec, ok := st.Environments[name]
if ok {
for _, envvalFile := range envSpec.Values {
envvalFullPath := filepath.Join(st.basePath, envvalFile)
valuesFiles, err := st.ExpandPaths(envSpec.Values, glob)
if err != nil {
return nil, err
}
for _, envvalFullPath := range valuesFiles {
tmplData := EnvironmentTemplateData{Environment: environment.EmptyEnvironment, Namespace: ""}
r := tmpl.NewFileRenderer(readFile, filepath.Dir(envvalFullPath), tmplData)
bytes, err := r.RenderToBytes(envvalFullPath)
if err != nil {
return nil, fmt.Errorf("failed to load environment values file \"%s\": %v", envvalFile, err)
return nil, fmt.Errorf("failed to load environment values file \"%s\": %v", envvalFullPath, err)
}
m := map[string]interface{}{}
if err := yaml.Unmarshal(bytes, &m); err != nil {
return nil, fmt.Errorf("failed to load environment values file \"%s\": %v", envvalFile, err)
return nil, fmt.Errorf("failed to load environment values file \"%s\": %v", envvalFullPath, err)
}
if err := mergo.Merge(&envVals, &m, mergo.WithOverride); err != nil {
return nil, fmt.Errorf("failed to load \"%s\": %v", envvalFile, err)
return nil, fmt.Errorf("failed to load \"%s\": %v", envvalFullPath, err)
}
}
if len(envSpec.Secrets) > 0 {
secretsFiles, err := st.ExpandPaths(envSpec.Secrets, glob)
if err != nil {
return nil, err
}
helm := helmexec.New(st.logger, "")
for _, secFile := range envSpec.Secrets {
path := filepath.Join(st.basePath, secFile)
for _, path := range secretsFiles {
if _, err := os.Stat(path); os.IsNotExist(err) {
return nil, err
}
decFile, err := helm.DecryptSecret(path)
// Work-around to allow decrypting environment secrets
//
// We don't have releases loaded yet and therefore unable to decide whether
// helmfile should use helm-tiller to call helm-secrets or not.
//
// This means that, when you use environment secrets + tillerless setup, you still need a tiller
// installed on the cluster, just for decrypting secrets!
// Related: https://github.com/futuresimple/helm-secrets/issues/83
release := &ReleaseSpec{}
flags := st.appendTillerFlags([]string{}, release)
decFile, err := helm.DecryptSecret(st.createHelmContext(release, 0), path, flags...)
if err != nil {
return nil, err
}
bytes, err := readFile(decFile)
if err != nil {
return nil, fmt.Errorf("failed to load environment secrets file \"%s\": %v", secFile, err)
return nil, fmt.Errorf("failed to load environment secrets file \"%s\": %v", path, err)
}
m := map[string]interface{}{}
if err := yaml.Unmarshal(bytes, &m); err != nil {
return nil, fmt.Errorf("failed to load environment secrets file \"%s\": %v", secFile, err)
return nil, fmt.Errorf("failed to load environment secrets file \"%s\": %v", path, err)
}
if err := mergo.Merge(&envVals, &m, mergo.WithOverride); err != nil {
return nil, fmt.Errorf("failed to load \"%s\": %v", secFile, err)
return nil, fmt.Errorf("failed to load \"%s\": %v", path, err)
}
}
}
} else if name != DefaultEnv {
} else if ctxEnv == nil && name != DefaultEnv {
return nil, &UndefinedEnvError{msg: fmt.Sprintf("environment \"%s\" is not defined", name)}
}
return &environment.Environment{Name: name, Values: envVals}, nil
newEnv := &environment.Environment{Name: name, Values: envVals}
if ctxEnv != nil {
intEnv := *ctxEnv
if err := mergo.Merge(&intEnv, newEnv, mergo.WithAppendSlice); err != nil {
return nil, fmt.Errorf("error while merging environment values for \"%s\": %v", name, err)
}
newEnv = &intEnv
}
return newEnv, nil
}
+116 -16
View File
@@ -1,12 +1,26 @@
package state
import (
"fmt"
"go.uber.org/zap"
"io/ioutil"
"path/filepath"
"reflect"
"testing"
. "gotest.tools/assert"
"gotest.tools/assert/cmp"
)
func createFromYaml(content []byte, file string, env string, logger *zap.SugaredLogger) (*HelmState, error) {
c := &StateCreator{
logger: logger,
readFile: ioutil.ReadFile,
abs: filepath.Abs,
Strict: true,
}
return c.ParseAndLoad(content, filepath.Dir(file), file, env, false, nil)
}
func TestReadFromYaml(t *testing.T) {
yamlFile := "example/path/to/yaml/file"
yamlContent := []byte(`releases:
@@ -84,23 +98,17 @@ bar: {{ readFile "bar.txt" }}
expectedValues := `env: production`
readFile := func(filename string) ([]byte, error) {
switch filename {
case fooYamlFile:
return fooYamlContent, nil
case barYamlFile:
return barYamlContent, nil
case barTextFile:
return barTextContent, nil
case valuesFile:
return valuesContent, nil
}
return nil, fmt.Errorf("unexpected filename: %s", filename)
}
testFs := NewTestFs(map[string]string{
fooYamlFile: string(fooYamlContent),
barYamlFile: string(barYamlContent),
barTextFile: string(barTextContent),
valuesFile: string(valuesContent),
})
testFs.Cwd = "/example/path/to"
state, err := NewCreator(logger, readFile, filepath.Abs).CreateFromYaml(yamlContent, yamlFile, "production")
state, err := NewCreator(logger, testFs.ReadFile, testFs.Abs, testFs.Glob).ParseAndLoad(yamlContent, filepath.Dir(yamlFile), yamlFile, "production", false, nil)
if err != nil {
t.Errorf("unexpected error: %v", err)
t.Fatalf("unexpected error: %v", err)
}
actual := state.Env.Values
@@ -247,3 +255,95 @@ func TestReadFromYaml_FilterNegatives(t *testing.T) {
}
}
}
func TestReadFromYaml_Helmfiles_Selectors(t *testing.T) {
tests := []struct {
path string
content []byte
wantErr bool
helmfiles []SubHelmfileSpec
}{
{
path: "working/selector",
content: []byte(`helmfiles:
- simple/helmfile.yaml
- path: path/prefix/selector.yaml
selectors:
- name=zorba
- foo=bar
- path: path/prefix/empty/selector.yaml
selectors: []
- path: path/prefix/inherits/selector.yaml
selectorsInherited: true
`),
wantErr: false,
helmfiles: []SubHelmfileSpec{{Path: "simple/helmfile.yaml", Selectors: nil, SelectorsInherited: false},
{Path: "path/prefix/selector.yaml", Selectors: []string{"name=zorba", "foo=bar"}, SelectorsInherited: false},
{Path: "path/prefix/empty/selector.yaml", Selectors: []string{}, SelectorsInherited: false},
{Path: "path/prefix/inherits/selector.yaml", Selectors: nil, SelectorsInherited: true},
},
},
{
path: "failing2/selector",
content: []byte(`helmfiles:
- path: failing2/helmfile.yaml
wrongkey:
`),
wantErr: true,
},
{
path: "failing3/selector",
content: []byte(`helmfiles:
- path: failing3/helmfile.yaml
selectors: foo
`),
wantErr: true,
},
{
path: "failing4/selector",
content: []byte(`helmfiles:
- path: failing4/helmfile.yaml
selectors:
`),
wantErr: true,
},
{
path: "failing4/selector",
content: []byte(`helmfiles:
- path: failing4/helmfile.yaml
selectors:
- colon: not-authorized
`),
wantErr: true,
},
{
path: "failing6/selector",
content: []byte(`helmfiles:
- selectors:
- whatever
`),
wantErr: true,
},
{
path: "failing7/selector",
content: []byte(`helmfiles:
- path: foo/bar
selectors:
- foo=bar
selectorsInherited: true
`),
wantErr: true,
},
}
for _, test := range tests {
st, err := createFromYaml(test.content, test.path, DefaultEnv, logger)
if err != nil {
if test.wantErr {
continue
} else {
t.Error("unexpected error:", err)
}
}
Assert(t, cmp.DeepEqual(st.Helmfiles, test.helmfiles), "for path %v", test.path)
}
}
+19
View File
@@ -0,0 +1,19 @@
package state
import "fmt"
const ReleaseErrorCodeFailure = 1
type ReleaseError struct {
*ReleaseSpec
err error
Code int
}
func (e *ReleaseError) Error() string {
return fmt.Sprintf("failed processing release %s: %v", e.Name, e.err.Error())
}
func newReleaseError(release *ReleaseSpec, err error) *ReleaseError {
return &ReleaseError{release, err, ReleaseErrorCodeFailure}
}
+331 -119
View File
@@ -15,29 +15,31 @@ import (
"regexp"
"os/exec"
"syscall"
"net/url"
"github.com/roboll/helmfile/environment"
"github.com/roboll/helmfile/event"
"github.com/roboll/helmfile/tmpl"
"github.com/tatsushid/go-prettytable"
"go.uber.org/zap"
"gopkg.in/yaml.v2"
"net/url"
)
// HelmState structure for the helmfile
type HelmState struct {
basePath string
Environments map[string]EnvironmentSpec
FilePath string
HelmDefaults HelmSpec `yaml:"helmDefaults"`
Helmfiles []string `yaml:"helmfiles"`
DeprecatedContext string `yaml:"context"`
DeprecatedReleases []ReleaseSpec `yaml:"charts"`
Namespace string `yaml:"namespace"`
Repositories []RepositorySpec `yaml:"repositories"`
Releases []ReleaseSpec `yaml:"releases"`
basePath string
Environments map[string]EnvironmentSpec
FilePath string
Bases []string `yaml:"bases"`
HelmDefaults HelmSpec `yaml:"helmDefaults"`
Helmfiles []SubHelmfileSpec `yaml:"helmfiles"`
DeprecatedContext string `yaml:"context"`
DeprecatedReleases []ReleaseSpec `yaml:"charts"`
Namespace string `yaml:"namespace"`
Repositories []RepositorySpec `yaml:"repositories"`
Releases []ReleaseSpec `yaml:"releases"`
Selectors []string
Templates map[string]TemplateSpec `yaml:"templates"`
@@ -49,14 +51,23 @@ type HelmState struct {
removeFile func(string) error
fileExists func(string) (bool, error)
tempDir func(string, string) (string, error)
runner helmexec.Runner
}
// SubHelmfileSpec defines the subhelmfile path and options
type SubHelmfileSpec struct {
Path string //path or glob pattern for the sub helmfiles
Selectors []string //chosen selectors for the sub helmfiles
SelectorsInherited bool //do the sub helmfiles inherits from parent selectors
}
// HelmSpec to defines helmDefault values
type HelmSpec struct {
KubeContext string `yaml:"kubeContext"`
TillerNamespace string `yaml:"tillerNamespace"`
Tillerless bool `yaml:"tillerless"`
Args []string `yaml:"args"`
Verify bool `yaml:"verify"`
// Devel, when set to true, use development versions, too. Equivalent to version '>0.0.0-0'
@@ -130,6 +141,7 @@ type ReleaseSpec struct {
ValuesPathPrefix string `yaml:"valuesPathPrefix"`
TillerNamespace string `yaml:"tillerNamespace"`
Tillerless *bool `yaml:"tillerless"`
TLS *bool `yaml:"tls"`
TLSCACert string `yaml:"tlsCACert"`
@@ -138,6 +150,8 @@ type ReleaseSpec struct {
// generatedValues are values that need cleaned up on exit
generatedValues []string
//version of the chart that has really been installed cause desired version may be fuzzy (~2.0.0)
installedVersion string
}
// SetValue are the key values to set on a helm release
@@ -148,6 +162,13 @@ type SetValue struct {
Values []string `yaml:"values"`
}
// AffectedReleases hold the list of released that where updated, deleted, or in error
type AffectedReleases struct {
Upgraded []*ReleaseSpec
Deleted []*ReleaseSpec
Failed []*ReleaseSpec
}
const DefaultEnv = "default"
func (st *HelmState) applyDefaultsTo(spec *ReleaseSpec) {
@@ -181,15 +202,6 @@ func (st *HelmState) SyncRepos(helm RepoUpdater) []error {
return nil
}
type ReleaseError struct {
*ReleaseSpec
underlying error
}
func (e *ReleaseError) Error() string {
return e.underlying.Error()
}
type syncResult struct {
errors []*ReleaseError
}
@@ -204,9 +216,6 @@ type syncPrepareResult struct {
func (st *HelmState) prepareSyncReleases(helm helmexec.Interface, additionalValues []string, concurrency int) ([]syncPrepareResult, []error) {
releases := []*ReleaseSpec{}
for i, _ := range st.Releases {
if !st.Releases[i].Desired() {
continue
}
releases = append(releases, &st.Releases[i])
}
@@ -226,13 +235,24 @@ func (st *HelmState) prepareSyncReleases(helm helmexec.Interface, additionalValu
}
close(jobs)
},
func(_ int) {
func(workerIndex int) {
for release := range jobs {
st.applyDefaultsTo(release)
flags, flagsErr := st.flagsForUpgrade(helm, release)
// If `installed: false`, the only potential operation on this release would be uninstalling.
// We skip generating values files in that case, because for an uninstall with `helm delete`, we don't need to those.
// The values files are for `helm upgrade -f values.yaml` calls that happens when the release has `installed: true`.
// This logic addresses:
// - https://github.com/roboll/helmfile/issues/519
// - https://github.com/roboll/helmfile/issues/616
if !release.Desired() {
results <- syncPrepareResult{release: release, flags: []string{}, errors: []*ReleaseError{}}
continue
}
flags, flagsErr := st.flagsForUpgrade(helm, release, workerIndex)
if flagsErr != nil {
results <- syncPrepareResult{errors: []*ReleaseError{&ReleaseError{release, flagsErr}}}
results <- syncPrepareResult{errors: []*ReleaseError{newReleaseError(release, flagsErr)}}
continue
}
@@ -240,14 +260,14 @@ func (st *HelmState) prepareSyncReleases(helm helmexec.Interface, additionalValu
for _, value := range additionalValues {
valfile, err := filepath.Abs(value)
if err != nil {
errs = append(errs, &ReleaseError{release, err})
errs = append(errs, newReleaseError(release, err))
}
ok, err := st.fileExists(valfile)
if err != nil {
errs = append(errs, &ReleaseError{release, err})
errs = append(errs, newReleaseError(release, err))
} else if !ok {
errs = append(errs, &ReleaseError{release, fmt.Errorf("file does not exist: %s", valfile)})
errs = append(errs, newReleaseError(release, fmt.Errorf("file does not exist: %s", valfile)))
}
flags = append(flags, "--values", valfile)
}
@@ -277,8 +297,8 @@ func (st *HelmState) prepareSyncReleases(helm helmexec.Interface, additionalValu
return res, errs
}
func isReleaseInstalled(helm helmexec.Interface, release ReleaseSpec) (bool, error) {
out, err := helm.List("^" + release.Name + "$")
func (st *HelmState) isReleaseInstalled(context helmexec.HelmContext, helm helmexec.Interface, release ReleaseSpec) (bool, error) {
out, err := helm.List(context, "^"+release.Name+"$", st.tillerFlags(&release)...)
if err != nil {
return false, err
} else if out != "" {
@@ -289,14 +309,17 @@ func isReleaseInstalled(helm helmexec.Interface, release ReleaseSpec) (bool, err
func (st *HelmState) DetectReleasesToBeDeleted(helm helmexec.Interface) ([]*ReleaseSpec, error) {
detected := []*ReleaseSpec{}
for i, _ := range st.Releases {
for i := range st.Releases {
release := st.Releases[i]
if !release.Desired() {
installed, err := isReleaseInstalled(helm, release)
installed, err := st.isReleaseInstalled(st.createHelmContext(&release, 0), helm, release)
if err != nil {
return nil, err
} else if installed {
detected = append(detected, &release)
// Otherwise `release` messed up(https://github.com/roboll/helmfile/issues/554)
r := release
detected = append(detected, &r)
}
}
}
@@ -304,7 +327,7 @@ func (st *HelmState) DetectReleasesToBeDeleted(helm helmexec.Interface) ([]*Rele
}
// SyncReleases wrapper for executing helm upgrade on the releases
func (st *HelmState) SyncReleases(helm helmexec.Interface, additionalValues []string, workerLimit int) []error {
func (st *HelmState) SyncReleases(affectedReleases *AffectedReleases, helm helmexec.Interface, additionalValues []string, workerLimit int) []error {
preps, prepErrs := st.prepareSyncReleases(helm, additionalValues, workerLimit)
if len(prepErrs) > 0 {
return prepErrs
@@ -323,23 +346,39 @@ func (st *HelmState) SyncReleases(helm helmexec.Interface, additionalValues []st
}
close(jobQueue)
},
func(_ int) {
func(workerIndex int) {
for prep := range jobQueue {
release := prep.release
flags := prep.flags
chart := normalizeChart(st.basePath, release.Chart)
var relErr *ReleaseError
if !release.Desired() {
installed, err := isReleaseInstalled(helm, *release)
context := st.createHelmContext(release, workerIndex)
if _, err := st.triggerPresyncEvent(release, "sync"); err != nil {
relErr = newReleaseError(release, err)
} else if !release.Desired() {
installed, err := st.isReleaseInstalled(context, helm, *release)
if err != nil {
relErr = &ReleaseError{release, err}
relErr = newReleaseError(release, err)
} else if installed {
if err := helm.DeleteRelease(release.Name, "--purge"); err != nil {
relErr = &ReleaseError{release, err}
if err := helm.DeleteRelease(context, release.Name, "--purge"); err != nil {
affectedReleases.Failed = append(affectedReleases.Failed, release)
relErr = newReleaseError(release, err)
} else {
affectedReleases.Deleted = append(affectedReleases.Deleted, release)
}
}
} else if err := helm.SyncRelease(release.Name, chart, flags...); err != nil {
relErr = &ReleaseError{release, err}
} else if err := helm.SyncRelease(context, release.Name, chart, flags...); err != nil {
affectedReleases.Failed = append(affectedReleases.Failed, release)
relErr = newReleaseError(release, err)
} else {
affectedReleases.Upgraded = append(affectedReleases.Upgraded, release)
installedVersion, err := st.getDeployedVersion(context, helm, release)
if err != nil { //err is not really impacting so just log it
st.logger.Debugf("getting deployed release version failed:%v", err)
} else {
release.installedVersion = installedVersion
}
}
if relErr == nil {
@@ -348,7 +387,11 @@ func (st *HelmState) SyncReleases(helm helmexec.Interface, additionalValues []st
results <- syncResult{errors: []*ReleaseError{relErr}}
}
if _, err := st.triggerCleanupEvent(prep.release, "sync"); err != nil {
if _, err := st.triggerPostsyncEvent(release, "sync"); err != nil {
st.logger.Warnf("warn: %v\n", err)
}
if _, err := st.triggerCleanupEvent(release, "sync"); err != nil {
st.logger.Warnf("warn: %v\n", err)
}
}
@@ -367,14 +410,29 @@ func (st *HelmState) SyncReleases(helm helmexec.Interface, additionalValues []st
}
},
)
if len(errs) > 0 {
return errs
}
return nil
}
func (st *HelmState) getDeployedVersion(context helmexec.HelmContext, helm helmexec.Interface, release *ReleaseSpec) (string, error) {
//retrieve the version
if out, err := helm.List(context, "^"+release.Name+"$", st.tillerFlags(release)...); err == nil {
chartName := filepath.Base(release.Chart)
pat := regexp.MustCompile(chartName + "-(.*?)\\s")
versions := pat.FindStringSubmatch(out)
if len(versions) > 0 {
return versions[1], nil
} else {
//fails to find the version
return "failed to get version", errors.New("Failed to get the version for:" + chartName)
}
} else {
return "failed to get version", err
}
}
// downloadCharts will download and untar charts for Lint and Template
func (st *HelmState) downloadCharts(helm helmexec.Interface, dir string, concurrency int, helmfileCommand string) (map[string]string, []error) {
temp := make(map[string]string, len(st.Releases))
@@ -410,6 +468,10 @@ func (st *HelmState) downloadCharts(helm helmexec.Interface, dir string, concurr
chartPath = path.Join(dir, release.Name, "latest", release.Chart)
}
if st.isDevelopment(release) {
fetchFlags = append(fetchFlags, "--devel")
}
// only fetch chart if it is not already fetched
if _, err := os.Stat(chartPath); os.IsNotExist(err) {
fetchFlags = append(fetchFlags, "--untar", "--untardir", chartPath)
@@ -423,7 +485,6 @@ func (st *HelmState) downloadCharts(helm helmexec.Interface, dir string, concurr
chartPath = filepath.Dir(fullChartPath)
}
}
results <- &downloadResults{release.Name, chartPath}
}
},
@@ -463,12 +524,14 @@ func (st *HelmState) TemplateReleases(helm helmexec.Interface, additionalValues
helm.SetExtraArgs(args...)
}
for _, release := range st.Releases {
for i := range st.Releases {
release := st.Releases[i]
if !release.Desired() {
continue
}
flags, err := st.flagsForTemplate(helm, &release)
flags, err := st.flagsForTemplate(helm, &release, 0)
if err != nil {
errs = append(errs, err)
}
@@ -523,12 +586,14 @@ func (st *HelmState) LintReleases(helm helmexec.Interface, additionalValues []st
helm.SetExtraArgs(args...)
}
for _, release := range st.Releases {
for i := range st.Releases {
release := st.Releases[i]
if !release.Desired() {
continue
}
flags, err := st.flagsForLint(helm, &release)
flags, err := st.flagsForLint(helm, &release, 0)
if err != nil {
errs = append(errs, err)
}
@@ -562,18 +627,8 @@ func (st *HelmState) LintReleases(helm helmexec.Interface, additionalValues []st
return nil
}
type DiffError struct {
*ReleaseSpec
err error
Code int
}
func (e *DiffError) Error() string {
return e.err.Error()
}
type diffResult struct {
err *DiffError
err *ReleaseError
}
type diffPrepareResult struct {
@@ -607,13 +662,13 @@ func (st *HelmState) prepareDiffReleases(helm helmexec.Interface, additionalValu
}
close(jobs)
},
func(_ int) {
func(workerIndex int) {
for release := range jobs {
errs := []error{}
st.applyDefaultsTo(release)
flags, err := st.flagsForDiff(helm, release)
flags, err := st.flagsForDiff(helm, release, workerIndex)
if err != nil {
errs = append(errs, err)
}
@@ -641,7 +696,7 @@ func (st *HelmState) prepareDiffReleases(helm helmexec.Interface, additionalValu
if len(errs) > 0 {
rsErrs := make([]*ReleaseError, len(errs))
for i, e := range errs {
rsErrs[i] = &ReleaseError{release, e}
rsErrs[i] = newReleaseError(release, e)
}
results <- diffPrepareResult{errors: rsErrs}
} else {
@@ -666,6 +721,23 @@ func (st *HelmState) prepareDiffReleases(helm helmexec.Interface, additionalValu
return rs, errs
}
func (st *HelmState) createHelmContext(spec *ReleaseSpec, workerIndex int) helmexec.HelmContext {
namespace := st.HelmDefaults.TillerNamespace
if spec.TillerNamespace != "" {
namespace = spec.TillerNamespace
}
tillerless := st.HelmDefaults.Tillerless
if spec.Tillerless != nil {
tillerless = *spec.Tillerless
}
return helmexec.HelmContext{
Tillerless: tillerless,
TillerNamespace: namespace,
WorkerIndex: workerIndex,
}
}
// DiffReleases wrapper for executing helm diff on the releases
// It returns releases that had any changes
func (st *HelmState) DiffReleases(helm helmexec.Interface, additionalValues []string, workerLimit int, detailedExitCode, suppressSecrets bool, triggerCleanupEvents bool) ([]*ReleaseSpec, []error) {
@@ -689,18 +761,17 @@ func (st *HelmState) DiffReleases(helm helmexec.Interface, additionalValues []st
}
close(jobQueue)
},
func(_ int) {
func(workerIndex int) {
for prep := range jobQueue {
flags := prep.flags
release := prep.release
if err := helm.DiffRelease(release.Name, normalizeChart(st.basePath, release.Chart), flags...); err != nil {
if err := helm.DiffRelease(st.createHelmContext(release, workerIndex), release.Name, normalizeChart(st.basePath, release.Chart), flags...); err != nil {
switch e := err.(type) {
case *exec.ExitError:
case helmexec.ExitError:
// Propagate any non-zero exit status from the external command like `helm` that is failed under the hood
status := e.Sys().(syscall.WaitStatus)
results <- diffResult{&DiffError{release, err, status.ExitStatus()}}
results <- diffResult{&ReleaseError{release, err, e.ExitStatus()}}
default:
results <- diffResult{&DiffError{release, err, 0}}
results <- diffResult{&ReleaseError{release, err, 0}}
}
} else {
// diff succeeded, found no changes
@@ -731,17 +802,21 @@ func (st *HelmState) DiffReleases(helm helmexec.Interface, additionalValues []st
}
func (st *HelmState) ReleaseStatuses(helm helmexec.Interface, workerLimit int) []error {
return st.scatterGatherReleases(helm, workerLimit, func(release ReleaseSpec) error {
return st.scatterGatherReleases(helm, workerLimit, func(release ReleaseSpec, workerIndex int) error {
if !release.Desired() {
return nil
}
return helm.ReleaseStatus(release.Name)
flags := []string{}
flags = st.appendTillerFlags(flags, &release)
return helm.ReleaseStatus(st.createHelmContext(&release, workerIndex), release.Name, flags...)
})
}
// DeleteReleases wrapper for executing helm delete on the releases
func (st *HelmState) DeleteReleases(helm helmexec.Interface, purge bool) []error {
return st.scatterGatherReleases(helm, len(st.Releases), func(release ReleaseSpec) error {
func (st *HelmState) DeleteReleases(affectedReleases *AffectedReleases, helm helmexec.Interface, purge bool) []error {
return st.scatterGatherReleases(helm, len(st.Releases), func(release ReleaseSpec, workerIndex int) error {
if !release.Desired() {
return nil
}
@@ -750,12 +825,21 @@ func (st *HelmState) DeleteReleases(helm helmexec.Interface, purge bool) []error
if purge {
flags = append(flags, "--purge")
}
installed, err := isReleaseInstalled(helm, release)
flags = st.appendTillerFlags(flags, &release)
context := st.createHelmContext(&release, workerIndex)
installed, err := st.isReleaseInstalled(context, helm, release)
if err != nil {
return err
}
if installed {
return helm.DeleteRelease(release.Name, flags...)
if err := helm.DeleteRelease(context, release.Name, flags...); err != nil {
affectedReleases.Failed = append(affectedReleases.Failed, &release)
return err
} else {
affectedReleases.Deleted = append(affectedReleases.Deleted, &release)
return nil
}
}
return nil
})
@@ -763,7 +847,7 @@ func (st *HelmState) DeleteReleases(helm helmexec.Interface, purge bool) []error
// TestReleases wrapper for executing helm test on the releases
func (st *HelmState) TestReleases(helm helmexec.Interface, cleanup bool, timeout int, concurrency int) []error {
return st.scatterGatherReleases(helm, concurrency, func(release ReleaseSpec) error {
return st.scatterGatherReleases(helm, concurrency, func(release ReleaseSpec, workerIndex int) error {
if !release.Desired() {
return nil
}
@@ -773,7 +857,9 @@ func (st *HelmState) TestReleases(helm helmexec.Interface, cleanup bool, timeout
flags = append(flags, "--cleanup")
}
flags = append(flags, "--timeout", strconv.Itoa(timeout))
return helm.TestRelease(release.Name, flags...)
flags = st.appendTillerFlags(flags, &release)
return helm.TestRelease(st.createHelmContext(&release, workerIndex), release.Name, flags...)
})
}
@@ -798,11 +884,11 @@ func (st *HelmState) Clean() []error {
}
// FilterReleases allows for the execution of helm commands against a subset of the releases in the helmfile.
func (st *HelmState) FilterReleases(labels []string) error {
func (st *HelmState) FilterReleases() error {
var filteredReleases []ReleaseSpec
releaseSet := map[string][]ReleaseSpec{}
filters := []ReleaseFilter{}
for _, label := range labels {
for _, label := range st.Selectors {
f, err := ParseLabels(label)
if err != nil {
return err
@@ -834,22 +920,32 @@ func (st *HelmState) FilterReleases(labels []string) error {
}
st.Releases = filteredReleases
numFound := len(filteredReleases)
st.logger.Debugf("%d release(s) matching %s found in %s\n", numFound, strings.Join(labels, ","), st.FilePath)
st.logger.Debugf("%d release(s) matching %s found in %s\n", numFound, strings.Join(st.Selectors, ","), st.FilePath)
return nil
}
func (st *HelmState) PrepareRelease(helm helmexec.Interface, helmfileCommand string) []error {
func (st *HelmState) PrepareReleases(helm helmexec.Interface, helmfileCommand string) []error {
errs := []error{}
for _, release := range st.Releases {
for i := range st.Releases {
release := st.Releases[i]
if _, err := st.triggerPrepareEvent(&release, helmfileCommand); err != nil {
errs = append(errs, &ReleaseError{&release, err})
errs = append(errs, newReleaseError(&release, err))
continue
}
}
if len(errs) != 0 {
return errs
}
updated, err := st.ResolveDeps()
if err != nil {
return []error{err}
}
*st = *updated
return nil
}
@@ -861,6 +957,14 @@ func (st *HelmState) triggerCleanupEvent(r *ReleaseSpec, helmfileCommand string)
return st.triggerReleaseEvent("cleanup", r, helmfileCommand)
}
func (st *HelmState) triggerPresyncEvent(r *ReleaseSpec, helmfileCommand string) (bool, error) {
return st.triggerReleaseEvent("presync", r, helmfileCommand)
}
func (st *HelmState) triggerPostsyncEvent(r *ReleaseSpec, helmfileCommand string) (bool, error) {
return st.triggerReleaseEvent("postsync", r, helmfileCommand)
}
func (st *HelmState) triggerReleaseEvent(evt string, r *ReleaseSpec, helmfileCmd string) (bool, error) {
bus := &event.Bus{
Hooks: r.Hooks,
@@ -878,6 +982,11 @@ func (st *HelmState) triggerReleaseEvent(evt string, r *ReleaseSpec, helmfileCmd
return bus.Trigger(evt, data)
}
// ResolveDeps returns a copy of this helmfile state with the concrete chart version numbers filled in for remote chart dependencies
func (st *HelmState) ResolveDeps() (*HelmState, error) {
return st.mergeLockedDependencies()
}
// UpdateDeps wrapper for updating dependencies on the releases
func (st *HelmState) UpdateDeps(helm helmexec.Interface) []error {
errs := []error{}
@@ -889,6 +998,18 @@ func (st *HelmState) UpdateDeps(helm helmexec.Interface) []error {
}
}
}
if len(errs) == 0 {
tempDir := st.tempDir
if tempDir == nil {
tempDir = ioutil.TempDir
}
_, err := st.updateDependenciesInTempDir(helm, tempDir)
if err != nil {
errs = append(errs, fmt.Errorf("unable to update deps: %v", err))
}
}
if len(errs) != 0 {
return errs
}
@@ -940,7 +1061,12 @@ func normalizeChart(basePath, chart string) string {
func isLocalChart(chart string) bool {
regex, _ := regexp.Compile("^[.]?./")
return regex.MatchString(chart)
matched := regex.MatchString(chart)
if matched {
return true
}
return chart == "" || chart[0] == '/' || len(strings.Split(chart, "/")) != 2
}
func pathExists(chart string) bool {
@@ -956,7 +1082,10 @@ func chartNameWithoutRepository(chart string) string {
// find "Chart.yaml"
func findChartDirectory(topLevelDir string) (string, error) {
var files []string
filepath.Walk(topLevelDir, func(path string, f os.FileInfo, _ error) error {
filepath.Walk(topLevelDir, func(path string, f os.FileInfo, err error) error {
if err != nil {
return fmt.Errorf("error walking through %s: %v", path, err)
}
if !f.IsDir() {
r, err := regexp.MatchString("Chart.yaml", f.Name())
if err == nil && r {
@@ -976,38 +1105,53 @@ func findChartDirectory(topLevelDir string) (string, error) {
}
func (st *HelmState) appendTillerFlags(flags []string, release *ReleaseSpec) []string {
if release.TillerNamespace != "" {
flags = append(flags, "--tiller-namespace", release.TillerNamespace)
} else if st.HelmDefaults.TillerNamespace != "" {
flags = append(flags, "--tiller-namespace", st.HelmDefaults.TillerNamespace)
adds := st.tillerFlags(release)
for _, a := range adds {
flags = append(flags, a)
}
return flags
}
if release.TLS != nil && *release.TLS || release.TLS == nil && st.HelmDefaults.TLS {
flags = append(flags, "--tls")
func (st *HelmState) tillerFlags(release *ReleaseSpec) []string {
flags := []string{}
tillerless := st.HelmDefaults.Tillerless
if release.Tillerless != nil {
tillerless = *release.Tillerless
}
if !tillerless {
if release.TillerNamespace != "" {
flags = append(flags, "--tiller-namespace", release.TillerNamespace)
} else if st.HelmDefaults.TillerNamespace != "" {
flags = append(flags, "--tiller-namespace", st.HelmDefaults.TillerNamespace)
}
if release.TLSKey != "" {
flags = append(flags, "--tls-key", release.TLSKey)
} else if st.HelmDefaults.TLSKey != "" {
flags = append(flags, "--tls-key", st.HelmDefaults.TLSKey)
}
if release.TLS != nil && *release.TLS || release.TLS == nil && st.HelmDefaults.TLS {
flags = append(flags, "--tls")
}
if release.TLSCert != "" {
flags = append(flags, "--tls-cert", release.TLSCert)
} else if st.HelmDefaults.TLSCert != "" {
flags = append(flags, "--tls-cert", st.HelmDefaults.TLSCert)
}
if release.TLSKey != "" {
flags = append(flags, "--tls-key", release.TLSKey)
} else if st.HelmDefaults.TLSKey != "" {
flags = append(flags, "--tls-key", st.HelmDefaults.TLSKey)
}
if release.TLSCACert != "" {
flags = append(flags, "--tls-ca-cert", release.TLSCACert)
} else if st.HelmDefaults.TLSCACert != "" {
flags = append(flags, "--tls-ca-cert", st.HelmDefaults.TLSCACert)
if release.TLSCert != "" {
flags = append(flags, "--tls-cert", release.TLSCert)
} else if st.HelmDefaults.TLSCert != "" {
flags = append(flags, "--tls-cert", st.HelmDefaults.TLSCert)
}
if release.TLSCACert != "" {
flags = append(flags, "--tls-ca-cert", release.TLSCACert)
} else if st.HelmDefaults.TLSCACert != "" {
flags = append(flags, "--tls-ca-cert", st.HelmDefaults.TLSCACert)
}
}
return flags
}
func (st *HelmState) flagsForUpgrade(helm helmexec.Interface, release *ReleaseSpec) ([]string, error) {
func (st *HelmState) flagsForUpgrade(helm helmexec.Interface, release *ReleaseSpec, workerIndex int) ([]string, error) {
flags := []string{}
if release.Version != "" {
flags = append(flags, "--version", release.Version)
@@ -1047,25 +1191,25 @@ func (st *HelmState) flagsForUpgrade(helm helmexec.Interface, release *ReleaseSp
flags = st.appendTillerFlags(flags, release)
common, err := st.namespaceAndValuesFlags(helm, release)
common, err := st.namespaceAndValuesFlags(helm, release, workerIndex)
if err != nil {
return nil, err
}
return append(flags, common...), nil
}
func (st *HelmState) flagsForTemplate(helm helmexec.Interface, release *ReleaseSpec) ([]string, error) {
func (st *HelmState) flagsForTemplate(helm helmexec.Interface, release *ReleaseSpec, workerIndex int) ([]string, error) {
flags := []string{
"--name", release.Name,
}
common, err := st.namespaceAndValuesFlags(helm, release)
common, err := st.namespaceAndValuesFlags(helm, release, workerIndex)
if err != nil {
return nil, err
}
return append(flags, common...), nil
}
func (st *HelmState) flagsForDiff(helm helmexec.Interface, release *ReleaseSpec) ([]string, error) {
func (st *HelmState) flagsForDiff(helm helmexec.Interface, release *ReleaseSpec, workerIndex int) ([]string, error) {
flags := []string{}
if release.Version != "" {
flags = append(flags, "--version", release.Version)
@@ -1077,7 +1221,7 @@ func (st *HelmState) flagsForDiff(helm helmexec.Interface, release *ReleaseSpec)
flags = st.appendTillerFlags(flags, release)
common, err := st.namespaceAndValuesFlags(helm, release)
common, err := st.namespaceAndValuesFlags(helm, release, workerIndex)
if err != nil {
return nil, err
}
@@ -1093,8 +1237,8 @@ func (st *HelmState) isDevelopment(release *ReleaseSpec) bool {
return result
}
func (st *HelmState) flagsForLint(helm helmexec.Interface, release *ReleaseSpec) ([]string, error) {
return st.namespaceAndValuesFlags(helm, release)
func (st *HelmState) flagsForLint(helm helmexec.Interface, release *ReleaseSpec, workerIndex int) ([]string, error) {
return st.namespaceAndValuesFlags(helm, release, workerIndex)
}
func (st *HelmState) RenderValuesFileToBytes(path string) ([]byte, error) {
@@ -1164,7 +1308,7 @@ func (st *HelmState) generateTemporaryValuesFiles(values []interface{}, missingF
return generatedFiles, nil
}
func (st *HelmState) namespaceAndValuesFlags(helm helmexec.Interface, release *ReleaseSpec) ([]string, error) {
func (st *HelmState) namespaceAndValuesFlags(helm helmexec.Interface, release *ReleaseSpec, workerIndex int) ([]string, error) {
flags := []string{}
if release.Namespace != "" {
flags = append(flags, "--namespace", release.Namespace)
@@ -1213,7 +1357,8 @@ func (st *HelmState) namespaceAndValuesFlags(helm helmexec.Interface, release *R
}
}
valfile, err := helm.DecryptSecret(path)
decryptFlags := st.appendTillerFlags([]string{}, release)
valfile, err := helm.DecryptSecret(st.createHelmContext(release, workerIndex), path, decryptFlags...)
if err != nil {
return nil, err
}
@@ -1268,8 +1413,75 @@ func (st *HelmState) namespaceAndValuesFlags(helm helmexec.Interface, release *R
return flags, nil
}
// DisplayAffectedReleases logs the upgraded, deleted and in error releases
func (ar *AffectedReleases) DisplayAffectedReleases(logger *zap.SugaredLogger) {
if ar.Upgraded != nil {
logger.Info("\nList of updated releases :")
tbl, _ := prettytable.NewTable(prettytable.Column{Header: "RELEASE"},
prettytable.Column{Header: "CHART", MinWidth: 6},
prettytable.Column{Header: "VERSION", AlignRight: true},
)
tbl.Separator = " "
for _, release := range ar.Upgraded {
tbl.AddRow(release.Name, release.Chart, release.installedVersion)
}
tbl.Print()
}
if ar.Deleted != nil {
logger.Info("\nList of deleted releases :")
logger.Info("RELEASE")
for _, release := range ar.Deleted {
logger.Info(release.Name)
}
}
if ar.Failed != nil {
logger.Info("\nList of releases in error :")
logger.Info("RELEASE")
for _, release := range ar.Failed {
logger.Info(release.Name)
}
}
}
func escape(value string) string {
intermediate := strings.Replace(value, "{", "\\{", -1)
intermediate = strings.Replace(intermediate, "}", "\\}", -1)
return strings.Replace(intermediate, ",", "\\,", -1)
}
//UnmarshalYAML will unmarshal the helmfile yaml section and fill the SubHelmfileSpec structure
//this is required to keep allowing string scalar for defining helmfile
func (hf *SubHelmfileSpec) UnmarshalYAML(unmarshal func(interface{}) error) error {
var tmp interface{}
if err := unmarshal(&tmp); err != nil {
return err
}
switch i := tmp.(type) {
case string: // single path definition without sub items, legacy sub helmfile definition
hf.Path = i
case map[interface{}]interface{}: // helmfile path with sub section
var subHelmfileSpecTmp struct {
Path string `yaml:"path"`
Selectors []string `yaml:"selectors"`
SelectorsInherited bool `yaml:"selectorsInherited"`
}
if err := unmarshal(&subHelmfileSpecTmp); err != nil {
return err
}
hf.Path = subHelmfileSpecTmp.Path
hf.Selectors = subHelmfileSpecTmp.Selectors
hf.SelectorsInherited = subHelmfileSpecTmp.SelectorsInherited
}
//since we cannot make sur the "console" string can be red after the "path" we must check we don't have
//a SubHelmfileSpec with only selector and no path
if hf.Selectors != nil && hf.Path == "" {
return fmt.Errorf("found 'selectors' definition without path: %v", hf.Selectors)
}
//also exclude SelectorsInherited to true and explicit selectors
if hf.SelectorsInherited && len(hf.Selectors) > 0 {
return fmt.Errorf("You cannot use 'SelectorsInherited: true' along with and explicit selector for path: %v", hf.Path)
}
return nil
}
+2 -1
View File
@@ -36,7 +36,8 @@ func TestHelmState_executeTemplates(t *testing.T) {
},
}
for _, tt := range tests {
for i := range tests {
tt := tests[i]
t.Run(tt.name, func(t *testing.T) {
state := &HelmState{
basePath: ".",
+3 -2
View File
@@ -40,7 +40,8 @@ func (st *HelmState) scatterGather(concurrency int, items int, produceInputs fun
waitGroup.Wait()
}
func (st *HelmState) scatterGatherReleases(helm helmexec.Interface, concurrency int, do func(ReleaseSpec) error) []error {
func (st *HelmState) scatterGatherReleases(helm helmexec.Interface, concurrency int,
do func(ReleaseSpec, int) error) []error {
var errs []error
inputs := st.Releases
@@ -60,7 +61,7 @@ func (st *HelmState) scatterGatherReleases(helm helmexec.Interface, concurrency
},
func(id int) {
for release := range releases {
err := do(release)
err := do(release, id)
st.logger.Debugf("sending result for release: %s\n", release.Name)
results <- result{release: release, err: err}
st.logger.Debugf("sent result for release: %s\n", release.Name)
+703 -64
View File
File diff suppressed because it is too large Load Diff
+134
View File
@@ -0,0 +1,134 @@
package state
import (
"fmt"
"path/filepath"
"strings"
)
type TestFs struct {
Cwd string
dirs map[string]bool
files map[string]string
GlobFixtures map[string][]string
fileReaderCalls int
successfulReads []string
}
func NewTestFs(files map[string]string) *TestFs {
dirs := map[string]bool{}
for abs, _ := range files {
d := filepath.Dir(abs)
dirs[d] = true
}
return &TestFs{
Cwd: "/path/to",
dirs: dirs,
files: files,
successfulReads: []string{},
GlobFixtures: map[string][]string{},
}
}
func (f *TestFs) FileExistsAt(path string) bool {
var ok bool
if strings.Contains(path, "/") {
_, ok = f.files[path]
} else {
_, ok = f.files[filepath.Join(f.Cwd, path)]
}
return ok
}
func (f *TestFs) FileExists(path string) (bool, error) {
return f.FileExistsAt(path), nil
}
func (f *TestFs) DirectoryExistsAt(path string) bool {
var ok bool
if strings.Contains(path, "/") {
_, ok = f.dirs[path]
} else {
_, ok = f.dirs[filepath.Join(f.Cwd, path)]
}
return ok
}
func (f *TestFs) ReadFile(filename string) ([]byte, error) {
var str string
var ok bool
if filename[0] == '/' {
str, ok = f.files[filename]
} else {
str, ok = f.files[filepath.Join(f.Cwd, filename)]
}
if !ok {
return []byte(nil), fmt.Errorf("no registered file found: %s", filename)
}
f.fileReaderCalls += 1
f.successfulReads = append(f.successfulReads, filename)
return []byte(str), nil
}
func (f *TestFs) SuccessfulReads() []string {
return f.successfulReads
}
func (f *TestFs) FileReaderCalls() int {
return f.fileReaderCalls
}
func (f *TestFs) Glob(relPattern string) ([]string, error) {
var pattern string
if relPattern[0] == '/' {
pattern = relPattern
} else {
pattern = filepath.Join(f.Cwd, relPattern)
}
fixtures, ok := f.GlobFixtures[pattern]
if ok {
return fixtures, nil
}
matches := []string{}
for name, _ := range f.files {
matched, err := filepath.Match(pattern, name)
if err != nil {
return nil, err
}
if matched {
matches = append(matches, name)
}
}
return matches, nil
}
func (f *TestFs) Abs(path string) (string, error) {
var p string
if path[0] == '/' {
p = path
} else {
p = filepath.Join(f.Cwd, path)
}
return filepath.Clean(p), nil
}
func (f *TestFs) Getwd() (string, error) {
return f.Cwd, nil
}
func (f *TestFs) Chdir(dir string) error {
if _, ok := f.dirs[dir]; ok {
f.Cwd = dir
return nil
}
return fmt.Errorf("unexpected chdir \"%s\"", dir)
}
+1
View File
@@ -0,0 +1 @@
mysecret: MYSECRET
+13
View File
@@ -1,6 +1,13 @@
environments:
default:
values:
- environment.values.yaml
---
repositories:
- name: stable
url: https://kubernetes-charts.storage.googleapis.com/
- name: incubator
url: https://kubernetes-charts-incubator.storage.googleapis.com
helmDefaults:
kubeContext: minikube
@@ -12,3 +19,9 @@ releases:
set:
- name: ingress.enabled
value: false
- name: raw
chart: incubator/raw
values:
- mysecret: {{ .Environment.Values.mysecret }}
- values.yaml
+10
View File
@@ -76,6 +76,16 @@ ${helmfile} -f ${dir}/happypath.yaml apply
code=$?
[ ${code} -eq 0 ] || fail "unexpected exit code returned by helmfile apply: ${code}"
info "Locking dependencies"
${helmfile} -f ${dir}/happypath.yaml deps
code=$?
[ ${code} -eq 0 ] || fail "unexpected exit code returned by helmfile deps: ${code}"
info "Applying ${dir}/happypath.yaml with locked dependencies"
${helmfile} -f ${dir}/happypath.yaml apply
code=$?
[ ${code} -eq 0 ] || fail "unexpected exit code returned by helmfile apply: ${code}"
info "Deleting release"
${helmfile} -f ${dir}/happypath.yaml delete
${helm} status --namespace=${test_ns} httpbin &> /dev/null && fail "release should not exist anymore after a delete"
+9
View File
@@ -0,0 +1,9 @@
resources: []
templates:
- |
apiVersion: v1
kind: Secret
metadata:
name: common-secret
stringData:
mykey: {{ .Values.mysecret }}
+2 -1
View File
@@ -230,7 +230,8 @@ func Test_renderTemplateToString(t *testing.T) {
wantErr: true,
},
}
for _, tt := range tests {
for i := range tests {
tt := tests[i]
t.Run(tt.name, func(t *testing.T) {
for k, v := range tt.args.envs {
err := os.Setenv(k, v)
-27
View File
@@ -1,27 +0,0 @@
language: go
go:
- 1.6.x
- 1.7.x
- 1.8.x
- 1.9.x
- 1.10.x
- tip
# Setting sudo access to false will let Travis CI use containers rather than
# VMs to run the tests. For more details see:
# - http://docs.travis-ci.com/user/workers/container-based-infrastructure/
# - http://docs.travis-ci.com/user/workers/standard-infrastructure/
sudo: false
script:
- make setup
- make test
notifications:
webhooks:
urls:
- https://webhooks.gitter.im/e/06e3328629952dabe3e0
on_success: change # options: [always|never|change] default: always
on_failure: always # options: [always|never|change] default: always
on_start: never # options: [always|never|change] default: always
-77
View File
@@ -1,77 +0,0 @@
# 1.4.1 (2018-04-02)
## Fixed
- Fixed #64: Fix pre-release precedence issue (thanks @uudashr)
# 1.4.0 (2017-10-04)
## Changed
- #61: Update NewVersion to parse ints with a 64bit int size (thanks @zknill)
# 1.3.1 (2017-07-10)
## Fixed
- Fixed #57: number comparisons in prerelease sometimes inaccurate
# 1.3.0 (2017-05-02)
## Added
- #45: Added json (un)marshaling support (thanks @mh-cbon)
- Stability marker. See https://masterminds.github.io/stability/
## Fixed
- #51: Fix handling of single digit tilde constraint (thanks @dgodd)
## Changed
- #55: The godoc icon moved from png to svg
# 1.2.3 (2017-04-03)
## Fixed
- #46: Fixed 0.x.x and 0.0.x in constraints being treated as *
# Release 1.2.2 (2016-12-13)
## Fixed
- #34: Fixed issue where hyphen range was not working with pre-release parsing.
# Release 1.2.1 (2016-11-28)
## Fixed
- #24: Fixed edge case issue where constraint "> 0" does not handle "0.0.1-alpha"
properly.
# Release 1.2.0 (2016-11-04)
## Added
- #20: Added MustParse function for versions (thanks @adamreese)
- #15: Added increment methods on versions (thanks @mh-cbon)
## Fixed
- Issue #21: Per the SemVer spec (section 9) a pre-release is unstable and
might not satisfy the intended compatibility. The change here ignores pre-releases
on constraint checks (e.g., ~ or ^) when a pre-release is not part of the
constraint. For example, `^1.2.3` will ignore pre-releases while
`^1.2.3-alpha` will include them.
# Release 1.1.1 (2016-06-30)
## Changed
- Issue #9: Speed up version comparison performance (thanks @sdboyer)
- Issue #8: Added benchmarks (thanks @sdboyer)
- Updated Go Report Card URL to new location
- Updated Readme to add code snippet formatting (thanks @mh-cbon)
- Updating tagging to v[SemVer] structure for compatibility with other tools.
# Release 1.1.0 (2016-03-11)
- Issue #2: Implemented validation to provide reasons a versions failed a
constraint.
# Release 1.0.1 (2015-12-31)
- Fixed #1: * constraint failing on valid versions.
# Release 1.0.0 (2015-10-20)
- Initial release
-20
View File
@@ -1,20 +0,0 @@
The Masterminds
Copyright (C) 2014-2015, Matt Butcher and Matt Farina
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-36
View File
@@ -1,36 +0,0 @@
.PHONY: setup
setup:
go get -u gopkg.in/alecthomas/gometalinter.v1
gometalinter.v1 --install
.PHONY: test
test: validate lint
@echo "==> Running tests"
go test -v
.PHONY: validate
validate:
@echo "==> Running static validations"
@gometalinter.v1 \
--disable-all \
--enable deadcode \
--severity deadcode:error \
--enable gofmt \
--enable gosimple \
--enable ineffassign \
--enable misspell \
--enable vet \
--tests \
--vendor \
--deadline 60s \
./... || exit_code=1
.PHONY: lint
lint:
@echo "==> Running linters"
@gometalinter.v1 \
--disable-all \
--enable golint \
--vendor \
--deadline 60s \
./... || :
-165
View File
@@ -1,165 +0,0 @@
# SemVer
The `semver` package provides the ability to work with [Semantic Versions](http://semver.org) in Go. Specifically it provides the ability to:
* Parse semantic versions
* Sort semantic versions
* Check if a semantic version fits within a set of constraints
* Optionally work with a `v` prefix
[![Stability:
Active](https://masterminds.github.io/stability/active.svg)](https://masterminds.github.io/stability/active.html)
[![Build Status](https://travis-ci.org/Masterminds/semver.svg)](https://travis-ci.org/Masterminds/semver) [![Build status](https://ci.appveyor.com/api/projects/status/jfk66lib7hb985k8/branch/master?svg=true&passingText=windows%20build%20passing&failingText=windows%20build%20failing)](https://ci.appveyor.com/project/mattfarina/semver/branch/master) [![GoDoc](https://godoc.org/github.com/Masterminds/semver?status.svg)](https://godoc.org/github.com/Masterminds/semver) [![Go Report Card](https://goreportcard.com/badge/github.com/Masterminds/semver)](https://goreportcard.com/report/github.com/Masterminds/semver)
## Parsing Semantic Versions
To parse a semantic version use the `NewVersion` function. For example,
```go
v, err := semver.NewVersion("1.2.3-beta.1+build345")
```
If there is an error the version wasn't parseable. The version object has methods
to get the parts of the version, compare it to other versions, convert the
version back into a string, and get the original string. For more details
please see the [documentation](https://godoc.org/github.com/Masterminds/semver).
## Sorting Semantic Versions
A set of versions can be sorted using the [`sort`](https://golang.org/pkg/sort/)
package from the standard library. For example,
```go
raw := []string{"1.2.3", "1.0", "1.3", "2", "0.4.2",}
vs := make([]*semver.Version, len(raw))
for i, r := range raw {
v, err := semver.NewVersion(r)
if err != nil {
t.Errorf("Error parsing version: %s", err)
}
vs[i] = v
}
sort.Sort(semver.Collection(vs))
```
## Checking Version Constraints
Checking a version against version constraints is one of the most featureful
parts of the package.
```go
c, err := semver.NewConstraint(">= 1.2.3")
if err != nil {
// Handle constraint not being parseable.
}
v, _ := semver.NewVersion("1.3")
if err != nil {
// Handle version not being parseable.
}
// Check if the version meets the constraints. The a variable will be true.
a := c.Check(v)
```
## Basic Comparisons
There are two elements to the comparisons. First, a comparison string is a list
of comma separated and comparisons. These are then separated by || separated or
comparisons. For example, `">= 1.2, < 3.0.0 || >= 4.2.3"` is looking for a
comparison that's greater than or equal to 1.2 and less than 3.0.0 or is
greater than or equal to 4.2.3.
The basic comparisons are:
* `=`: equal (aliased to no operator)
* `!=`: not equal
* `>`: greater than
* `<`: less than
* `>=`: greater than or equal to
* `<=`: less than or equal to
_Note, according to the Semantic Version specification pre-releases may not be
API compliant with their release counterpart. It says,_
> _A pre-release version indicates that the version is unstable and might not satisfy the intended compatibility requirements as denoted by its associated normal version._
_SemVer comparisons without a pre-release value will skip pre-release versions.
For example, `>1.2.3` will skip pre-releases when looking at a list of values
while `>1.2.3-alpha.1` will evaluate pre-releases._
## Hyphen Range Comparisons
There are multiple methods to handle ranges and the first is hyphens ranges.
These look like:
* `1.2 - 1.4.5` which is equivalent to `>= 1.2, <= 1.4.5`
* `2.3.4 - 4.5` which is equivalent to `>= 2.3.4, <= 4.5`
## Wildcards In Comparisons
The `x`, `X`, and `*` characters can be used as a wildcard character. This works
for all comparison operators. When used on the `=` operator it falls
back to the pack level comparison (see tilde below). For example,
* `1.2.x` is equivalent to `>= 1.2.0, < 1.3.0`
* `>= 1.2.x` is equivalent to `>= 1.2.0`
* `<= 2.x` is equivalent to `<= 3`
* `*` is equivalent to `>= 0.0.0`
## Tilde Range Comparisons (Patch)
The tilde (`~`) comparison operator is for patch level ranges when a minor
version is specified and major level changes when the minor number is missing.
For example,
* `~1.2.3` is equivalent to `>= 1.2.3, < 1.3.0`
* `~1` is equivalent to `>= 1, < 2`
* `~2.3` is equivalent to `>= 2.3, < 2.4`
* `~1.2.x` is equivalent to `>= 1.2.0, < 1.3.0`
* `~1.x` is equivalent to `>= 1, < 2`
## Caret Range Comparisons (Major)
The caret (`^`) comparison operator is for major level changes. This is useful
when comparisons of API versions as a major change is API breaking. For example,
* `^1.2.3` is equivalent to `>= 1.2.3, < 2.0.0`
* `^1.2.x` is equivalent to `>= 1.2.0, < 2.0.0`
* `^2.3` is equivalent to `>= 2.3, < 3`
* `^2.x` is equivalent to `>= 2.0.0, < 3`
# Validation
In addition to testing a version against a constraint, a version can be validated
against a constraint. When validation fails a slice of errors containing why a
version didn't meet the constraint is returned. For example,
```go
c, err := semver.NewConstraint("<= 1.2.3, >= 1.4")
if err != nil {
// Handle constraint not being parseable.
}
v, _ := semver.NewVersion("1.3")
if err != nil {
// Handle version not being parseable.
}
// Validate a version against a constraint.
a, msgs := c.Validate(v)
// a is false
for _, m := range msgs {
fmt.Println(m)
// Loops over the errors which would read
// "1.3 is greater than 1.2.3"
// "1.3 is less than 1.4"
}
```
# Contribute
If you find an issue or want to contribute please file an [issue](https://github.com/Masterminds/semver/issues)
or [create a pull request](https://github.com/Masterminds/semver/pulls).
-44
View File
@@ -1,44 +0,0 @@
version: build-{build}.{branch}
clone_folder: C:\gopath\src\github.com\Masterminds\semver
shallow_clone: true
environment:
GOPATH: C:\gopath
platform:
- x64
install:
- go version
- go env
- go get -u gopkg.in/alecthomas/gometalinter.v1
- set PATH=%PATH%;%GOPATH%\bin
- gometalinter.v1.exe --install
build_script:
- go install -v ./...
test_script:
- "gometalinter.v1 \
--disable-all \
--enable deadcode \
--severity deadcode:error \
--enable gofmt \
--enable gosimple \
--enable ineffassign \
--enable misspell \
--enable vet \
--tests \
--vendor \
--deadline 60s \
./... || exit_code=1"
- "gometalinter.v1 \
--disable-all \
--enable golint \
--vendor \
--deadline 60s \
./... || :"
- go test -v
deploy: off
-24
View File
@@ -1,24 +0,0 @@
package semver
// Collection is a collection of Version instances and implements the sort
// interface. See the sort package for more details.
// https://golang.org/pkg/sort/
type Collection []*Version
// Len returns the length of a collection. The number of Version instances
// on the slice.
func (c Collection) Len() int {
return len(c)
}
// Less is needed for the sort interface to compare two Version objects on the
// slice. If checks if one is less than the other.
func (c Collection) Less(i, j int) bool {
return c[i].LessThan(c[j])
}
// Swap is needed for the sort interface to replace the Version objects
// at two different positions in the slice.
func (c Collection) Swap(i, j int) {
c[i], c[j] = c[j], c[i]
}
-426
View File
@@ -1,426 +0,0 @@
package semver
import (
"errors"
"fmt"
"regexp"
"strings"
)
// Constraints is one or more constraint that a semantic version can be
// checked against.
type Constraints struct {
constraints [][]*constraint
}
// NewConstraint returns a Constraints instance that a Version instance can
// be checked against. If there is a parse error it will be returned.
func NewConstraint(c string) (*Constraints, error) {
// Rewrite - ranges into a comparison operation.
c = rewriteRange(c)
ors := strings.Split(c, "||")
or := make([][]*constraint, len(ors))
for k, v := range ors {
cs := strings.Split(v, ",")
result := make([]*constraint, len(cs))
for i, s := range cs {
pc, err := parseConstraint(s)
if err != nil {
return nil, err
}
result[i] = pc
}
or[k] = result
}
o := &Constraints{constraints: or}
return o, nil
}
// Check tests if a version satisfies the constraints.
func (cs Constraints) Check(v *Version) bool {
// loop over the ORs and check the inner ANDs
for _, o := range cs.constraints {
joy := true
for _, c := range o {
if !c.check(v) {
joy = false
break
}
}
if joy {
return true
}
}
return false
}
// Validate checks if a version satisfies a constraint. If not a slice of
// reasons for the failure are returned in addition to a bool.
func (cs Constraints) Validate(v *Version) (bool, []error) {
// loop over the ORs and check the inner ANDs
var e []error
for _, o := range cs.constraints {
joy := true
for _, c := range o {
if !c.check(v) {
em := fmt.Errorf(c.msg, v, c.orig)
e = append(e, em)
joy = false
}
}
if joy {
return true, []error{}
}
}
return false, e
}
var constraintOps map[string]cfunc
var constraintMsg map[string]string
var constraintRegex *regexp.Regexp
func init() {
constraintOps = map[string]cfunc{
"": constraintTildeOrEqual,
"=": constraintTildeOrEqual,
"!=": constraintNotEqual,
">": constraintGreaterThan,
"<": constraintLessThan,
">=": constraintGreaterThanEqual,
"=>": constraintGreaterThanEqual,
"<=": constraintLessThanEqual,
"=<": constraintLessThanEqual,
"~": constraintTilde,
"~>": constraintTilde,
"^": constraintCaret,
}
constraintMsg = map[string]string{
"": "%s is not equal to %s",
"=": "%s is not equal to %s",
"!=": "%s is equal to %s",
">": "%s is less than or equal to %s",
"<": "%s is greater than or equal to %s",
">=": "%s is less than %s",
"=>": "%s is less than %s",
"<=": "%s is greater than %s",
"=<": "%s is greater than %s",
"~": "%s does not have same major and minor version as %s",
"~>": "%s does not have same major and minor version as %s",
"^": "%s does not have same major version as %s",
}
ops := make([]string, 0, len(constraintOps))
for k := range constraintOps {
ops = append(ops, regexp.QuoteMeta(k))
}
constraintRegex = regexp.MustCompile(fmt.Sprintf(
`^\s*(%s)\s*(%s)\s*$`,
strings.Join(ops, "|"),
cvRegex))
constraintRangeRegex = regexp.MustCompile(fmt.Sprintf(
`\s*(%s)\s+-\s+(%s)\s*`,
cvRegex, cvRegex))
}
// An individual constraint
type constraint struct {
// The callback function for the restraint. It performs the logic for
// the constraint.
function cfunc
msg string
// The version used in the constraint check. For example, if a constraint
// is '<= 2.0.0' the con a version instance representing 2.0.0.
con *Version
// The original parsed version (e.g., 4.x from != 4.x)
orig string
// When an x is used as part of the version (e.g., 1.x)
minorDirty bool
dirty bool
patchDirty bool
}
// Check if a version meets the constraint
func (c *constraint) check(v *Version) bool {
return c.function(v, c)
}
type cfunc func(v *Version, c *constraint) bool
func parseConstraint(c string) (*constraint, error) {
m := constraintRegex.FindStringSubmatch(c)
if m == nil {
return nil, fmt.Errorf("improper constraint: %s", c)
}
ver := m[2]
orig := ver
minorDirty := false
patchDirty := false
dirty := false
if isX(m[3]) {
ver = "0.0.0"
dirty = true
} else if isX(strings.TrimPrefix(m[4], ".")) || m[4] == "" {
minorDirty = true
dirty = true
ver = fmt.Sprintf("%s.0.0%s", m[3], m[6])
} else if isX(strings.TrimPrefix(m[5], ".")) {
dirty = true
patchDirty = true
ver = fmt.Sprintf("%s%s.0%s", m[3], m[4], m[6])
}
con, err := NewVersion(ver)
if err != nil {
// The constraintRegex should catch any regex parsing errors. So,
// we should never get here.
return nil, errors.New("constraint Parser Error")
}
cs := &constraint{
function: constraintOps[m[1]],
msg: constraintMsg[m[1]],
con: con,
orig: orig,
minorDirty: minorDirty,
patchDirty: patchDirty,
dirty: dirty,
}
return cs, nil
}
// Constraint functions
func constraintNotEqual(v *Version, c *constraint) bool {
if c.dirty {
// If there is a pre-release on the version but the constraint isn't looking
// for them assume that pre-releases are not compatible. See issue 21 for
// more details.
if v.Prerelease() != "" && c.con.Prerelease() == "" {
return false
}
if c.con.Major() != v.Major() {
return true
}
if c.con.Minor() != v.Minor() && !c.minorDirty {
return true
} else if c.minorDirty {
return false
}
return false
}
return !v.Equal(c.con)
}
func constraintGreaterThan(v *Version, c *constraint) bool {
// An edge case the constraint is 0.0.0 and the version is 0.0.0-someprerelease
// exists. This that case.
if !isNonZero(c.con) && isNonZero(v) {
return true
}
// If there is a pre-release on the version but the constraint isn't looking
// for them assume that pre-releases are not compatible. See issue 21 for
// more details.
if v.Prerelease() != "" && c.con.Prerelease() == "" {
return false
}
return v.Compare(c.con) == 1
}
func constraintLessThan(v *Version, c *constraint) bool {
// If there is a pre-release on the version but the constraint isn't looking
// for them assume that pre-releases are not compatible. See issue 21 for
// more details.
if v.Prerelease() != "" && c.con.Prerelease() == "" {
return false
}
if !c.dirty {
return v.Compare(c.con) < 0
}
if v.Major() > c.con.Major() {
return false
} else if v.Minor() > c.con.Minor() && !c.minorDirty {
return false
}
return true
}
func constraintGreaterThanEqual(v *Version, c *constraint) bool {
// An edge case the constraint is 0.0.0 and the version is 0.0.0-someprerelease
// exists. This that case.
if !isNonZero(c.con) && isNonZero(v) {
return true
}
// If there is a pre-release on the version but the constraint isn't looking
// for them assume that pre-releases are not compatible. See issue 21 for
// more details.
if v.Prerelease() != "" && c.con.Prerelease() == "" {
return false
}
return v.Compare(c.con) >= 0
}
func constraintLessThanEqual(v *Version, c *constraint) bool {
// If there is a pre-release on the version but the constraint isn't looking
// for them assume that pre-releases are not compatible. See issue 21 for
// more details.
if v.Prerelease() != "" && c.con.Prerelease() == "" {
return false
}
if !c.dirty {
return v.Compare(c.con) <= 0
}
if v.Major() > c.con.Major() {
return false
} else if v.Minor() > c.con.Minor() && !c.minorDirty {
return false
}
return true
}
// ~*, ~>* --> >= 0.0.0 (any)
// ~2, ~2.x, ~2.x.x, ~>2, ~>2.x ~>2.x.x --> >=2.0.0, <3.0.0
// ~2.0, ~2.0.x, ~>2.0, ~>2.0.x --> >=2.0.0, <2.1.0
// ~1.2, ~1.2.x, ~>1.2, ~>1.2.x --> >=1.2.0, <1.3.0
// ~1.2.3, ~>1.2.3 --> >=1.2.3, <1.3.0
// ~1.2.0, ~>1.2.0 --> >=1.2.0, <1.3.0
func constraintTilde(v *Version, c *constraint) bool {
// If there is a pre-release on the version but the constraint isn't looking
// for them assume that pre-releases are not compatible. See issue 21 for
// more details.
if v.Prerelease() != "" && c.con.Prerelease() == "" {
return false
}
if v.LessThan(c.con) {
return false
}
// ~0.0.0 is a special case where all constraints are accepted. It's
// equivalent to >= 0.0.0.
if c.con.Major() == 0 && c.con.Minor() == 0 && c.con.Patch() == 0 &&
!c.minorDirty && !c.patchDirty {
return true
}
if v.Major() != c.con.Major() {
return false
}
if v.Minor() != c.con.Minor() && !c.minorDirty {
return false
}
return true
}
// When there is a .x (dirty) status it automatically opts in to ~. Otherwise
// it's a straight =
func constraintTildeOrEqual(v *Version, c *constraint) bool {
// If there is a pre-release on the version but the constraint isn't looking
// for them assume that pre-releases are not compatible. See issue 21 for
// more details.
if v.Prerelease() != "" && c.con.Prerelease() == "" {
return false
}
if c.dirty {
c.msg = constraintMsg["~"]
return constraintTilde(v, c)
}
return v.Equal(c.con)
}
// ^* --> (any)
// ^2, ^2.x, ^2.x.x --> >=2.0.0, <3.0.0
// ^2.0, ^2.0.x --> >=2.0.0, <3.0.0
// ^1.2, ^1.2.x --> >=1.2.0, <2.0.0
// ^1.2.3 --> >=1.2.3, <2.0.0
// ^1.2.0 --> >=1.2.0, <2.0.0
func constraintCaret(v *Version, c *constraint) bool {
// If there is a pre-release on the version but the constraint isn't looking
// for them assume that pre-releases are not compatible. See issue 21 for
// more details.
if v.Prerelease() != "" && c.con.Prerelease() == "" {
return false
}
if v.LessThan(c.con) {
return false
}
if v.Major() != c.con.Major() {
return false
}
return true
}
var constraintRangeRegex *regexp.Regexp
const cvRegex string = `v?([0-9|x|X|\*]+)(\.[0-9|x|X|\*]+)?(\.[0-9|x|X|\*]+)?` +
`(-([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?` +
`(\+([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?`
func isX(x string) bool {
switch x {
case "x", "*", "X":
return true
default:
return false
}
}
func rewriteRange(i string) string {
m := constraintRangeRegex.FindAllStringSubmatch(i, -1)
if m == nil {
return i
}
o := i
for _, v := range m {
t := fmt.Sprintf(">= %s, <= %s", v[1], v[11])
o = strings.Replace(o, v[0], t, 1)
}
return o
}
// Detect if a version is not zero (0.0.0)
func isNonZero(v *Version) bool {
if v.Major() != 0 || v.Minor() != 0 || v.Patch() != 0 || v.Prerelease() != "" {
return true
}
return false
}
-115
View File
@@ -1,115 +0,0 @@
/*
Package semver provides the ability to work with Semantic Versions (http://semver.org) in Go.
Specifically it provides the ability to:
* Parse semantic versions
* Sort semantic versions
* Check if a semantic version fits within a set of constraints
* Optionally work with a `v` prefix
Parsing Semantic Versions
To parse a semantic version use the `NewVersion` function. For example,
v, err := semver.NewVersion("1.2.3-beta.1+build345")
If there is an error the version wasn't parseable. The version object has methods
to get the parts of the version, compare it to other versions, convert the
version back into a string, and get the original string. For more details
please see the documentation at https://godoc.org/github.com/Masterminds/semver.
Sorting Semantic Versions
A set of versions can be sorted using the `sort` package from the standard library.
For example,
raw := []string{"1.2.3", "1.0", "1.3", "2", "0.4.2",}
vs := make([]*semver.Version, len(raw))
for i, r := range raw {
v, err := semver.NewVersion(r)
if err != nil {
t.Errorf("Error parsing version: %s", err)
}
vs[i] = v
}
sort.Sort(semver.Collection(vs))
Checking Version Constraints
Checking a version against version constraints is one of the most featureful
parts of the package.
c, err := semver.NewConstraint(">= 1.2.3")
if err != nil {
// Handle constraint not being parseable.
}
v, _ := semver.NewVersion("1.3")
if err != nil {
// Handle version not being parseable.
}
// Check if the version meets the constraints. The a variable will be true.
a := c.Check(v)
Basic Comparisons
There are two elements to the comparisons. First, a comparison string is a list
of comma separated and comparisons. These are then separated by || separated or
comparisons. For example, `">= 1.2, < 3.0.0 || >= 4.2.3"` is looking for a
comparison that's greater than or equal to 1.2 and less than 3.0.0 or is
greater than or equal to 4.2.3.
The basic comparisons are:
* `=`: equal (aliased to no operator)
* `!=`: not equal
* `>`: greater than
* `<`: less than
* `>=`: greater than or equal to
* `<=`: less than or equal to
Hyphen Range Comparisons
There are multiple methods to handle ranges and the first is hyphens ranges.
These look like:
* `1.2 - 1.4.5` which is equivalent to `>= 1.2, <= 1.4.5`
* `2.3.4 - 4.5` which is equivalent to `>= 2.3.4, <= 4.5`
Wildcards In Comparisons
The `x`, `X`, and `*` characters can be used as a wildcard character. This works
for all comparison operators. When used on the `=` operator it falls
back to the pack level comparison (see tilde below). For example,
* `1.2.x` is equivalent to `>= 1.2.0, < 1.3.0`
* `>= 1.2.x` is equivalent to `>= 1.2.0`
* `<= 2.x` is equivalent to `<= 3`
* `*` is equivalent to `>= 0.0.0`
Tilde Range Comparisons (Patch)
The tilde (`~`) comparison operator is for patch level ranges when a minor
version is specified and major level changes when the minor number is missing.
For example,
* `~1.2.3` is equivalent to `>= 1.2.3, < 1.3.0`
* `~1` is equivalent to `>= 1, < 2`
* `~2.3` is equivalent to `>= 2.3, < 2.4`
* `~1.2.x` is equivalent to `>= 1.2.0, < 1.3.0`
* `~1.x` is equivalent to `>= 1, < 2`
Caret Range Comparisons (Major)
The caret (`^`) comparison operator is for major level changes. This is useful
when comparisons of API versions as a major change is API breaking. For example,
* `^1.2.3` is equivalent to `>= 1.2.3, < 2.0.0`
* `^1.2.x` is equivalent to `>= 1.2.0, < 2.0.0`
* `^2.3` is equivalent to `>= 2.3, < 3`
* `^2.x` is equivalent to `>= 2.0.0, < 3`
*/
package semver
-421
View File
@@ -1,421 +0,0 @@
package semver
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"regexp"
"strconv"
"strings"
)
// The compiled version of the regex created at init() is cached here so it
// only needs to be created once.
var versionRegex *regexp.Regexp
var validPrereleaseRegex *regexp.Regexp
var (
// ErrInvalidSemVer is returned a version is found to be invalid when
// being parsed.
ErrInvalidSemVer = errors.New("Invalid Semantic Version")
// ErrInvalidMetadata is returned when the metadata is an invalid format
ErrInvalidMetadata = errors.New("Invalid Metadata string")
// ErrInvalidPrerelease is returned when the pre-release is an invalid format
ErrInvalidPrerelease = errors.New("Invalid Prerelease string")
)
// SemVerRegex is the regular expression used to parse a semantic version.
const SemVerRegex string = `v?([0-9]+)(\.[0-9]+)?(\.[0-9]+)?` +
`(-([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?` +
`(\+([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?`
// ValidPrerelease is the regular expression which validates
// both prerelease and metadata values.
const ValidPrerelease string = `^([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*)`
// Version represents a single semantic version.
type Version struct {
major, minor, patch int64
pre string
metadata string
original string
}
func init() {
versionRegex = regexp.MustCompile("^" + SemVerRegex + "$")
validPrereleaseRegex = regexp.MustCompile(ValidPrerelease)
}
// NewVersion parses a given version and returns an instance of Version or
// an error if unable to parse the version.
func NewVersion(v string) (*Version, error) {
m := versionRegex.FindStringSubmatch(v)
if m == nil {
return nil, ErrInvalidSemVer
}
sv := &Version{
metadata: m[8],
pre: m[5],
original: v,
}
var temp int64
temp, err := strconv.ParseInt(m[1], 10, 64)
if err != nil {
return nil, fmt.Errorf("Error parsing version segment: %s", err)
}
sv.major = temp
if m[2] != "" {
temp, err = strconv.ParseInt(strings.TrimPrefix(m[2], "."), 10, 64)
if err != nil {
return nil, fmt.Errorf("Error parsing version segment: %s", err)
}
sv.minor = temp
} else {
sv.minor = 0
}
if m[3] != "" {
temp, err = strconv.ParseInt(strings.TrimPrefix(m[3], "."), 10, 64)
if err != nil {
return nil, fmt.Errorf("Error parsing version segment: %s", err)
}
sv.patch = temp
} else {
sv.patch = 0
}
return sv, nil
}
// MustParse parses a given version and panics on error.
func MustParse(v string) *Version {
sv, err := NewVersion(v)
if err != nil {
panic(err)
}
return sv
}
// String converts a Version object to a string.
// Note, if the original version contained a leading v this version will not.
// See the Original() method to retrieve the original value. Semantic Versions
// don't contain a leading v per the spec. Instead it's optional on
// impelementation.
func (v *Version) String() string {
var buf bytes.Buffer
fmt.Fprintf(&buf, "%d.%d.%d", v.major, v.minor, v.patch)
if v.pre != "" {
fmt.Fprintf(&buf, "-%s", v.pre)
}
if v.metadata != "" {
fmt.Fprintf(&buf, "+%s", v.metadata)
}
return buf.String()
}
// Original returns the original value passed in to be parsed.
func (v *Version) Original() string {
return v.original
}
// Major returns the major version.
func (v *Version) Major() int64 {
return v.major
}
// Minor returns the minor version.
func (v *Version) Minor() int64 {
return v.minor
}
// Patch returns the patch version.
func (v *Version) Patch() int64 {
return v.patch
}
// Prerelease returns the pre-release version.
func (v *Version) Prerelease() string {
return v.pre
}
// Metadata returns the metadata on the version.
func (v *Version) Metadata() string {
return v.metadata
}
// originalVPrefix returns the original 'v' prefix if any.
func (v *Version) originalVPrefix() string {
// Note, only lowercase v is supported as a prefix by the parser.
if v.original != "" && v.original[:1] == "v" {
return v.original[:1]
}
return ""
}
// IncPatch produces the next patch version.
// If the current version does not have prerelease/metadata information,
// it unsets metadata and prerelease values, increments patch number.
// If the current version has any of prerelease or metadata information,
// it unsets both values and keeps curent patch value
func (v Version) IncPatch() Version {
vNext := v
// according to http://semver.org/#spec-item-9
// Pre-release versions have a lower precedence than the associated normal version.
// according to http://semver.org/#spec-item-10
// Build metadata SHOULD be ignored when determining version precedence.
if v.pre != "" {
vNext.metadata = ""
vNext.pre = ""
} else {
vNext.metadata = ""
vNext.pre = ""
vNext.patch = v.patch + 1
}
vNext.original = v.originalVPrefix() + "" + vNext.String()
return vNext
}
// IncMinor produces the next minor version.
// Sets patch to 0.
// Increments minor number.
// Unsets metadata.
// Unsets prerelease status.
func (v Version) IncMinor() Version {
vNext := v
vNext.metadata = ""
vNext.pre = ""
vNext.patch = 0
vNext.minor = v.minor + 1
vNext.original = v.originalVPrefix() + "" + vNext.String()
return vNext
}
// IncMajor produces the next major version.
// Sets patch to 0.
// Sets minor to 0.
// Increments major number.
// Unsets metadata.
// Unsets prerelease status.
func (v Version) IncMajor() Version {
vNext := v
vNext.metadata = ""
vNext.pre = ""
vNext.patch = 0
vNext.minor = 0
vNext.major = v.major + 1
vNext.original = v.originalVPrefix() + "" + vNext.String()
return vNext
}
// SetPrerelease defines the prerelease value.
// Value must not include the required 'hypen' prefix.
func (v Version) SetPrerelease(prerelease string) (Version, error) {
vNext := v
if len(prerelease) > 0 && !validPrereleaseRegex.MatchString(prerelease) {
return vNext, ErrInvalidPrerelease
}
vNext.pre = prerelease
vNext.original = v.originalVPrefix() + "" + vNext.String()
return vNext, nil
}
// SetMetadata defines metadata value.
// Value must not include the required 'plus' prefix.
func (v Version) SetMetadata(metadata string) (Version, error) {
vNext := v
if len(metadata) > 0 && !validPrereleaseRegex.MatchString(metadata) {
return vNext, ErrInvalidMetadata
}
vNext.metadata = metadata
vNext.original = v.originalVPrefix() + "" + vNext.String()
return vNext, nil
}
// LessThan tests if one version is less than another one.
func (v *Version) LessThan(o *Version) bool {
return v.Compare(o) < 0
}
// GreaterThan tests if one version is greater than another one.
func (v *Version) GreaterThan(o *Version) bool {
return v.Compare(o) > 0
}
// Equal tests if two versions are equal to each other.
// Note, versions can be equal with different metadata since metadata
// is not considered part of the comparable version.
func (v *Version) Equal(o *Version) bool {
return v.Compare(o) == 0
}
// Compare compares this version to another one. It returns -1, 0, or 1 if
// the version smaller, equal, or larger than the other version.
//
// Versions are compared by X.Y.Z. Build metadata is ignored. Prerelease is
// lower than the version without a prerelease.
func (v *Version) Compare(o *Version) int {
// Compare the major, minor, and patch version for differences. If a
// difference is found return the comparison.
if d := compareSegment(v.Major(), o.Major()); d != 0 {
return d
}
if d := compareSegment(v.Minor(), o.Minor()); d != 0 {
return d
}
if d := compareSegment(v.Patch(), o.Patch()); d != 0 {
return d
}
// At this point the major, minor, and patch versions are the same.
ps := v.pre
po := o.Prerelease()
if ps == "" && po == "" {
return 0
}
if ps == "" {
return 1
}
if po == "" {
return -1
}
return comparePrerelease(ps, po)
}
// UnmarshalJSON implements JSON.Unmarshaler interface.
func (v *Version) UnmarshalJSON(b []byte) error {
var s string
if err := json.Unmarshal(b, &s); err != nil {
return err
}
temp, err := NewVersion(s)
if err != nil {
return err
}
v.major = temp.major
v.minor = temp.minor
v.patch = temp.patch
v.pre = temp.pre
v.metadata = temp.metadata
v.original = temp.original
temp = nil
return nil
}
// MarshalJSON implements JSON.Marshaler interface.
func (v *Version) MarshalJSON() ([]byte, error) {
return json.Marshal(v.String())
}
func compareSegment(v, o int64) int {
if v < o {
return -1
}
if v > o {
return 1
}
return 0
}
func comparePrerelease(v, o string) int {
// split the prelease versions by their part. The separator, per the spec,
// is a .
sparts := strings.Split(v, ".")
oparts := strings.Split(o, ".")
// Find the longer length of the parts to know how many loop iterations to
// go through.
slen := len(sparts)
olen := len(oparts)
l := slen
if olen > slen {
l = olen
}
// Iterate over each part of the prereleases to compare the differences.
for i := 0; i < l; i++ {
// Since the lentgh of the parts can be different we need to create
// a placeholder. This is to avoid out of bounds issues.
stemp := ""
if i < slen {
stemp = sparts[i]
}
otemp := ""
if i < olen {
otemp = oparts[i]
}
d := comparePrePart(stemp, otemp)
if d != 0 {
return d
}
}
// Reaching here means two versions are of equal value but have different
// metadata (the part following a +). They are not identical in string form
// but the version comparison finds them to be equal.
return 0
}
func comparePrePart(s, o string) int {
// Fastpath if they are equal
if s == o {
return 0
}
// When s or o are empty we can use the other in an attempt to determine
// the response.
if s == "" {
if o != "" {
return -1
}
return 1
}
if o == "" {
if s != "" {
return 1
}
return -1
}
// When comparing strings "99" is greater than "103". To handle
// cases like this we need to detect numbers and compare them.
oi, n1 := strconv.ParseInt(o, 10, 64)
si, n2 := strconv.ParseInt(s, 10, 64)
// The case where both are strings compare the strings
if n1 != nil && n2 != nil {
if s > o {
return 1
}
return -1
} else if n1 != nil {
// o is a string and s is a number
return -1
} else if n2 != nil {
// s is a string and o is a number
return 1
}
// Both are numbers
if si > oi {
return 1
}
return -1
}
-2
View File
@@ -1,2 +0,0 @@
vendor/
/.glide
-23
View File
@@ -1,23 +0,0 @@
language: go
go:
- 1.9.x
- 1.10.x
- tip
# Setting sudo access to false will let Travis CI use containers rather than
# VMs to run the tests. For more details see:
# - http://docs.travis-ci.com/user/workers/container-based-infrastructure/
# - http://docs.travis-ci.com/user/workers/standard-infrastructure/
sudo: false
script:
- make setup test
notifications:
webhooks:
urls:
- https://webhooks.gitter.im/e/06e3328629952dabe3e0
on_success: change # options: [always|never|change] default: always
on_failure: always # options: [always|never|change] default: always
on_start: never # options: [always|never|change] default: always
-153
View File
@@ -1,153 +0,0 @@
# Changelog
## Release 2.15.0 (2018-04-02)
### Added
- #68 and #69: Add json helpers to docs (thanks @arunvelsriram)
- #66: Add ternary function (thanks @binoculars)
- #67: Allow keys function to take multiple dicts (thanks @binoculars)
- #89: Added sha1sum to crypto function (thanks @benkeil)
- #81: Allow customizing Root CA that used by genSignedCert (thanks @chenzhiwei)
- #92: Add travis testing for go 1.10
- #93: Adding appveyor config for windows testing
### Changed
- #90: Updating to more recent dependencies
- #73: replace satori/go.uuid with google/uuid (thanks @petterw)
### Fixed
- #76: Fixed documentation typos (thanks @Thiht)
- Fixed rounding issue on the `ago` function. Note, the removes support for Go 1.8 and older
## Release 2.14.1 (2017-12-01)
### Fixed
- #60: Fix typo in function name documentation (thanks @neil-ca-moore)
- #61: Removing line with {{ due to blocking github pages genertion
- #64: Update the list functions to handle int, string, and other slices for compatibility
## Release 2.14.0 (2017-10-06)
This new version of Sprig adds a set of functions for generating and working with SSL certificates.
- `genCA` generates an SSL Certificate Authority
- `genSelfSignedCert` generates an SSL self-signed certificate
- `genSignedCert` generates an SSL certificate and key based on a given CA
## Release 2.13.0 (2017-09-18)
This release adds new functions, including:
- `regexMatch`, `regexFindAll`, `regexFind`, `regexReplaceAll`, `regexReplaceAllLiteral`, and `regexSplit` to work with regular expressions
- `floor`, `ceil`, and `round` math functions
- `toDate` converts a string to a date
- `nindent` is just like `indent` but also prepends a new line
- `ago` returns the time from `time.Now`
### Added
- #40: Added basic regex functionality (thanks @alanquillin)
- #41: Added ceil floor and round functions (thanks @alanquillin)
- #48: Added toDate function (thanks @andreynering)
- #50: Added nindent function (thanks @binoculars)
- #46: Added ago function (thanks @slayer)
### Changed
- #51: Updated godocs to include new string functions (thanks @curtisallen)
- #49: Added ability to merge multiple dicts (thanks @binoculars)
## Release 2.12.0 (2017-05-17)
- `snakecase`, `camelcase`, and `shuffle` are three new string functions
- `fail` allows you to bail out of a template render when conditions are not met
## Release 2.11.0 (2017-05-02)
- Added `toJson` and `toPrettyJson`
- Added `merge`
- Refactored documentation
## Release 2.10.0 (2017-03-15)
- Added `semver` and `semverCompare` for Semantic Versions
- `list` replaces `tuple`
- Fixed issue with `join`
- Added `first`, `last`, `intial`, `rest`, `prepend`, `append`, `toString`, `toStrings`, `sortAlpha`, `reverse`, `coalesce`, `pluck`, `pick`, `compact`, `keys`, `omit`, `uniq`, `has`, `without`
## Release 2.9.0 (2017-02-23)
- Added `splitList` to split a list
- Added crypto functions of `genPrivateKey` and `derivePassword`
## Release 2.8.0 (2016-12-21)
- Added access to several path functions (`base`, `dir`, `clean`, `ext`, and `abs`)
- Added functions for _mutating_ dictionaries (`set`, `unset`, `hasKey`)
## Release 2.7.0 (2016-12-01)
- Added `sha256sum` to generate a hash of an input
- Added functions to convert a numeric or string to `int`, `int64`, `float64`
## Release 2.6.0 (2016-10-03)
- Added a `uuidv4` template function for generating UUIDs inside of a template.
## Release 2.5.0 (2016-08-19)
- New `trimSuffix`, `trimPrefix`, `hasSuffix`, and `hasPrefix` functions
- New aliases have been added for a few functions that didn't follow the naming conventions (`trimAll` and `abbrevBoth`)
- `trimall` and `abbrevboth` (notice the case) are deprecated and will be removed in 3.0.0
## Release 2.4.0 (2016-08-16)
- Adds two functions: `until` and `untilStep`
## Release 2.3.0 (2016-06-21)
- cat: Concatenate strings with whitespace separators.
- replace: Replace parts of a string: `replace " " "-" "Me First"` renders "Me-First"
- plural: Format plurals: `len "foo" | plural "one foo" "many foos"` renders "many foos"
- indent: Indent blocks of text in a way that is sensitive to "\n" characters.
## Release 2.2.0 (2016-04-21)
- Added a `genPrivateKey` function (Thanks @bacongobbler)
## Release 2.1.0 (2016-03-30)
- `default` now prints the default value when it does not receive a value down the pipeline. It is much safer now to do `{{.Foo | default "bar"}}`.
- Added accessors for "hermetic" functions. These return only functions that, when given the same input, produce the same output.
## Release 2.0.0 (2016-03-29)
Because we switched from `int` to `int64` as the return value for all integer math functions, the library's major version number has been incremented.
- `min` complements `max` (formerly `biggest`)
- `empty` indicates that a value is the empty value for its type
- `tuple` creates a tuple inside of a template: `{{$t := tuple "a", "b" "c"}}`
- `dict` creates a dictionary inside of a template `{{$d := dict "key1" "val1" "key2" "val2"}}`
- Date formatters have been added for HTML dates (as used in `date` input fields)
- Integer math functions can convert from a number of types, including `string` (via `strconv.ParseInt`).
## Release 1.2.0 (2016-02-01)
- Added quote and squote
- Added b32enc and b32dec
- add now takes varargs
- biggest now takes varargs
## Release 1.1.0 (2015-12-29)
- Added #4: Added contains function. strings.Contains, but with the arguments
switched to simplify common pipelines. (thanks krancour)
- Added Travis-CI testing support
## Release 1.0.0 (2015-12-23)
- Initial release
-20
View File
@@ -1,20 +0,0 @@
Sprig
Copyright (C) 2013 Masterminds
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-13
View File
@@ -1,13 +0,0 @@
HAS_GLIDE := $(shell command -v glide;)
.PHONY: test
test:
go test -v .
.PHONY: setup
setup:
ifndef HAS_GLIDE
go get -u github.com/Masterminds/glide
endif
glide install
-81
View File
@@ -1,81 +0,0 @@
# Sprig: Template functions for Go templates
[![Stability: Sustained](https://masterminds.github.io/stability/sustained.svg)](https://masterminds.github.io/stability/sustained.html)
[![Build Status](https://travis-ci.org/Masterminds/sprig.svg?branch=master)](https://travis-ci.org/Masterminds/sprig)
The Go language comes with a [built-in template
language](http://golang.org/pkg/text/template/), but not
very many template functions. This library provides a group of commonly
used template functions.
It is inspired by the template functions found in
[Twig](http://twig.sensiolabs.org/documentation) and also in various
JavaScript libraries, such as [underscore.js](http://underscorejs.org/).
## Usage
Template developers can read the [Sprig function documentation](http://masterminds.github.io/sprig/) to
learn about the >100 template functions available.
For Go developers wishing to include Sprig as a library in their programs,
API documentation is available [at GoDoc.org](http://godoc.org/github.com/Masterminds/sprig), but
read on for standard usage.
### Load the Sprig library
To load the Sprig `FuncMap`:
```go
import (
"github.com/Masterminds/sprig"
"html/template"
)
// This example illustrates that the FuncMap *must* be set before the
// templates themselves are loaded.
tpl := template.Must(
template.New("base").Funcs(sprig.FuncMap()).ParseGlob("*.html")
)
```
### Call the functions inside of templates
By convention, all functions are lowercase. This seems to follow the Go
idiom for template functions (as opposed to template methods, which are
TitleCase).
Example:
```
{{ "hello!" | upper | repeat 5 }}
```
Produces:
```
HELLO!HELLO!HELLO!HELLO!HELLO!
```
## Principles:
The following principles were used in deciding on which functions to add, and
determining how to implement them.
- Template functions should be used to build layout. Therefore, the following
types of operations are within the domain of template functions:
- Formatting
- Layout
- Simple type conversions
- Utilities that assist in handling common formatting and layout needs (e.g. arithmetic)
- Template functions should not return errors unless there is no way to print
a sensible value. For example, converting a string to an integer should not
produce an error if conversion fails. Instead, it should display a default
value that can be displayed.
- Simple math is necessary for grid layouts, pagers, and so on. Complex math
(anything other than arithmetic) should be done outside of templates.
- Template functions only deal with the data passed into them. They never retrieve
data from a source.
- Finally, do not override core Go template functions.
-26
View File
@@ -1,26 +0,0 @@
version: build-{build}.{branch}
clone_folder: C:\gopath\src\github.com\Masterminds\sprig
shallow_clone: true
environment:
GOPATH: C:\gopath
platform:
- x64
install:
- go get -u github.com/Masterminds/glide
- set PATH=%GOPATH%\bin;%PATH%
- go version
- go env
build_script:
- glide install
- go install ./...
test_script:
- go test -v
deploy: off
-430
View File
@@ -1,430 +0,0 @@
package sprig
import (
"bytes"
"crypto/dsa"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/hmac"
"crypto/rand"
"crypto/rsa"
"crypto/sha1"
"crypto/sha256"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/base64"
"encoding/binary"
"encoding/hex"
"encoding/pem"
"errors"
"fmt"
"math/big"
"net"
"time"
"github.com/google/uuid"
"golang.org/x/crypto/scrypt"
)
func sha256sum(input string) string {
hash := sha256.Sum256([]byte(input))
return hex.EncodeToString(hash[:])
}
func sha1sum(input string) string {
hash := sha1.Sum([]byte(input))
return hex.EncodeToString(hash[:])
}
// uuidv4 provides a safe and secure UUID v4 implementation
func uuidv4() string {
return fmt.Sprintf("%s", uuid.New())
}
var master_password_seed = "com.lyndir.masterpassword"
var password_type_templates = map[string][][]byte{
"maximum": {[]byte("anoxxxxxxxxxxxxxxxxx"), []byte("axxxxxxxxxxxxxxxxxno")},
"long": {[]byte("CvcvnoCvcvCvcv"), []byte("CvcvCvcvnoCvcv"), []byte("CvcvCvcvCvcvno"), []byte("CvccnoCvcvCvcv"), []byte("CvccCvcvnoCvcv"),
[]byte("CvccCvcvCvcvno"), []byte("CvcvnoCvccCvcv"), []byte("CvcvCvccnoCvcv"), []byte("CvcvCvccCvcvno"), []byte("CvcvnoCvcvCvcc"),
[]byte("CvcvCvcvnoCvcc"), []byte("CvcvCvcvCvccno"), []byte("CvccnoCvccCvcv"), []byte("CvccCvccnoCvcv"), []byte("CvccCvccCvcvno"),
[]byte("CvcvnoCvccCvcc"), []byte("CvcvCvccnoCvcc"), []byte("CvcvCvccCvccno"), []byte("CvccnoCvcvCvcc"), []byte("CvccCvcvnoCvcc"),
[]byte("CvccCvcvCvccno")},
"medium": {[]byte("CvcnoCvc"), []byte("CvcCvcno")},
"short": {[]byte("Cvcn")},
"basic": {[]byte("aaanaaan"), []byte("aannaaan"), []byte("aaannaaa")},
"pin": {[]byte("nnnn")},
}
var template_characters = map[byte]string{
'V': "AEIOU",
'C': "BCDFGHJKLMNPQRSTVWXYZ",
'v': "aeiou",
'c': "bcdfghjklmnpqrstvwxyz",
'A': "AEIOUBCDFGHJKLMNPQRSTVWXYZ",
'a': "AEIOUaeiouBCDFGHJKLMNPQRSTVWXYZbcdfghjklmnpqrstvwxyz",
'n': "0123456789",
'o': "@&%?,=[]_:-+*$#!'^~;()/.",
'x': "AEIOUaeiouBCDFGHJKLMNPQRSTVWXYZbcdfghjklmnpqrstvwxyz0123456789!@#$%^&*()",
}
func derivePassword(counter uint32, password_type, password, user, site string) string {
var templates = password_type_templates[password_type]
if templates == nil {
return fmt.Sprintf("cannot find password template %s", password_type)
}
var buffer bytes.Buffer
buffer.WriteString(master_password_seed)
binary.Write(&buffer, binary.BigEndian, uint32(len(user)))
buffer.WriteString(user)
salt := buffer.Bytes()
key, err := scrypt.Key([]byte(password), salt, 32768, 8, 2, 64)
if err != nil {
return fmt.Sprintf("failed to derive password: %s", err)
}
buffer.Truncate(len(master_password_seed))
binary.Write(&buffer, binary.BigEndian, uint32(len(site)))
buffer.WriteString(site)
binary.Write(&buffer, binary.BigEndian, counter)
var hmacv = hmac.New(sha256.New, key)
hmacv.Write(buffer.Bytes())
var seed = hmacv.Sum(nil)
var temp = templates[int(seed[0])%len(templates)]
buffer.Truncate(0)
for i, element := range temp {
pass_chars := template_characters[element]
pass_char := pass_chars[int(seed[i+1])%len(pass_chars)]
buffer.WriteByte(pass_char)
}
return buffer.String()
}
func generatePrivateKey(typ string) string {
var priv interface{}
var err error
switch typ {
case "", "rsa":
// good enough for government work
priv, err = rsa.GenerateKey(rand.Reader, 4096)
case "dsa":
key := new(dsa.PrivateKey)
// again, good enough for government work
if err = dsa.GenerateParameters(&key.Parameters, rand.Reader, dsa.L2048N256); err != nil {
return fmt.Sprintf("failed to generate dsa params: %s", err)
}
err = dsa.GenerateKey(key, rand.Reader)
priv = key
case "ecdsa":
// again, good enough for government work
priv, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
default:
return "Unknown type " + typ
}
if err != nil {
return fmt.Sprintf("failed to generate private key: %s", err)
}
return string(pem.EncodeToMemory(pemBlockForKey(priv)))
}
type DSAKeyFormat struct {
Version int
P, Q, G, Y, X *big.Int
}
func pemBlockForKey(priv interface{}) *pem.Block {
switch k := priv.(type) {
case *rsa.PrivateKey:
return &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(k)}
case *dsa.PrivateKey:
val := DSAKeyFormat{
P: k.P, Q: k.Q, G: k.G,
Y: k.Y, X: k.X,
}
bytes, _ := asn1.Marshal(val)
return &pem.Block{Type: "DSA PRIVATE KEY", Bytes: bytes}
case *ecdsa.PrivateKey:
b, _ := x509.MarshalECPrivateKey(k)
return &pem.Block{Type: "EC PRIVATE KEY", Bytes: b}
default:
return nil
}
}
type certificate struct {
Cert string
Key string
}
func buildCustomCertificate(b64cert string, b64key string) (certificate, error) {
crt := certificate{}
cert, err := base64.StdEncoding.DecodeString(b64cert)
if err != nil {
return crt, errors.New("unable to decode base64 certificate")
}
key, err := base64.StdEncoding.DecodeString(b64key)
if err != nil {
return crt, errors.New("unable to decode base64 private key")
}
decodedCert, _ := pem.Decode(cert)
if decodedCert == nil {
return crt, errors.New("unable to decode certificate")
}
_, err = x509.ParseCertificate(decodedCert.Bytes)
if err != nil {
return crt, fmt.Errorf(
"error parsing certificate: decodedCert.Bytes: %s",
err,
)
}
decodedKey, _ := pem.Decode(key)
if decodedKey == nil {
return crt, errors.New("unable to decode key")
}
_, err = x509.ParsePKCS1PrivateKey(decodedKey.Bytes)
if err != nil {
return crt, fmt.Errorf(
"error parsing prive key: decodedKey.Bytes: %s",
err,
)
}
crt.Cert = string(cert)
crt.Key = string(key)
return crt, nil
}
func generateCertificateAuthority(
cn string,
daysValid int,
) (certificate, error) {
ca := certificate{}
template, err := getBaseCertTemplate(cn, nil, nil, daysValid)
if err != nil {
return ca, err
}
// Override KeyUsage and IsCA
template.KeyUsage = x509.KeyUsageKeyEncipherment |
x509.KeyUsageDigitalSignature |
x509.KeyUsageCertSign
template.IsCA = true
priv, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return ca, fmt.Errorf("error generating rsa key: %s", err)
}
ca.Cert, ca.Key, err = getCertAndKey(template, priv, template, priv)
if err != nil {
return ca, err
}
return ca, nil
}
func generateSelfSignedCertificate(
cn string,
ips []interface{},
alternateDNS []interface{},
daysValid int,
) (certificate, error) {
cert := certificate{}
template, err := getBaseCertTemplate(cn, ips, alternateDNS, daysValid)
if err != nil {
return cert, err
}
priv, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return cert, fmt.Errorf("error generating rsa key: %s", err)
}
cert.Cert, cert.Key, err = getCertAndKey(template, priv, template, priv)
if err != nil {
return cert, err
}
return cert, nil
}
func generateSignedCertificate(
cn string,
ips []interface{},
alternateDNS []interface{},
daysValid int,
ca certificate,
) (certificate, error) {
cert := certificate{}
decodedSignerCert, _ := pem.Decode([]byte(ca.Cert))
if decodedSignerCert == nil {
return cert, errors.New("unable to decode certificate")
}
signerCert, err := x509.ParseCertificate(decodedSignerCert.Bytes)
if err != nil {
return cert, fmt.Errorf(
"error parsing certificate: decodedSignerCert.Bytes: %s",
err,
)
}
decodedSignerKey, _ := pem.Decode([]byte(ca.Key))
if decodedSignerKey == nil {
return cert, errors.New("unable to decode key")
}
signerKey, err := x509.ParsePKCS1PrivateKey(decodedSignerKey.Bytes)
if err != nil {
return cert, fmt.Errorf(
"error parsing prive key: decodedSignerKey.Bytes: %s",
err,
)
}
template, err := getBaseCertTemplate(cn, ips, alternateDNS, daysValid)
if err != nil {
return cert, err
}
priv, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return cert, fmt.Errorf("error generating rsa key: %s", err)
}
cert.Cert, cert.Key, err = getCertAndKey(
template,
priv,
signerCert,
signerKey,
)
if err != nil {
return cert, err
}
return cert, nil
}
func getCertAndKey(
template *x509.Certificate,
signeeKey *rsa.PrivateKey,
parent *x509.Certificate,
signingKey *rsa.PrivateKey,
) (string, string, error) {
derBytes, err := x509.CreateCertificate(
rand.Reader,
template,
parent,
&signeeKey.PublicKey,
signingKey,
)
if err != nil {
return "", "", fmt.Errorf("error creating certificate: %s", err)
}
certBuffer := bytes.Buffer{}
if err := pem.Encode(
&certBuffer,
&pem.Block{Type: "CERTIFICATE", Bytes: derBytes},
); err != nil {
return "", "", fmt.Errorf("error pem-encoding certificate: %s", err)
}
keyBuffer := bytes.Buffer{}
if err := pem.Encode(
&keyBuffer,
&pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(signeeKey),
},
); err != nil {
return "", "", fmt.Errorf("error pem-encoding key: %s", err)
}
return string(certBuffer.Bytes()), string(keyBuffer.Bytes()), nil
}
func getBaseCertTemplate(
cn string,
ips []interface{},
alternateDNS []interface{},
daysValid int,
) (*x509.Certificate, error) {
ipAddresses, err := getNetIPs(ips)
if err != nil {
return nil, err
}
dnsNames, err := getAlternateDNSStrs(alternateDNS)
if err != nil {
return nil, err
}
return &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
CommonName: cn,
},
IPAddresses: ipAddresses,
DNSNames: dnsNames,
NotBefore: time.Now(),
NotAfter: time.Now().Add(time.Hour * 24 * time.Duration(daysValid)),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{
x509.ExtKeyUsageServerAuth,
x509.ExtKeyUsageClientAuth,
},
BasicConstraintsValid: true,
}, nil
}
func getNetIPs(ips []interface{}) ([]net.IP, error) {
if ips == nil {
return []net.IP{}, nil
}
var ipStr string
var ok bool
var netIP net.IP
netIPs := make([]net.IP, len(ips))
for i, ip := range ips {
ipStr, ok = ip.(string)
if !ok {
return nil, fmt.Errorf("error parsing ip: %v is not a string", ip)
}
netIP = net.ParseIP(ipStr)
if netIP == nil {
return nil, fmt.Errorf("error parsing ip: %s", ipStr)
}
netIPs[i] = netIP
}
return netIPs, nil
}
func getAlternateDNSStrs(alternateDNS []interface{}) ([]string, error) {
if alternateDNS == nil {
return []string{}, nil
}
var dnsStr string
var ok bool
alternateDNSStrs := make([]string, len(alternateDNS))
for i, dns := range alternateDNS {
dnsStr, ok = dns.(string)
if !ok {
return nil, fmt.Errorf(
"error processing alternate dns name: %v is not a string",
dns,
)
}
alternateDNSStrs[i] = dnsStr
}
return alternateDNSStrs, nil
}
-76
View File
@@ -1,76 +0,0 @@
package sprig
import (
"time"
)
// Given a format and a date, format the date string.
//
// Date can be a `time.Time` or an `int, int32, int64`.
// In the later case, it is treated as seconds since UNIX
// epoch.
func date(fmt string, date interface{}) string {
return dateInZone(fmt, date, "Local")
}
func htmlDate(date interface{}) string {
return dateInZone("2006-01-02", date, "Local")
}
func htmlDateInZone(date interface{}, zone string) string {
return dateInZone("2006-01-02", date, zone)
}
func dateInZone(fmt string, date interface{}, zone string) string {
var t time.Time
switch date := date.(type) {
default:
t = time.Now()
case time.Time:
t = date
case int64:
t = time.Unix(date, 0)
case int:
t = time.Unix(int64(date), 0)
case int32:
t = time.Unix(int64(date), 0)
}
loc, err := time.LoadLocation(zone)
if err != nil {
loc, _ = time.LoadLocation("UTC")
}
return t.In(loc).Format(fmt)
}
func dateModify(fmt string, date time.Time) time.Time {
d, err := time.ParseDuration(fmt)
if err != nil {
return date
}
return date.Add(d)
}
func dateAgo(date interface{}) string {
var t time.Time
switch date := date.(type) {
default:
t = time.Now()
case time.Time:
t = date
case int64:
t = time.Unix(date, 0)
case int:
t = time.Unix(int64(date), 0)
}
// Drop resolution to seconds
duration := time.Since(t).Round(time.Second)
return duration.String()
}
func toDate(fmt, str string) time.Time {
t, _ := time.ParseInLocation(fmt, str, time.Local)
return t
}
-84
View File
@@ -1,84 +0,0 @@
package sprig
import (
"encoding/json"
"reflect"
)
// dfault checks whether `given` is set, and returns default if not set.
//
// This returns `d` if `given` appears not to be set, and `given` otherwise.
//
// For numeric types 0 is unset.
// For strings, maps, arrays, and slices, len() = 0 is considered unset.
// For bool, false is unset.
// Structs are never considered unset.
//
// For everything else, including pointers, a nil value is unset.
func dfault(d interface{}, given ...interface{}) interface{} {
if empty(given) || empty(given[0]) {
return d
}
return given[0]
}
// empty returns true if the given value has the zero value for its type.
func empty(given interface{}) bool {
g := reflect.ValueOf(given)
if !g.IsValid() {
return true
}
// Basically adapted from text/template.isTrue
switch g.Kind() {
default:
return g.IsNil()
case reflect.Array, reflect.Slice, reflect.Map, reflect.String:
return g.Len() == 0
case reflect.Bool:
return g.Bool() == false
case reflect.Complex64, reflect.Complex128:
return g.Complex() == 0
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return g.Int() == 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return g.Uint() == 0
case reflect.Float32, reflect.Float64:
return g.Float() == 0
case reflect.Struct:
return false
}
return true
}
// coalesce returns the first non-empty value.
func coalesce(v ...interface{}) interface{} {
for _, val := range v {
if !empty(val) {
return val
}
}
return nil
}
// toJson encodes an item into a JSON string
func toJson(v interface{}) string {
output, _ := json.Marshal(v)
return string(output)
}
// toPrettyJson encodes an item into a pretty (indented) JSON string
func toPrettyJson(v interface{}) string {
output, _ := json.MarshalIndent(v, "", " ")
return string(output)
}
// ternary returns the first value if the last value is true, otherwise returns the second value.
func ternary(vt interface{}, vf interface{}, v bool) interface{} {
if v {
return vt
}
return vf
}
-88
View File
@@ -1,88 +0,0 @@
package sprig
import "github.com/imdario/mergo"
func set(d map[string]interface{}, key string, value interface{}) map[string]interface{} {
d[key] = value
return d
}
func unset(d map[string]interface{}, key string) map[string]interface{} {
delete(d, key)
return d
}
func hasKey(d map[string]interface{}, key string) bool {
_, ok := d[key]
return ok
}
func pluck(key string, d ...map[string]interface{}) []interface{} {
res := []interface{}{}
for _, dict := range d {
if val, ok := dict[key]; ok {
res = append(res, val)
}
}
return res
}
func keys(dicts ...map[string]interface{}) []string {
k := []string{}
for _, dict := range dicts {
for key := range dict {
k = append(k, key)
}
}
return k
}
func pick(dict map[string]interface{}, keys ...string) map[string]interface{} {
res := map[string]interface{}{}
for _, k := range keys {
if v, ok := dict[k]; ok {
res[k] = v
}
}
return res
}
func omit(dict map[string]interface{}, keys ...string) map[string]interface{} {
res := map[string]interface{}{}
omit := make(map[string]bool, len(keys))
for _, k := range keys {
omit[k] = true
}
for k, v := range dict {
if _, ok := omit[k]; !ok {
res[k] = v
}
}
return res
}
func dict(v ...interface{}) map[string]interface{} {
dict := map[string]interface{}{}
lenv := len(v)
for i := 0; i < lenv; i += 2 {
key := strval(v[i])
if i+1 >= lenv {
dict[key] = ""
continue
}
dict[key] = v[i+1]
}
return dict
}
func merge(dst map[string]interface{}, srcs ...map[string]interface{}) interface{} {
for _, src := range srcs {
if err := mergo.Merge(&dst, src); err != nil {
// Swallow errors inside of a template.
return ""
}
}
return dst
}
-233
View File
@@ -1,233 +0,0 @@
/*
Sprig: Template functions for Go.
This package contains a number of utility functions for working with data
inside of Go `html/template` and `text/template` files.
To add these functions, use the `template.Funcs()` method:
t := templates.New("foo").Funcs(sprig.FuncMap())
Note that you should add the function map before you parse any template files.
In several cases, Sprig reverses the order of arguments from the way they
appear in the standard library. This is to make it easier to pipe
arguments into functions.
Date Functions
- date FORMAT TIME: Format a date, where a date is an integer type or a time.Time type, and
format is a time.Format formatting string.
- dateModify: Given a date, modify it with a duration: `date_modify "-1.5h" now`. If the duration doesn't
parse, it returns the time unaltered. See `time.ParseDuration` for info on duration strings.
- now: Current time.Time, for feeding into date-related functions.
- htmlDate TIME: Format a date for use in the value field of an HTML "date" form element.
- dateInZone FORMAT TIME TZ: Like date, but takes three arguments: format, timestamp,
timezone.
- htmlDateInZone TIME TZ: Like htmlDate, but takes two arguments: timestamp,
timezone.
String Functions
- abbrev: Truncate a string with ellipses. `abbrev 5 "hello world"` yields "he..."
- abbrevboth: Abbreviate from both sides, yielding "...lo wo..."
- trunc: Truncate a string (no suffix). `trunc 5 "Hello World"` yields "hello".
- trim: strings.TrimSpace
- trimAll: strings.Trim, but with the argument order reversed `trimAll "$" "$5.00"` or `"$5.00 | trimAll "$"`
- trimSuffix: strings.TrimSuffix, but with the argument order reversed: `trimSuffix "-" "ends-with-"`
- trimPrefix: strings.TrimPrefix, but with the argument order reversed `trimPrefix "$" "$5"`
- upper: strings.ToUpper
- lower: strings.ToLower
- nospace: Remove all space characters from a string. `nospace "h e l l o"` becomes "hello"
- title: strings.Title
- untitle: Remove title casing
- repeat: strings.Repeat, but with the arguments switched: `repeat count str`. (This simplifies common pipelines)
- substr: Given string, start, and length, return a substr.
- initials: Given a multi-word string, return the initials. `initials "Matt Butcher"` returns "MB"
- randAlphaNum: Given a length, generate a random alphanumeric sequence
- randAlpha: Given a length, generate an alphabetic string
- randAscii: Given a length, generate a random ASCII string (symbols included)
- randNumeric: Given a length, generate a string of digits.
- swapcase: SwapCase swaps the case of a string using a word based algorithm. see https://godoc.org/github.com/Masterminds/goutils#SwapCase
- shuffle: Shuffle randomizes runes in a string and returns the result. It uses default random source in `math/rand`
- snakecase: convert all upper case characters in a string to underscore format.
- camelcase: convert all lower case characters behind underscores to upper case character
- wrap: Force a line wrap at the given width. `wrap 80 "imagine a longer string"`
- wrapWith: Wrap a line at the given length, but using 'sep' instead of a newline. `wrapWith 50, "<br>", $html`
- contains: strings.Contains, but with the arguments switched: `contains substr str`. (This simplifies common pipelines)
- hasPrefix: strings.hasPrefix, but with the arguments switched
- hasSuffix: strings.hasSuffix, but with the arguments switched
- quote: Wrap string(s) in double quotation marks, escape the contents by adding '\' before '"'.
- squote: Wrap string(s) in double quotation marks, does not escape content.
- cat: Concatenate strings, separating them by spaces. `cat $a $b $c`.
- indent: Indent a string using space characters. `indent 4 "foo\nbar"` produces " foo\n bar"
- nindent: Indent a string using space characters and prepend a new line. `indent 4 "foo\nbar"` produces "\n foo\n bar"
- replace: Replace an old with a new in a string: `$name | replace " " "-"`
- plural: Choose singular or plural based on length: `len $fish | plural "one anchovy" "many anchovies"`
- sha256sum: Generate a hex encoded sha256 hash of the input
- toString: Convert something to a string
String Slice Functions:
- join: strings.Join, but as `join SEP SLICE`
- split: strings.Split, but as `split SEP STRING`. The results are returned
as a map with the indexes set to _N, where N is an integer starting from 0.
Use it like this: `{{$v := "foo/bar/baz" | split "/"}}{{$v._0}}` (Prints `foo`)
- splitList: strings.Split, but as `split SEP STRING`. The results are returned
as an array.
- toStrings: convert a list to a list of strings. 'list 1 2 3 | toStrings' produces '["1" "2" "3"]'
- sortAlpha: sort a list lexicographically.
Integer Slice Functions:
- until: Given an integer, returns a slice of counting integers from 0 to one
less than the given integer: `range $i, $e := until 5`
- untilStep: Given start, stop, and step, return an integer slice starting at
'start', stopping at `stop`, and incrementing by 'step. This is the same
as Python's long-form of 'range'.
Conversions:
- atoi: Convert a string to an integer. 0 if the integer could not be parsed.
- int64: Convert a string or another numeric type to an int64.
- int: Convert a string or another numeric type to an int.
- float64: Convert a string or another numeric type to a float64.
Defaults:
- default: Give a default value. Used like this: trim " "| default "empty".
Since trim produces an empty string, the default value is returned. For
things with a length (strings, slices, maps), len(0) will trigger the default.
For numbers, the value 0 will trigger the default. For booleans, false will
trigger the default. For structs, the default is never returned (there is
no clear empty condition). For everything else, nil value triggers a default.
- empty: Return true if the given value is the zero value for its type.
Caveats: structs are always non-empty. This should match the behavior of
{{if pipeline}}, but can be used inside of a pipeline.
- coalesce: Given a list of items, return the first non-empty one.
This follows the same rules as 'empty'. '{{ coalesce .someVal 0 "hello" }}`
will return `.someVal` if set, or else return "hello". The 0 is skipped
because it is an empty value.
- compact: Return a copy of a list with all of the empty values removed.
'list 0 1 2 "" | compact' will return '[1 2]'
- ternary: Given a value,'true | ternary "b" "c"' will return "b".
'false | ternary "b" "c"' will return '"c"'. Similar to the JavaScript ternary
operator.
OS:
- env: Resolve an environment variable
- expandenv: Expand a string through the environment
File Paths:
- base: Return the last element of a path. https://golang.org/pkg/path#Base
- dir: Remove the last element of a path. https://golang.org/pkg/path#Dir
- clean: Clean a path to the shortest equivalent name. (e.g. remove "foo/.."
from "foo/../bar.html") https://golang.org/pkg/path#Clean
- ext: https://golang.org/pkg/path#Ext
- isAbs: https://golang.org/pkg/path#IsAbs
Encoding:
- b64enc: Base 64 encode a string.
- b64dec: Base 64 decode a string.
Reflection:
- typeOf: Takes an interface and returns a string representation of the type.
For pointers, this will return a type prefixed with an asterisk(`*`). So
a pointer to type `Foo` will be `*Foo`.
- typeIs: Compares an interface with a string name, and returns true if they match.
Note that a pointer will not match a reference. For example `*Foo` will not
match `Foo`.
- typeIsLike: Compares an interface with a string name and returns true if
the interface is that `name` or that `*name`. In other words, if the given
value matches the given type or is a pointer to the given type, this returns
true.
- kindOf: Takes an interface and returns a string representation of its kind.
- kindIs: Returns true if the given string matches the kind of the given interface.
Note: None of these can test whether or not something implements a given
interface, since doing so would require compiling the interface in ahead of
time.
Data Structures:
- tuple: Takes an arbitrary list of items and returns a slice of items. Its
tuple-ish properties are mainly gained through the template idiom, and not
through an API provided here. WARNING: The implementation of tuple will
change in the future.
- list: An arbitrary ordered list of items. (This is prefered over tuple.)
- dict: Takes a list of name/values and returns a map[string]interface{}.
The first parameter is converted to a string and stored as a key, the
second parameter is treated as the value. And so on, with odds as keys and
evens as values. If the function call ends with an odd, the last key will
be assigned the empty string. Non-string keys are converted to strings as
follows: []byte are converted, fmt.Stringers will have String() called.
errors will have Error() called. All others will be passed through
fmt.Sprintf("%v").
Lists Functions:
These are used to manipulate lists: '{{ list 1 2 3 | reverse | first }}'
- first: Get the first item in a 'list'. 'list 1 2 3 | first' prints '1'
- last: Get the last item in a 'list': 'list 1 2 3 | last ' prints '3'
- rest: Get all but the first item in a list: 'list 1 2 3 | rest' returns '[2 3]'
- initial: Get all but the last item in a list: 'list 1 2 3 | initial' returns '[1 2]'
- append: Add an item to the end of a list: 'append $list 4' adds '4' to the end of '$list'
- prepend: Add an item to the beginning of a list: 'prepend $list 4' puts 4 at the beginning of the list.
- reverse: Reverse the items in a list.
- uniq: Remove duplicates from a list.
- without: Return a list with the given values removed: 'without (list 1 2 3) 1' would return '[2 3]'
- has: Return 'true' if the item is found in the list: 'has "foo" $list' will return 'true' if the list contains "foo"
Dict Functions:
These are used to manipulate dicts.
- set: Takes a dict, a key, and a value, and sets that key/value pair in
the dict. `set $dict $key $value`. For convenience, it returns the dict,
even though the dict was modified in place.
- unset: Takes a dict and a key, and deletes that key/value pair from the
dict. `unset $dict $key`. This returns the dict for convenience.
- hasKey: Takes a dict and a key, and returns boolean true if the key is in
the dict.
- pluck: Given a key and one or more maps, get all of the values for that key.
- keys: Get an array of all of the keys in one or more dicts.
- pick: Select just the given keys out of the dict, and return a new dict.
- omit: Return a dict without the given keys.
Math Functions:
Integer functions will convert integers of any width to `int64`. If a
string is passed in, functions will attempt to convert with
`strconv.ParseInt(s, 1064)`. If this fails, the value will be treated as 0.
- add1: Increment an integer by 1
- add: Sum an arbitrary number of integers
- sub: Subtract the second integer from the first
- div: Divide the first integer by the second
- mod: Module of first integer divided by second
- mul: Multiply integers
- max: Return the biggest of a series of one or more integers
- min: Return the smallest of a series of one or more integers
- biggest: DEPRECATED. Return the biggest of a series of one or more integers
Crypto Functions:
- genPrivateKey: Generate a private key for the given cryptosystem. If no
argument is supplied, by default it will generate a private key using
the RSA algorithm. Accepted values are `rsa`, `dsa`, and `ecdsa`.
- derivePassword: Derive a password from the given parameters according to the ["Master Password" algorithm](http://masterpasswordapp.com/algorithm.html)
Given parameters (in order) are:
`counter` (starting with 1), `password_type` (maximum, long, medium, short, basic, or pin), `password`,
`user`, and `site`
SemVer Functions:
These functions provide version parsing and comparisons for SemVer 2 version
strings.
- semver: Parse a semantic version and return a Version object.
- semverCompare: Compare a SemVer range to a particular version.
*/
package sprig
-281
View File
@@ -1,281 +0,0 @@
package sprig
import (
"errors"
"html/template"
"os"
"path"
"strconv"
"strings"
ttemplate "text/template"
"time"
util "github.com/aokoli/goutils"
"github.com/huandu/xstrings"
)
// Produce the function map.
//
// Use this to pass the functions into the template engine:
//
// tpl := template.New("foo").Funcs(sprig.FuncMap()))
//
func FuncMap() template.FuncMap {
return HtmlFuncMap()
}
// HermeticTextFuncMap returns a 'text/template'.FuncMap with only repeatable functions.
func HermeticTxtFuncMap() ttemplate.FuncMap {
r := TxtFuncMap()
for _, name := range nonhermeticFunctions {
delete(r, name)
}
return r
}
// HermeticHtmlFuncMap returns an 'html/template'.Funcmap with only repeatable functions.
func HermeticHtmlFuncMap() template.FuncMap {
r := HtmlFuncMap()
for _, name := range nonhermeticFunctions {
delete(r, name)
}
return r
}
// TextFuncMap returns a 'text/template'.FuncMap
func TxtFuncMap() ttemplate.FuncMap {
return ttemplate.FuncMap(GenericFuncMap())
}
// HtmlFuncMap returns an 'html/template'.Funcmap
func HtmlFuncMap() template.FuncMap {
return template.FuncMap(GenericFuncMap())
}
// GenericFuncMap returns a copy of the basic function map as a map[string]interface{}.
func GenericFuncMap() map[string]interface{} {
gfm := make(map[string]interface{}, len(genericMap))
for k, v := range genericMap {
gfm[k] = v
}
return gfm
}
// These functions are not guaranteed to evaluate to the same result for given input, because they
// refer to the environemnt or global state.
var nonhermeticFunctions = []string{
// Date functions
"date",
"date_in_zone",
"date_modify",
"now",
"htmlDate",
"htmlDateInZone",
"dateInZone",
"dateModify",
// Strings
"randAlphaNum",
"randAlpha",
"randAscii",
"randNumeric",
"uuidv4",
// OS
"env",
"expandenv",
}
var genericMap = map[string]interface{}{
"hello": func() string { return "Hello!" },
// Date functions
"date": date,
"date_in_zone": dateInZone,
"date_modify": dateModify,
"now": func() time.Time { return time.Now() },
"htmlDate": htmlDate,
"htmlDateInZone": htmlDateInZone,
"dateInZone": dateInZone,
"dateModify": dateModify,
"ago": dateAgo,
"toDate": toDate,
// Strings
"abbrev": abbrev,
"abbrevboth": abbrevboth,
"trunc": trunc,
"trim": strings.TrimSpace,
"upper": strings.ToUpper,
"lower": strings.ToLower,
"title": strings.Title,
"untitle": untitle,
"substr": substring,
// Switch order so that "foo" | repeat 5
"repeat": func(count int, str string) string { return strings.Repeat(str, count) },
// Deprecated: Use trimAll.
"trimall": func(a, b string) string { return strings.Trim(b, a) },
// Switch order so that "$foo" | trimall "$"
"trimAll": func(a, b string) string { return strings.Trim(b, a) },
"trimSuffix": func(a, b string) string { return strings.TrimSuffix(b, a) },
"trimPrefix": func(a, b string) string { return strings.TrimPrefix(b, a) },
"nospace": util.DeleteWhiteSpace,
"initials": initials,
"randAlphaNum": randAlphaNumeric,
"randAlpha": randAlpha,
"randAscii": randAscii,
"randNumeric": randNumeric,
"swapcase": util.SwapCase,
"shuffle": xstrings.Shuffle,
"snakecase": xstrings.ToSnakeCase,
"camelcase": xstrings.ToCamelCase,
"wrap": func(l int, s string) string { return util.Wrap(s, l) },
"wrapWith": func(l int, sep, str string) string { return util.WrapCustom(str, l, sep, true) },
// Switch order so that "foobar" | contains "foo"
"contains": func(substr string, str string) bool { return strings.Contains(str, substr) },
"hasPrefix": func(substr string, str string) bool { return strings.HasPrefix(str, substr) },
"hasSuffix": func(substr string, str string) bool { return strings.HasSuffix(str, substr) },
"quote": quote,
"squote": squote,
"cat": cat,
"indent": indent,
"nindent": nindent,
"replace": replace,
"plural": plural,
"sha1sum": sha1sum,
"sha256sum": sha256sum,
"toString": strval,
// Wrap Atoi to stop errors.
"atoi": func(a string) int { i, _ := strconv.Atoi(a); return i },
"int64": toInt64,
"int": toInt,
"float64": toFloat64,
//"gt": func(a, b int) bool {return a > b},
//"gte": func(a, b int) bool {return a >= b},
//"lt": func(a, b int) bool {return a < b},
//"lte": func(a, b int) bool {return a <= b},
// split "/" foo/bar returns map[int]string{0: foo, 1: bar}
"split": split,
"splitList": func(sep, orig string) []string { return strings.Split(orig, sep) },
"toStrings": strslice,
"until": until,
"untilStep": untilStep,
// VERY basic arithmetic.
"add1": func(i interface{}) int64 { return toInt64(i) + 1 },
"add": func(i ...interface{}) int64 {
var a int64 = 0
for _, b := range i {
a += toInt64(b)
}
return a
},
"sub": func(a, b interface{}) int64 { return toInt64(a) - toInt64(b) },
"div": func(a, b interface{}) int64 { return toInt64(a) / toInt64(b) },
"mod": func(a, b interface{}) int64 { return toInt64(a) % toInt64(b) },
"mul": func(a interface{}, v ...interface{}) int64 {
val := toInt64(a)
for _, b := range v {
val = val * toInt64(b)
}
return val
},
"biggest": max,
"max": max,
"min": min,
"ceil": ceil,
"floor": floor,
"round": round,
// string slices. Note that we reverse the order b/c that's better
// for template processing.
"join": join,
"sortAlpha": sortAlpha,
// Defaults
"default": dfault,
"empty": empty,
"coalesce": coalesce,
"compact": compact,
"toJson": toJson,
"toPrettyJson": toPrettyJson,
"ternary": ternary,
// Reflection
"typeOf": typeOf,
"typeIs": typeIs,
"typeIsLike": typeIsLike,
"kindOf": kindOf,
"kindIs": kindIs,
// OS:
"env": func(s string) string { return os.Getenv(s) },
"expandenv": func(s string) string { return os.ExpandEnv(s) },
// File Paths:
"base": path.Base,
"dir": path.Dir,
"clean": path.Clean,
"ext": path.Ext,
"isAbs": path.IsAbs,
// Encoding:
"b64enc": base64encode,
"b64dec": base64decode,
"b32enc": base32encode,
"b32dec": base32decode,
// Data Structures:
"tuple": list, // FIXME: with the addition of append/prepend these are no longer immutable.
"list": list,
"dict": dict,
"set": set,
"unset": unset,
"hasKey": hasKey,
"pluck": pluck,
"keys": keys,
"pick": pick,
"omit": omit,
"merge": merge,
"append": push, "push": push,
"prepend": prepend,
"first": first,
"rest": rest,
"last": last,
"initial": initial,
"reverse": reverse,
"uniq": uniq,
"without": without,
"has": has,
// Crypto:
"genPrivateKey": generatePrivateKey,
"derivePassword": derivePassword,
"buildCustomCert": buildCustomCertificate,
"genCA": generateCertificateAuthority,
"genSelfSignedCert": generateSelfSignedCertificate,
"genSignedCert": generateSignedCertificate,
// UUIDs:
"uuidv4": uuidv4,
// SemVer:
"semver": semver,
"semverCompare": semverCompare,
// Flow Control:
"fail": func(msg string) (string, error) { return "", errors.New(msg) },
// Regex
"regexMatch": regexMatch,
"regexFindAll": regexFindAll,
"regexFind": regexFind,
"regexReplaceAll": regexReplaceAll,
"regexReplaceAllLiteral": regexReplaceAllLiteral,
"regexSplit": regexSplit,
}
-33
View File
@@ -1,33 +0,0 @@
hash: 770b6a1132b743dadf6a0bb5fb8bf7083b1a5209f6d6c07826234ab2a97aade9
updated: 2018-04-02T23:08:56.947456531+02:00
imports:
- name: github.com/aokoli/goutils
version: 9c37978a95bd5c709a15883b6242714ea6709e64
- name: github.com/google/uuid
version: 064e2069ce9c359c118179501254f67d7d37ba24
- name: github.com/huandu/xstrings
version: 3959339b333561bf62a38b424fd41517c2c90f40
- name: github.com/imdario/mergo
version: 7fe0c75c13abdee74b09fcacef5ea1c6bba6a874
- name: github.com/Masterminds/goutils
version: 3391d3790d23d03408670993e957e8f408993c34
- name: github.com/Masterminds/semver
version: 59c29afe1a994eacb71c833025ca7acf874bb1da
- name: github.com/stretchr/testify
version: e3a8ff8ce36581f87a15341206f205b1da467059
subpackages:
- assert
- name: golang.org/x/crypto
version: d172538b2cfce0c13cee31e647d0367aa8cd2486
subpackages:
- pbkdf2
- scrypt
testImports:
- name: github.com/davecgh/go-spew
version: 5215b55f46b2b919f50a1df0eaa5886afe4e3b3d
subpackages:
- spew
- name: github.com/pmezard/go-difflib
version: d8ed2627bdf02c080bf22230dbb337003b7aba2d
subpackages:
- difflib
-15
View File
@@ -1,15 +0,0 @@
package: github.com/Masterminds/sprig
import:
- package: github.com/Masterminds/goutils
version: ^1.0.0
- package: github.com/google/uuid
version: ^0.2
- package: golang.org/x/crypto
subpackages:
- scrypt
- package: github.com/Masterminds/semver
version: v1.2.2
- package: github.com/stretchr/testify
- package: github.com/imdario/mergo
version: ~0.2.2
- package: github.com/huandu/xstrings
-259
View File
@@ -1,259 +0,0 @@
package sprig
import (
"fmt"
"reflect"
"sort"
)
// Reflection is used in these functions so that slices and arrays of strings,
// ints, and other types not implementing []interface{} can be worked with.
// For example, this is useful if you need to work on the output of regexs.
func list(v ...interface{}) []interface{} {
return v
}
func push(list interface{}, v interface{}) []interface{} {
tp := reflect.TypeOf(list).Kind()
switch tp {
case reflect.Slice, reflect.Array:
l2 := reflect.ValueOf(list)
l := l2.Len()
nl := make([]interface{}, l)
for i := 0; i < l; i++ {
nl[i] = l2.Index(i).Interface()
}
return append(nl, v)
default:
panic(fmt.Sprintf("Cannot push on type %s", tp))
}
}
func prepend(list interface{}, v interface{}) []interface{} {
//return append([]interface{}{v}, list...)
tp := reflect.TypeOf(list).Kind()
switch tp {
case reflect.Slice, reflect.Array:
l2 := reflect.ValueOf(list)
l := l2.Len()
nl := make([]interface{}, l)
for i := 0; i < l; i++ {
nl[i] = l2.Index(i).Interface()
}
return append([]interface{}{v}, nl...)
default:
panic(fmt.Sprintf("Cannot prepend on type %s", tp))
}
}
func last(list interface{}) interface{} {
tp := reflect.TypeOf(list).Kind()
switch tp {
case reflect.Slice, reflect.Array:
l2 := reflect.ValueOf(list)
l := l2.Len()
if l == 0 {
return nil
}
return l2.Index(l - 1).Interface()
default:
panic(fmt.Sprintf("Cannot find last on type %s", tp))
}
}
func first(list interface{}) interface{} {
tp := reflect.TypeOf(list).Kind()
switch tp {
case reflect.Slice, reflect.Array:
l2 := reflect.ValueOf(list)
l := l2.Len()
if l == 0 {
return nil
}
return l2.Index(0).Interface()
default:
panic(fmt.Sprintf("Cannot find first on type %s", tp))
}
}
func rest(list interface{}) []interface{} {
tp := reflect.TypeOf(list).Kind()
switch tp {
case reflect.Slice, reflect.Array:
l2 := reflect.ValueOf(list)
l := l2.Len()
if l == 0 {
return nil
}
nl := make([]interface{}, l-1)
for i := 1; i < l; i++ {
nl[i-1] = l2.Index(i).Interface()
}
return nl
default:
panic(fmt.Sprintf("Cannot find rest on type %s", tp))
}
}
func initial(list interface{}) []interface{} {
tp := reflect.TypeOf(list).Kind()
switch tp {
case reflect.Slice, reflect.Array:
l2 := reflect.ValueOf(list)
l := l2.Len()
if l == 0 {
return nil
}
nl := make([]interface{}, l-1)
for i := 0; i < l-1; i++ {
nl[i] = l2.Index(i).Interface()
}
return nl
default:
panic(fmt.Sprintf("Cannot find initial on type %s", tp))
}
}
func sortAlpha(list interface{}) []string {
k := reflect.Indirect(reflect.ValueOf(list)).Kind()
switch k {
case reflect.Slice, reflect.Array:
a := strslice(list)
s := sort.StringSlice(a)
s.Sort()
return s
}
return []string{strval(list)}
}
func reverse(v interface{}) []interface{} {
tp := reflect.TypeOf(v).Kind()
switch tp {
case reflect.Slice, reflect.Array:
l2 := reflect.ValueOf(v)
l := l2.Len()
// We do not sort in place because the incoming array should not be altered.
nl := make([]interface{}, l)
for i := 0; i < l; i++ {
nl[l-i-1] = l2.Index(i).Interface()
}
return nl
default:
panic(fmt.Sprintf("Cannot find reverse on type %s", tp))
}
}
func compact(list interface{}) []interface{} {
tp := reflect.TypeOf(list).Kind()
switch tp {
case reflect.Slice, reflect.Array:
l2 := reflect.ValueOf(list)
l := l2.Len()
nl := []interface{}{}
var item interface{}
for i := 0; i < l; i++ {
item = l2.Index(i).Interface()
if !empty(item) {
nl = append(nl, item)
}
}
return nl
default:
panic(fmt.Sprintf("Cannot compact on type %s", tp))
}
}
func uniq(list interface{}) []interface{} {
tp := reflect.TypeOf(list).Kind()
switch tp {
case reflect.Slice, reflect.Array:
l2 := reflect.ValueOf(list)
l := l2.Len()
dest := []interface{}{}
var item interface{}
for i := 0; i < l; i++ {
item = l2.Index(i).Interface()
if !inList(dest, item) {
dest = append(dest, item)
}
}
return dest
default:
panic(fmt.Sprintf("Cannot find uniq on type %s", tp))
}
}
func inList(haystack []interface{}, needle interface{}) bool {
for _, h := range haystack {
if reflect.DeepEqual(needle, h) {
return true
}
}
return false
}
func without(list interface{}, omit ...interface{}) []interface{} {
tp := reflect.TypeOf(list).Kind()
switch tp {
case reflect.Slice, reflect.Array:
l2 := reflect.ValueOf(list)
l := l2.Len()
res := []interface{}{}
var item interface{}
for i := 0; i < l; i++ {
item = l2.Index(i).Interface()
if !inList(omit, item) {
res = append(res, item)
}
}
return res
default:
panic(fmt.Sprintf("Cannot find without on type %s", tp))
}
}
func has(needle interface{}, haystack interface{}) bool {
tp := reflect.TypeOf(haystack).Kind()
switch tp {
case reflect.Slice, reflect.Array:
l2 := reflect.ValueOf(haystack)
var item interface{}
l := l2.Len()
for i := 0; i < l; i++ {
item = l2.Index(i).Interface()
if reflect.DeepEqual(needle, item) {
return true
}
}
return false
default:
panic(fmt.Sprintf("Cannot find has on type %s", tp))
}
}
-159
View File
@@ -1,159 +0,0 @@
package sprig
import (
"math"
"reflect"
"strconv"
)
// toFloat64 converts 64-bit floats
func toFloat64(v interface{}) float64 {
if str, ok := v.(string); ok {
iv, err := strconv.ParseFloat(str, 64)
if err != nil {
return 0
}
return iv
}
val := reflect.Indirect(reflect.ValueOf(v))
switch val.Kind() {
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
return float64(val.Int())
case reflect.Uint8, reflect.Uint16, reflect.Uint32:
return float64(val.Uint())
case reflect.Uint, reflect.Uint64:
return float64(val.Uint())
case reflect.Float32, reflect.Float64:
return val.Float()
case reflect.Bool:
if val.Bool() == true {
return 1
}
return 0
default:
return 0
}
}
func toInt(v interface{}) int {
//It's not optimal. Bud I don't want duplicate toInt64 code.
return int(toInt64(v))
}
// toInt64 converts integer types to 64-bit integers
func toInt64(v interface{}) int64 {
if str, ok := v.(string); ok {
iv, err := strconv.ParseInt(str, 10, 64)
if err != nil {
return 0
}
return iv
}
val := reflect.Indirect(reflect.ValueOf(v))
switch val.Kind() {
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
return val.Int()
case reflect.Uint8, reflect.Uint16, reflect.Uint32:
return int64(val.Uint())
case reflect.Uint, reflect.Uint64:
tv := val.Uint()
if tv <= math.MaxInt64 {
return int64(tv)
}
// TODO: What is the sensible thing to do here?
return math.MaxInt64
case reflect.Float32, reflect.Float64:
return int64(val.Float())
case reflect.Bool:
if val.Bool() == true {
return 1
}
return 0
default:
return 0
}
}
func max(a interface{}, i ...interface{}) int64 {
aa := toInt64(a)
for _, b := range i {
bb := toInt64(b)
if bb > aa {
aa = bb
}
}
return aa
}
func min(a interface{}, i ...interface{}) int64 {
aa := toInt64(a)
for _, b := range i {
bb := toInt64(b)
if bb < aa {
aa = bb
}
}
return aa
}
func until(count int) []int {
step := 1
if count < 0 {
step = -1
}
return untilStep(0, count, step)
}
func untilStep(start, stop, step int) []int {
v := []int{}
if stop < start {
if step >= 0 {
return v
}
for i := start; i > stop; i += step {
v = append(v, i)
}
return v
}
if step <= 0 {
return v
}
for i := start; i < stop; i += step {
v = append(v, i)
}
return v
}
func floor(a interface{}) float64 {
aa := toFloat64(a)
return math.Floor(aa)
}
func ceil(a interface{}) float64 {
aa := toFloat64(a)
return math.Ceil(aa)
}
func round(a interface{}, p int, r_opt ...float64) float64 {
roundOn := .5
if len(r_opt) > 0 {
roundOn = r_opt[0]
}
val := toFloat64(a)
places := toFloat64(p)
var round float64
pow := math.Pow(10, places)
digit := pow * val
_, div := math.Modf(digit)
if div >= roundOn {
round = math.Ceil(digit)
} else {
round = math.Floor(digit)
}
return round / pow
}
-28
View File
@@ -1,28 +0,0 @@
package sprig
import (
"fmt"
"reflect"
)
// typeIs returns true if the src is the type named in target.
func typeIs(target string, src interface{}) bool {
return target == typeOf(src)
}
func typeIsLike(target string, src interface{}) bool {
t := typeOf(src)
return target == t || "*"+target == t
}
func typeOf(src interface{}) string {
return fmt.Sprintf("%T", src)
}
func kindIs(target string, src interface{}) bool {
return target == kindOf(src)
}
func kindOf(src interface{}) string {
return reflect.ValueOf(src).Kind().String()
}
-35
View File
@@ -1,35 +0,0 @@
package sprig
import (
"regexp"
)
func regexMatch(regex string, s string) bool {
match, _ := regexp.MatchString(regex, s)
return match
}
func regexFindAll(regex string, s string, n int) []string {
r := regexp.MustCompile(regex)
return r.FindAllString(s, n)
}
func regexFind(regex string, s string) string {
r := regexp.MustCompile(regex)
return r.FindString(s)
}
func regexReplaceAll(regex string, s string, repl string) string {
r := regexp.MustCompile(regex)
return r.ReplaceAllString(s, repl)
}
func regexReplaceAllLiteral(regex string, s string, repl string) string {
r := regexp.MustCompile(regex)
return r.ReplaceAllLiteralString(s, repl)
}
func regexSplit(regex string, s string, n int) []string {
r := regexp.MustCompile(regex)
return r.Split(s, n)
}
-23
View File
@@ -1,23 +0,0 @@
package sprig
import (
sv2 "github.com/Masterminds/semver"
)
func semverCompare(constraint, version string) (bool, error) {
c, err := sv2.NewConstraint(constraint)
if err != nil {
return false, err
}
v, err := sv2.NewVersion(version)
if err != nil {
return false, err
}
return c.Check(v), nil
}
func semver(version string) (*sv2.Version, error) {
return sv2.NewVersion(version)
}
-201
View File
@@ -1,201 +0,0 @@
package sprig
import (
"encoding/base32"
"encoding/base64"
"fmt"
"reflect"
"strconv"
"strings"
util "github.com/aokoli/goutils"
)
func base64encode(v string) string {
return base64.StdEncoding.EncodeToString([]byte(v))
}
func base64decode(v string) string {
data, err := base64.StdEncoding.DecodeString(v)
if err != nil {
return err.Error()
}
return string(data)
}
func base32encode(v string) string {
return base32.StdEncoding.EncodeToString([]byte(v))
}
func base32decode(v string) string {
data, err := base32.StdEncoding.DecodeString(v)
if err != nil {
return err.Error()
}
return string(data)
}
func abbrev(width int, s string) string {
if width < 4 {
return s
}
r, _ := util.Abbreviate(s, width)
return r
}
func abbrevboth(left, right int, s string) string {
if right < 4 || left > 0 && right < 7 {
return s
}
r, _ := util.AbbreviateFull(s, left, right)
return r
}
func initials(s string) string {
// Wrap this just to eliminate the var args, which templates don't do well.
return util.Initials(s)
}
func randAlphaNumeric(count int) string {
// It is not possible, it appears, to actually generate an error here.
r, _ := util.RandomAlphaNumeric(count)
return r
}
func randAlpha(count int) string {
r, _ := util.RandomAlphabetic(count)
return r
}
func randAscii(count int) string {
r, _ := util.RandomAscii(count)
return r
}
func randNumeric(count int) string {
r, _ := util.RandomNumeric(count)
return r
}
func untitle(str string) string {
return util.Uncapitalize(str)
}
func quote(str ...interface{}) string {
out := make([]string, len(str))
for i, s := range str {
out[i] = fmt.Sprintf("%q", strval(s))
}
return strings.Join(out, " ")
}
func squote(str ...interface{}) string {
out := make([]string, len(str))
for i, s := range str {
out[i] = fmt.Sprintf("'%v'", s)
}
return strings.Join(out, " ")
}
func cat(v ...interface{}) string {
r := strings.TrimSpace(strings.Repeat("%v ", len(v)))
return fmt.Sprintf(r, v...)
}
func indent(spaces int, v string) string {
pad := strings.Repeat(" ", spaces)
return pad + strings.Replace(v, "\n", "\n"+pad, -1)
}
func nindent(spaces int, v string) string {
return "\n" + indent(spaces, v)
}
func replace(old, new, src string) string {
return strings.Replace(src, old, new, -1)
}
func plural(one, many string, count int) string {
if count == 1 {
return one
}
return many
}
func strslice(v interface{}) []string {
switch v := v.(type) {
case []string:
return v
case []interface{}:
l := len(v)
b := make([]string, l)
for i := 0; i < l; i++ {
b[i] = strval(v[i])
}
return b
default:
val := reflect.ValueOf(v)
switch val.Kind() {
case reflect.Array, reflect.Slice:
l := val.Len()
b := make([]string, l)
for i := 0; i < l; i++ {
b[i] = strval(val.Index(i).Interface())
}
return b
default:
return []string{strval(v)}
}
}
}
func strval(v interface{}) string {
switch v := v.(type) {
case string:
return v
case []byte:
return string(v)
case error:
return v.Error()
case fmt.Stringer:
return v.String()
default:
return fmt.Sprintf("%v", v)
}
}
func trunc(c int, s string) string {
if len(s) <= c {
return s
}
return s[0:c]
}
func join(sep string, v interface{}) string {
return strings.Join(strslice(v), sep)
}
func split(sep, orig string) map[string]string {
parts := strings.Split(orig, sep)
res := make(map[string]string, len(parts))
for i, v := range parts {
res["_"+strconv.Itoa(i)] = v
}
return res
}
// substring creates a substring of the given string.
//
// If start is < 0, this calls string[:length].
//
// If start is >= 0 and length < 0, this calls string[start:]
//
// Otherwise, this calls string[start, length].
func substring(start, length int, s string) string {
if start < 0 {
return s[:length]
}
if length < 0 {
return s[start:]
}
return s[start:length]
}
-18
View File
@@ -1,18 +0,0 @@
language: go
go:
- 1.6
- 1.7
- 1.8
- tip
script:
- go test -v
notifications:
webhooks:
urls:
- https://webhooks.gitter.im/e/06e3328629952dabe3e0
on_success: change # options: [always|never|change] default: always
on_failure: always # options: [always|never|change] default: always
on_start: never # options: [always|never|change] default: always
-8
View File
@@ -1,8 +0,0 @@
# 1.0.1 (2017-05-31)
## Fixed
- #21: Fix generation of alphanumeric strings (thanks @dbarranco)
# 1.0.0 (2014-04-30)
- Initial release.
-202
View File
@@ -1,202 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-70
View File
@@ -1,70 +0,0 @@
GoUtils
===========
[![Stability: Maintenance](https://masterminds.github.io/stability/maintenance.svg)](https://masterminds.github.io/stability/maintenance.html)
[![GoDoc](https://godoc.org/github.com/Masterminds/goutils?status.png)](https://godoc.org/github.com/Masterminds/goutils) [![Build Status](https://travis-ci.org/Masterminds/goutils.svg?branch=master)](https://travis-ci.org/Masterminds/goutils) [![Build status](https://ci.appveyor.com/api/projects/status/sc2b1ew0m7f0aiju?svg=true)](https://ci.appveyor.com/project/mattfarina/goutils)
GoUtils provides users with utility functions to manipulate strings in various ways. It is a Go implementation of some
string manipulation libraries of Java Apache Commons. GoUtils includes the following Java Apache Commons classes:
* WordUtils
* RandomStringUtils
* StringUtils (partial implementation)
## Installation
If you have Go set up on your system, from the GOPATH directory within the command line/terminal, enter this:
go get github.com/Masterminds/goutils
If you do not have Go set up on your system, please follow the [Go installation directions from the documenation](http://golang.org/doc/install), and then follow the instructions above to install GoUtils.
## Documentation
GoUtils doc is available here: [![GoDoc](https://godoc.org/github.com/Masterminds/goutils?status.png)](https://godoc.org/github.com/Masterminds/goutils)
## Usage
The code snippets below show examples of how to use GoUtils. Some functions return errors while others do not. The first instance below, which does not return an error, is the `Initials` function (located within the `wordutils.go` file).
package main
import (
"fmt"
"github.com/Masterminds/goutils"
)
func main() {
// EXAMPLE 1: A goutils function which returns no errors
fmt.Println (goutils.Initials("John Doe Foo")) // Prints out "JDF"
}
Some functions return errors mainly due to illegal arguements used as parameters. The code example below illustrates how to deal with function that returns an error. In this instance, the function is the `Random` function (located within the `randomstringutils.go` file).
package main
import (
"fmt"
"github.com/Masterminds/goutils"
)
func main() {
// EXAMPLE 2: A goutils function which returns an error
rand1, err1 := goutils.Random (-1, 0, 0, true, true)
if err1 != nil {
fmt.Println(err1) // Prints out error message because -1 was entered as the first parameter in goutils.Random(...)
} else {
fmt.Println(rand1)
}
}
## License
GoUtils is licensed under the Apache License, Version 2.0. Please check the LICENSE.txt file or visit http://www.apache.org/licenses/LICENSE-2.0 for a copy of the license.
## Issue Reporting
Make suggestions or report issues using the Git issue tracker: https://github.com/Masterminds/goutils/issues
## Website
* [GoUtils webpage](http://Masterminds.github.io/goutils/)
-21
View File
@@ -1,21 +0,0 @@
version: build-{build}.{branch}
clone_folder: C:\gopath\src\github.com\Masterminds\goutils
shallow_clone: true
environment:
GOPATH: C:\gopath
platform:
- x64
build: off
install:
- go version
- go env
test_script:
- go test -v
deploy: off
-268
View File
@@ -1,268 +0,0 @@
/*
Copyright 2014 Alexander Okoli
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package goutils
import (
"fmt"
"math"
"math/rand"
"regexp"
"time"
"unicode"
)
// RANDOM provides the time-based seed used to generate random numbers
var RANDOM = rand.New(rand.NewSource(time.Now().UnixNano()))
/*
RandomNonAlphaNumeric creates a random string whose length is the number of characters specified.
Characters will be chosen from the set of all characters (ASCII/Unicode values between 0 to 2,147,483,647 (math.MaxInt32)).
Parameter:
count - the length of random string to create
Returns:
string - the random string
error - an error stemming from an invalid parameter within underlying function, RandomSeed(...)
*/
func RandomNonAlphaNumeric(count int) (string, error) {
return RandomAlphaNumericCustom(count, false, false)
}
/*
RandomAscii creates a random string whose length is the number of characters specified.
Characters will be chosen from the set of characters whose ASCII value is between 32 and 126 (inclusive).
Parameter:
count - the length of random string to create
Returns:
string - the random string
error - an error stemming from an invalid parameter within underlying function, RandomSeed(...)
*/
func RandomAscii(count int) (string, error) {
return Random(count, 32, 127, false, false)
}
/*
RandomNumeric creates a random string whose length is the number of characters specified.
Characters will be chosen from the set of numeric characters.
Parameter:
count - the length of random string to create
Returns:
string - the random string
error - an error stemming from an invalid parameter within underlying function, RandomSeed(...)
*/
func RandomNumeric(count int) (string, error) {
return Random(count, 0, 0, false, true)
}
/*
RandomAlphabetic creates a random string whose length is the number of characters specified.
Characters will be chosen from the set of alpha-numeric characters as indicated by the arguments.
Parameters:
count - the length of random string to create
letters - if true, generated string may include alphabetic characters
numbers - if true, generated string may include numeric characters
Returns:
string - the random string
error - an error stemming from an invalid parameter within underlying function, RandomSeed(...)
*/
func RandomAlphabetic(count int) (string, error) {
return Random(count, 0, 0, true, false)
}
/*
RandomAlphaNumeric creates a random string whose length is the number of characters specified.
Characters will be chosen from the set of alpha-numeric characters.
Parameter:
count - the length of random string to create
Returns:
string - the random string
error - an error stemming from an invalid parameter within underlying function, RandomSeed(...)
*/
func RandomAlphaNumeric(count int) (string, error) {
RandomString, err := Random(count, 0, 0, true, true)
if err != nil {
return "", fmt.Errorf("Error: %s", err)
}
match, err := regexp.MatchString("([0-9]+)", RandomString)
if err != nil {
panic(err)
}
if !match {
//Get the position between 0 and the length of the string-1 to insert a random number
position := rand.Intn(count)
//Insert a random number between [0-9] in the position
RandomString = RandomString[:position] + string('0'+rand.Intn(10)) + RandomString[position+1:]
return RandomString, err
}
return RandomString, err
}
/*
RandomAlphaNumericCustom creates a random string whose length is the number of characters specified.
Characters will be chosen from the set of alpha-numeric characters as indicated by the arguments.
Parameters:
count - the length of random string to create
letters - if true, generated string may include alphabetic characters
numbers - if true, generated string may include numeric characters
Returns:
string - the random string
error - an error stemming from an invalid parameter within underlying function, RandomSeed(...)
*/
func RandomAlphaNumericCustom(count int, letters bool, numbers bool) (string, error) {
return Random(count, 0, 0, letters, numbers)
}
/*
Random creates a random string based on a variety of options, using default source of randomness.
This method has exactly the same semantics as RandomSeed(int, int, int, bool, bool, []char, *rand.Rand), but
instead of using an externally supplied source of randomness, it uses the internal *rand.Rand instance.
Parameters:
count - the length of random string to create
start - the position in set of chars (ASCII/Unicode int) to start at
end - the position in set of chars (ASCII/Unicode int) to end before
letters - if true, generated string may include alphabetic characters
numbers - if true, generated string may include numeric characters
chars - the set of chars to choose randoms from. If nil, then it will use the set of all chars.
Returns:
string - the random string
error - an error stemming from an invalid parameter within underlying function, RandomSeed(...)
*/
func Random(count int, start int, end int, letters bool, numbers bool, chars ...rune) (string, error) {
return RandomSeed(count, start, end, letters, numbers, chars, RANDOM)
}
/*
RandomSeed creates a random string based on a variety of options, using supplied source of randomness.
If the parameters start and end are both 0, start and end are set to ' ' and 'z', the ASCII printable characters, will be used,
unless letters and numbers are both false, in which case, start and end are set to 0 and math.MaxInt32, respectively.
If chars is not nil, characters stored in chars that are between start and end are chosen.
This method accepts a user-supplied *rand.Rand instance to use as a source of randomness. By seeding a single *rand.Rand instance
with a fixed seed and using it for each call, the same random sequence of strings can be generated repeatedly and predictably.
Parameters:
count - the length of random string to create
start - the position in set of chars (ASCII/Unicode decimals) to start at
end - the position in set of chars (ASCII/Unicode decimals) to end before
letters - if true, generated string may include alphabetic characters
numbers - if true, generated string may include numeric characters
chars - the set of chars to choose randoms from. If nil, then it will use the set of all chars.
random - a source of randomness.
Returns:
string - the random string
error - an error stemming from invalid parameters: if count < 0; or the provided chars array is empty; or end <= start; or end > len(chars)
*/
func RandomSeed(count int, start int, end int, letters bool, numbers bool, chars []rune, random *rand.Rand) (string, error) {
if count == 0 {
return "", nil
} else if count < 0 {
err := fmt.Errorf("randomstringutils illegal argument: Requested random string length %v is less than 0.", count) // equiv to err := errors.New("...")
return "", err
}
if chars != nil && len(chars) == 0 {
err := fmt.Errorf("randomstringutils illegal argument: The chars array must not be empty")
return "", err
}
if start == 0 && end == 0 {
if chars != nil {
end = len(chars)
} else {
if !letters && !numbers {
end = math.MaxInt32
} else {
end = 'z' + 1
start = ' '
}
}
} else {
if end <= start {
err := fmt.Errorf("randomstringutils illegal argument: Parameter end (%v) must be greater than start (%v)", end, start)
return "", err
}
if chars != nil && end > len(chars) {
err := fmt.Errorf("randomstringutils illegal argument: Parameter end (%v) cannot be greater than len(chars) (%v)", end, len(chars))
return "", err
}
}
buffer := make([]rune, count)
gap := end - start
// high-surrogates range, (\uD800-\uDBFF) = 55296 - 56319
// low-surrogates range, (\uDC00-\uDFFF) = 56320 - 57343
for count != 0 {
count--
var ch rune
if chars == nil {
ch = rune(random.Intn(gap) + start)
} else {
ch = chars[random.Intn(gap)+start]
}
if letters && unicode.IsLetter(ch) || numbers && unicode.IsDigit(ch) || !letters && !numbers {
if ch >= 56320 && ch <= 57343 { // low surrogate range
if count == 0 {
count++
} else {
// Insert low surrogate
buffer[count] = ch
count--
// Insert high surrogate
buffer[count] = rune(55296 + random.Intn(128))
}
} else if ch >= 55296 && ch <= 56191 { // High surrogates range (Partial)
if count == 0 {
count++
} else {
// Insert low surrogate
buffer[count] = rune(56320 + random.Intn(128))
count--
// Insert high surrogate
buffer[count] = ch
}
} else if ch >= 56192 && ch <= 56319 {
// private high surrogate, skip it
count++
} else {
// not one of the surrogates*
buffer[count] = ch
}
} else {
count++
}
}
return string(buffer), nil
}
-224
View File
@@ -1,224 +0,0 @@
/*
Copyright 2014 Alexander Okoli
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package goutils
import (
"bytes"
"fmt"
"strings"
"unicode"
)
// Typically returned by functions where a searched item cannot be found
const INDEX_NOT_FOUND = -1
/*
Abbreviate abbreviates a string using ellipses. This will turn the string "Now is the time for all good men" into "Now is the time for..."
Specifically, the algorithm is as follows:
- If str is less than maxWidth characters long, return it.
- Else abbreviate it to (str[0:maxWidth - 3] + "...").
- If maxWidth is less than 4, return an illegal argument error.
- In no case will it return a string of length greater than maxWidth.
Parameters:
str - the string to check
maxWidth - maximum length of result string, must be at least 4
Returns:
string - abbreviated string
error - if the width is too small
*/
func Abbreviate(str string, maxWidth int) (string, error) {
return AbbreviateFull(str, 0, maxWidth)
}
/*
AbbreviateFull abbreviates a string using ellipses. This will turn the string "Now is the time for all good men" into "...is the time for..."
This function works like Abbreviate(string, int), but allows you to specify a "left edge" offset. Note that this left edge is not
necessarily going to be the leftmost character in the result, or the first character following the ellipses, but it will appear
somewhere in the result.
In no case will it return a string of length greater than maxWidth.
Parameters:
str - the string to check
offset - left edge of source string
maxWidth - maximum length of result string, must be at least 4
Returns:
string - abbreviated string
error - if the width is too small
*/
func AbbreviateFull(str string, offset int, maxWidth int) (string, error) {
if str == "" {
return "", nil
}
if maxWidth < 4 {
err := fmt.Errorf("stringutils illegal argument: Minimum abbreviation width is 4")
return "", err
}
if len(str) <= maxWidth {
return str, nil
}
if offset > len(str) {
offset = len(str)
}
if len(str)-offset < (maxWidth - 3) { // 15 - 5 < 10 - 3 = 10 < 7
offset = len(str) - (maxWidth - 3)
}
abrevMarker := "..."
if offset <= 4 {
return str[0:maxWidth-3] + abrevMarker, nil // str.substring(0, maxWidth - 3) + abrevMarker;
}
if maxWidth < 7 {
err := fmt.Errorf("stringutils illegal argument: Minimum abbreviation width with offset is 7")
return "", err
}
if (offset + maxWidth - 3) < len(str) { // 5 + (10-3) < 15 = 12 < 15
abrevStr, _ := Abbreviate(str[offset:len(str)], (maxWidth - 3))
return abrevMarker + abrevStr, nil // abrevMarker + abbreviate(str.substring(offset), maxWidth - 3);
}
return abrevMarker + str[(len(str)-(maxWidth-3)):len(str)], nil // abrevMarker + str.substring(str.length() - (maxWidth - 3));
}
/*
DeleteWhiteSpace deletes all whitespaces from a string as defined by unicode.IsSpace(rune).
It returns the string without whitespaces.
Parameter:
str - the string to delete whitespace from, may be nil
Returns:
the string without whitespaces
*/
func DeleteWhiteSpace(str string) string {
if str == "" {
return str
}
sz := len(str)
var chs bytes.Buffer
count := 0
for i := 0; i < sz; i++ {
ch := rune(str[i])
if !unicode.IsSpace(ch) {
chs.WriteRune(ch)
count++
}
}
if count == sz {
return str
}
return chs.String()
}
/*
IndexOfDifference compares two strings, and returns the index at which the strings begin to differ.
Parameters:
str1 - the first string
str2 - the second string
Returns:
the index where str1 and str2 begin to differ; -1 if they are equal
*/
func IndexOfDifference(str1 string, str2 string) int {
if str1 == str2 {
return INDEX_NOT_FOUND
}
if IsEmpty(str1) || IsEmpty(str2) {
return 0
}
var i int
for i = 0; i < len(str1) && i < len(str2); i++ {
if rune(str1[i]) != rune(str2[i]) {
break
}
}
if i < len(str2) || i < len(str1) {
return i
}
return INDEX_NOT_FOUND
}
/*
IsBlank checks if a string is whitespace or empty (""). Observe the following behavior:
goutils.IsBlank("") = true
goutils.IsBlank(" ") = true
goutils.IsBlank("bob") = false
goutils.IsBlank(" bob ") = false
Parameter:
str - the string to check
Returns:
true - if the string is whitespace or empty ("")
*/
func IsBlank(str string) bool {
strLen := len(str)
if str == "" || strLen == 0 {
return true
}
for i := 0; i < strLen; i++ {
if unicode.IsSpace(rune(str[i])) == false {
return false
}
}
return true
}
/*
IndexOf returns the index of the first instance of sub in str, with the search beginning from the
index start point specified. -1 is returned if sub is not present in str.
An empty string ("") will return -1 (INDEX_NOT_FOUND). A negative start position is treated as zero.
A start position greater than the string length returns -1.
Parameters:
str - the string to check
sub - the substring to find
start - the start position; negative treated as zero
Returns:
the first index where the sub string was found (always >= start)
*/
func IndexOf(str string, sub string, start int) int {
if start < 0 {
start = 0
}
if len(str) < start {
return INDEX_NOT_FOUND
}
if IsEmpty(str) || IsEmpty(sub) {
return INDEX_NOT_FOUND
}
partialIndex := strings.Index(str[start:len(str)], sub)
if partialIndex == -1 {
return INDEX_NOT_FOUND
}
return partialIndex + start
}
// IsEmpty checks if a string is empty (""). Returns true if empty, and false otherwise.
func IsEmpty(str string) bool {
return len(str) == 0
}
-356
View File
@@ -1,356 +0,0 @@
/*
Copyright 2014 Alexander Okoli
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/*
Package goutils provides utility functions to manipulate strings in various ways.
The code snippets below show examples of how to use goutils. Some functions return
errors while others do not, so usage would vary as a result.
Example:
package main
import (
"fmt"
"github.com/aokoli/goutils"
)
func main() {
// EXAMPLE 1: A goutils function which returns no errors
fmt.Println (goutils.Initials("John Doe Foo")) // Prints out "JDF"
// EXAMPLE 2: A goutils function which returns an error
rand1, err1 := goutils.Random (-1, 0, 0, true, true)
if err1 != nil {
fmt.Println(err1) // Prints out error message because -1 was entered as the first parameter in goutils.Random(...)
} else {
fmt.Println(rand1)
}
}
*/
package goutils
import (
"bytes"
"strings"
"unicode"
)
// VERSION indicates the current version of goutils
const VERSION = "1.0.0"
/*
Wrap wraps a single line of text, identifying words by ' '.
New lines will be separated by '\n'. Very long words, such as URLs will not be wrapped.
Leading spaces on a new line are stripped. Trailing spaces are not stripped.
Parameters:
str - the string to be word wrapped
wrapLength - the column (a column can fit only one character) to wrap the words at, less than 1 is treated as 1
Returns:
a line with newlines inserted
*/
func Wrap(str string, wrapLength int) string {
return WrapCustom(str, wrapLength, "", false)
}
/*
WrapCustom wraps a single line of text, identifying words by ' '.
Leading spaces on a new line are stripped. Trailing spaces are not stripped.
Parameters:
str - the string to be word wrapped
wrapLength - the column number (a column can fit only one character) to wrap the words at, less than 1 is treated as 1
newLineStr - the string to insert for a new line, "" uses '\n'
wrapLongWords - true if long words (such as URLs) should be wrapped
Returns:
a line with newlines inserted
*/
func WrapCustom(str string, wrapLength int, newLineStr string, wrapLongWords bool) string {
if str == "" {
return ""
}
if newLineStr == "" {
newLineStr = "\n" // TODO Assumes "\n" is seperator. Explore SystemUtils.LINE_SEPARATOR from Apache Commons
}
if wrapLength < 1 {
wrapLength = 1
}
inputLineLength := len(str)
offset := 0
var wrappedLine bytes.Buffer
for inputLineLength-offset > wrapLength {
if rune(str[offset]) == ' ' {
offset++
continue
}
end := wrapLength + offset + 1
spaceToWrapAt := strings.LastIndex(str[offset:end], " ") + offset
if spaceToWrapAt >= offset {
// normal word (not longer than wrapLength)
wrappedLine.WriteString(str[offset:spaceToWrapAt])
wrappedLine.WriteString(newLineStr)
offset = spaceToWrapAt + 1
} else {
// long word or URL
if wrapLongWords {
end := wrapLength + offset
// long words are wrapped one line at a time
wrappedLine.WriteString(str[offset:end])
wrappedLine.WriteString(newLineStr)
offset += wrapLength
} else {
// long words aren't wrapped, just extended beyond limit
end := wrapLength + offset
spaceToWrapAt = strings.IndexRune(str[end:len(str)], ' ') + end
if spaceToWrapAt >= 0 {
wrappedLine.WriteString(str[offset:spaceToWrapAt])
wrappedLine.WriteString(newLineStr)
offset = spaceToWrapAt + 1
} else {
wrappedLine.WriteString(str[offset:len(str)])
offset = inputLineLength
}
}
}
}
wrappedLine.WriteString(str[offset:len(str)])
return wrappedLine.String()
}
/*
Capitalize capitalizes all the delimiter separated words in a string. Only the first letter of each word is changed.
To convert the rest of each word to lowercase at the same time, use CapitalizeFully(str string, delimiters ...rune).
The delimiters represent a set of characters understood to separate words. The first string character
and the first non-delimiter character after a delimiter will be capitalized. A "" input string returns "".
Capitalization uses the Unicode title case, normally equivalent to upper case.
Parameters:
str - the string to capitalize
delimiters - set of characters to determine capitalization, exclusion of this parameter means whitespace would be delimeter
Returns:
capitalized string
*/
func Capitalize(str string, delimiters ...rune) string {
var delimLen int
if delimiters == nil {
delimLen = -1
} else {
delimLen = len(delimiters)
}
if str == "" || delimLen == 0 {
return str
}
buffer := []rune(str)
capitalizeNext := true
for i := 0; i < len(buffer); i++ {
ch := buffer[i]
if isDelimiter(ch, delimiters...) {
capitalizeNext = true
} else if capitalizeNext {
buffer[i] = unicode.ToTitle(ch)
capitalizeNext = false
}
}
return string(buffer)
}
/*
CapitalizeFully converts all the delimiter separated words in a string into capitalized words, that is each word is made up of a
titlecase character and then a series of lowercase characters. The delimiters represent a set of characters understood
to separate words. The first string character and the first non-delimiter character after a delimiter will be capitalized.
Capitalization uses the Unicode title case, normally equivalent to upper case.
Parameters:
str - the string to capitalize fully
delimiters - set of characters to determine capitalization, exclusion of this parameter means whitespace would be delimeter
Returns:
capitalized string
*/
func CapitalizeFully(str string, delimiters ...rune) string {
var delimLen int
if delimiters == nil {
delimLen = -1
} else {
delimLen = len(delimiters)
}
if str == "" || delimLen == 0 {
return str
}
str = strings.ToLower(str)
return Capitalize(str, delimiters...)
}
/*
Uncapitalize uncapitalizes all the whitespace separated words in a string. Only the first letter of each word is changed.
The delimiters represent a set of characters understood to separate words. The first string character and the first non-delimiter
character after a delimiter will be uncapitalized. Whitespace is defined by unicode.IsSpace(char).
Parameters:
str - the string to uncapitalize fully
delimiters - set of characters to determine capitalization, exclusion of this parameter means whitespace would be delimeter
Returns:
uncapitalized string
*/
func Uncapitalize(str string, delimiters ...rune) string {
var delimLen int
if delimiters == nil {
delimLen = -1
} else {
delimLen = len(delimiters)
}
if str == "" || delimLen == 0 {
return str
}
buffer := []rune(str)
uncapitalizeNext := true // TODO Always makes capitalize/un apply to first char.
for i := 0; i < len(buffer); i++ {
ch := buffer[i]
if isDelimiter(ch, delimiters...) {
uncapitalizeNext = true
} else if uncapitalizeNext {
buffer[i] = unicode.ToLower(ch)
uncapitalizeNext = false
}
}
return string(buffer)
}
/*
SwapCase swaps the case of a string using a word based algorithm.
Conversion algorithm:
Upper case character converts to Lower case
Title case character converts to Lower case
Lower case character after Whitespace or at start converts to Title case
Other Lower case character converts to Upper case
Whitespace is defined by unicode.IsSpace(char).
Parameters:
str - the string to swap case
Returns:
the changed string
*/
func SwapCase(str string) string {
if str == "" {
return str
}
buffer := []rune(str)
whitespace := true
for i := 0; i < len(buffer); i++ {
ch := buffer[i]
if unicode.IsUpper(ch) {
buffer[i] = unicode.ToLower(ch)
whitespace = false
} else if unicode.IsTitle(ch) {
buffer[i] = unicode.ToLower(ch)
whitespace = false
} else if unicode.IsLower(ch) {
if whitespace {
buffer[i] = unicode.ToTitle(ch)
whitespace = false
} else {
buffer[i] = unicode.ToUpper(ch)
}
} else {
whitespace = unicode.IsSpace(ch)
}
}
return string(buffer)
}
/*
Initials extracts the initial letters from each word in the string. The first letter of the string and all first
letters after the defined delimiters are returned as a new string. Their case is not changed. If the delimiters
parameter is excluded, then Whitespace is used. Whitespace is defined by unicode.IsSpacea(char). An empty delimiter array returns an empty string.
Parameters:
str - the string to get initials from
delimiters - set of characters to determine words, exclusion of this parameter means whitespace would be delimeter
Returns:
string of initial letters
*/
func Initials(str string, delimiters ...rune) string {
if str == "" {
return str
}
if delimiters != nil && len(delimiters) == 0 {
return ""
}
strLen := len(str)
var buf bytes.Buffer
lastWasGap := true
for i := 0; i < strLen; i++ {
ch := rune(str[i])
if isDelimiter(ch, delimiters...) {
lastWasGap = true
} else if lastWasGap {
buf.WriteRune(ch)
lastWasGap = false
}
}
return buf.String()
}
// private function (lower case func name)
func isDelimiter(ch rune, delimiters ...rune) bool {
if delimiters == nil {
return unicode.IsSpace(ch)
}
for _, delimiter := range delimiters {
if ch == delimiter {
return true
}
}
return false
}
-9
View File
@@ -1,9 +0,0 @@
language: go
go:
- 1.4.3
- 1.5.3
- tip
script:
- go test -v ./...
-10
View File
@@ -1,10 +0,0 @@
# How to contribute
We definitely welcome patches and contribution to this project!
### Legal requirements
In order to protect both you and ourselves, you will need to sign the
[Contributor License Agreement](https://cla.developers.google.com/clas).
You may have already signed it for other Google projects.
-9
View File
@@ -1,9 +0,0 @@
Paul Borman <borman@google.com>
bmatsuo
shawnps
theory
jboverfelt
dsymonds
cd1
wallclockbuilder
dansouza
-27
View File
@@ -1,27 +0,0 @@
Copyright (c) 2009,2014 Google Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-23
View File
@@ -1,23 +0,0 @@
**This package is currently in development and the API may not be stable.**
The API will become stable with v1.
# uuid ![build status](https://travis-ci.org/google/uuid.svg?branch=master)
The uuid package generates and inspects UUIDs based on
[RFC 4122](http://tools.ietf.org/html/rfc4122)
and DCE 1.1: Authentication and Security Services.
This package is based on the github.com/pborman/uuid package (previously named
code.google.com/p/go-uuid). It differs from these earlier packages in that
a UUID is a 16 byte array rather than a byte slice. One loss due to this
change is the ability to represent an invalid UUID (vs a NIL UUID).
###### Install
`go get github.com/google/uuid`
###### Documentation
[![GoDoc](https://godoc.org/github.com/google/uuid?status.svg)](http://godoc.org/github.com/google/uuid)
Full `go doc` style documentation for the package can be viewed online without
installing this package by using the GoDoc site here:
http://godoc.org/github.com/google/uuid
-80
View File
@@ -1,80 +0,0 @@
// Copyright 2016 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import (
"encoding/binary"
"fmt"
"os"
)
// A Domain represents a Version 2 domain
type Domain byte
// Domain constants for DCE Security (Version 2) UUIDs.
const (
Person = Domain(0)
Group = Domain(1)
Org = Domain(2)
)
// NewDCESecurity returns a DCE Security (Version 2) UUID.
//
// The domain should be one of Person, Group or Org.
// On a POSIX system the id should be the users UID for the Person
// domain and the users GID for the Group. The meaning of id for
// the domain Org or on non-POSIX systems is site defined.
//
// For a given domain/id pair the same token may be returned for up to
// 7 minutes and 10 seconds.
func NewDCESecurity(domain Domain, id uint32) (UUID, error) {
uuid, err := NewUUID()
if err == nil {
uuid[6] = (uuid[6] & 0x0f) | 0x20 // Version 2
uuid[9] = byte(domain)
binary.BigEndian.PutUint32(uuid[0:], id)
}
return uuid, err
}
// NewDCEPerson returns a DCE Security (Version 2) UUID in the person
// domain with the id returned by os.Getuid.
//
// NewDCEPerson(Person, uint32(os.Getuid()))
func NewDCEPerson() (UUID, error) {
return NewDCESecurity(Person, uint32(os.Getuid()))
}
// NewDCEGroup returns a DCE Security (Version 2) UUID in the group
// domain with the id returned by os.Getgid.
//
// NewDCEGroup(Group, uint32(os.Getgid()))
func NewDCEGroup() (UUID, error) {
return NewDCESecurity(Group, uint32(os.Getgid()))
}
// Domain returns the domain for a Version 2 UUID. Domains are only defined
// for Version 2 UUIDs.
func (uuid UUID) Domain() Domain {
return Domain(uuid[9])
}
// ID returns the id for a Version 2 UUID. IDs are only defined for Version 2
// UUIDs.
func (uuid UUID) ID() uint32 {
return binary.BigEndian.Uint32(uuid[0:4])
}
func (d Domain) String() string {
switch d {
case Person:
return "Person"
case Group:
return "Group"
case Org:
return "Org"
}
return fmt.Sprintf("Domain%d", int(d))
}
-12
View File
@@ -1,12 +0,0 @@
// Copyright 2016 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package uuid generates and inspects UUIDs.
//
// UUIDs are based on RFC 4122 and DCE 1.1: Authentication and Security
// Services.
//
// A UUID is a 16 byte (128 bit) array. UUIDs may be used as keys to
// maps or compared directly.
package uuid
-53
View File
@@ -1,53 +0,0 @@
// Copyright 2016 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import (
"crypto/md5"
"crypto/sha1"
"hash"
)
// Well known namespace IDs and UUIDs
var (
NameSpaceDNS = Must(Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c8"))
NameSpaceURL = Must(Parse("6ba7b811-9dad-11d1-80b4-00c04fd430c8"))
NameSpaceOID = Must(Parse("6ba7b812-9dad-11d1-80b4-00c04fd430c8"))
NameSpaceX500 = Must(Parse("6ba7b814-9dad-11d1-80b4-00c04fd430c8"))
Nil UUID // empty UUID, all zeros
)
// NewHash returns a new UUID derived from the hash of space concatenated with
// data generated by h. The hash should be at least 16 byte in length. The
// first 16 bytes of the hash are used to form the UUID. The version of the
// UUID will be the lower 4 bits of version. NewHash is used to implement
// NewMD5 and NewSHA1.
func NewHash(h hash.Hash, space UUID, data []byte, version int) UUID {
h.Reset()
h.Write(space[:])
h.Write([]byte(data))
s := h.Sum(nil)
var uuid UUID
copy(uuid[:], s)
uuid[6] = (uuid[6] & 0x0f) | uint8((version&0xf)<<4)
uuid[8] = (uuid[8] & 0x3f) | 0x80 // RFC 4122 variant
return uuid
}
// NewMD5 returns a new MD5 (Version 3) UUID based on the
// supplied name space and data. It is the same as calling:
//
// NewHash(md5.New(), space, data, 3)
func NewMD5(space UUID, data []byte) UUID {
return NewHash(md5.New(), space, data, 3)
}
// NewSHA1 returns a new SHA1 (Version 5) UUID based on the
// supplied name space and data. It is the same as calling:
//
// NewHash(sha1.New(), space, data, 5)
func NewSHA1(space UUID, data []byte) UUID {
return NewHash(sha1.New(), space, data, 5)
}
-39
View File
@@ -1,39 +0,0 @@
// Copyright 2016 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import "fmt"
// MarshalText implements encoding.TextMarshaler.
func (uuid UUID) MarshalText() ([]byte, error) {
var js [36]byte
encodeHex(js[:], uuid)
return js[:], nil
}
// UnmarshalText implements encoding.TextUnmarshaler.
func (uuid *UUID) UnmarshalText(data []byte) error {
// See comment in ParseBytes why we do this.
// id, err := ParseBytes(data)
id, err := ParseBytes(data)
if err == nil {
*uuid = id
}
return err
}
// MarshalBinary implements encoding.BinaryMarshaler.
func (uuid UUID) MarshalBinary() ([]byte, error) {
return uuid[:], nil
}
// UnmarshalBinary implements encoding.BinaryUnmarshaler.
func (uuid *UUID) UnmarshalBinary(data []byte) error {
if len(data) != 16 {
return fmt.Errorf("invalid UUID (got %d bytes)", len(data))
}
copy(uuid[:], data)
return nil
}
-103
View File
@@ -1,103 +0,0 @@
// Copyright 2016 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import (
"net"
"sync"
)
var (
nodeMu sync.Mutex
interfaces []net.Interface // cached list of interfaces
ifname string // name of interface being used
nodeID [6]byte // hardware for version 1 UUIDs
zeroID [6]byte // nodeID with only 0's
)
// NodeInterface returns the name of the interface from which the NodeID was
// derived. The interface "user" is returned if the NodeID was set by
// SetNodeID.
func NodeInterface() string {
defer nodeMu.Unlock()
nodeMu.Lock()
return ifname
}
// SetNodeInterface selects the hardware address to be used for Version 1 UUIDs.
// If name is "" then the first usable interface found will be used or a random
// Node ID will be generated. If a named interface cannot be found then false
// is returned.
//
// SetNodeInterface never fails when name is "".
func SetNodeInterface(name string) bool {
defer nodeMu.Unlock()
nodeMu.Lock()
return setNodeInterface(name)
}
func setNodeInterface(name string) bool {
if interfaces == nil {
var err error
interfaces, err = net.Interfaces()
if err != nil && name != "" {
return false
}
}
for _, ifs := range interfaces {
if len(ifs.HardwareAddr) >= 6 && (name == "" || name == ifs.Name) {
copy(nodeID[:], ifs.HardwareAddr)
ifname = ifs.Name
return true
}
}
// We found no interfaces with a valid hardware address. If name
// does not specify a specific interface generate a random Node ID
// (section 4.1.6)
if name == "" {
randomBits(nodeID[:])
return true
}
return false
}
// NodeID returns a slice of a copy of the current Node ID, setting the Node ID
// if not already set.
func NodeID() []byte {
defer nodeMu.Unlock()
nodeMu.Lock()
if nodeID == zeroID {
setNodeInterface("")
}
nid := nodeID
return nid[:]
}
// SetNodeID sets the Node ID to be used for Version 1 UUIDs. The first 6 bytes
// of id are used. If id is less than 6 bytes then false is returned and the
// Node ID is not set.
func SetNodeID(id []byte) bool {
if len(id) < 6 {
return false
}
defer nodeMu.Unlock()
nodeMu.Lock()
copy(nodeID[:], id)
ifname = "user"
return true
}
// NodeID returns the 6 byte node id encoded in uuid. It returns nil if uuid is
// not valid. The NodeID is only well defined for version 1 and 2 UUIDs.
func (uuid UUID) NodeID() []byte {
if len(uuid) != 16 {
return nil
}
var node [6]byte
copy(node[:], uuid[10:])
return node[:]
}
-58
View File
@@ -1,58 +0,0 @@
// Copyright 2016 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import (
"database/sql/driver"
"fmt"
)
// Scan implements sql.Scanner so UUIDs can be read from databases transparently
// Currently, database types that map to string and []byte are supported. Please
// consult database-specific driver documentation for matching types.
func (uuid *UUID) Scan(src interface{}) error {
switch src.(type) {
case string:
// if an empty UUID comes from a table, we return a null UUID
if src.(string) == "" {
return nil
}
// see Parse for required string format
u, err := Parse(src.(string))
if err != nil {
return fmt.Errorf("Scan: %v", err)
}
*uuid = u
case []byte:
b := src.([]byte)
// if an empty UUID comes from a table, we return a null UUID
if len(b) == 0 {
return nil
}
// assumes a simple slice of bytes if 16 bytes
// otherwise attempts to parse
if len(b) != 16 {
return uuid.Scan(string(b))
}
copy((*uuid)[:], b)
default:
return fmt.Errorf("Scan: unable to scan type %T into UUID", src)
}
return nil
}
// Value implements sql.Valuer so that UUIDs can be written to databases
// transparently. Currently, UUIDs map to strings. Please consult
// database-specific driver documentation for matching types.
func (uuid UUID) Value() (driver.Value, error) {
return uuid.String(), nil
}
-123
View File
@@ -1,123 +0,0 @@
// Copyright 2016 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import (
"encoding/binary"
"sync"
"time"
)
// A Time represents a time as the number of 100's of nanoseconds since 15 Oct
// 1582.
type Time int64
const (
lillian = 2299160 // Julian day of 15 Oct 1582
unix = 2440587 // Julian day of 1 Jan 1970
epoch = unix - lillian // Days between epochs
g1582 = epoch * 86400 // seconds between epochs
g1582ns100 = g1582 * 10000000 // 100s of a nanoseconds between epochs
)
var (
timeMu sync.Mutex
lasttime uint64 // last time we returned
clockSeq uint16 // clock sequence for this run
timeNow = time.Now // for testing
)
// UnixTime converts t the number of seconds and nanoseconds using the Unix
// epoch of 1 Jan 1970.
func (t Time) UnixTime() (sec, nsec int64) {
sec = int64(t - g1582ns100)
nsec = (sec % 10000000) * 100
sec /= 10000000
return sec, nsec
}
// GetTime returns the current Time (100s of nanoseconds since 15 Oct 1582) and
// clock sequence as well as adjusting the clock sequence as needed. An error
// is returned if the current time cannot be determined.
func GetTime() (Time, uint16, error) {
defer timeMu.Unlock()
timeMu.Lock()
return getTime()
}
func getTime() (Time, uint16, error) {
t := timeNow()
// If we don't have a clock sequence already, set one.
if clockSeq == 0 {
setClockSequence(-1)
}
now := uint64(t.UnixNano()/100) + g1582ns100
// If time has gone backwards with this clock sequence then we
// increment the clock sequence
if now <= lasttime {
clockSeq = ((clockSeq + 1) & 0x3fff) | 0x8000
}
lasttime = now
return Time(now), clockSeq, nil
}
// ClockSequence returns the current clock sequence, generating one if not
// already set. The clock sequence is only used for Version 1 UUIDs.
//
// The uuid package does not use global static storage for the clock sequence or
// the last time a UUID was generated. Unless SetClockSequence is used, a new
// random clock sequence is generated the first time a clock sequence is
// requested by ClockSequence, GetTime, or NewUUID. (section 4.2.1.1)
func ClockSequence() int {
defer timeMu.Unlock()
timeMu.Lock()
return clockSequence()
}
func clockSequence() int {
if clockSeq == 0 {
setClockSequence(-1)
}
return int(clockSeq & 0x3fff)
}
// SetClockSeq sets the clock sequence to the lower 14 bits of seq. Setting to
// -1 causes a new sequence to be generated.
func SetClockSequence(seq int) {
defer timeMu.Unlock()
timeMu.Lock()
setClockSequence(seq)
}
func setClockSequence(seq int) {
if seq == -1 {
var b [2]byte
randomBits(b[:]) // clock sequence
seq = int(b[0])<<8 | int(b[1])
}
old_seq := clockSeq
clockSeq = uint16(seq&0x3fff) | 0x8000 // Set our variant
if old_seq != clockSeq {
lasttime = 0
}
}
// Time returns the time in 100s of nanoseconds since 15 Oct 1582 encoded in
// uuid. The time is only defined for version 1 and 2 UUIDs.
func (uuid UUID) Time() Time {
time := int64(binary.BigEndian.Uint32(uuid[0:4]))
time |= int64(binary.BigEndian.Uint16(uuid[4:6])) << 32
time |= int64(binary.BigEndian.Uint16(uuid[6:8])&0xfff) << 48
return Time(time)
}
// ClockSequence returns the clock sequence encoded in uuid.
// The clock sequence is only well defined for version 1 and 2 UUIDs.
func (uuid UUID) ClockSequence() int {
return int(binary.BigEndian.Uint16(uuid[8:10])) & 0x3fff
}
-43
View File
@@ -1,43 +0,0 @@
// Copyright 2016 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import (
"io"
)
// randomBits completely fills slice b with random data.
func randomBits(b []byte) {
if _, err := io.ReadFull(rander, b); err != nil {
panic(err.Error()) // rand should never fail
}
}
// xvalues returns the value of a byte as a hexadecimal digit or 255.
var xvalues = [256]byte{
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 255, 255, 255, 255, 255, 255,
255, 10, 11, 12, 13, 14, 15, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 10, 11, 12, 13, 14, 15, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
}
// xtob converts hex characters x1 and x2 into a byte.
func xtob(x1, x2 byte) (byte, bool) {
b1 := xvalues[x1]
b2 := xvalues[x2]
return (b1 << 4) | b2, b1 != 255 && b2 != 255
}
-191
View File
@@ -1,191 +0,0 @@
// Copyright 2016 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import (
"bytes"
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"io"
"strings"
)
// A UUID is a 128 bit (16 byte) Universal Unique IDentifier as defined in RFC
// 4122.
type UUID [16]byte
// A Version represents a UUID's version.
type Version byte
// A Variant represents a UUID's variant.
type Variant byte
// Constants returned by Variant.
const (
Invalid = Variant(iota) // Invalid UUID
RFC4122 // The variant specified in RFC4122
Reserved // Reserved, NCS backward compatibility.
Microsoft // Reserved, Microsoft Corporation backward compatibility.
Future // Reserved for future definition.
)
var rander = rand.Reader // random function
// Parse decodes s into a UUID or returns an error. Both the UUID form of
// xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx and
// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx are decoded.
func Parse(s string) (UUID, error) {
var uuid UUID
if len(s) != 36 {
if len(s) != 36+9 {
return uuid, fmt.Errorf("invalid UUID length: %d", len(s))
}
if strings.ToLower(s[:9]) != "urn:uuid:" {
return uuid, fmt.Errorf("invalid urn prefix: %q", s[:9])
}
s = s[9:]
}
if s[8] != '-' || s[13] != '-' || s[18] != '-' || s[23] != '-' {
return uuid, errors.New("invalid UUID format")
}
for i, x := range [16]int{
0, 2, 4, 6,
9, 11,
14, 16,
19, 21,
24, 26, 28, 30, 32, 34} {
if v, ok := xtob(s[x], s[x+1]); !ok {
return uuid, errors.New("invalid UUID format")
} else {
uuid[i] = v
}
}
return uuid, nil
}
// ParseBytes is like Parse, except it parses a byte slice instead of a string.
func ParseBytes(b []byte) (UUID, error) {
var uuid UUID
if len(b) != 36 {
if len(b) != 36+9 {
return uuid, fmt.Errorf("invalid UUID length: %d", len(b))
}
if !bytes.Equal(bytes.ToLower(b[:9]), []byte("urn:uuid:")) {
return uuid, fmt.Errorf("invalid urn prefix: %q", b[:9])
}
b = b[9:]
}
if b[8] != '-' || b[13] != '-' || b[18] != '-' || b[23] != '-' {
return uuid, errors.New("invalid UUID format")
}
for i, x := range [16]int{
0, 2, 4, 6,
9, 11,
14, 16,
19, 21,
24, 26, 28, 30, 32, 34} {
if v, ok := xtob(b[x], b[x+1]); !ok {
return uuid, errors.New("invalid UUID format")
} else {
uuid[i] = v
}
}
return uuid, nil
}
// Must returns uuid if err is nil and panics otherwise.
func Must(uuid UUID, err error) UUID {
if err != nil {
panic(err)
}
return uuid
}
// String returns the string form of uuid, xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
// , or "" if uuid is invalid.
func (uuid UUID) String() string {
var buf [36]byte
encodeHex(buf[:], uuid)
return string(buf[:])
}
// URN returns the RFC 2141 URN form of uuid,
// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx, or "" if uuid is invalid.
func (uuid UUID) URN() string {
var buf [36 + 9]byte
copy(buf[:], "urn:uuid:")
encodeHex(buf[9:], uuid)
return string(buf[:])
}
func encodeHex(dst []byte, uuid UUID) {
hex.Encode(dst[:], uuid[:4])
dst[8] = '-'
hex.Encode(dst[9:13], uuid[4:6])
dst[13] = '-'
hex.Encode(dst[14:18], uuid[6:8])
dst[18] = '-'
hex.Encode(dst[19:23], uuid[8:10])
dst[23] = '-'
hex.Encode(dst[24:], uuid[10:])
}
// Variant returns the variant encoded in uuid.
func (uuid UUID) Variant() Variant {
switch {
case (uuid[8] & 0xc0) == 0x80:
return RFC4122
case (uuid[8] & 0xe0) == 0xc0:
return Microsoft
case (uuid[8] & 0xe0) == 0xe0:
return Future
default:
return Reserved
}
}
// Version returns the version of uuid.
func (uuid UUID) Version() Version {
return Version(uuid[6] >> 4)
}
func (v Version) String() string {
if v > 15 {
return fmt.Sprintf("BAD_VERSION_%d", v)
}
return fmt.Sprintf("VERSION_%d", v)
}
func (v Variant) String() string {
switch v {
case RFC4122:
return "RFC4122"
case Reserved:
return "Reserved"
case Microsoft:
return "Microsoft"
case Future:
return "Future"
case Invalid:
return "Invalid"
}
return fmt.Sprintf("BadVariant%d", int(v))
}
// SetRand sets the random number generator to r, which implents io.Reader.
// If r.Read returns an error when the package requests random data then
// a panic will be issued.
//
// Calling SetRand with nil sets the random number generator to the default
// generator.
func SetRand(r io.Reader) {
if r == nil {
rander = rand.Reader
return
}
rander = r
}
-44
View File
@@ -1,44 +0,0 @@
// Copyright 2016 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import (
"encoding/binary"
)
// NewUUID returns a Version 1 UUID based on the current NodeID and clock
// sequence, and the current time. If the NodeID has not been set by SetNodeID
// or SetNodeInterface then it will be set automatically. If the NodeID cannot
// be set NewUUID returns nil. If clock sequence has not been set by
// SetClockSequence then it will be set automatically. If GetTime fails to
// return the current NewUUID returns Nil and an error.
//
// In most cases, New should be used.
func NewUUID() (UUID, error) {
nodeMu.Lock()
if nodeID == zeroID {
setNodeInterface("")
}
nodeMu.Unlock()
var uuid UUID
now, seq, err := GetTime()
if err != nil {
return uuid, err
}
timeLow := uint32(now & 0xffffffff)
timeMid := uint16((now >> 32) & 0xffff)
timeHi := uint16((now >> 48) & 0x0fff)
timeHi |= 0x1000 // Version 1
binary.BigEndian.PutUint32(uuid[0:], timeLow)
binary.BigEndian.PutUint16(uuid[4:], timeMid)
binary.BigEndian.PutUint16(uuid[6:], timeHi)
binary.BigEndian.PutUint16(uuid[8:], seq)
copy(uuid[10:], nodeID[:])
return uuid, nil
}
-38
View File
@@ -1,38 +0,0 @@
// Copyright 2016 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import "io"
// New is creates a new random UUID or panics. New is equivalent to
// the expression
//
// uuid.Must(uuid.NewRandom())
func New() UUID {
return Must(NewRandom())
}
// NewRandom returns a Random (Version 4) UUID or panics.
//
// The strength of the UUIDs is based on the strength of the crypto/rand
// package.
//
// A note about uniqueness derived from from the UUID Wikipedia entry:
//
// Randomly generated UUIDs have 122 random bits. One's annual risk of being
// hit by a meteorite is estimated to be one chance in 17 billion, that
// means the probability is about 0.00000000006 (6 × 10−11),
// equivalent to the odds of creating a few tens of trillions of UUIDs in a
// year and having one duplicate.
func NewRandom() (UUID, error) {
var uuid UUID
_, err := io.ReadFull(rander, uuid[:])
if err != nil {
return Nil, err
}
uuid[6] = (uuid[6] & 0x0f) | 0x40 // Version 4
uuid[8] = (uuid[8] & 0x3f) | 0x80 // Variant is 10
return uuid, nil
}
-24
View File
@@ -1,24 +0,0 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.prof
-1
View File
@@ -1 +0,0 @@
language: go
-23
View File
@@ -1,23 +0,0 @@
# Contributing #
Thanks for your contribution in advance. No matter what you will contribute to this project, pull request or bug report or feature discussion, it's always highly appreciated.
## New API or feature ##
I want to speak more about how to add new functions to this package.
Package `xstring` is a collection of useful string functions which should be implemented in Go. It's a bit subject to say which function should be included and which should not. I set up following rules in order to make it clear and as objective as possible.
* Rule 1: Only string algorithm, which takes string as input, can be included.
* Rule 2: If a function has been implemented in package `string`, it must not be included.
* Rule 3: If a function is not language neutral, it must not be included.
* Rule 4: If a function is a part of standard library in other languages, it can be included.
* Rule 5: If a function is quite useful in some famous framework or library, it can be included.
New function must be discussed in project issues before submitting any code. If a pull request with new functions is sent without any ref issue, it will be rejected.
## Pull request ##
Pull request is always welcome. Just make sure you have run `go fmt` and all test cases passed before submit.
If the pull request is to add a new API or feature, don't forget to update README.md and add new API in function list.
-22
View File
@@ -1,22 +0,0 @@
The MIT License (MIT)
Copyright (c) 2015 Huan Du
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Some files were not shown because too many files have changed in this diff Show More