Compare commits

...
125 Commits
Author SHA1 Message Date
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
KUOKA Yusuke 8f1a15c9cd feat: helmfile destroy deletes and purges releases (#530)
* feat: `helmfile destroy` deletes and purges releases

This adds `helmfile destroy` that is basically `helmfile delete --purge`.

I've also tweaked the behavior of `delete` and `destroy` for releases with `installed: false`, so that it becomes consistent with other helmfile commands.
It now delete releases only when `installed: true` AND the release is already installed.

**Why an another command?**

Because it's easy to remember, and it also makes it easier to iterate on your helmfile.

We've been using `helmfile delete` from the beginning of helmfile,
and several months have been passed since we've added `--purge` to it.

We noticed that we always prefer to use `--purge` so that we can quickly iterate on helmfile by
e.g. `helmfile delete --purge && helmfile sync`. But making `--purge` default makes the `delete` command inconsistent with the helm's `delete`.

`destroy`, on the other hand, doesn't have such problem, and is still easy to remember for terraform users.

Resolves #511

* Update docs about `helmfile delete` and `helmfile destroy`
2019-04-02 21:17:38 +09:00
Patrick Valsecchi fa95e0dd92 Allow the same release name on different tillerNamespaces (#529)
Closes #528
2019-04-02 18:51:11 +09:00
KUOKA Yusuke eb36e93c5a fix: helmfile should not complain on missing values files for undesired releases (#526)
Fixes #519
2019-04-01 22:29:16 +09:00
KUOKA Yusuke 3edc7d6f33 fix: helmfile diff should not leave temp values files (#525)
Fixes #503 once again. A follow-up to #520 which was incomplete.
2019-04-01 22:24:18 +09:00
sgandon 57df25c3ff doc(team): changed the sub-helmfile as template (#524)
try to make the doc more clear.
2019-04-01 07:41:11 +09:00
KUOKA Yusuke 14a392666e feat: easier way to share configuration across helmfiles (#522)
This basically allows to define both `releases` and `helmfiles` within a helmfile.yaml, so that you can start using sub-helmfiles easily, by extracting only reused releases.

Resolves #445
2019-03-31 22:51:06 +09:00
KUOKA Yusuke 283dac1531 fix: Various helmfile commands should not leave temp values files (#520)
Fixes #504
2019-03-31 14:49:51 +09:00
KUOKA Yusuke f5e565ea3e feat: Tiller configuration per release (#516)
Resolves #486
2019-03-29 23:45:31 +09:00
KUOKA Yusuke 5f8b2e5c7f feat: Enhanced get template function (#513)
`get` is now able to take one more optional argument, that is used as the default value when the value for the key does not exist.

Resolves #465
Fixes #427
Fixes #357
Ref #460
2019-03-29 12:38:16 +09:00
KUOKA Yusuke 870d33f418 fix: helmfile delete should not stop on uninstalled release(s) (#509)
Resolves #481
2019-03-29 10:11:19 +09:00
KUOKA Yusuke d3c5417177 fix: prevent confusing error messages when installed: false (#508)
This removes `release: "your_release_name" not found` errors seen for releases with `installed: false` when running `helmfile sync` and `helmfile apply`.

The problem was that helmfile had been running `helm status` to detect releases to be deleted. helmfile now use `helm list ^YOUR_RELEASE_NAME$` to detect if the release is currently installed or not, which emits no error-like logs on against uninstalled releases.

Fixes #507

Fixes #507
2019-03-28 18:14:29 +09:00
KUOKA Yusuke 0639714136 feat: tpl template function (#504)
Add `tpl` template function that looks exactly like helm's one.

Resolves #420
2019-03-25 18:08:47 +09:00
sgandon 056d150856 doc(readme): changed from chart to helmfile (#501) 2019-03-22 21:35:42 +09:00
Yusuke KUOKA 9bcec38b3b fix sha256 checksum of the helm release archive, for successful helmfile release 2019-03-20 17:57:46 +09:00
Yusuke KUOKA 3e40e90a2a releasing: automatically checkout the master branch back 2019-03-20 17:51:47 +09:00
KUOKA Yusuke acbb416028 fix: absolute paths to helmfiles should work (#498)
Fixes #488
2019-03-20 17:45:48 +09:00
Mike Eves c4eb95f3ec Fix: Release options should override helm defaults (#497)
Options specified in releases (e.g. `recreatePods`) should override the respective options set in `helmDefaults`. Currently, `helmDefaults` takes precedence.

In the below example, `--force` should not be passed as an additional deployment argument:
```
helmDefaults:
  force: true

releases:
  - name: example
    namespace: example
    chart: some/repo
    version: ~1.24.1
    force: false
```

Fixes #492
2019-03-20 11:22:26 +09:00
Mike Eves f2996e2452 Support --atomic as a first class directive in helmfile (#491)
Resolves #487
2019-03-19 19:17:26 +09:00
KUOKA Yusuke 79ae2df71f fix: Always getting deprecated warning (#493)
Fixes #485
2019-03-19 19:15:37 +09:00
Dan O'Brien 8f7ab740bd minor typo (#496) 2019-03-19 19:14:52 +09:00
KUOKA Yusuke 841580f8b3 fix: helmfile sync should NOT continue when secret decryption fails (#490)
Fixes #336
2019-03-15 10:36:24 +09:00
kinzal c6c54fdbed fixes read absolute path woth readFile (#475) 2019-02-15 12:49:35 +09:00
KUOKA Yusuke 35fd5ff117 fix: remote file provided to set.file should not break helmfile (#474)
Fixes #473
2019-02-14 18:13:32 +09:00
KUOKA Yusuke f2bdcb9113 fix: sync should not hang on installed: false (#464)
Fixes #423
2019-02-04 17:16:58 +09:00
Yusuke KUOKA 9cfd6aeae7 Ensure helm-diff is installed for integration testing 2019-02-04 17:13:17 +09:00
Yusuke KUOKA 27da8a6b7a fix integration test 2019-02-04 15:15:56 +09:00
KUOKA Yusuke c6236a15bb feat: Opt-out dep-builds and repo-updates (#463)
* Improve code organization

To make sure it is still readable after upcoming changes to helmfile

* feat: `helmfile deps` to update dependencies of all the local charts

Resolves #450

* feat: helmfile updates repos and build deps by default

But not update deps. Use `helmfile deps` to update deps, and provide `--skip-deps` to skip updating repos and builds deps in sync/diff/apply/template

Resolves #415

* Improve integration test coverage
2019-02-04 12:07:25 +09:00
KUOKA Yusuke b894012c80 feat: Automatically update repos on diffing by default (#462)
Resolves #452
2019-02-04 11:12:34 +09:00
Shane Starcher fb256b0161 support labels for namespace and chart (#459) 2019-01-31 11:25:29 +09:00
Simon Li abeccc5e8e Fix indentation in writing-helmfile.md example (#454) 2019-01-25 23:32:33 +09:00
KUOKA Yusuke f84ed93754 fix: helmfle should not break when run without -f (#453)
Fixes #451
2019-01-25 12:13:56 +09:00
KUOKA Yusuke bf42d2519d fix: Relative path in helmfile not relative to file in multi file setup (#448)
Fixes #431
2019-01-24 10:48:01 +09:00
Maxim Mironenko 99ce8570c7 helmfiles examples link URL fix (#447)
* helmfiles examples link URL fix

* Update README.md

Co-Authored-By: maximmi <mmironenko@chanzuckerberg.com>
2019-01-24 10:46:59 +09:00
KUOKA Yusuke 571f351a8f feat: configurable concurrency for helmfile test (#442)
`helmfile test --concurency N` to set a concurrency number.
It is automatically ceiled at the number of releases just to reduce wasting computing resources.

Also, I've refactored the scatter-gather logic scattered across the code-base.

Resolves #433
2019-01-23 15:46:24 +09:00
KUOKA Yusuke b9a097ed14 fix: first-pass rendering of helmfile.yaml (#441)
There was a regression introduced by #439 that broke the first-pass rendering completely. This fixes that.
2019-01-22 15:55:03 +09:00
Yusuke KUOKA 344c20d790 release: fix make tasks for auto-tagging 2019-01-22 11:25:55 +09:00
KUOKA Yusuke 5f52f96709 fix missingFileHandler (#440)
- Fix panics when `missingFileHandler` is not defined
- Fix `missingFileHandler: Error` had been writing errors at the Warn level
- Add `Info` and `Debug` as valid values

Ref https://github.com/roboll/helmfile/pull/439#issuecomment-456155521
2019-01-22 11:22:53 +09:00
Yusuke KUOKA 171eacfb3c release: fix hack/semtag when there are one or more git remotes before origin 2019-01-22 01:29:28 +09:00
Yusuke KUOKA 7d976e5271 Semi-automate minor/patch version tagging 2019-01-22 01:23:22 +09:00
KUOKA Yusuke f813ac2642 feat: Release Template (#439)
This feature is supposed to help advanced use-cases like Conventional Directory Structure explained in several issues like #428.

Newly added configuration keys `templates`, `missingFileHandler`, and the ability to defer executing template expressions in `values`, `secrets`, `namespace`, and `chart` of releases allows you to abstract away repetitions into a reusable template:

```yaml
templates:
  default: &default
    missingFileHandler: Warn
    namespace: "{{`{{ .Release.Name }}`}}"
    chart: stable/{{`{{ .Release.Name }}`}}
    values:
    - config/{{`{{ .Release.Name }}`}}/values.yaml
    - config/{{`{{ .Release.Name }}`}}/{{`{{ .Environment.Name }}`}}.yaml
    secrets:
    - config/{{`{{ .Release.Name }}`}}/secrets.yaml
    - config/{{`{{ .Release.Name }}`}}/{{`{{ .Environment.Name }}`}}-secrets.yaml

releases:
- name: envoy
  <<: *default
```

See the updated documentation for more details.

Resolves #428
2019-01-22 01:19:07 +09:00
KUOKA Yusuke 23178b398c docs: Remove go get from the recommended installation methods (#437)
Add a contribution guide instead and note about the `go get` way there according to the discussion made in the relevant issue.

Resolves #393
2019-01-17 12:03:58 +09:00
Vincent Behar fea62032e9 fix: cleanup hook executed too early for apply command (#426)
fixes https://github.com/roboll/helmfile/issues/412

the `apply` command first runs the `diff` command, which triggers the execution of the `cleanup` hooks at the end of the `diff`.
2019-01-01 01:33:16 +09:00
Vincent Behar 1f7a8ddbe3 fix: prepare hook is not executed for diff command (#425)
fixes https://github.com/roboll/helmfile/issues/424

note that the "cleanup" hook is already executed as it should
2019-01-01 01:30:25 +09:00
Raphael Sampaio 413ab4f55b adds Homebrew as an installation method (#418) 2018-12-21 12:06:33 +09:00
Hung Tran Duc 39bc28c23f feat: configurable common path prefix for values files (#408)
Removes duplicated declaration path for cleaner helmfile, for example, this configuration.

```
- chart: projectABC
  name: {{ .Environment.Name }}-projectABC
  namespace: abc
  version: {{ .Environment.Values.projectABC.chart.version }}
  valuesPathPrefix: ../values/{{ .Environment.Name }}/
  values:
  - common.yaml
  - project-abc.yaml
  - platform.yaml
  - ...
  secrets:
  - secrets.yaml
```
2018-12-13 18:07:50 +09:00
Matteo Gazzetta 4b22d4fc78 Update Dockerfile and CircleCI (#417) 2018-12-11 07:01:23 +09:00
Eric Chin 2a35b6864c Remove extra appending of chartName for lint and template (#406)
Closes #407
2018-12-11 06:54:57 +09:00
Anatoly Rugalev 4b23213ce4 Added new installation method via AUR (#405) 2018-11-27 14:13:45 -08:00
Anatoly Rugalev 971cbcf1db Added path normalization in - set directive. (#404)
Fixes #399
2018-11-27 14:13:04 -08:00
Anatoly Rugalev d898dfbd0d Updated documentation related to deprecation of context directive (#403) 2018-11-27 14:12:40 -08:00
Anatoly Rugalev b41b44c313 Fixes #383. Deprecated context directive in favor of helmDefaults.kubeContext (#401) 2018-11-22 12:39:03 +09:00
Sam Weston b3e27db8b3 Improve documentation of Environment Secrets (#395) 2018-11-09 16:38:23 +09:00
harmjanblok 9cab7b00f6 Bump helm to v2.11.0 (#394) 2018-11-09 16:36:25 +09:00
Sam Weston bfc86de92d Correct minor spelling and grammar issues in README.md (#385) 2018-10-23 15:21:00 +09:00
Martin Mauch 5ad916003f Add instructions for installing via scoop on Windows (#372) 2018-10-16 16:22:08 +09:00
KUOKA Yusuke b0f9062e08 fix: incomplete environment values merging (#378)
Fixes #377

Ref https://github.com/imdario/mergo/pull/81
2018-10-03 22:39:37 +09:00
KUOKA Yusuke 770c3daa5f feat: get and getOrNil template funcs to allow defaulting in templates (#370)
* feat: `get` and `getOrNil` template funcs to allow defaulting in templates

Ref #357

* Add docs about missing keys and default values in templates
2018-09-28 11:44:49 +09:00
KUOKA Yusuke 6f0dc6e069 bump go to 1.11 (#376)
In go 1.11, the code formatter seems to have changed its formatting rule a bit, that resulted in `make fmt` producing changes introduced in this commit/pr.
2018-09-28 10:48:07 +09:00
KUOKA Yusuke f23a43c80c fix(ci,doc): follow up for default non-interactivity of helmfile (#375)
The integration test was failing due to that it was still using `--auto-approve` flag which was removed. The documentation was still refering to `--auto-approve` and had no explanation about the new `--interactive` flag. This fixes all these issues.

This a follow up for #368
See https://github.com/roboll/helmfile/pull/374#issuecomment-425291468 for more context.
2018-09-28 10:40:57 +09:00
David Genest 942f9a20b0 fix first pass rendering crash on syntax error (#374) 2018-09-28 10:27:11 +09:00
KUOKA Yusuke 429c42d429 doc: mark release as installed/uninstalled (#371)
Resolves #362
2018-09-27 11:17:00 +09:00
KUOKA Yusuke 7d1787d0b6 One Ctrl-C should exit helmfile gracefully (#369)
Perhaps in latest commits I unexpectedly changed it to require double Ctrl-C to actually interrupt. This fixes it so that one Ctrl-C exists helmfile.

On the way to the fix, I changed the exit-code to 128 + SIGNAL(2 for SIGINT, 15 for SIGTERM) according to common *nix commands.
2018-09-27 04:36:34 +09:00
KUOKA Yusuke 98be623701 helmfile should be non-interactive by default (#368)
Request for user confirmation only when a global `--interactive` flag is provided.

Resolves #354
2018-09-27 04:36:14 +09:00
KUOKA Yusuke 76122738c8 fix: cleanup hook commands output should be printed when debug logging enabled (#367)
There was a race condition due to that helmfile had not been waiting for goroutines to exit

Ref #363
2018-09-27 03:21:58 +09:00
KUOKA Yusuke 973d47bfc0 fix: sync repos always failing (#366) 2018-09-27 02:32:56 +09:00
KUOKA Yusuke 2eb13c25c5 fix: helmfile should pass release names to helm template (#365)
Fixes #360
2018-09-27 02:22:18 +09:00
KUOKA Yusuke b94265122f feat: least frequent repository update (#356)
Prevents helmfile from consuming unnecessarily much time in running `helm repo update` over and over.

helmfile now marks which repository was updated, and skip second and further `helm repo update` when all the `repositories` found in a helmfile.yaml was marked as already updated.

Let's say you had two helmfiles, the first one with repositories `foo` and `bar`, an the second one with only `bar`. `helmfile repos` will run `helm update repo` for the first helmfile, marking `foo` and `bar` as already updated. The second helmfile.yaml contains only `bar`, which is marked as already updated. So helmfile won't run `helm repo update` for the second.

This applies to all the helmfile command that results in `helm repo update`, like `repos`, `sync`, `diff` and so on.

Resolves #335
2018-09-27 02:10:11 +09:00
KUOKA Yusuke f2b610afdf feat: helmfile repos updates all repos regardless of selector and releases (#353)
Resolves #338
2018-09-27 01:44:05 +09:00
KUOKA Yusuke 6cba77d4f2 fix(hook): event hooks should run on remote charts, too (#364)
And fixed the bug that "diff" was not triggering "prepare" hooks.

Ref #363
2018-09-27 01:01:25 +09:00
KUOKA Yusuke b9de22b256 feat: prepare and cleanup release event hooks (#349)
Resolves #295
Resolves #330
Resolves #329 (Supports templating of only `releases[].hooks[].command` and `args` right now
Resolves #324
2018-09-21 10:35:12 +09:00
KUOKA Yusuke 4da13b44ee fix: --helm-binary does not work with sync when updating repo (#352)
Fixes #350
2018-09-20 09:31:16 +09:00
KUOKA Yusuke 5d1a92d3a5 fix: Forbid extraneous arguments (#348)
Resolves #296
2018-09-20 09:12:12 +09:00
KUOKA Yusuke 9808849986 feat: Merge multiple docs inside a single helmfile.yaml (#270)
Resolves #254
2018-09-19 15:42:01 +09:00
Dan Helfman 1ade353c1a Use --namespace value within Helmfile template (#343)
* Use --namespace value within Helmfile template (#326).

* Tabs and spaces.

* Tab/spaces fix.

* Pacifying "go fmt".

* Hard-coding namespace in test convenience function.

* MR feedback: Add comment to exported identifier.

* MR feedback: Put comment in correct location.

* fix(ci): fix never-ending build issue by auto-approving `helmfile delete`
2018-09-17 21:28:45 +09:00
Terry Corley 6453768f2e Fix small typo in error string in state.go (#345) 2018-09-17 20:27:39 +09:00
KUOKA Yusuke cf31d02f25 feat: Option to mark release as not installed (#342)
Resolves #197
2018-09-16 22:44:47 +09:00
KUOKA Yusuke c5bc932ba2 feat: Prompt before delete (#341)
Resolves #182
2018-09-16 22:43:29 +09:00
KUOKA Yusuke 81d796c167 fix: readFile and exec should run in the directory the template file resides in (#340)
Fixes #299
2018-09-16 22:43:17 +09:00
KUOKA Yusuke 37ea442515 feat: delete in reverse order (#339)
Resolves #312
2018-09-16 22:42:13 +09:00
David Genest 43a54486a1 docs: add precision on what value files will be rendered (#337) 2018-09-14 21:44:38 +09:00
David Genest b3da9a9a07 fix: evaluate helm-binary BEFORE any helm invocation (#333)
closes #332
2018-09-13 09:59:35 +09:00
David Genest 30155d8742 fix: use channel to harvest download results (#328)
Fixes #327
2018-09-12 09:12:23 +09:00
David Genest 7bfb58c0e4 feat: double render the helmfile (#308)
This allows using the environment values defined in the environments: section of helmfile.yaml to be used from other sections of the file.

This works by having two template renderers, the first-pass and the second-pass renderer.

The first-pass render renders a helmfile.yaml template with replacing template functions has side-effects with noop. So, use only funcs that don't have side-effects to compose your environment values.

Then the second-pass renderer renders the same helmfile.yaml template, but with the environment values loaded by the first-pass renderer.

The implementation uses a buffer instead of re-reading the file twice.

Resolves #297
2018-09-12 08:55:42 +09:00
KUOKA Yusuke 751e549253 fix: prevent panicing on invalid selector format (#323)
Fixes #322
2018-09-09 17:20:44 +09:00
KUOKA Yusuke 4f073167b2 fix: prevent panicing when an undefined environment name was specified via --environment (#321)
Fixes #320
2018-09-09 08:35:15 +09:00
KUOKA Yusuke 7c65b2ed2c fix: unexpected no releases found for any helmfiles on helmfiles: config (#318)
Fixes #315
Fixes #316
2018-09-08 13:50:51 +09:00
Shane Starcher 595c70f85b feat: add devel flag support (#313)
Resolves #161

Changelog:

* add support for devel flag

* allow for explicit disable of the devel default
2018-09-08 08:32:14 +09:00
KUOKA Yusuke 8f96f0aff5 fix: do not panic on invalid yaml (#314)
Fixes #311
2018-09-08 02:15:39 +09:00
Takuma Hashimoto c3934495fe fix: {{ .Environment.Name }} being always blank in helmfile.yaml
Correctly sets the environment name passed via the flag `--environment NAME`, so that it can be referenced by `{{ .Environment.Name }}`
2018-09-06 18:12:32 +09:00
KUOKA Yusuke a205f0d586 fix: concurrency issue in diff and sync (#309)
fixes #301
2018-09-06 10:14:10 +09:00
David Genest 2e44da1b54 ensure helmfiles are searched relative to base helmfile (#307) 2018-09-06 10:08:45 +09:00
Karl Stoney d0a4008965 Add missing repos on lint command. (#305)
https://github.com/roboll/helmfile/issues/303
2018-09-06 10:07:43 +09:00
Karl Stoney af121b85b5 feat: Concurrent chart download in template and lint commands
This enables `helmfile lint` and `helmfile template` commands to fetch and untar all the required charts concurrently. The concurrency is configurable via the `--concurrency` flag, that defaults to `0`.

Ref #292
2018-09-05 08:19:57 +09:00
KUOKA Yusuke 1c3bfcca10 feat(environment): Allow missing environment in helmfile.yaml, partly. (#294)
helmfile as of today ensures that all the targeted helmfile.yaml to have the specified environment defined in it.
That is, `helmfile --environment prod -f helmfile.d/ sync` fails if any helmfile under `helmfile.d/` is missing the `production` environment.

This changes the validation logic, so that helmfile fails only when all the helmfiles miss the environment.

Resolves #279
2018-09-04 22:04:37 +09:00
Hans Kristian Flaatten 7d7ca74a05 docs: update link to documentation for latest released version (#293) 2018-09-04 21:49:21 +09:00
KUOKA Yusuke f1ac50bf44 feat: apply run sync against only affected releases (#291)
Enhance the `diff` functionality to be able to return affected releases that has any changes,
so that the succeeding `sync` can be run against only the affected releases.

This provides us extra idempotency.

Resolves #277
2018-09-04 21:48:43 +09:00
KUOKA Yusuke 60843cc224 fix: Setting concurrency>1 with single release breaks sync (#290)
Fixes #287
2018-09-04 20:59:24 +09:00
Karl Stoney 53dea091e3 Fix set in helmfile template and helmfile lint (#289)
This removes --concurrenty flag from `helmfile template` and `helmfile lint`. YAGNI.
Also separates out the downloadCharts into a function so it can be used by lint too.

Fixes #288
2018-09-04 20:55:32 +09:00
KUOKA Yusuke 5510de4765 feat(apply): update repos and and deps before running diff (#286)
Resolves #271
2018-09-04 16:12:43 +09:00
Karl Stoney 93c5d4c219 feat: helmfile template (#284)
`helmfile template` runs `helm template` over releases within the helmfiles, and provide you a stream of generated yaml documents of Kubernetes resources via stdout.

Resolves #283
2018-09-04 11:31:43 +09:00
KUOKA Yusuke 8a90e5320c feat: Ability to call arbitrary command from a template (#282)
Resolves #244
2018-09-03 16:48:03 +09:00
245 changed files with 6789 additions and 31773 deletions
+37 -13
View File
@@ -4,31 +4,51 @@ jobs:
build:
docker:
- image: circleci/golang:1.10.1
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.10.1
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
# thanks to https://raw.githubusercontent.com/weaveworks/launcher/master/.circleci/config.yml
integration_tests:
machine: true
machine:
image: circleci/classic:201808-01
environment:
CHANGE_MINIKUBE_NONE_USER: true
MINIKUBE_WANTUPDATENOTIFICATION: false
MINIKUBE_WANTREPORTERRORPROMPT: false
steps:
- checkout
- run: mkdir ~/build
@@ -38,7 +58,7 @@ jobs:
- run:
name: Install helm
environment:
HELM_VERSION: v2.9.0
HELM_VERSION: v2.13.0
command: |
HELM_FILENAME="helm-${HELM_VERSION}-linux-amd64.tar.gz"
curl -Lo ${HELM_FILENAME} "https://kubernetes-helm.storage.googleapis.com/${HELM_FILENAME}"
@@ -49,14 +69,13 @@ jobs:
name: Deploy minikube
environment:
CHANGE_MINIKUBE_NONE_USER: true
K8S_VERSION: v1.9.0
MINIKUBE_VERSION: v0.25.2
K8S_VERSION: v1.12.3
MINIKUBE_VERSION: v0.30.0
command: |
curl -Lo kubectl https://storage.googleapis.com/kubernetes-release/release/${K8S_VERSION}/bin/linux/amd64/kubectl
chmod +x kubectl && sudo mv kubectl /usr/local/bin/
curl -Lo minikube https://storage.googleapis.com/minikube/releases/${MINIKUBE_VERSION}/minikube-linux-amd64
chmod +x minikube && sudo mv minikube /usr/local/bin/
sudo minikube config set WantReportErrorPrompt false
sudo -E minikube start --vm-driver=none --kubernetes-version=${K8S_VERSION}
sudo -E minikube update-context
- run:
@@ -72,16 +91,21 @@ jobs:
release:
docker:
- image: circleci/golang:1.10.1
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
+39
View File
@@ -0,0 +1,39 @@
# DCO + License
By contributing to `helmfile`, you accept and agree to the following DCO and license terms and
conditions for your present and future Contributions submitted to the `helmfile` project.
[DCO](https://developercertificate.org/)
[License](https://github.com/roboll/helmfile/blob/master/LICENSE)
# Developing helmfile
Locate your `GOPATH`, usually `~/go`, and run:
```console
$ go get github.com/roboll/helmfile
$ cd $GOPATH/src/github.com/roboll/helmfile
$ git checkout -b your-shiny-new-feature origin/master
...
$ git commit -m 'feat: do whatever for whatever purpose
This adds ... by:
- Adding ...
- Changing ...
- Removing...
Resolves #ISSUE_NUMBER
'
$ hub fork
$ git push YOUR_GITHUB_USER your-shiny-new-feature
$ hub pull-request
```
Note that the above tutorial uses [hub](https://github.com/github/hub) just for ease of explanation.
Please use whatever tool or way to author your pull request!
+7 -7
View File
@@ -1,20 +1,20 @@
FROM golang:1.10.1-alpine3.7 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
# -----------------------------------------------------------------------------
FROM alpine:3.7
FROM alpine:3.8
RUN apk add --no-cache ca-certificates git bash curl
ARG HELM_VERSION=v2.10.0
ARG HELM_VERSION=v2.13.0
ARG HELM_LOCATION="https://kubernetes-helm.storage.googleapis.com"
ARG HELM_FILENAME="helm-${HELM_VERSION}-linux-amd64.tar.gz"
ARG HELM_SHA256="0fa2ed4983b1e4a3f90f776d08b88b0c73fd83f305b5b634175cb15e61342ffe"
ARG HELM_SHA256="15eca6ad225a8279de80c7ced42305e24bc5ac60bb7d96f2d2fa4af86e02c794"
RUN wget ${HELM_LOCATION}/${HELM_FILENAME} && \
sha256sum ${HELM_FILENAME} | grep -q "${HELM_SHA256}" && \
tar zxf ${HELM_FILENAME} && mv /linux-amd64/helm /usr/local/bin/ && \
@@ -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
-90
View File
@@ -1,90 +0,0 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
name = "github.com/Masterminds/semver"
packages = ["."]
revision = "8d82589cda2d7b5b9167396841975c4535c9bec6"
version = "v1.4.1"
[[projects]]
name = "github.com/Masterminds/sprig"
packages = ["."]
revision = "6b2a58267f6a8b1dc8e2eb5519b984008fa85e8c"
version = "v2.15.0"
[[projects]]
name = "github.com/aokoli/goutils"
packages = ["."]
revision = "3391d3790d23d03408670993e957e8f408993c34"
version = "v1.0.1"
[[projects]]
name = "github.com/google/uuid"
packages = ["."]
revision = "064e2069ce9c359c118179501254f67d7d37ba24"
version = "0.2"
[[projects]]
name = "github.com/huandu/xstrings"
packages = ["."]
revision = "2bf18b218c51864a87384c06996e40ff9dcff8e1"
version = "v1.0.0"
[[projects]]
name = "github.com/imdario/mergo"
packages = ["."]
revision = "9d5f1277e9a8ed20c3684bda8fde67c05628518c"
version = "v0.3.4"
[[projects]]
name = "github.com/urfave/cli"
packages = ["."]
revision = "6011f165dc288c72abd8acd7722f837c5c64198d"
[[projects]]
name = "go.uber.org/atomic"
packages = ["."]
revision = "1ea20fb1cbb1cc08cbd0d913a96dead89aa18289"
version = "v1.3.2"
[[projects]]
name = "go.uber.org/multierr"
packages = ["."]
revision = "3c4937480c32f4c13a875a1829af76c98ca3d40a"
version = "v1.1.0"
[[projects]]
name = "go.uber.org/zap"
packages = [
".",
"buffer",
"internal/bufferpool",
"internal/color",
"internal/exit",
"zapcore"
]
revision = "eeedf312bc6c57391d84767a4cd413f02a917974"
version = "v1.8.0"
[[projects]]
branch = "master"
name = "golang.org/x/crypto"
packages = [
"pbkdf2",
"scrypt"
]
revision = "b2aa35443fbc700ab74c586ae79b81c171851023"
[[projects]]
name = "gopkg.in/yaml.v2"
packages = ["."]
revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183"
version = "v2.2.1"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "b1f000751afc0a44973307c69b6a4b8e8c1b807fd9881a13f370c30fcbcab7a2"
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.4"
+22 -1
View File
@@ -33,12 +33,17 @@ static-linux:
env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o "dist/helmfile_linux_amd64" -ldflags '-X main.Version=${TAG}' ${TARGETS}
.PHONY: linux
install:
env CGO_ENABLED=0 go install -ldflags '-X main.Version=${TAG}' ${TARGETS}
.PHONY: install
clean:
rm dist/helmfile_*
.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
@@ -61,3 +66,19 @@ tools:
TAG = $(shell git describe --tags --abbrev=0 HEAD)
LAST = $(shell git describe --tags --abbrev=0 HEAD^)
BODY = "`git log ${LAST}..HEAD --oneline --decorate` `printf '\n\#\#\# [Build Info](${BUILD_URL})'`"
release/minor:
git checkout master
git pull --rebase origin master
bash -c 'if git branch | grep autorelease; then git branch -D autorelease; else echo no branch to be cleaned; fi'
git checkout -b autorelease origin/master
bash -c 'SEMTAG_REMOTE=origin hack/semtag final -s minor'
git checkout master
release/patch:
git checkout master
git pull --rebase origin master
bash -c 'if git branch | grep autorelease; then git branch -D autorelease; else echo no branch to be cleaned; fi'
git checkout -b autorelease origin/master
bash -c 'SEMTAG_REMOTE=origin hack/semtag final -s patch'
git checkout master
+335 -43
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
@@ -24,7 +25,7 @@ To avoid upgrades for each iteration of `helm`, the `helmfile` executable delega
## configuration syntax
**CAUTION**: This documentation is for the development version of helmfile. If you are looking for the documentation for any of releases, please switch to the corresponding branch like [v0.12.0](https://github.com/roboll/helmfile/tree/v0.12.0).
**CAUTION**: This documentation is for the development version of Helmfile. If you are looking for the documentation for any of releases, please switch to the corresponding release tag like [v0.31.0](https://github.com/roboll/helmfile/tree/v0.31.0).
The default helmfile is `helmfile.yaml`:
@@ -37,12 +38,13 @@ repositories:
username: optional_username
password: optional_password
context: kube-context # kube-context (--kube-context)
# context: kube-context # this directive is deprecated, please consider using helmDefaults.kubeContext
#default values to set for args along with dedicated keys that can be set by contributers, cli args take precedence overe these
helmDefaults:
#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
kubeContext: kube-context #dedicated default key for kube-context
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:
- "--set k=v"
@@ -52,6 +54,14 @@ helmDefaults:
timeout: 600
recreatePods: true
force: true
# enable TLS for request to Tiller
tls: true
# path to TLS CA certificate file (default "$HELM_HOME/ca.pem")
tlsCACert: "path/to/ca.pem"
# path to TLS certificate file (default "$HELM_HOME/cert.pem")
tlsCert: "path/to/cert.pem"
# path to TLS key file (default "$HELM_HOME/key.pem")
tlsKey: "path/to/key.pem"
releases:
# Published chart example
@@ -61,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
@@ -83,6 +94,9 @@ releases:
values:
- 1
- 2
# set a templated value
- name: namespace
value: {{ .Namespace }}
# will attempt to decrypt it using helm-secrets plugin
secrets:
- vault_secret.yaml
@@ -94,11 +108,27 @@ releases:
recreatePods: true
# forces resource update through delete/recreate if needed
force: true
# set `false` to uninstall on sync
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")
tlsCACert: "path/to/ca.pem"
# path to TLS certificate file (default "$HELM_HOME/cert.pem")
tlsCert: "path/to/cert.pem"
# path to TLS key file (default "$HELM_HOME/key.pem")
tlsKey: "path/to/key.pem"
# Local chart example
- name: grafana # name of this release
namespace: another # target namespace
chart: ../my-charts/grafana # the chart being installed to create this release, referenced by relative path to local chart
chart: ../my-charts/grafana # the chart being installed to create this release, referenced by relative path to local helmfile
values:
- "../../my-values/grafana/values.yaml" # Values file (relative path to manifest)
- ./values/{{ requiredEnv "PLATFORM_ENV" }}/config.yaml # Values file taken from path with environment variable. $PLATFORM_ENV must be set in the calling environment.
@@ -121,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/
```
@@ -144,9 +174,11 @@ releases:
## installation
- `go get github.com/roboll/helmfile` or
- download one of [releases](https://github.com/roboll/helmfile/releases) or
- run as a [container](https://quay.io/roboll/helmfile)
- run as a [container](https://quay.io/roboll/helmfile) or
- install from [AUR](https://aur.archlinux.org/packages/kubernetes-helmfile-bin/) for Archlinux or
- Windows (using [scoop](https://scoop.sh/)): `scoop install helmfile`
- macOS (using [homebrew](https://brew.sh/)): `brew install helmfile`
## getting started
@@ -167,12 +199,16 @@ releases:
Sync your Kubernetes cluster state to the desired one by running:
```console
helmfile sync
helmfile apply
```
Congratulations! You now have your first Prometheus deployment running inside your cluster.
Iterate on the `helmfile.yaml` by referencing the [configuration syntax](#configuration-syntax) and the [cli reference](#cli-reference).
Iterate on the `helmfile.yaml` by referencing:
- [Configuration syntax](#configuration-syntax)
- [CLI reference](#cli-reference).
- [Helmfile Best Practices Guide](https://github.com/roboll/helmfile/blob/master/docs/writing-helmfile.md)
## cli reference
@@ -183,28 +219,35 @@ NAME:
USAGE:
helmfile [global options] command [command options] [arguments...]
VERSION:
v0.52.0
COMMANDS:
repos sync repositories from state file (helm repo add && helm repo update)
charts sync releases from state file (helm upgrade --install)
diff diff releases from state file against env (helm diff)
lint lint charts from state file (helm lint)
sync sync all resources from state file (repos, releases and chart deps)
apply apply all resources from state file only when there are changes
status retrieve status of releases in state file
delete delete releases from state file (helm delete)
test test releases from state file (helm test)
repos sync repositories from state file (helm repo add && helm repo update)
charts DEPRECATED: sync releases from state file (helm upgrade --install)
diff diff releases from state file against env (helm diff)
template template releases from state file against env (helm template)
lint lint charts from state file (helm lint)
sync sync all resources from state file (repos, releases and chart deps)
apply apply all resources from state file only when there are changes
status retrieve status of releases in state file
delete DEPRECATED: delete releases from state file (helm delete)
destroy deletes and then purges releases
test test releases from state file (helm test)
GLOBAL OPTIONS:
--helm-binary value, -b value path to helm binary
--file helmfile.yaml, -f helmfile.yaml load config from file or directory. defaults to helmfile.yaml or `helmfile.d`(means `helmfile.d/*.yaml`) in this preference
--environment default, -e default specify the environment name. defaults to default
--quiet, -q Silence output. Equivalent to log-level warn
--kube-context value Set kubectl context. Uses current context by default
--log-level value Set log level, default info
--namespace value, -n value Set namespace. Uses the namespace set in the context by default
--namespace value, -n value Set namespace. Uses the namespace set in the context by default, and is available in templates as {{ .Namespace }}
--selector value, -l value Only run using the releases that match labels. Labels can take the form of foo=bar or foo!=bar.
A release must match all labels in a group in order to be used. Multiple groups can be specified at once.
--selector tier=frontend,tier!=proxy --selector tier=backend. Will match all frontend, non-proxy releases AND all backend releases.
The name of a release can be used as a label. --selector name=myrelease
--interactive, -i Request confirmation before attempting to modify clusters
--help, -h show help
--version, -v print the version
```
@@ -229,13 +272,23 @@ please look at their [documentation](https://github.com/databus23/helm-diff#helm
### apply
The `helmfile apply` sub-command begins by executing `diff`. If `diff` finds that there is any changes, `sync` is executed after prompting you for an confirmation. `--auto-approve` skips the confirmation.
The `helmfile apply` sub-command begins by executing `diff`. If `diff` finds that there is any changes, `sync` is executed. Adding `--interactive` instructs Helmfile to request your confirmation before `sync`.
An expected use-case of `apply` is to schedule it to run periodically, so that you can auto-fix skews between the desired and the current state of your apps running on Kubernetes clusters.
### delete
### destroy
The `helmfile delete` sub-command deletes all the releases defined in the manfiests
The `helmfile destroy` sub-command deletes and purges all the releases defined in the manifests.
`helmfile --interactive destroy` instructs Helmfile to request your confirmation before actually deleting releases.
`destroy` basically runs `helm delete --purge` on all the targeted releases. If you don't want purging, use `helmfile delete` instead.
### delete (DEPRECATED)
The `helmfile delete` sub-command deletes all the releases defined in the manifests.
`helmfile --interactive delete` instructs Helmfile to request your confirmation before actually deleting releases.
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`.
@@ -263,13 +316,13 @@ Using manifest files in conjunction with command line argument can be a bit conf
A few rules to clear up this ambiguity:
- Absolute paths are always resolved as absolute paths
- Relative paths referenced *in* the helmfile manifest itself are relative to that manifest
- Relative paths referenced *in* the Helmfile manifest itself are relative to that manifest
- Relative paths referenced on the command line are relative to the current working directory the user is in
For additional context, take a look at [paths examples](PATHS.md)
## Labels Overview
A selector can be used to only target a subset of releases when running helmfile. This is useful for large helmfiles with releases that are logically grouped together.
A selector can be used to only target a subset of releases when running Helmfile. This is useful for large helmfiles with releases that are logically grouped together.
Labels are simple key value pairs that are an optional field of the release spec. When selecting by label, the search can be inverted. `tier!=backend` would match all releases that do NOT have the `tier: backend` label. `tier=fronted` would only match releases with the `tier: frontend` label.
@@ -279,15 +332,20 @@ 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, 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
You can use go's text/template expressions in `helmfile.yaml` and `values.yaml`(helm values files).
You can use go's text/template expressions in `helmfile.yaml` and `values.yaml.gotmpl` (templated helm values files). `values.yaml` references will be used verbatim. In other words:
- for value files ending with `.gotmpl`, template expressions will be rendered
- for plain value files (ending in `.yaml`), content will be used as-is
In addition to built-in ones, the following custom template functions are available:
- `readFile` reads the specified local file and generate a golang string
- `fromYaml` reads a golang string and generates a map
- `setValuleAtPath PATH NEW_VALUE` traverses a golang map, replaces the value at the PATH with NEW_VALUE
- `setValueAtPath PATH NEW_VALUE` traverses a golang map, replaces the value at the PATH with NEW_VALUE
- `toYaml` marshals a map into a string
### Values Files Templates
@@ -302,7 +360,7 @@ releases
- values.yaml.gotmpl
```
Every values file whose file extension is `.gotmpl` is considered as a tempalte file.
Every values file whose file extension is `.gotmpl` is considered as a template file.
Suppose `values.yaml.gotmpl` was something like:
@@ -391,12 +449,12 @@ environments:
releases:
{{ if (eq .Environment.Name "production" }}
{{ if eq .Environment.Name "production" }}
- name: newrelic-agent
# snip
{{ end }}
- name: myapp
# snip
# snip
```
## Environment Values
@@ -424,30 +482,73 @@ releases:
```yaml
domain: prod.example.com
releaseName: prod
```
`values.yaml.gotmpl`
```yaml
domain: {{ .Environment.Values.domain | default "dev.example.com" }}
domain: {{ .Environment.Values | getOrNil "my.domain" | default "dev.example.com" }}
```
`helmfile sync` installs `myapp` with the value `domain=dev.example.com`,
whereas `helmfile --environment production sync` installs the app with the value `domain=production.example.com`.
For even more flexibility, you can now use values declared in the `environments:` section in other parts of your helmfiles:
consider:
`default.yaml`
```yaml
domain: dev.example.com
releaseName: dev
```
```yaml
environments:
default:
values:
- default.yaml
production:
values:
- production.yaml # bare .yaml file, content will be used verbatim
- other.yaml.gotmpl # template directives with potential side-effects like `exec` and `readFile` will be honoured
releases:
- name: myapp-{{ .Environment.Values.releaseName }} # release name will be one of `dev` or `prod` depending on selected environment
values:
- values.yaml.gotmpl
{{ if eq (.Environment.Values.releaseName "prod" ) }}
# this release would be installed only if selected environment is `production`
- name: production-specific-release
...
{{ end }}
```
## Environment Secrets
Environment Secrets are encrypted versions of `Environment Values`.
Environment Secrets (not to be confused with Kubernetes Secrets) are encrypted versions of `Environment Values`.
You can list any number of `secrets.yaml` files created using `helm secrets` or `sops`, so that
helmfile could automatically decrypt and merge the secrets into the environment values.
Helmfile could automatically decrypt and merge the secrets into the environment values.
Suppose you have environment secrets defined in `hemlfile.yaml`:
First you must have the [helm-secrets](https://github.com/futuresimple/helm-secrets) plugin installed along with a
`.sops.yaml` file to configure the method of encryption (this can be in the same directory as your helmfile or
in the sub-directory containing your secrets files).
Then suppose you have a a foo.bar secret defined in `environments/production/secrets.yaml`:
```yaml
foo.bar: "mysupersecretstring"
```
You can then encrypt it with `helm secrets enc environments/production/secrets.yaml`
Then reference that encrypted file in `helmfile.yaml`:
```yaml
environments:
production:
secrets:
- environments/produdction/secrets.yaml
- environments/production/secrets.yaml
releases:
- name: myapp
@@ -456,12 +557,21 @@ releases:
- values.yaml.gotmpl
```
an environment secret `foo.bar` can be referenced by the below template expression in your `values.yaml.gotmpl`:
Then the environment secret `foo.bar` can be referenced by the below template expression in your `values.yaml.gotmpl`:
```yaml
{{ .Values.foo.bar }
{{ .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
@@ -491,7 +601,7 @@ All the yaml files under the specified directory are processed in the alphabetic
In case you want more control over how multiple `helmfile.yaml` files are organized, use `helmfiles:` configuration key in the `helmfile.yaml`:
Suppose you have multiple microservices organized in a Git reposistory that looks like:
Suppose you have multiple microservices organized in a Git repository that looks like:
- `myteam/` (sometimes it is equivalent to a k8s ns, that is `kube-system` for `clusterops` team)
- `apps/`
@@ -519,7 +629,7 @@ for d in apps/*; do helmfile -f $d diff; if [ $? -eq 2 ]; then helmfile -f $d sy
At this point, you'll start writing a `Makefile` under `myteam/` so that `make sync-all` will do the job.
It does work, but you can rely on the helmfile's feature instead.
It does work, but you can rely on the Helmfile feature instead.
Put `myteam/helmfile.yaml` that looks like:
@@ -533,9 +643,171 @@ 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
that is accessible by running a command:
A usual usage of `exec` would look like this:
```
mysetting: |
{{ exec "./mycmd" (list "arg1" "arg2" "--flag1") | indent 2 }}
```
Or even with a pipeline:
```
mysetting: |
{{ yourinput | exec "./mycmd-consume-stdin" (list "arg1" "arg2") | indent 2 }}
```
The possibility is endless. Try importing values from your golang app, bash script, jsonnet, or anything!
## Hooks
A Helmfile hook is a per-release extension point that is composed of:
- `events`
- `command`
- `args`
Helmfile triggers various `events` while it is running.
Once `events` are triggered, associated `hooks` are executed, by running the `command` with `args`.
Currently supported `events` are:
- `prepare`
- `cleanup`
- `presync`
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`.
The following is an example hook that just prints the contextual information provided to hook:
```
releases:
- name: myapp
chart: mychart
# *snip*
hooks:
- events: ["prepare", "cleanup"]
command: "echo"
args: ["{{`{{.Environment.Name}}`}}", "{{`{{.Release.Name}}`}}", "{{`{{.HelmfileCommand}}`}}\
"]
```
Let's say you ran `helmfile --environment prod sync`, the above hook results in executing:
```
echo {{Environment.Name}} {{.Release.Name}} {{.HelmfileCommand}}
```
Whereas the template expressions are executed thus the command becomes:
```
echo prod myapp sync
```
Now, replace `echo` with any command you like, and rewrite `args` that actually conforms to the command, so that you can integrate any command that does:
- templating
- linting
- testing
For templating, imagine that you created a hook that generates a helm chart on-the-fly by running an external tool like ksonnet, kustomize, or your own template engine.
It will allow you to write your helm releases with any language you like, while still leveraging goodies provided by helm.
### Helmfile + Kustomize
Do you prefer `kustomize` to write and organize your Kubernetes apps, but still want to leverage helm's useful features
like rollback, history, and so on? This section is for you!
The combination of `hooks` and [helmify-kustomize](https://gist.github.com/mumoshu/f9d0bd98e0eb77f636f79fc2fb130690)
enables you to integrate [kustomize](https://github.com/kubernetes-sigs/kustomize) into Helmfile.
That is, you can use `kustommize` to build a local helm chart from a kustomize overlay.
Let's assume you have a kustomize project named `foo-kustomize` like this:
```
foo-kustomize/
├── base
│   ├── configMap.yaml
│   ├── deployment.yaml
│   ├── kustomization.yaml
│   └── service.yaml
└── overlays
├── default
│   ├── kustomization.yaml
│   └── map.yaml
├── production
│   ├── deployment.yaml
│   └── kustomization.yaml
└── staging
├── kustomization.yaml
└── map.yaml
5 directories, 10 files
```
Write `helmfile.yaml`:
```yaml
- name: kustomize
chart: ./foo
hooks:
- events: ["prepare", "cleanup"]
command: "../helmify"
args: ["{{`{{if eq .Event.Name \"prepare\"}}build{{else}}clean{{end}}`}}", "{{`{{.Release.Ch\
art}}`}}", "{{`{{.Environment.Name}}`}}"]
```
Run `helmfile --environment staging sync` and see it results in helmfile running `kustomize build foo-kustomize/overlays/staging > foo/templates/all.yaml`.
Voilà! You can mix helm releases that are backed by remote charts, local charts, and even kustomize overlays.
## Guides
Use the [Helmfile Best Practices Guide](/docs/writing-helmfile.md) to write advanced helmfiles that feature:
- Default values
- Layering
We also have dedicated documentation on the following topics which might interest you:
- [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
helmfile itself doesn't have an ability to load env files. But you can write some bash script to achieve the goal:
Helmfile itself doesn't have an ability to load env files. But you can write some bash script to achieve the goal:
```console
set -a; . .env; set +a; helmfile sync
@@ -543,13 +815,33 @@ set -a; . .env; set +a; helmfile sync
Please see #203 for more context.
## Running helmfile without an Internet connection
## Running helmfile interactively
`helmfile --interactive [apply|destroy]` requests confirmation from you before actually modifying your cluster.
Use it when you're running `helmfile` manually on your local machine or a kind of secure administrative hosts.
For your local use-case, aliasing it like `alias hi='helmfile --interactive'` would be convenient.
## Running Helmfile without an Internet connection
Once you download all required charts into your machine, you can run `helmfile charts` to deploy your apps.
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
For more examples, see the [examples/README.md](https://github.com/roboll/helmfile/blob/master/examples/README.md) or the [`helmfile.d`](https://github.com/cloudposse/helmfiles/tree/master/helmfile.d) distribution of helmfiles by [Cloud Posse](https://github.com/cloudposse/).
For more examples, see the [examples/README.md](https://github.com/roboll/helmfile/blob/master/examples/README.md) or the [`helmfile`](https://github.com/cloudposse/helmfiles/tree/master/releases) distribution by [Cloud Posse](https://github.com/cloudposse/).
# Attribution
We use:
- [semtag](https://github.com/pnikosis/semtag) for automated semver tagging. I greatly appreciate the author(pnikosis)'s effort on creating it and their kindness to share it!
-3
View File
@@ -83,9 +83,6 @@ func GetArgs(args string, state *state.HelmState) []string {
}
}
if state.HelmDefaults.TillerNamespace != "" {
argsMap.SetArg("--tiller-namespace", state.HelmDefaults.TillerNamespace, false)
}
if state.HelmDefaults.KubeContext != "" {
argsMap.SetArg("--kube-context", state.HelmDefaults.KubeContext, false)
}
+100
View File
@@ -0,0 +1,100 @@
package cmd
import (
"fmt"
"github.com/roboll/helmfile/helmexec"
"github.com/roboll/helmfile/pkg/app"
"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 {
a, fileOrDir, err := InitAppEntry(c, false)
if err != nil {
return err
}
ctx := app.NewContext()
convergeWithHelmBinary := func(st *state.HelmState, helm helmexec.Interface) (bool, []error) {
if c.GlobalString("helm-binary") != "" {
helm.SetHelmBinary(c.GlobalString("helm-binary"))
}
return converge(st, helm, ctx)
}
err = a.VisitDesiredStates(fileOrDir, a.Selectors, convergeWithHelmBinary)
return toCliError(err)
}
func InitAppEntry(c *cli.Context, reverse bool) (*app.App, string, error) {
if c.NArg() > 0 {
cli.ShowAppHelp(c)
return nil, "", fmt.Errorf("err: extraneous arguments: %s", strings.Join(c.Args(), ", "))
}
fileOrDir := c.GlobalString("file")
kubeContext := c.GlobalString("kube-context")
namespace := c.GlobalString("namespace")
selectors := c.GlobalStringSlice("selector")
logger := c.App.Metadata["logger"].(*zap.SugaredLogger)
env := c.GlobalString("environment")
if env == "" {
env = state.DefaultEnv
}
app := app.Init(&app.App{
KubeContext: kubeContext,
Logger: logger,
Reverse: reverse,
Env: env,
Namespace: namespace,
Selectors: selectors,
})
return app, fileOrDir, nil
}
func FindAndIterateOverDesiredStatesUsingFlagsWithReverse(c *cli.Context, reverse bool, converge func(*state.HelmState, helmexec.Interface, app.Context) []error) error {
a, fileOrDir, err := InitAppEntry(c, reverse)
if err != nil {
return err
}
ctx := app.NewContext()
convergeWithHelmBinary := func(st *state.HelmState, helm helmexec.Interface) []error {
if c.GlobalString("helm-binary") != "" {
helm.SetHelmBinary(c.GlobalString("helm-binary"))
}
return converge(st, helm, ctx)
}
err = a.VisitDesiredStatesWithReleasesFiltered(fileOrDir, convergeWithHelmBinary)
return toCliError(err)
}
func toCliError(err error) error {
if err != nil {
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)
default:
return cli.NewExitError(e.Error(), 1)
}
}
return err
}
+37
View File
@@ -0,0 +1,37 @@
package cmd
import (
"github.com/roboll/helmfile/args"
"github.com/roboll/helmfile/helmexec"
"github.com/roboll/helmfile/pkg/app"
"github.com/roboll/helmfile/state"
"github.com/urfave/cli"
)
func Deps(a *app.App) cli.Command {
return cli.Command{
Name: "deps",
Usage: "update charts based on the contents of requirements.yaml",
Flags: []cli.Flag{
cli.StringFlag{
Name: "args",
Value: "",
Usage: "pass args to helm exec",
},
},
Action: func(c *cli.Context) error {
return VisitAllDesiredStates(c, func(state *state.HelmState, helm helmexec.Interface, ctx app.Context) (bool, []error) {
args := args.GetArgs(c.String("args"), state)
if len(args) > 0 {
helm.SetExtraArgs(args...)
}
errs := state.UpdateDeps(helm)
ok := len(errs) == 0
return ok, errs
})
},
}
}
+152
View File
@@ -0,0 +1,152 @@
# Shared Configuration Across Teams
Assume you have a two or more teams, each works for a different internal or external service, like:
- Product 1
- Product 2
- Observability
The simplest `helmfile.yaml` that declares the whole cluster that is composed of the three services would look like the below:
```yaml
releases:
- name: product1-api
chart: product1-charts/api
# snip
- name: product1-web
chart: product1-charts/web
# snip
- name: product2-api
chart: saas-charts/api
# snip
- name: product2-web
chart: product2-charts/web
# snip
- name: observability-prometheus-operator
chart: stable/prometheus-operator
# snip
- name: observability-process-exporter
chart: stable/prometheus-operator
# snip
```
This works, but what if you wanted to a separate cluster per service to achieve smaller blast radius?
Let's start by creating a `helmfile.yaml` for each service.
`product1/helmfile.yaml`:
```yaml
releases:
- name: product1-api
chart: product1-charts/api
# snip
- name: product1-web
chart: product1-charts/web
# snip
- name: observability-prometheus-operator
chart: stable/prometheus-operator
# snip
- name: observability-process-exporter
chart: stable/prometheus-operator
# snip
```
`product2/helmfile.yaml`:
```yaml
releases:
- name: product2-api
chart: product2-charts/api
# snip
- name: product2-web
chart: product2-charts/web
# snip
- name: observability-prometheus-operator
chart: stable/prometheus-operator
# snip
- name: observability-process-exporter
chart: stable/prometheus-operator
# snip
```
You will (of course!) notice this isn't DRY.
To remove the duplication of observability stack between the two helmfiles, create a "sub-helmfile" for the observability stack.
`observability/helmfile.yaml`:
```yaml
- name: observability-prometheus-operator
chart: stable/prometheus-operator
# snip
- name: observability-process-exporter
chart: stable/prometheus-operator
# snip
```
As you might have imagined, the observability helmfile can be reused from the two product helmfiles by declaring `helmfiles`.
`product1/helmfile.yaml`:
```yaml
helmfiles:
- ../observability/helmfile.yaml
releases:
- name: product1-api
chart: product1-charts/api
# snip
- name: product1-web
chart: product1-charts/web
# snip
```
`product2/helmfile.yaml`:
```yaml
helmfiles:
- ../observability/helmfile.yaml
releases:
- name: product2-api
chart: product2-charts/api
# snip
- name: product2-web
chart: product2-charts/web
# snip
```
## Using sub-helmfile as a template
You can go even further by generalizing the product related releases as a pair of `api` and `web`:
`shared/helmfile.yaml`:
```yaml
releases:
- name: product{{ env "PRODUCT_ID" }}-api
chart: product{{ env "PRODUCT_ID" }}-charts/api
# snip
- name: product{{ env "PRODUCT_ID" }}-web
chart: product{{ env "PRODUCT_ID" }}-charts/web
# snip
```
Then you only need one single product helmfile
`product/helmfile.yaml`:
```yaml
helmfiles:
- ../observability/helmfile.yaml
- ../shared/helmfile.yaml
```
Now that we use the environment variable `PRODUCT_ID` to as the parameters of release names, you need to set it before running `helmfile`, so that it produces the differently named releases per product:
```console
$ PRODUCT_ID=1 helmfile -f product/helmfile.yaml apply
$ PRODUCT_ID=2 helmfile -f product/helmfile.yaml apply
```
+161
View File
@@ -0,0 +1,161 @@
# The Helmfile Best Practices Guide
This guide covers the Helmfile’s considered patterns for writing advanced helmfiles. It focuses on how helmfile should be structured and executed.
## Missing keys and Default values
helmfile tries its best to inform users for noticing potential mistakes.
One example of how helmfile achieves it is that, `helmfile` fails when you tried to access missing keys in environment values.
That is, the following example let `helmfile` fail when you have no `eventApi.replicas` defined in environment values.
```
{{ .Environment.Values.eventApi.replicas | default 1 }}
```
In case it isn't a mistake and you do want to allow missing keys, use the `getOrNil` template function:
```
{{ .Environment.Values | getOrNil "eventApi.replicas" }}
```
This result in printing `<no value` in your template, that may or may not result in a failure.
If you want a kind of default values that is used when a missing key was referenced, use `default` like:
```
{{ .Environment.Values | getOrNil "eventApi.replicas" | default 1 }}
```
Now, you get `1` when there is no `eventApi.replicas` defined in environment values.
## Release Template / Conventional Directory Structure
Introducing helmfile into a large-scale project that involes dozens of releases often results in a lot of repetitions in `helmfile.yaml` files.
The example below shows repetitions in `namespace`, `chart`, `values`, and `secrets`:
```yaml
releases:
# *snip*
- name: heapster
namespace: kube-system
chart: stable/heapster
version: 0.3.2
values:
- "./config/heapster/values.yaml"
- "./config/heapster/{{ .Environment.Name }}.yaml"
secrets:
- "./config/heapster/secrets.yaml"
- "./config/heapster/{{ .Environment.Name }}-secrets.yaml"
- name: kubernetes-dashboard
namespace: kube-system
chart: stable/kubernetes-dashboard
version: 0.10.0
values:
- "./config/kubernetes-dashboard/values.yaml"
- "./config/kubernetes-dashboard/{{ .Environment.Name }}.yaml"
values:
- "./config/kubernetes-dashboard/secrets.yaml"
- "./config/kubernetes-dashboard/{{ .Environment.Name }}-secrets.yaml"
```
This is where Helmfile's advanced feature called Release Template comes handy.
It allows you to abstract away the repetitions in releases into a template, which is then included and executed by using YAML anchor/alias:
```yaml
templates:
default: &default
chart: stable/{{`{{ .Release.Name }}`}}
namespace: kube-system
# This prevents helmfile exiting when it encounters a missing file
# Valid values are "Error", "Warn", "Info", "Debug". The default is "Error"
# Use "Debug" to make missing files errors invisible at the default log level(--log-level=INFO)
missingFileHandler: Warn
values:
- config/{{`{{ .Release.Name }}`}}/values.yaml
- config/{{`{{ .Release.Name }}`}}/{{`{{ .Environment.Name }}`}}.yaml
secrets:
- config/{{`{{ .Release.Name }}`}}/secrets.yaml
- config/{{`{{ .Release.Name }}`}}/{{`{{ .Environment.Name }}`}}-secrets.yaml
releases:
- name: heapster
<<: *default
- name: kubernetes-dashboard
<<: *default
```
See the [issue 428](https://github.com/roboll/helmfile/issues/428) for more context on how this is supposed to work.
## Layering
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.
Use Layering to extract the common parts into a dedicated *library helmfile*s, so that each helmfile becomes DRY.
Let's assume that your `helmfile.yaml` looks like:
```
{ readFile "commons.yaml" }}
---
{{ readFile "environments.yaml" }}
---
releases:
- name: myapp
chart: mychart
```
Whereas `commons.yaml` contained a monitoring agent:
```yaml
releases:
- name: metricbaet
chart: stable/metricbeat
```
And `environments.yaml` contained well-known environments:
```yaml
environments:
development:
production:
```
At run time, template expressions in your `helmfile.yaml` are executed:
```yaml
releases:
- name: metricbaet
chart: stable/metricbeat
---
environments:
development:
production:
---
releases:
- name: myapp
chart: mychart
```
Resulting YAML documents are merged in the order of occurrence,
so that your `helmfile.yaml` becomes:
```yaml
environments:
development:
production:
releases:
- name: metricbaet
chart: stable/metricbeat
- name: myapp
chart: mychart
```
Great!
Now, repeat the above steps for each your `helmfile.yaml`, so that all your helmfiles becomes DRY.
+102
View File
@@ -0,0 +1,102 @@
package event
import (
"fmt"
"github.com/roboll/helmfile/environment"
"github.com/roboll/helmfile/helmexec"
"github.com/roboll/helmfile/tmpl"
"go.uber.org/zap"
"os"
)
type Hook struct {
Name string `yaml:"name"`
Events []string `yaml:"events"`
Command string `yaml:"command"`
Args []string `yaml:"args"`
}
type event struct {
Name string
}
type Bus struct {
Runner helmexec.Runner
Hooks []Hook
BasePath string
StateFilePath string
Namespace string
Env environment.Environment
ReadFile func(string) ([]byte, error)
Logger *zap.SugaredLogger
}
func (bus *Bus) Trigger(evt string, context map[string]interface{}) (bool, error) {
if bus.Runner == nil {
bus.Runner = helmexec.ShellRunner{
Dir: bus.BasePath,
}
}
executed := false
for _, hook := range bus.Hooks {
contained := false
for _, e := range hook.Events {
contained = contained || e == evt
}
if !contained {
continue
}
var err error
name := hook.Name
if name == "" {
name = hook.Command
}
fmt.Fprintf(os.Stderr, "%s: basePath=%s\n", bus.StateFilePath, bus.BasePath)
data := map[string]interface{}{
"Environment": bus.Env,
"Namespace": bus.Namespace,
"Event": event{
Name: evt,
},
}
for k, v := range context {
data[k] = v
}
render := tmpl.NewTextRenderer(bus.ReadFile, bus.BasePath, data)
bus.Logger.Debugf("hook[%s]: triggered by event \"%s\"\n", name, evt)
command, err := render.RenderTemplateText(hook.Command)
if err != nil {
return false, fmt.Errorf("hook[%s]: %v", name, err)
}
args := make([]string, len(hook.Args))
for i, raw := range hook.Args {
args[i], err = render.RenderTemplateText(raw)
if err != nil {
return false, fmt.Errorf("hook[%s]: %v", name, err)
}
}
bytes, err := bus.Runner.Execute(command, args, map[string]string{})
bus.Logger.Debugf("hook[%s]: %s\n", name, string(bytes))
if err != nil {
return false, fmt.Errorf("hook[%s]: command `%s` failed: %v", name, command, err)
}
executed = true
}
return executed, nil
}
+113
View File
@@ -0,0 +1,113 @@
package event
import (
"fmt"
"github.com/roboll/helmfile/environment"
"github.com/roboll/helmfile/helmexec"
"os"
"testing"
)
var logger = helmexec.NewLogger(os.Stdout, "warn")
type runner struct {
}
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)
}
for _, a := range args {
if a == "ng" {
return nil, fmt.Errorf("cmd failed due to invalid arg: %s", a)
}
}
return []byte(""), nil
}
func TestTrigger(t *testing.T) {
cases := []struct {
name string
hook *Hook
triggeredEvt string
expectedResult bool
expectedErr string
}{
{
"okhook1",
&Hook{"okhook1", []string{"foo"}, "ok", []string{}},
"foo",
true,
"",
},
{
"missinghook1",
&Hook{"okhook1", []string{"foo"}, "ok", []string{}},
"bar",
false,
"",
},
{
"nohook1",
nil,
"bar",
false,
"",
},
{
"nghook1",
&Hook{"nghook1", []string{"foo"}, "ng", []string{}},
"foo",
false,
"hook[nghook1]: command `ng` failed: cmd failed due to invalid cmd: ng",
},
{
"nghook2",
&Hook{"nghook2", []string{"foo"}, "ok", []string{"ng"}},
"foo",
false,
"hook[nghook2]: command `ok` failed: cmd failed due to invalid arg: ng",
},
}
readFile := func(filename string) ([]byte, error) {
return nil, fmt.Errorf("unexpected call to readFile: %s", filename)
}
for _, c := range cases {
hooks := []Hook{}
if c.hook != nil {
hooks = append(hooks, *c.hook)
}
bus := &Bus{
Hooks: hooks,
StateFilePath: "path/to/helmfile.yaml",
BasePath: "path/to",
Namespace: "myns",
Env: environment.Environment{Name: "prod"},
Logger: logger,
ReadFile: readFile,
}
bus.Runner = &runner{}
data := map[string]interface{}{
"Release": "myrel",
"HelmfileCommand": "mycmd",
}
ok, err := bus.Trigger(c.triggeredEvt, data)
if ok != c.expectedResult {
t.Errorf("unexpected result for case \"%s\": expected=%v, actual=%v", c.name, c.expectedResult, ok)
}
if c.expectedErr != "" {
if err == nil {
t.Error("error expected, but not occurred")
} else if err.Error() != c.expectedErr {
t.Errorf("unexpected error for case \"%s\": expected=%s, actual=%v", c.name, c.expectedErr, err)
}
} else {
if err != nil {
t.Errorf("unexpected error for case \"%s\": %v", c.name, err)
}
}
}
}
+23
View File
@@ -0,0 +1,23 @@
module github.com/roboll/helmfile
go 1.12
require (
github.com/Masterminds/semver v1.4.1 // indirect
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=
Executable
+623
View File
@@ -0,0 +1,623 @@
#!/usr/bin/env bash
PROG=semtag
PROG_VERSION="v0.1.0"
SEMVER_REGEX="^v?(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(\-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$"
IDENTIFIER_REGEX="^\-([0-9A-Za-z-]+)\.([0-9A-Za-z-]+)*$"
# Global variables
FIRST_VERSION="v0.0.0"
finalversion=$FIRST_VERSION
lastversion=$FIRST_VERSION
hasversiontag="false"
scope="patch"
displayonly="false"
forcetag="false"
forcedversion=
versionname=
identifier=
HELP="\
Usage:
$PROG
$PROG getlast
$PROG getfinal
$PROG (final|alpha|beta|candidate) [-s <scope> (major|minor|patch|auto) | -o]
$PROG --help
$PROG --version
Options:
-s The scope that must be increased, can be major, minor or patch.
The resulting version will match X.Y.Z(-PRERELEASE)(+BUILD)
where X, Y and Z are positive integers, PRERELEASE is an optionnal
string composed of alphanumeric characters describing if the build is
a release candidate, alpha or beta version, with a number.
BUILD is also an optional string composed of alphanumeric
characters and hyphens.
Setting the scope as 'auto', the script will chose the scope between
'minor' and 'patch', depending on the amount of lines added (<10% will
choose patch).
-v Specifies manually the version to be tagged, must be a valid semantic version
in the format X.Y.Z where X, Y and Z are positive integers.
-o Output the version only, shows the bumped version, but doesn't tag.
-f Forces to tag, even if there are unstaged or uncommited changes.
Commands:
--help Print this help message.
--version Prints the program's version.
get Returns both current final version and last tagged version.
getlast Returns the latest tagged version.
getfinal Returns the latest tagged final version.
getcurrent Returns the current version, based on the latest one, if there are uncommited or
unstaged changes, they will be reflected in the version, adding the number of
pending commits, current branch and commit hash.
final Tags the current build as a final version, this only can be done on the master branch.
candidate Tags the current build as a release candidate, the tag will contain all
the commits from the last final version.
alpha Tags the current build as an alpha version, the tag will contain all
the commits from the last final version.
beta Tags the current build as a beta version, the tag will contain all
the commits from the last final version."
# Commands and options
ACTION="getlast"
ACTION="$1"
shift
# We get the parameters
while getopts "v:s:of" opt; do
case $opt in
v)
forcedversion="$OPTARG"
;;
s)
scope="$OPTARG"
;;
o)
displayonly="true"
;;
f)
forcetag="true"
;;
\?)
echo "Invalid option: -$OPTARG" >&2
exit 1
;;
:)
echo "Option -$OPTARG requires an argument." >&2
exit 1
;;
esac
done
# Gets a string with the version and returns an array of maximum size of 5 with all the parts of the sematinc version
# $1 The string containing the version in semantic format
# $2 The variable to store the result array:
# position 0: major number
# position 1: minor number
# position 2: patch number
# position 3: identifier (or prerelease identifier)
# position 4: build info
function explode_version {
local __version=$1
local __result=$2
if [[ $__version =~ $SEMVER_REGEX ]] ; then
local __major=${BASH_REMATCH[1]}
local __minor=${BASH_REMATCH[2]}
local __patch=${BASH_REMATCH[3]}
local __prere=${BASH_REMATCH[4]}
local __build=${BASH_REMATCH[5]}
eval "$__result=(\"$__major\" \"$__minor\" \"$__patch\" \"$__prere\" \"$__build\")"
else
eval "$__result="
fi
}
# Compare two versions and returns -1, 0 or 1
# $1 The first version to compare
# $2 The second version to compare
# $3 The variable where to store the result
function compare_versions {
local __first
local __second
explode_version $1 __first
explode_version $2 __second
local lv=$3
# Compares MAJOR, MINOR and PATCH
for i in 0 1 2; do
local __numberfirst=${__first[$i]}
local __numbersecond=${__second[$i]}
case $(($__numberfirst - $__numbersecond)) in
0)
;;
-[0-9]*)
eval "$lv=-1"
return 0
;;
[0-9]*)
eval "$lv=1"
return 0
;;
esac
done
# Identifiers should compare with the ASCII order.
local __identifierfirst=${__first[3]}
local __identifiersecond=${__second[3]}
if [[ -n "$__identifierfirst" ]] && [[ -n "$__identifiersecond" ]]; then
if [[ "$__identifierfirst" > "$__identifiersecond" ]]; then
eval "$lv=1"
return 0
elif [[ "$__identifierfirst" < "$__identifiersecond" ]]; then
eval "$lv=-1"
return 0
fi
elif [[ -z "$__identifierfirst" ]] && [[ -n "$__identifiersecond" ]]; then
eval "$lv=1"
return 0
elif [[ -n "$__identifierfirst" ]] && [[ -z "$__identifiersecond" ]]; then
eval "$lv=-1"
return 0
fi
eval "$lv=0"
}
# Returns the last version of two
# $1 The first version to compare
# $2 The second version to compare
# $3 The variable where to store the last one
function get_latest_of_two {
local __first=$1
local __second=$2
local __result
local __latest=$3
compare_versions $__first $__second __result
case $__result in
0)
eval "$__latest=$__second"
;;
-1)
eval "$__latest=$__second"
;;
1)
eval "$__latest=$__first"
;;
esac
}
# Assigns a 2 size array with the identifier, having the identifier at pos 0, and the number in pos 1
# $1 The identifier in the format -id.#
# $2 The vferiable where to store the 2 size array
function explode_identifier {
local __identifier=$1
local __result=$2
if [[ $__identifier =~ $IDENTIFIER_REGEX ]] ; then
local __id=${BASH_REMATCH[1]}
local __number=${BASH_REMATCH[2]}
if [[ -z "$__number" ]]; then
__number=1
fi
eval "$__result=(\"$__id\" \"$__number\")"
else
eval "$__result="
fi
}
# Gets a list of tags and assigns the base and latest versions
# Receives an array with the tags containing the versions
# Assigns to the global variables finalversion and lastversion the final version and the latest version
function get_latest {
local __taglist=("$@")
local __tagsnumber=${#__taglist[@]}
local __current
case $__tagsnumber in
0)
finalversion=$FIRST_VERSION
lastversion=$FIRST_VERSION
;;
1)
__current=${__taglist[0]}
explode_version $__current ver
if [ -n "$ver" ]; then
if [ -n "${ver[3]}" ]; then
finalversion=$FIRST_VERSION
else
finalversion=$__current
fi
lastversion=$__current
else
finalversion=$FIRST_VERSION
lastversion=$FIRST_VERSION
fi
;;
*)
local __lastpos=$(($__tagsnumber-1))
for i in $(seq 0 $__lastpos)
do
__current=${__taglist[i]}
explode_version ${__taglist[i]} ver
if [ -n "$ver" ]; then
if [ -z "${ver[3]}" ]; then
get_latest_of_two $finalversion $__current finalversion
get_latest_of_two $lastversion $finalversion lastversion
else
get_latest_of_two $lastversion $__current lastversion
fi
fi
done
;;
esac
if git rev-parse -q --verify "refs/tags/$lastversion" >/dev/null; then
hasversiontag="true"
else
hasversiontag="false"
fi
}
# Gets the next version given the provided scope
# $1 The version that is going to be bumped
# $2 The scope to bump
# $3 The variable where to stoer the result
function get_next_version {
local __exploded
local __fromversion=$1
local __scope=$2
local __result=$3
explode_version $__fromversion __exploded
case $__scope in
major)
__exploded[0]=$((${__exploded[0]}+1))
__exploded[1]=0
__exploded[2]=0
;;
minor)
__exploded[1]=$((${__exploded[1]}+1))
__exploded[2]=0
;;
patch)
__exploded[2]=$((${__exploded[2]}+1))
;;
esac
eval "$__result=v${__exploded[0]}.${__exploded[1]}.${__exploded[2]}"
}
function bump_version {
## First we try to get the next version based on the existing last one
if [ "$scope" == "auto" ]; then
get_scope_auto scope
fi
local __candidatefromlast=$FIRST_VERSION
local __explodedlast
explode_version $lastversion __explodedlast
if [[ -n "${__explodedlast[3]}" ]]; then
# Last version is not final
local __idlast
explode_identifier ${__explodedlast[3]} __idlast
# We get the last, given the desired id based on the scope
__candidatefromlast="v${__explodedlast[0]}.${__explodedlast[1]}.${__explodedlast[2]}"
if [[ -n "$identifier" ]]; then
local __nextid="$identifier.1"
if [ "$identifier" == "${__idlast[0]}" ]; then
# We target the same identifier as the last so we increase one
__nextid="$identifier.$(( ${__idlast[1]}+1 ))"
__candidatefromlast="$__candidatefromlast-$__nextid"
else
# Different identifiers, we make sure we are assigning a higher identifier, if not, we increase the version
__candidatefromlast="$__candidatefromlast-$__nextid"
local __comparedwithlast
compare_versions $__candidatefromlast $lastversion __comparedwithlast
if [ "$__comparedwithlast" == -1 ]; then
get_next_version $__candidatefromlast $scope __candidatefromlast
__candidatefromlast="$__candidatefromlast-$__nextid"
fi
fi
fi
fi
# Then we try to get the version based on the latest final one
local __candidatefromfinal=$FIRST_VERSION
get_next_version $finalversion $scope __candidatefromfinal
if [[ -n "$identifier" ]]; then
__candidatefromfinal="$__candidatefromfinal-$identifier.1"
fi
# Finally we compare both candidates
local __resultversion
local __result
compare_versions $__candidatefromlast $__candidatefromfinal __result
case $__result in
0)
__resultversion=$__candidatefromlast
;;
-1)
__resultversion="$__candidatefromfinal"
;;
1)
__resultversion=$__candidatefromlast
;;
esac
eval "$1=$__resultversion"
}
function increase_version {
local __version=
if [ -z $forcedversion ]; then
bump_version __version
else
if [[ $forcedversion =~ $SEMVER_REGEX ]] ; then
compare_versions $forcedversion $lastversion __result
if [ $__result -le 0 ]; then
echo "Version can't be lower than last version: $lastversion"
exit 1
fi
else
echo "Non valid version to bump"
exit 1
fi
__version=$forcedversion
fi
if [ "$displayonly" == "true" ]; then
echo "$__version"
else
if [ "$forcetag" == "false" ]; then
check_git_dirty_status
fi
local __commitlist
if [ "$finalversion" == "$FIRST_VERSION" ] || [ "$hasversiontag" != "true" ]; then
__commitlist="$(git log --pretty=oneline | cat)"
else
__commitlist="$(git log --pretty=oneline $finalversion... | cat)"
fi
# If we are forcing a bump, we add bump to the commit list
if [[ -z $__commitlist && "$forcetag" == "true" ]]; then
__commitlist="bump"
fi
if [[ -z $__commitlist ]]; then
echo "No commits since the last final version, not bumping version"
else
if [[ -z $versionname ]]; then
versionname=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
fi
local __message="$versionname
$__commitlist"
# We check we have info on the user
local __username=$(git config user.name)
if [ -z "$__username" ]; then
__username=$(id -u -n)
git config user.name $__username
fi
local __useremail=$(git config user.email)
if [ -z "$__useremail" ]; then
__useremail=$(hostname)
git config user.email "$__username@$__useremail"
fi
git tag -a $__version -m "$__message"
# If we have a remote, we push there
local __remote=${SEMTAG_REMOTE:-$(git remote)}
if [[ -n $__remote ]]; then
git push $__remote $__version > /dev/null
if [ $? -eq 0 ]; then
echo "$__version"
else
echo "Error pushing the tag $__version to $__remote"
exit 1
fi
else
echo "$__version"
fi
fi
fi
}
function check_git_dirty_status {
local __repostatus=
get_work_tree_status __repostatus
if [ "$__repostatus" == "uncommitted" ]; then
echo "ERROR: You have uncommitted changes"
git status --porcelain
exit 1
fi
if [ "$__repostatus" == "unstaged" ]; then
echo "ERROR: You have unstaged changes"
git status --porcelain
exit 1
fi
}
# Get the total amount of lines of code in the repo
function get_total_lines {
local __empty_id="$(git hash-object -t tree /dev/null)"
local __changes="$(git diff --numstat $__empty_id | cat)"
local __added_deleted=$1
get_changed_lines "$__changes" $__added_deleted
}
# Get the total amount of lines of code since the provided tag
function get_sincetag_lines {
local __sincetag=$1
local __changes="$(git diff --numstat $__sincetag | cat)"
local __added_deleted=$2
get_changed_lines "$__changes" $__added_deleted
}
function get_changed_lines {
local __changes_numstat=$1
local __result=$2
IFS=$'\n' read -rd '' -a __changes_array <<<"$__changes_numstat"
local __diff_regex="^([0-9]+)[[:space:]]+([0-9]+)[[:space:]]+.+$"
local __total_added=0
local __total_deleted=0
for i in "${__changes_array[@]}"
do
if [[ $i =~ $__diff_regex ]] ; then
local __added=${BASH_REMATCH[1]}
local __deleted=${BASH_REMATCH[2]}
__total_added=$(( $__total_added+$__added ))
__total_deleted=$(( $__total_deleted+$__deleted ))
fi
done
eval "$2=( $__total_added $__total_deleted )"
}
function get_scope_auto {
local __verbose=$2
local __total=0
local __since=0
local __scope=
get_total_lines __total
get_sincetag_lines $finalversion __since
local __percentage=0
if [ "$__total" != "0" ]; then
local __percentage=$(( 100*$__since/$__total ))
if [ $__percentage -gt "10" ]; then
__scope="minor"
else
__scope="patch"
fi
fi
eval "$1=$__scope"
if [[ -n "$__verbose" ]]; then
echo "[Auto Scope] Percentage of lines changed: $__percentage"
echo "[Auto Scope] : $__scope"
fi
}
function get_work_tree_status {
# Update the index
git update-index -q --ignore-submodules --refresh > /dev/null
eval "$1="
if ! git diff-files --quiet --ignore-submodules -- > /dev/null
then
eval "$1=unstaged"
fi
if ! git diff-index --cached --quiet HEAD --ignore-submodules -- > /dev/null
then
eval "$1=uncommitted"
fi
}
function get_current {
if [ "$hasversiontag" == "true" ]; then
local __commitcount="$(git rev-list $lastversion.. --count)"
else
local __commitcount="$(git rev-list --count HEAD)"
fi
local __status=
get_work_tree_status __status
if [ "$__commitcount" == "0" ] && [ -z "$__status" ]; then
eval "$1=$lastversion"
else
local __buildinfo="$(git rev-parse --short HEAD)"
local __currentbranch="$(git rev-parse --abbrev-ref HEAD)"
if [ "$__currentbranch" != "master" ]; then
__buildinfo="$__currentbranch.$__buildinfo"
fi
local __suffix=
if [ "$__commitcount" != "0" ]; then
if [ -n "$__suffix" ]; then
__suffix="$__suffix."
fi
__suffix="$__suffix$__commitcount"
fi
if [ -n "$__status" ]; then
if [ -n "$__suffix" ]; then
__suffix="$__suffix."
fi
__suffix="$__suffix$__status"
fi
__suffix="$__suffix+$__buildinfo"
if [ "$lastversion" == "$finalversion" ]; then
scope="patch"
identifier=
local __bumped=
bump_version __bumped
eval "$1=$__bumped-dev.$__suffix"
else
eval "$1=$lastversion.$__suffix"
fi
fi
}
function init {
git fetch > /dev/null
TAGS="$(git tag)"
IFS=$'\n' read -rd '' -a TAG_ARRAY <<<"$TAGS"
get_latest ${TAG_ARRAY[@]}
currentbranch="$(git rev-parse --abbrev-ref HEAD)"
}
case $ACTION in
--help)
echo -e "$HELP"
;;
--version)
echo -e "${PROG}: $PROG_VERSION"
;;
final)
init
diff=$(git diff master | cat)
if [ "$forcetag" == "false" ]; then
if [ -n "$diff" ]; then
echo "ERROR: Branch must be updated with master for final versions"
exit 1
fi
fi
increase_version
;;
alpha|beta)
init
identifier="$ACTION"
increase_version
;;
candidate)
init
identifier="rc"
increase_version
;;
getlast)
init
echo "$lastversion"
;;
getfinal)
init
echo "$finalversion"
;;
getcurrent)
init
get_current current
echo "$current"
;;
get)
init
echo "Current final version: $finalversion"
echo "Last tagged version: $lastversion"
;;
*)
echo "'$ACTION' is not a valid command, see --help for available commands."
;;
esac
+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{}
}
}
+58 -20
View File
@@ -6,9 +6,10 @@ import (
"os"
"strings"
"sync"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"sync"
)
const (
@@ -69,47 +70,72 @@ 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...)
out, err := helm.exec(args, map[string]string{})
helm.write(out)
return err
}
func (helm *execer) UpdateRepo() error {
helm.logger.Info("Updating repo")
out, err := helm.exec("repo", "update")
out, err := helm.exec([]string{"repo", "update"}, map[string]string{})
helm.write(out)
return err
}
func (helm *execer) UpdateDeps(chart string) error {
helm.logger.Infof("Updating dependency %v", chart)
out, err := helm.exec("dependency", "update", chart)
out, err := helm.exec([]string{"dependency", "update", chart}, map[string]string{})
helm.write(out)
return err
}
func (helm *execer) SyncRelease(name, chart string, flags ...string) error {
func (helm *execer) BuildDeps(chart string) error {
helm.logger.Infof("Building dependency %v", chart)
out, err := helm.exec([]string{"dependency", "build", chart}, map[string]string{})
helm.write(out)
return err
}
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) DecryptSecret(name string) (string, error) {
func (helm *execer) List(context HelmContext, filter string, flags ...string) (string, error) {
helm.logger.Infof("Listing releases matching %v", 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(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
}
tmpFile, err := ioutil.TempFile("", "secret")
if err != nil {
@@ -143,42 +169,54 @@ func (helm *execer) DecryptSecret(name string) (string, error) {
return tmpFile.Name(), err
}
func (helm *execer) DiffRelease(name, chart string, flags ...string) error {
func (helm *execer) TemplateRelease(chart string, flags ...string) error {
out, err := helm.exec(append([]string{"template", chart}, flags...), map[string]string{})
helm.write(out)
return err
}
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...)...)
preArgs := context.GetTillerlessArgs(helm.helmBinary)
env := context.getTillerlessEnv()
out, err := helm.exec(append(append(preArgs, "diff", "upgrade", "--allow-unreleased", name, chart), flags...), env)
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...)...)
out, err := helm.exec(append([]string{"fetch", chart}, flags...), map[string]string{})
helm.write(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...)
@@ -187,7 +225,7 @@ func (helm *execer) exec(args ...string) ([]byte, error) {
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)
return helm.runner.Execute(helm.helmBinary, cmdargs, env)
}
func (helm *execer) write(out []byte) {
+103 -17
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
}
@@ -118,7 +119,7 @@ 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
`
@@ -127,7 +128,7 @@ exec: helm upgrade --install --reset-values release chart --timeout 10 --wait --
}
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
`
@@ -136,6 +137,20 @@ exec: helm upgrade --install --reset-values release chart --kube-context dev
}
}
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
`
if buffer.String() != expected {
t.Errorf("helmexec.SyncRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
}
func Test_UpdateDeps(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
@@ -159,11 +174,34 @@ exec: helm dependency update ./chart/foo --verify --kube-context dev
}
}
func Test_BuildDeps(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "dev")
helm.BuildDeps("./chart/foo")
expected := `Building dependency ./chart/foo
exec: helm dependency build ./chart/foo --kube-context dev
`
if buffer.String() != expected {
t.Errorf("helmexec.BuildDeps()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
buffer.Reset()
helm.SetExtraArgs("--verify")
helm.BuildDeps("./chart/foo")
expected = `Building dependency ./chart/foo
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)
}
}
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
`
@@ -176,7 +214,7 @@ 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
`
@@ -185,7 +223,7 @@ exec: helm diff upgrade --allow-unreleased release chart --timeout 10 --wait --k
}
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
`
@@ -194,11 +232,24 @@ exec: helm diff upgrade --allow-unreleased release chart --kube-context dev
}
}
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
`
if buffer.String() != expected {
t.Errorf("helmexec.DiffRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
}
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
`
@@ -210,7 +261,7 @@ 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
`
@@ -223,7 +274,7 @@ 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
`
@@ -235,7 +286,7 @@ 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
`
@@ -248,7 +299,7 @@ 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
`
@@ -261,28 +312,29 @@ func Test_exec(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "")
helm.exec("version")
env := map[string]string{}
helm.exec([]string{"version"}, env)
expected := "exec: helm version\n"
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")
helm.exec([]string{"diff", "release", "chart", "--timeout 10", "--wait"}, env)
expected = "exec: helm diff release chart --timeout 10 --wait --kube-context dev\n"
if buffer.String() != expected {
t.Errorf("helmexec.exec()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
buffer.Reset()
helm.exec("version")
helm.exec([]string{"version"}, env)
expected = "exec: helm version --kube-context dev\n"
if buffer.String() != expected {
t.Errorf("helmexec.exec()\nactual = %v\nexpect = %v", buffer.String(), expected)
@@ -290,7 +342,7 @@ func Test_exec(t *testing.T) {
buffer.Reset()
helm.SetExtraArgs("foo")
helm.exec("version")
helm.exec([]string{"version"}, env)
expected = "exec: helm version foo --kube-context dev\n"
if buffer.String() != expected {
t.Errorf("helmexec.exec()\nactual = %v\nexpect = %v", buffer.String(), expected)
@@ -299,7 +351,7 @@ func Test_exec(t *testing.T) {
buffer.Reset()
helm = MockExecer(logger, "")
helm.SetHelmBinary("overwritten")
helm.exec("version")
helm.exec([]string{"version"}, env)
expected = "exec: overwritten version\n"
if buffer.String() != expected {
t.Errorf("helmexec.exec()\nactual = %v\nexpect = %v", buffer.String(), expected)
@@ -353,3 +405,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)
}
}
+9 -7
View File
@@ -7,14 +7,16 @@ type Interface interface {
AddRepo(name, repository, certfile, keyfile, username, password string) error
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
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)
}
+34 -3
View File
@@ -1,7 +1,9 @@
package helmexec
import (
"os"
"os/exec"
"strings"
)
const (
@@ -11,14 +13,43 @@ 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{}
type ShellRunner struct {
Dir string
}
// 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
preparedCmd.Env = mergeEnv(os.Environ(), env)
return preparedCmd.CombinedOutput()
}
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
}
+266 -335
View File
@@ -2,30 +2,16 @@ package main
import (
"fmt"
"log"
"os"
"os/signal"
"path/filepath"
"sort"
"syscall"
"os/exec"
"github.com/roboll/helmfile/args"
"github.com/roboll/helmfile/environment"
"github.com/roboll/helmfile/cmd"
"github.com/roboll/helmfile/helmexec"
"github.com/roboll/helmfile/pkg/app"
"github.com/roboll/helmfile/state"
"github.com/roboll/helmfile/tmpl"
"github.com/urfave/cli"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"io/ioutil"
)
const (
DefaultHelmfile = "helmfile.yaml"
DeprecatedHelmfile = "charts.yaml"
DefaultHelmfileDirectory = "helmfile.d"
"os"
"strings"
)
var Version string
@@ -56,11 +42,11 @@ func configureLogging(c *cli.Context) error {
func main() {
app := cli.NewApp()
app.Name = "helmfile"
app.Usage = ""
app.Version = Version
app.Flags = []cli.Flag{
cliApp := cli.NewApp()
cliApp.Name = "helmfile"
cliApp.Usage = ""
cliApp.Version = Version
cliApp.Flags = []cli.Flag{
cli.StringFlag{
Name: "helm-binary, b",
Usage: "path to helm binary",
@@ -87,7 +73,7 @@ func main() {
},
cli.StringFlag{
Name: "namespace, n",
Usage: "Set namespace. Uses the namespace set in the context by default",
Usage: "Set namespace. Uses the namespace set in the context by default, and is available in templates as {{ .Namespace }}",
},
cli.StringSliceFlag{
Name: "selector, l",
@@ -96,10 +82,14 @@ func main() {
--selector tier=frontend,tier!=proxy --selector tier=backend. Will match all frontend, non-proxy releases AND all backend releases.
The name of a release can be used as a label. --selector name=myrelease`,
},
cli.BoolFlag{
Name: "interactive, i",
Usage: "Request confirmation before attempting to modify clusters",
},
}
app.Before = configureLogging
app.Commands = []cli.Command{
cliApp.Before = configureLogging
cliApp.Commands = []cli.Command{
{
Name: "repos",
Usage: "sync repositories from state file (helm repo add && helm repo update)",
@@ -111,22 +101,23 @@ func main() {
},
},
Action: func(c *cli.Context) error {
return findAndIterateOverDesiredStatesUsingFlags(c, func(state *state.HelmState, helm helmexec.Interface) []error {
return cmd.VisitAllDesiredStates(c, func(state *state.HelmState, helm helmexec.Interface, ctx app.Context) (bool, []error) {
args := args.GetArgs(c.String("args"), state)
if len(args) > 0 {
helm.SetExtraArgs(args...)
}
if c.GlobalString("helm-binary") != "" {
helm.SetHelmBinary(c.GlobalString("helm-binary"))
}
return state.SyncRepos(helm)
errs := ctx.SyncReposOnce(state, helm)
ok := len(state.Repositories) > 0 && len(errs) == 0
return ok, errs
})
},
},
{
Name: "charts",
Usage: "sync releases from state file (helm upgrade --install)",
Usage: "DEPRECATED: sync releases from state file (helm upgrade --install)",
Flags: []cli.Flag{
cli.StringFlag{
Name: "args",
@@ -144,20 +135,12 @@ func main() {
},
},
Action: func(c *cli.Context) error {
return findAndIterateOverDesiredStatesUsingFlags(c, func(state *state.HelmState, helm helmexec.Interface) []error {
args := args.GetArgs(c.String("args"), state)
if len(args) > 0 {
helm.SetExtraArgs(args...)
}
if c.GlobalString("helm-binary") != "" {
helm.SetHelmBinary(c.GlobalString("helm-binary"))
}
values := c.StringSlice("values")
workers := c.Int("concurrency")
return state.SyncReleases(helm, values, workers)
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
},
},
{
@@ -174,8 +157,8 @@ func main() {
Usage: "additional value files to be merged into the command",
},
cli.BoolFlag{
Name: "sync-repos",
Usage: "enable a repo sync prior to diffing",
Name: "skip-deps",
Usage: "skip running `helm repo update` and `helm dependency build`",
},
cli.BoolFlag{
Name: "detailed-exitcode",
@@ -192,8 +175,61 @@ func main() {
},
},
Action: func(c *cli.Context) error {
return findAndIterateOverDesiredStatesUsingFlags(c, func(state *state.HelmState, helm helmexec.Interface) []error {
return executeDiffCommand(c, state, helm, c.Bool("detailed-exitcode"), c.Bool("suppress-secrets"))
return findAndIterateOverDesiredStatesUsingFlags(c, func(state *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 {
return errs
}
if errs := state.BuildDeps(helm); errs != nil && len(errs) > 0 {
return errs
}
}
if errs := state.PrepareRelease(helm, "diff"); errs != nil && len(errs) > 0 {
return errs
}
_, errs := executeDiffCommand(c, state, helm, c.Bool("detailed-exitcode"), c.Bool("suppress-secrets"))
return errs
})
},
},
{
Name: "template",
Usage: "template releases from state file against env (helm template)",
Flags: []cli.Flag{
cli.StringFlag{
Name: "args",
Value: "",
Usage: "pass args to helm template",
},
cli.StringSliceFlag{
Name: "values",
Usage: "additional value files to be merged into the command",
},
cli.IntFlag{
Name: "concurrency",
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 {
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, "template"); errs != nil && len(errs) > 0 {
return errs
}
return executeTemplateCommand(c, state, helm)
})
},
},
@@ -213,23 +249,21 @@ func main() {
cli.IntFlag{
Name: "concurrency",
Value: 0,
Usage: "maximum number of concurrent helm processes to run, 0 is unlimited",
Usage: "maximum number of concurrent downloads of release charts",
},
},
Action: func(c *cli.Context) error {
return findAndIterateOverDesiredStatesUsingFlags(c, func(state *state.HelmState, helm helmexec.Interface) []error {
args := args.GetArgs(c.String("args"), state)
if len(args) > 0 {
helm.SetExtraArgs(args...)
}
if c.GlobalString("helm-binary") != "" {
helm.SetHelmBinary(c.GlobalString("helm-binary"))
}
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")
return state.LintReleases(helm, values, workers)
if errs := ctx.SyncReposOnce(state, helm); errs != nil && len(errs) > 0 {
return errs
}
if errs := state.PrepareRelease(helm, "lint"); errs != nil && len(errs) > 0 {
return errs
}
return state.LintReleases(helm, values, args, workers)
})
},
},
@@ -251,11 +285,29 @@ func main() {
Value: "",
Usage: "pass args to helm exec",
},
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) []error {
return executeSyncCommand(c, state, helm)
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
}
if errs := st.BuildDeps(helm); errs != nil && len(errs) > 0 {
return errs
}
}
if errs := st.PrepareRelease(helm, "sync"); errs != nil && len(errs) > 0 {
return errs
}
return executeSyncCommand(c, &affectedReleases, st, helm)
})
affectedReleases.DisplayAffectedReleases(c.App.Metadata["logger"].(*zap.SugaredLogger))
return errs
},
},
{
@@ -276,47 +328,90 @@ func main() {
Value: "",
Usage: "pass args to helm exec",
},
cli.BoolFlag{
Name: "auto-approve",
Usage: "Skip interactive approval before applying",
},
cli.BoolFlag{
Name: "suppress-secrets",
Usage: "suppress secrets in the diff output. highly recommended to specify on CI/CD use-cases",
},
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) []error {
errs := executeDiffCommand(c, state, helm, true, c.Bool("suppress-secrets"))
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
}
if errs := st.BuildDeps(helm); errs != nil && len(errs) > 0 {
return errs
}
}
if errs := st.PrepareRelease(helm, "apply"); errs != nil && len(errs) > 0 {
return errs
}
releases, errs := executeDiffCommand(c, st, helm, true, c.Bool("suppress-secrets"))
releasesToBeDeleted, err := st.DetectReleasesToBeDeleted(helm)
if err != nil {
errs = append(errs, err)
}
noError := true
for _, e := range errs {
switch err := e.(type) {
case *state.DiffError:
noError = noError && err.Code == 2
default:
noError = false
}
}
// sync only when there are changes
if len(errs) > 0 {
allErrsIndicateChanges := true
for _, err := range errs {
switch e := err.(type) {
case *exec.ExitError:
status := e.Sys().(syscall.WaitStatus)
// `helm diff --detailed-exitcode` returns 2 when there are changes
allErrsIndicateChanges = allErrsIndicateChanges && status.ExitStatus() == 2
default:
allErrsIndicateChanges = false
if noError {
if len(releases) == 0 && len(releasesToBeDeleted) == 0 {
// TODO better way to get the logger
logger := c.App.Metadata["logger"].(*zap.SugaredLogger)
logger.Infof("")
logger.Infof("No affected releases")
} else {
names := []string{}
for _, r := range releases {
names = append(names, fmt.Sprintf(" %s (%s) UPDATED", r.Name, r.Chart))
}
for _, r := range releasesToBeDeleted {
names = append(names, fmt.Sprintf(" %s (%s) DELETED", r.Name, r.Chart))
}
}
msg := `Do you really want to apply?
msg := fmt.Sprintf(`Affected releases are:
%s
Do you really want to apply?
Helmfile will apply all your changes, as shown above.
`
if allErrsIndicateChanges {
autoApprove := c.Bool("auto-approve")
if autoApprove || !autoApprove && askForConfirmation(msg) {
return executeSyncCommand(c, state, helm)
`, strings.Join(names, "\n"))
interactive := c.GlobalBool("interactive")
if !interactive || interactive && askForConfirmation(msg) {
rs := []state.ReleaseSpec{}
for _, r := range releases {
rs = append(rs, *r)
}
for _, r := range releasesToBeDeleted {
rs = append(rs, *r)
}
st.Releases = rs
return executeSyncCommand(c, &affectedReleases, st, helm)
}
}
}
return errs
})
affectedReleases.DisplayAffectedReleases(c.App.Metadata["logger"].(*zap.SugaredLogger))
return errs
},
},
{
@@ -335,16 +430,13 @@ func main() {
},
},
Action: func(c *cli.Context) error {
return findAndIterateOverDesiredStatesUsingFlags(c, func(state *state.HelmState, helm helmexec.Interface) []error {
return findAndIterateOverDesiredStatesUsingFlags(c, func(state *state.HelmState, helm helmexec.Interface, _ app.Context) []error {
workers := c.Int("concurrency")
args := args.GetArgs(c.String("args"), state)
if len(args) > 0 {
helm.SetExtraArgs(args...)
}
if c.GlobalString("helm-binary") != "" {
helm.SetHelmBinary(c.GlobalString("helm-binary"))
}
return state.ReleaseStatuses(helm, workers)
})
@@ -352,7 +444,7 @@ func main() {
},
{
Name: "delete",
Usage: "delete releases from state file (helm delete)",
Usage: "DEPRECATED: delete releases from state file (helm delete)",
Flags: []cli.Flag{
cli.StringFlag{
Name: "args",
@@ -365,7 +457,8 @@ func main() {
},
},
Action: func(c *cli.Context) error {
return findAndIterateOverDesiredStatesUsingFlags(c, func(state *state.HelmState, helm helmexec.Interface) []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)
@@ -373,12 +466,66 @@ func main() {
helm.SetExtraArgs(args...)
}
if c.GlobalString("helm-binary") != "" {
helm.SetHelmBinary(c.GlobalString("helm-binary"))
names := make([]string, len(state.Releases))
for i, r := range state.Releases {
names[i] = fmt.Sprintf(" %s (%s)", r.Name, r.Chart)
}
return state.DeleteReleases(helm, purge)
msg := fmt.Sprintf(`Affected releases are:
%s
Do you really want to delete?
Helmfile will delete all your releases, as shown above.
`, strings.Join(names, "\n"))
interactive := c.GlobalBool("interactive")
if !interactive || interactive && askForConfirmation(msg) {
return state.DeleteReleases(&affectedReleases, helm, purge)
}
return nil
})
affectedReleases.DisplayAffectedReleases(c.App.Metadata["logger"].(*zap.SugaredLogger))
return errs
},
},
{
Name: "destroy",
Usage: "deletes and then purges releases",
Flags: []cli.Flag{
cli.StringFlag{
Name: "args",
Value: "",
Usage: "pass args to helm exec",
},
},
Action: func(c *cli.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...)
}
names := make([]string, len(state.Releases))
for i, r := range state.Releases {
names[i] = fmt.Sprintf(" %s (%s)", r.Name, r.Chart)
}
msg := fmt.Sprintf(`Affected releases are:
%s
Do you really want to delete?
Helmfile will delete all your releases, as shown above.
`, strings.Join(names, "\n"))
interactive := c.GlobalBool("interactive")
if !interactive || interactive && askForConfirmation(msg) {
return state.DeleteReleases(&affectedReleases, helm, true)
}
return nil
})
affectedReleases.DisplayAffectedReleases(c.App.Metadata["logger"].(*zap.SugaredLogger))
return errs
},
},
{
@@ -399,285 +546,69 @@ func main() {
Value: 300,
Usage: "maximum time for tests to run before being considered failed",
},
cli.IntFlag{
Name: "concurrency",
Value: 0,
Usage: "maximum number of concurrent helm processes to run, 0 is unlimited",
},
},
Action: func(c *cli.Context) error {
return findAndIterateOverDesiredStatesUsingFlags(c, func(state *state.HelmState, helm helmexec.Interface) []error {
return findAndIterateOverDesiredStatesUsingFlags(c, func(state *state.HelmState, helm helmexec.Interface, _ app.Context) []error {
cleanup := c.Bool("cleanup")
timeout := c.Int("timeout")
concurrency := c.Int("concurrency")
args := args.GetArgs(c.String("args"), state)
if len(args) > 0 {
helm.SetExtraArgs(args...)
}
if c.GlobalString("helm-binary") != "" {
helm.SetHelmBinary(c.GlobalString("helm-binary"))
}
return state.TestReleases(helm, cleanup, timeout)
return state.TestReleases(helm, cleanup, timeout, concurrency)
})
},
},
}
err := app.Run(os.Args)
err := cliApp.Run(os.Args)
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(3)
}
}
func executeSyncCommand(c *cli.Context, state *state.HelmState, helm helmexec.Interface) []error {
if errs := state.SyncRepos(helm); errs != nil && len(errs) > 0 {
return errs
}
if errs := state.UpdateDeps(helm); errs != nil && len(errs) > 0 {
return errs
}
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...)
}
if c.GlobalString("helm-binary") != "" {
helm.SetHelmBinary(c.GlobalString("helm-binary"))
}
values := c.StringSlice("values")
workers := c.Int("concurrency")
return state.SyncReleases(helm, values, workers)
return state.SyncReleases(affectedReleases, helm, values, workers)
}
func executeDiffCommand(c *cli.Context, state *state.HelmState, helm helmexec.Interface, detailedExitCode, suppressSecrets bool) []error {
func executeTemplateCommand(c *cli.Context, state *state.HelmState, helm helmexec.Interface) []error {
args := args.GetArgs(c.String("args"), state)
values := c.StringSlice("values")
workers := c.Int("concurrency")
return state.TemplateReleases(helm, values, args, workers)
}
func executeDiffCommand(c *cli.Context, st *state.HelmState, helm helmexec.Interface, detailedExitCode, suppressSecrets bool) ([]*state.ReleaseSpec, []error) {
args := args.GetArgs(c.String("args"), st)
if len(args) > 0 {
helm.SetExtraArgs(args...)
}
if c.GlobalString("helm-binary") != "" {
helm.SetHelmBinary(c.GlobalString("helm-binary"))
}
if c.Bool("sync-repos") {
if errs := state.SyncRepos(helm); errs != nil && len(errs) > 0 {
return errs
}
}
values := c.StringSlice("values")
workers := c.Int("concurrency")
triggerCleanupEvents := c.Command.HasName("diff")
return state.DiffReleases(helm, values, workers, detailedExitCode, suppressSecrets)
return st.DiffReleases(helm, values, workers, detailedExitCode, suppressSecrets, triggerCleanupEvents)
}
func findAndIterateOverDesiredStatesUsingFlags(c *cli.Context, converge func(*state.HelmState, helmexec.Interface) []error) error {
fileOrDir := c.GlobalString("file")
kubeContext := c.GlobalString("kube-context")
namespace := c.GlobalString("namespace")
selectors := c.GlobalStringSlice("selector")
logger := c.App.Metadata["logger"].(*zap.SugaredLogger)
env := c.GlobalString("environment")
if env == "" {
env = state.DefaultEnv
}
return findAndIterateOverDesiredStates(fileOrDir, converge, kubeContext, namespace, selectors, env, logger)
}
func findAndIterateOverDesiredStates(fileOrDir string, converge func(*state.HelmState, helmexec.Interface) []error, kubeContext, namespace string, selectors []string, env string, logger *zap.SugaredLogger) error {
desiredStateFiles, err := findDesiredStateFiles(fileOrDir)
if err != nil {
return err
}
allSelectorNotMatched := true
for _, f := range desiredStateFiles {
logger.Debugf("Processing %s", f)
yamlBuf, err := tmpl.NewFileRenderer(ioutil.ReadFile, "", environment.EmptyEnvironment).RenderTemplateFileToBuffer(f)
if err != nil {
return err
}
state, helm, noReleases, err := loadDesiredStateFromFile(
yamlBuf.Bytes(),
f,
kubeContext,
namespace,
selectors,
env,
logger,
)
if err != nil {
return err
}
if len(state.Helmfiles) > 0 {
for _, globPattern := range state.Helmfiles {
matches, err := filepath.Glob(globPattern)
if err != nil {
return fmt.Errorf("failed processing %s: %v", globPattern, err)
}
sort.Strings(matches)
for _, m := range matches {
if err := findAndIterateOverDesiredStates(m, converge, kubeContext, namespace, selectors, env, logger); err != nil {
return fmt.Errorf("failed processing %s: %v", globPattern, err)
}
}
}
return nil
}
allSelectorNotMatched = allSelectorNotMatched && noReleases
if noReleases {
continue
}
errs := converge(state, helm)
if err := clean(state, errs); err != nil {
return err
}
}
if allSelectorNotMatched {
logger.Error("specified selector did not match any releases in any helmfile")
os.Exit(2)
}
return nil
}
func findDesiredStateFiles(specifiedPath string) ([]string, error) {
var helmfileDir string
if specifiedPath != "" {
if fileExistsAt(specifiedPath) {
return []string{specifiedPath}, nil
} else if directoryExistsAt(specifiedPath) {
helmfileDir = specifiedPath
} else {
return []string{}, fmt.Errorf("specified state file %s is not found", specifiedPath)
}
} else {
var defaultFile string
if fileExistsAt(DefaultHelmfile) {
defaultFile = DefaultHelmfile
} else if fileExistsAt(DeprecatedHelmfile) {
log.Printf(
"warn: %s is being loaded: %s is deprecated in favor of %s. See https://github.com/roboll/helmfile/issues/25 for more information",
DeprecatedHelmfile,
DeprecatedHelmfile,
DefaultHelmfile,
)
defaultFile = DeprecatedHelmfile
}
if directoryExistsAt(DefaultHelmfileDirectory) {
if defaultFile != "" {
return []string{}, fmt.Errorf("configuration conlict error: you can have either %s or %s, but not both", defaultFile, DefaultHelmfileDirectory)
}
helmfileDir = DefaultHelmfileDirectory
} 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)
}
}
files, err := filepath.Glob(filepath.Join(helmfileDir, "*.yaml"))
if err != nil {
return []string{}, err
}
sort.Slice(files, func(i, j int) bool {
return files[i] < files[j]
})
return files, nil
}
func fileExistsAt(path string) bool {
fileInfo, err := os.Stat(path)
return err == nil && fileInfo.Mode().IsRegular()
}
func directoryExistsAt(path string) bool {
fileInfo, err := os.Stat(path)
return err == nil && fileInfo.Mode().IsDir()
}
func loadDesiredStateFromFile(yaml []byte, file string, kubeContext, namespace string, labels []string, env string, logger *zap.SugaredLogger) (*state.HelmState, helmexec.Interface, bool, error) {
st, err := state.CreateFromYaml(yaml, file, env, logger)
if err != nil {
return nil, nil, false, fmt.Errorf("failed to read %s: %v", file, err)
}
if st.Context != "" {
if kubeContext != "" {
log.Printf("err: Cannot use option --kube-context and set attribute context.")
os.Exit(1)
}
kubeContext = st.Context
}
if namespace != "" {
if st.Namespace != "" {
log.Printf("err: Cannot use option --namespace and set attribute namespace.")
os.Exit(1)
}
st.Namespace = namespace
}
if len(labels) > 0 {
err = st.FilterReleases(labels)
if err != nil {
log.Print(err)
return nil, nil, true, nil
}
}
releaseNameCounts := map[string]int{}
for _, r := range st.Releases {
releaseNameCounts[r.Name] += 1
}
for name, c := range releaseNameCounts {
if c > 1 {
return nil, nil, false, fmt.Errorf("duplicate release \"%s\" found: there were %d releases named \"%s\" matching specified selector", name, c, name)
}
}
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)
}()
return st, helmexec.New(logger, kubeContext), len(st.Releases) == 0, nil
}
func clean(st *state.HelmState, errs []error) error {
if errs == nil {
errs = []error{}
}
cleanErrs := st.Clean()
if cleanErrs != nil {
errs = append(errs, cleanErrs...)
}
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)
default:
fmt.Printf("err: %v\n", e)
}
}
switch e := errs[0].(type) {
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)
os.Exit(status.ExitStatus())
default:
os.Exit(1)
}
}
return nil
func findAndIterateOverDesiredStatesUsingFlags(c *cli.Context, converge func(*state.HelmState, helmexec.Interface, app.Context) []error) error {
return cmd.FindAndIterateOverDesiredStatesUsingFlagsWithReverse(c, false, converge)
}
-26
View File
@@ -1,26 +0,0 @@
package main
import "testing"
// See https://github.com/roboll/helmfile/issues/193
func TestReadFromYaml_DuplicateReleaseName(t *testing.T) {
yamlFile := "example/path/to/yaml/file"
yamlContent := []byte(`releases:
- name: myrelease1
chart: mychart1
labels:
stage: pre
foo: bar
- name: myrelease1
chart: mychart2
labels:
stage: post
`)
_, _, _, err := loadDesiredStateFromFile(yamlContent, yamlFile, "default", "default", []string{}, "default", logger)
if err == nil {
t.Error("error expected but not happened")
}
if err.Error() != "duplicate release \"myrelease1\" found: there were 2 releases named \"myrelease1\" matching specified selector" {
t.Errorf("unexpected error happened: %v", err)
}
}
+407
View File
@@ -0,0 +1,407 @@
package app
import (
"fmt"
"io/ioutil"
"log"
"os"
"os/signal"
"github.com/roboll/helmfile/helmexec"
"github.com/roboll/helmfile/state"
"go.uber.org/zap"
"path/filepath"
"sort"
"syscall"
)
type App struct {
KubeContext string
Logger *zap.SugaredLogger
Reverse bool
Env string
Namespace string
Selectors []string
readFile func(string) ([]byte, error)
glob func(string) ([]string, error)
abs func(string) (string, error)
fileExistsAt func(string) bool
directoryExistsAt func(string) bool
getwd func() (string, error)
chdir func(string) error
}
func Init(app *App) *App {
app.readFile = ioutil.ReadFile
app.glob = filepath.Glob
app.abs = filepath.Abs
app.getwd = os.Getwd
app.chdir = os.Chdir
app.fileExistsAt = fileExistsAt
app.directoryExistsAt = directoryExistsAt
return app
}
func (a *App) within(dir string, do func() error) error {
if dir == "." {
return do()
}
prev, err := a.getwd()
if err != nil {
return fmt.Errorf("failed getting current working direcotyr: %v", err)
}
absDir, err := a.abs(dir)
if err != nil {
return err
}
a.Logger.Debugf("changing working directory to \"%s\"", absDir)
if err := a.chdir(absDir); err != nil {
return fmt.Errorf("failed changing working directory to \"%s\": %v", absDir, err)
}
appErr := do()
a.Logger.Debugf("changing working directory back to \"%s\"", prev)
if chdirBackErr := a.chdir(prev); chdirBackErr != nil {
if appErr != nil {
a.Logger.Warnf("%v", appErr)
}
return fmt.Errorf("failed chaging working directory back to \"%s\": %v", prev, chdirBackErr)
}
return appErr
}
func (a *App) visitStateFiles(fileOrDir string, do func(string) error) error {
desiredStateFiles, err := a.findDesiredStateFiles(fileOrDir)
if err != nil {
return err
}
for _, relPath := range desiredStateFiles {
var file string
var dir string
if a.directoryExistsAt(relPath) {
file = relPath
dir = relPath
} else {
file = filepath.Base(relPath)
dir = filepath.Dir(relPath)
}
a.Logger.Debugf("processing file \"%s\" in directory \"%s\"", file, dir)
err := a.within(dir, func() error {
return do(file)
})
if err != nil {
return err
}
}
return nil
}
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(
yamlBuf.Bytes(),
f,
a.Namespace,
a.Env,
)
helm := helmexec.New(a.Logger, a.KubeContext)
if err != nil {
switch stateLoadErr := err.(type) {
// Addresses https://github.com/roboll/helmfile/issues/279
case *state.StateLoadError:
switch stateLoadErr.Cause.(type) {
case *state.UndefinedEnvError:
return nil
default:
return err
}
default:
return err
}
}
st.Selectors = selector
if len(st.Helmfiles) > 0 {
noMatchInSubHelmfiles := true
for _, 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.Path, err)
}
} else {
noMatchInSubHelmfiles = false
}
}
noMatchInHelmfiles = noMatchInHelmfiles && noMatchInSubHelmfiles
}
templated, tmplErr := st.ExecuteTemplates()
if tmplErr != nil {
return fmt.Errorf("failed executing release templates in \"%s\": %v", f, tmplErr)
}
processed, errs := converge(templated, helm)
noMatchInHelmfiles = noMatchInHelmfiles && !processed
return clean(templated, errs)
})
if err != nil {
return err
}
if noMatchInHelmfiles {
return &NoMatchingHelmfileError{selectors: a.Selectors, env: a.Env}
}
return nil
}
func (a *App) VisitDesiredStatesWithReleasesFiltered(fileOrDir string, converge func(*state.HelmState, helmexec.Interface) []error) error {
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}
}
}
type Key struct {
TillerNamespace, Name string
}
releaseNameCounts := map[Key]int{}
for _, r := range st.Releases {
tillerNamespace := st.HelmDefaults.TillerNamespace
if r.TillerNamespace != "" {
tillerNamespace = r.TillerNamespace
}
releaseNameCounts[Key{tillerNamespace, r.Name}]++
}
for name, c := range releaseNameCounts {
if c > 1 {
return false, []error{fmt.Errorf("duplicate release \"%s\" found in \"%s\": there were %d releases named \"%s\" matching specified selector", name.Name, name.TillerNamespace, c, name.Name)}
}
}
errs := converge(st, helm)
processed := len(st.Releases) != 0 && len(errs) == 0
return processed, errs
})
if err != nil {
return err
}
return nil
}
func (a *App) findStateFilesInAbsPaths(specifiedPath string) ([]string, error) {
rels, err := a.findDesiredStateFiles(specifiedPath)
if err != nil {
return rels, err
}
files := make([]string, len(rels))
for i := range rels {
files[i], err = filepath.Abs(rels[i])
if err != nil {
return []string{}, err
}
}
return files, nil
}
func (a *App) findDesiredStateFiles(specifiedPath string) ([]string, error) {
var helmfileDir string
if specifiedPath != "" {
if a.fileExistsAt(specifiedPath) {
return []string{specifiedPath}, nil
} else if a.directoryExistsAt(specifiedPath) {
helmfileDir = specifiedPath
} else {
return []string{}, fmt.Errorf("specified state file %s is not found", specifiedPath)
}
} else {
var defaultFile string
if a.fileExistsAt(DefaultHelmfile) {
defaultFile = DefaultHelmfile
} else if a.fileExistsAt(DeprecatedHelmfile) {
log.Printf(
"warn: %s is being loaded: %s is deprecated in favor of %s. See https://github.com/roboll/helmfile/issues/25 for more information",
DeprecatedHelmfile,
DeprecatedHelmfile,
DefaultHelmfile,
)
defaultFile = DeprecatedHelmfile
}
if a.directoryExistsAt(DefaultHelmfileDirectory) {
if defaultFile != "" {
return []string{}, fmt.Errorf("configuration conlict error: you can have either %s or %s, but not both", defaultFile, DefaultHelmfileDirectory)
}
helmfileDir = DefaultHelmfileDirectory
} else if defaultFile != "" {
return []string{defaultFile}, nil
} else {
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, "*.y*ml"))
if err != nil {
return []string{}, err
}
sort.Slice(files, func(i, j int) bool {
return files[i] < files[j]
})
return files, nil
}
func fileExistsAt(path string) bool {
fileInfo, err := os.Stat(path)
return err == nil && fileInfo.Mode().IsRegular()
}
func directoryExistsAt(path string) bool {
fileInfo, err := os.Stat(path)
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
}
helmfiles := []state.SubHelmfileSpec{}
for _, hf := range st.Helmfiles {
globPattern := hf.Path
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)
for _, match := range matches {
newHelmfile := hf
newHelmfile.Path = match
helmfiles = append(helmfiles, newHelmfile)
}
}
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
}
func clean(st *state.HelmState, errs []error) error {
if errs == nil {
errs = []error{}
}
cleanErrs := st.Clean()
if cleanErrs != nil {
errs = append(errs, cleanErrs...)
}
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)
default:
fmt.Printf("err: %v\n", e)
}
}
return errs[0]
}
return nil
}
+654
View File
@@ -0,0 +1,654 @@
package app
import (
"fmt"
"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)
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
return app
}
func newTestFs(files map[string]string) *testFs {
dirs := map[string]bool{}
for abs, _ := range files {
d := filepath.Dir(abs)
dirs[d] = true
}
return &testFs{
wd: "/path/to",
dirs: dirs,
files: files,
}
}
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)]
}
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)]
}
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)]
}
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)
}
}
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
func TestVisitDesiredStatesWithReleasesFiltered(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
`,
}
noop := func(st *state.HelmState, helm helmexec.Interface) []error {
return []error{}
}
testcases := []struct {
name string
expectErr bool
}{
{name: "prometheus", expectErr: false},
{name: "zipkin", expectErr: false},
{name: "grafana", expectErr: false},
{name: "elasticsearch", expectErr: true},
}
for _, testcase := range testcases {
app := appWithFs(&App{
KubeContext: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
Selectors: []string{fmt.Sprintf("name=%s", testcase.name)},
Namespace: "",
Env: "default",
}, files)
err := app.VisitDesiredStatesWithReleasesFiltered(
"helmfile.yaml", noop,
)
if testcase.expectErr && err == nil {
t.Errorf("error expected but not happened for name=%s", testcase.name)
} else if !testcase.expectErr && err != nil {
t.Errorf("unexpected error for name=%s: %v", testcase.name, err)
}
}
}
// See https://github.com/roboll/helmfile/issues/320
func TestVisitDesiredStatesWithReleasesFiltered_UndefinedEnv(t *testing.T) {
files := map[string]string{
"/path/to/helmfile.yaml": `
environments:
prod:
helmfiles:
- helmfile.d/a*.yaml
`,
"/path/to/helmfile.d/a1.yaml": `
environments:
prod:
releases:
- name: zipkin
chart: stable/zipkin
`,
}
noop := func(st *state.HelmState, helm helmexec.Interface) []error {
return []error{}
}
testcases := []struct {
name string
expectErr bool
}{
{name: "undefined_env", expectErr: true},
{name: "default", expectErr: false},
{name: "prod", expectErr: false},
}
for _, testcase := range testcases {
app := appWithFs(&App{
KubeContext: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
Namespace: "",
Selectors: []string{},
Env: testcase.name,
}, files)
err := app.VisitDesiredStatesWithReleasesFiltered(
"helmfile.yaml", noop,
)
if testcase.expectErr && err == nil {
t.Errorf("error expected but not happened for environment=%s", testcase.name)
} else if !testcase.expectErr && err != nil {
t.Errorf("unexpected error for environment=%s: %v", testcase.name, err)
}
}
}
// See https://github.com/roboll/helmfile/issues/322
func TestVisitDesiredStatesWithReleasesFiltered_Selectors(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": `
helmDefaults:
tillerNamespace: zoo
releases:
- name: grafana
chart: stable/grafana
- name: foo
chart: charts/foo
labels:
duplicated: yes
- name: foo
chart: charts/foo
labels:
duplicated: yes
- name: bar
chart: charts/foo
tillerNamespace: bar1
labels:
duplicatedOK: yes
- name: bar
chart: charts/foo
tillerNamespace: bar2
labels:
duplicatedOK: yes
`,
}
testcases := []struct {
label string
expectedCount int
expectErr bool
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"},
// 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: "duplicatedOK=yes", expectedCount: 2, expectErr: false},
}
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("error expected but not happened for selector %s", testcase.label)
} else if err.Error() != testcase.errMsg {
t.Errorf("unexpected error message: expected=\"%s\", actual=\"%s\"", testcase.errMsg, err.Error())
}
} else if !testcase.expectErr && err != nil {
t.Errorf("unexpected error for selector %s: %v", testcase.label, err)
}
if len(actual) != testcase.expectedCount {
t.Errorf("unexpected release count for selector %s: expected=%d, actual=%d", testcase.label, testcase.expectedCount, len(actual))
}
}
}
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{
"/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
- name: elasticsearch
chart: stable/elasticsearch
`,
"/path/to/helmfile.d/b.yaml": `
releases:
- name: grafana
chart: stable/grafana
`,
}
expected := []string{"grafana", "elasticsearch", "prometheus", "zipkin"}
testcases := []struct {
reverse bool
expected []string
}{
{reverse: false, expected: []string{"zipkin", "prometheus", "elasticsearch", "grafana"}},
{reverse: true, expected: []string{"grafana", "elasticsearch", "prometheus", "zipkin"}},
}
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"),
Reverse: testcase.reverse,
Namespace: "",
Selectors: []string{},
Env: "default",
}, files)
err := app.VisitDesiredStatesWithReleasesFiltered(
"helmfile.yaml", collectReleases,
)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if !reflect.DeepEqual(testcase.expected, actual) {
t.Errorf("releases did not match: expected=%v actual=%v", expected, actual)
}
}
}
func TestLoadDesiredStateFromYaml_DuplicateReleaseName(t *testing.T) {
yamlFile := "example/path/to/yaml/file"
yamlContent := []byte(`releases:
- name: myrelease1
chart: mychart1
labels:
stage: pre
foo: bar
- name: myrelease1
chart: mychart2
labels:
stage: post
`)
app := &App{
readFile: ioutil.ReadFile,
glob: filepath.Glob,
abs: filepath.Abs,
KubeContext: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
}
_, err := app.loadDesiredStateFromYaml(yamlContent, yamlFile, "default", "default")
if err != nil {
t.Error("unexpected error")
}
}
+18
View File
@@ -0,0 +1,18 @@
package app
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 isExplicitSelectorInheritanceEnabled() bool {
return os.Getenv(ExperimentalEnvVar) == "true" || 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(t, is.Equal(os.Getenv(ExperimentalEnvVar), ""))
Check(t, !isExplicitSelectorInheritanceEnabled())
//check for env var ExperimentalEnvVar set to true
defer env.Patch(t, ExperimentalEnvVar, "true")()
Check(t, isExplicitSelectorInheritanceEnabled())
//check for env var ExperimentalEnvVar set to anything
defer env.Patch(t, ExperimentalEnvVar, "foo")()
Check(t, !isExplicitSelectorInheritanceEnabled())
//check for env var ExperimentalEnvVar set to ExperimentalSelectorExplicit
defer env.Patch(t, ExperimentalEnvVar, ExperimentalSelectorExplicit)()
Check(t, isExplicitSelectorInheritanceEnabled())
}
+33
View File
@@ -0,0 +1,33 @@
package app
import "github.com/roboll/helmfile/state"
type Context struct {
updatedRepos map[string]struct{}
}
func NewContext() Context {
return Context{
updatedRepos: map[string]struct{}{},
}
}
func (ctx Context) SyncReposOnce(st *state.HelmState, helm state.RepoUpdater) []error {
var errs []error
allUpdated := true
for _, r := range st.Repositories {
_, exists := ctx.updatedRepos[r.Name]
allUpdated = allUpdated && exists
}
if !allUpdated {
errs = st.SyncRepos(helm)
for _, r := range st.Repositories {
ctx.updatedRepos[r.Name] = struct{}{}
}
}
return errs
}
+19
View File
@@ -0,0 +1,19 @@
package app
import (
"fmt"
"strings"
)
type NoMatchingHelmfileError struct {
selectors []string
env string
}
func (e *NoMatchingHelmfileError) Error() string {
return fmt.Sprintf(
"err: no releases found that matches specified selector(%s) and environment(%s), in any helmfile",
strings.Join(e.selectors, ", "),
e.env,
)
}
+80
View File
@@ -0,0 +1,80 @@
package app
import (
"bytes"
"fmt"
"github.com/roboll/helmfile/environment"
"github.com/roboll/helmfile/state"
"github.com/roboll/helmfile/tmpl"
"go.uber.org/zap"
"path/filepath"
"strings"
)
func prependLineNumbers(text string) string {
buf := bytes.NewBufferString("")
lines := strings.Split(text, "\n")
for i, line := range lines {
buf.WriteString(fmt.Sprintf("%2d: %s\n", i, line))
}
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)}
tmplData := state.EnvironmentTemplateData{Environment: firstPassEnv, Namespace: r.namespace}
firstPassRenderer := tmpl.NewFirstPassRenderer(filepath.Dir(r.filename), 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)))
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.Strict = false
// create preliminary state, as we may have an environment. Tolerate errors.
prestate, err := c.CreateFromYaml(yamlBuf.Bytes(), r.filename, r.env)
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()))
}
if prestate != nil {
firstPassEnv = prestate.Env
}
return firstPassEnv
}
func (r *twoPassRenderer) renderTemplate(content []byte) (*bytes.Buffer, error) {
// try a first pass render. This will always succeed, but can produce a limited env
firstPassEnv := r.renderEnvironment(content)
tmplData := state.EnvironmentTemplateData{Environment: firstPassEnv, Namespace: r.namespace}
secondPassRenderer := tmpl.NewFileRenderer(r.reader, filepath.Dir(r.filename), 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)))
}
return nil, err
}
if r.logger != nil {
r.logger.Debugf("second-pass rendering result of \"%s\":\n%s", r.filename, prependLineNumbers(yamlBuf.String()))
}
return yamlBuf, nil
}
+256
View File
@@ -0,0 +1,256 @@
package app
import (
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"github.com/roboll/helmfile/helmexec"
"github.com/roboll/helmfile/state"
"gopkg.in/yaml.v2"
)
func makeRenderer(readFile func(string) ([]byte, error), env string) *twoPassRenderer {
return &twoPassRenderer{
reader: readFile,
env: env,
namespace: "namespace",
filename: "",
logger: helmexec.NewLogger(os.Stdout, "debug"),
abs: filepath.Abs,
}
}
func TestReadFromYaml_MakeEnvironmentHasNoSideEffects(t *testing.T) {
yamlContent := []byte(`
environments:
staging:
values:
- default/values.yaml
production:
releases:
- name: {{ readFile "other/default/values.yaml" }}
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
}
r := makeRenderer(fileReader, "staging")
yamlBuf, err := r.renderTemplate(yamlContent)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
var state state.HelmState
err = yaml.Unmarshal(yamlBuf.Bytes(), &state)
if fileReaderCalls > 2 {
t.Error("reader should be called only twice")
}
if state.Releases[0].Name != "SecondPass" {
t.Errorf("release name should have ben set as SecondPass")
}
}
func TestReadFromYaml_RenderTemplate(t *testing.T) {
defaultValuesYaml := []byte(`
releaseName: "hello"
conditionalReleaseTag: "yes"
`)
yamlContent := []byte(`
environments:
staging:
values:
- default/values.yaml
production:
releases:
- name: {{ .Environment.Values.releaseName }}
chart: mychart1
{{ if (eq .Environment.Values.conditionalReleaseTag "yes") }}
- name: conditionalRelease
{{ end }}
`)
// 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
}
r := makeRenderer(fileReader, "staging")
// test the double rendering
yamlBuf, err := r.renderTemplate(yamlContent)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
var state state.HelmState
err = yaml.Unmarshal(yamlBuf.Bytes(), &state)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if len(state.Releases) != 2 {
t.Fatal("there should be 2 releases")
}
if state.Releases[0].Name != "hello" {
t.Errorf("release name should be hello")
}
if state.Releases[1].Name != "conditionalRelease" {
t.Error("conditional release should have been present")
}
}
func TestReadFromYaml_RenderTemplateWithValuesReferenceError(t *testing.T) {
defaultValuesYaml := []byte("")
yamlContent := []byte(`
environments:
staging:
values:
- default/values.yaml
production:
{{ if (eq .Environment.Values.releaseName "a") }} # line 8
releases:
- name: a
chart: mychart1
{{ end }}
`)
// make a reader that returns a simulated context
fileReader := func(filename string) ([]byte, error) {
return defaultValuesYaml, nil
}
r := makeRenderer(fileReader, "staging")
// test the double rendering
_, err := r.renderTemplate(yamlContent)
if !strings.Contains(err.Error(), "stringTemplate:8") {
t.Fatalf("error should contain a stringTemplate error (reference to unknow key) %v", err)
}
}
// This test shows that a gotmpl reference will get rendered correctly
// even if the pre-render disables the readFile and exec functions.
// This does not apply to .gotmpl files, which is a nice side-effect.
func TestReadFromYaml_RenderTemplateWithGotmpl(t *testing.T) {
defaultValuesYamlGotmpl := []byte(`
releaseName: {{ readFile "nonIgnoredFile" }}
`)
yamlContent := []byte(`
environments:
staging:
values:
- values.yaml.gotmpl
production:
{{ if (eq .Environment.Values.releaseName "release-a") }} # line 8
releases:
- name: a
chart: mychart1
{{ end }}
`)
fileReader := func(filename string) ([]byte, error) {
if strings.HasSuffix(filename, "nonIgnoredFile") {
return []byte("release-a"), nil
}
return defaultValuesYamlGotmpl, nil
}
r := makeRenderer(fileReader, "staging")
rendered, _ := r.renderTemplate(yamlContent)
var state state.HelmState
yaml.Unmarshal(rendered.Bytes(), &state)
if len(state.Releases) != 1 {
t.Fatal("there should be 1 release")
}
if state.Releases[0].Name != "a" {
t.Fatal("release should have been declared")
}
}
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
}
r := makeRenderer(fileReader, "staging")
yamlBuf, err := r.renderTemplate(yamlContent)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
var state state.HelmState
err = yaml.Unmarshal(yamlBuf.Bytes(), &state)
if state.Releases[0].Name != "namespace-myrelease" {
t.Errorf("release name should be namespace-myrelease")
}
}
func TestReadFromYaml_HelfileShouldBeResilentToTemplateErrors(t *testing.T) {
yamlContent := []byte(`environments:
staging:
production:
releases:
{{ if (eq .Environment.Name "production" }} # notice syntax error: unclosed left paren
- name: prod-myrelease
{{ else }}
- name: myapp
{{ end }}
chart: mychart
`)
fileReader := func(filename string) ([]byte, error) {
return yamlContent, nil
}
r := makeRenderer(fileReader, "staging")
_, err := r.renderTemplate(yamlContent)
if err == nil {
t.Fatalf("wanted error, none returned")
}
}
+123 -28
View File
@@ -1,30 +1,98 @@
package state
import (
"bytes"
"fmt"
"github.com/imdario/mergo"
"github.com/roboll/helmfile/environment"
"github.com/roboll/helmfile/helmexec"
"github.com/roboll/helmfile/valuesfile"
"go.uber.org/zap"
"gopkg.in/yaml.v2"
"io"
"io/ioutil"
"os"
"path/filepath"
"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"
)
func CreateFromYaml(content []byte, file string, env string, logger *zap.SugaredLogger) (*HelmState, error) {
return createFromYamlWithFileReader(content, file, env, logger, ioutil.ReadFile)
type StateLoadError struct {
msg string
Cause error
}
func createFromYamlWithFileReader(content []byte, file string, env string, logger *zap.SugaredLogger, readFile func(string) ([]byte, error)) (*HelmState, error) {
func (e *StateLoadError) Error() string {
return fmt.Sprintf("%s: %v", e.msg, e.Cause)
}
type UndefinedEnvError struct {
msg string
}
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 {
logger *zap.SugaredLogger
readFile func(string) ([]byte, error)
abs func(string) (string, error)
Strict bool
}
func NewCreator(logger *zap.SugaredLogger, readFile func(string) ([]byte, error), abs func(string) (string, error)) *creator {
return &creator{
logger: logger,
readFile: readFile,
abs: abs,
Strict: true,
}
}
func (c *creator) CreateFromYaml(content []byte, file string, env string) (*HelmState, error) {
var state HelmState
state.basePath, _ = filepath.Abs(filepath.Dir(file))
if err := yaml.UnmarshalStrict(content, &state); err != nil {
return nil, err
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
decoder := yaml.NewDecoder(bytes.NewReader(content))
if !c.Strict {
decoder.SetStrict(false)
} else {
decoder.SetStrict(true)
}
i := 0
for {
i++
var intermediate HelmState
err := decoder.Decode(&intermediate)
if err == io.EOF {
break
} else if err != nil {
return nil, &StateLoadError{fmt.Sprintf("failed to read %s: reading document at index %d", file, i), err}
}
if err := mergo.Merge(&state, &intermediate, mergo.WithAppendSlice); err != nil {
return nil, &StateLoadError{fmt.Sprintf("failed to read %s: merging document at index %d", file, i), err}
}
}
if len(state.DeprecatedReleases) > 0 {
if len(state.Releases) > 0 {
@@ -34,26 +102,44 @@ func createFromYamlWithFileReader(content []byte, file string, env string, logge
state.DeprecatedReleases = []ReleaseSpec{}
}
state.logger = logger
e, err := state.loadEnv(env, readFile)
if err != nil {
return nil, err
if state.DeprecatedContext != "" && state.HelmDefaults.KubeContext == "" {
state.HelmDefaults.KubeContext = state.DeprecatedContext
}
state.env = *e
state.readFile = readFile
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) {
_, err := os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
return true, nil
}
return &state, nil
}
func (state *HelmState) loadEnv(name string, readFile func(string) ([]byte, error)) (*environment.Environment, error) {
func (st *HelmState) loadEnv(name string, readFile func(string) ([]byte, error)) (*environment.Environment, error) {
envVals := map[string]interface{}{}
envSpec, ok := state.Environments[name]
envSpec, ok := st.Environments[name]
if ok {
r := valuesfile.NewRenderer(readFile, state.basePath, environment.EmptyEnvironment)
for _, envvalFile := range envSpec.Values {
bytes, err := r.RenderToBytes(filepath.Join(state.basePath, envvalFile))
envvalFullPath := filepath.Join(st.basePath, envvalFile)
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)
}
@@ -67,14 +153,23 @@ func (state *HelmState) loadEnv(name string, readFile func(string) ([]byte, erro
}
if len(envSpec.Secrets) > 0 {
helm := helmexec.New(state.logger, "")
helm := helmexec.New(st.logger, "")
for _, secFile := range envSpec.Secrets {
path := filepath.Join(state.basePath, secFile)
path := filepath.Join(st.basePath, secFile)
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
}
@@ -92,7 +187,7 @@ func (state *HelmState) loadEnv(name string, readFile func(string) ([]byte, erro
}
}
} else if name != DefaultEnv {
return nil, fmt.Errorf("environment \"%s\" is not defined in \"%s\"", name, state.FilePath)
return nil, &UndefinedEnvError{msg: fmt.Sprintf("environment \"%s\" is not defined", name)}
}
return &environment.Environment{Name: name, Values: envVals}, nil
+105 -9
View File
@@ -2,8 +2,12 @@ package state
import (
"fmt"
"path/filepath"
"reflect"
"testing"
. "gotest.tools/assert"
"gotest.tools/assert/cmp"
)
func TestReadFromYaml(t *testing.T) {
@@ -13,7 +17,7 @@ func TestReadFromYaml(t *testing.T) {
namespace: mynamespace
chart: mychart
`)
state, err := CreateFromYaml(yamlContent, yamlFile, DefaultEnv, logger)
state, err := createFromYaml(yamlContent, yamlFile, DefaultEnv, logger)
if err != nil {
t.Errorf("unxpected error: %v", err)
}
@@ -36,7 +40,7 @@ func TestReadFromYaml_InexistentEnv(t *testing.T) {
namespace: mynamespace
chart: mychart
`)
_, err := CreateFromYaml(yamlContent, yamlFile, "production", logger)
_, err := createFromYaml(yamlContent, yamlFile, "production", logger)
if err == nil {
t.Error("expected error")
}
@@ -97,12 +101,12 @@ bar: {{ readFile "bar.txt" }}
return nil, fmt.Errorf("unexpected filename: %s", filename)
}
state, err := createFromYamlWithFileReader(yamlContent, yamlFile, "production", logger, readFile)
state, err := NewCreator(logger, readFile, filepath.Abs).CreateFromYaml(yamlContent, yamlFile, "production")
if err != nil {
t.Errorf("unexpected error: %v", err)
}
actual := state.env.Values
actual := state.Env.Values
if !reflect.DeepEqual(actual, expected) {
t.Errorf("unexpected environment values: expected=%v, actual=%v", expected, actual)
}
@@ -125,7 +129,7 @@ func TestReadFromYaml_StrictUnmarshalling(t *testing.T) {
namespace: mynamespace
releases: mychart
`)
_, err := CreateFromYaml(yamlContent, yamlFile, DefaultEnv, logger)
_, err := createFromYaml(yamlContent, yamlFile, DefaultEnv, logger)
if err == nil {
t.Error("expected an error for wrong key 'releases' which is not in struct")
}
@@ -137,7 +141,7 @@ func TestReadFromYaml_DeprecatedReleaseReferences(t *testing.T) {
- name: myrelease
chart: mychart
`)
state, err := CreateFromYaml(yamlContent, yamlFile, DefaultEnv, logger)
state, err := createFromYaml(yamlContent, yamlFile, DefaultEnv, logger)
if err != nil {
t.Errorf("unxpected error: %v", err)
}
@@ -159,7 +163,7 @@ releases:
- name: myrelease2
chart: mychart2
`)
_, err := CreateFromYaml(yamlContent, yamlFile, DefaultEnv, logger)
_, err := createFromYaml(yamlContent, yamlFile, DefaultEnv, logger)
if err == nil {
t.Error("expected error")
}
@@ -195,7 +199,7 @@ func TestReadFromYaml_FilterReleasesOnLabels(t *testing.T) {
{LabelFilter{positiveLabels: [][]string{[]string{"tier", "frontend"}}, negativeLabels: [][]string{[]string{"foo", "bar"}}},
[]bool{false, true, false}},
}
state, err := CreateFromYaml(yamlContent, yamlFile, DefaultEnv, logger)
state, err := createFromYaml(yamlContent, yamlFile, DefaultEnv, logger)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
@@ -234,7 +238,7 @@ func TestReadFromYaml_FilterNegatives(t *testing.T) {
{LabelFilter{negativeLabels: [][]string{[]string{"stage", "pre"}, []string{"stage", "post"}}},
[]bool{false, false, true}},
}
state, err := CreateFromYaml(yamlContent, yamlFile, DefaultEnv, logger)
state, err := createFromYaml(yamlContent, yamlFile, DefaultEnv, logger)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
@@ -246,3 +250,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)
}
}
+73
View File
@@ -0,0 +1,73 @@
package state
import (
"fmt"
"github.com/roboll/helmfile/tmpl"
"gopkg.in/yaml.v2"
)
func (r ReleaseSpec) ExecuteTemplateExpressions(renderer *tmpl.FileRenderer) (*ReleaseSpec, error) {
var result *ReleaseSpec
var err error
result, err = r.Clone()
if err != nil {
return nil, fmt.Errorf("failed executing template expressions in release \"%s\": %v", r.Name, err)
}
{
ts := result.Chart
result.Chart, err = renderer.RenderTemplateContentToString([]byte(ts))
if err != nil {
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".chart = \"%s\": %v", r.Name, ts, err)
}
}
{
ts := result.Namespace
result.Namespace, err = renderer.RenderTemplateContentToString([]byte(ts))
if err != nil {
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".namespace = \"%s\": %v", r.Name, ts, err)
}
}
for i, t := range result.Values {
switch ts := t.(type) {
case string:
s, err := renderer.RenderTemplateContentToBuffer([]byte(ts))
if err != nil {
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".values[%d] = \"%s\": %v", r.Name, i, ts, err)
}
result.Values[i] = s.String()
}
}
for i, ts := range result.Secrets {
s, err := renderer.RenderTemplateContentToBuffer([]byte(ts))
if err != nil {
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".secrets[%d] = \"%s\": %v", r.Name, i, ts, err)
}
result.Secrets[i] = s.String()
}
return result, nil
}
func (r ReleaseSpec) Clone() (*ReleaseSpec, error) {
serialized, err := yaml.Marshal(r)
if err != nil {
return nil, fmt.Errorf("failed cloning release \"%s\": %v", r.Name, err)
}
var deserialized ReleaseSpec
if err := yaml.Unmarshal(serialized, &deserialized); err != nil {
return nil, fmt.Errorf("failed cloning release \"%s\": %v", r.Name, err)
}
return &deserialized, nil
}
func (r ReleaseSpec) Desired() bool {
return r.Installed == nil || *r.Installed
}
+1 -1
View File
@@ -62,7 +62,7 @@ func ParseLabels(l string) (LabelFilter, error) {
kv := strings.Split(label, "=")
lf.positiveLabels = append(lf.positiveLabels, kv)
} else { // malformed case
err = fmt.Errorf("Malformed label: %s. Expected label in form k=v or k!=v", label)
return lf, fmt.Errorf("Malformed label: %s. Expected label in form k=v or k!=v", label)
}
}
return lf, err
+970 -356
View File
File diff suppressed because it is too large Load Diff
+32
View File
@@ -0,0 +1,32 @@
package state
import (
"fmt"
"github.com/roboll/helmfile/tmpl"
)
func (st *HelmState) envTemplateData() EnvironmentTemplateData {
return EnvironmentTemplateData{
st.Env,
st.Namespace,
}
}
func (st *HelmState) ExecuteTemplates() (*HelmState, error) {
r := *st
for i, rt := range st.Releases {
tmplData := ReleaseTemplateData{
Environment: st.Env,
Release: rt,
}
renderer := tmpl.NewFileRenderer(st.readFile, st.basePath, tmplData)
r, err := rt.ExecuteTemplateExpressions(renderer)
if err != nil {
return nil, fmt.Errorf("failed executing templates in release \"%s\".\"%s\": %v", st.FilePath, rt.Name, err)
}
st.Releases[i] = *r
}
return &r, nil
}
+76
View File
@@ -0,0 +1,76 @@
package state
import (
"reflect"
"testing"
"github.com/roboll/helmfile/environment"
)
func TestHelmState_executeTemplates(t *testing.T) {
tests := []struct {
name string
input ReleaseSpec
want ReleaseSpec
}{
{
name: "Has template expressions in chart, values, and secrets",
input: ReleaseSpec{
Chart: "test-charts/{{ .Release.Name }}",
Version: "0.1",
Verify: nil,
Name: "test-app",
Namespace: "test-namespace-{{ .Release.Name }}",
Values: []interface{}{"config/{{ .Environment.Name }}/{{ .Release.Name }}/values.yaml"},
Secrets: []string{"config/{{ .Environment.Name }}/{{ .Release.Name }}/secrets.yaml"},
},
want: ReleaseSpec{
Chart: "test-charts/test-app",
Version: "0.1",
Verify: nil,
Name: "test-app",
Namespace: "test-namespace-test-app",
Values: []interface{}{"config/test_env/test-app/values.yaml"},
Secrets: []string{"config/test_env/test-app/secrets.yaml"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
state := &HelmState{
basePath: ".",
HelmDefaults: HelmSpec{
KubeContext: "test_context",
},
Env: environment.Environment{Name: "test_env"},
Namespace: "test-namespace_",
Repositories: nil,
Releases: []ReleaseSpec{
tt.input,
},
}
r, err := state.ExecuteTemplates()
if err != nil {
t.Errorf("Unexpected error: %v", err)
t.FailNow()
}
actual := r.Releases[0]
if !reflect.DeepEqual(actual.Chart, tt.want.Chart) {
t.Errorf("expected %+v, got %+v", tt.want.Chart, actual.Chart)
}
if !reflect.DeepEqual(actual.Namespace, tt.want.Namespace) {
t.Errorf("expected %+v, got %+v", tt.want.Namespace, actual.Namespace)
}
if !reflect.DeepEqual(actual.Values, tt.want.Values) {
t.Errorf("expected %+v, got %+v", tt.want.Values, actual.Values)
}
if !reflect.DeepEqual(actual.Secrets, tt.want.Secrets) {
t.Errorf("expected %+v, got %+v", tt.want.Secrets, actual.Secrets)
}
})
}
}
+89
View File
@@ -0,0 +1,89 @@
package state
import (
"fmt"
"github.com/roboll/helmfile/helmexec"
"sync"
)
type result struct {
release ReleaseSpec
err error
}
func (st *HelmState) scatterGather(concurrency int, items int, produceInputs func(), receiveInputsAndProduceIntermediates func(int), aggregateIntermediates func()) {
numReleases := len(st.Releases)
if concurrency < 1 {
concurrency = numReleases
} else if concurrency > numReleases {
concurrency = numReleases
}
// WaitGroup is required to wait until goroutine per job in job queue cleanly stops.
var waitGroup sync.WaitGroup
waitGroup.Add(concurrency)
go produceInputs()
for w := 1; w <= concurrency; w++ {
go func(id int) {
st.logger.Debugf("worker %d/%d started", id, concurrency)
receiveInputsAndProduceIntermediates(id)
st.logger.Debugf("worker %d/%d finished", id, concurrency)
waitGroup.Done()
}(w)
}
aggregateIntermediates()
// Wait until all the goroutines to gracefully finish
waitGroup.Wait()
}
func (st *HelmState) scatterGatherReleases(helm helmexec.Interface, concurrency int,
do func(ReleaseSpec, int) error) []error {
var errs []error
inputs := st.Releases
inputsSize := len(inputs)
releases := make(chan ReleaseSpec)
results := make(chan result)
st.scatterGather(
concurrency,
inputsSize,
func() {
for _, release := range inputs {
releases <- release
}
close(releases)
},
func(id int) {
for release := range releases {
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)
}
},
func() {
for i := range inputs {
st.logger.Debugf("receiving result %d", i)
r := <-results
if r.err != nil {
errs = append(errs, fmt.Errorf("release \"%s\" failed: %v", r.release.Name, r.err))
} else {
st.logger.Debugf("received result for release \"%s\"", r.release.Name)
}
st.logger.Debugf("received result for %d", i)
}
},
)
if len(errs) != 0 {
return errs
}
return nil
}
+921 -28
View File
File diff suppressed because it is too large Load Diff
+24
View File
@@ -0,0 +1,24 @@
package state
import "github.com/roboll/helmfile/environment"
// TemplateSpec defines the structure of a reusable and composable template for helm releases.
type TemplateSpec struct {
ReleaseSpec `yaml:",inline"`
}
// EnvironmentTemplateData provides variables accessible while executing golang text/template expressions in helmfile and values YAML files
type EnvironmentTemplateData struct {
// Environment is accessible as `.Environment` from any template executed by the renderer
Environment environment.Environment
// Namespace is accessible as `.Namespace` from any non-values template executed by the renderer
Namespace string
}
// ReleaseTemplateData provides variables accessible while executing golang text/template expressions in releases of a helmfile YAML file
type ReleaseTemplateData struct {
// Environment is accessible as `.Environment` from any template expression executed by the renderer
Environment environment.Environment
// Release is accessible as `.Release` from any template expression executed by the renderer
Release ReleaseSpec
}
+2 -1
View File
@@ -2,7 +2,8 @@ repositories:
- name: stable
url: https://kubernetes-charts.storage.googleapis.com/
context: minikube
helmDefaults:
kubeContext: minikube
releases:
+20
View File
@@ -47,6 +47,7 @@ set -e
info "Using namespace: ${test_ns}"
info "Using Helm version: $(helm version --short --client | grep -o v.*$)"
${helm} init --wait --override spec.template.spec.automountServiceAccountToken=true
${helm} plugin install https://github.com/databus23/helm-diff --version master
${kubectl} get namespace ${test_ns} &> /dev/null && warn "Namespace ${test_ns} exists, from a previous test run?"
$kubectl create namespace ${test_ns} || fail "Could not create namespace ${test_ns}"
trap "{ $kubectl delete namespace ${test_ns}; }" EXIT # remove namespace whenever we exit this script
@@ -55,14 +56,33 @@ trap "{ $kubectl delete namespace ${test_ns}; }" EXIT # remove namespace wheneve
# TEST CASES----------------------------------------------------------------------------------------------------------
test_start "happypath - simple rollout of httpbin chart"
info "Diffing ${dir}/happypath.yaml"
bash -c "${helmfile} -f ${dir}/happypath.yaml diff --detailed-exitcode; code="'$?'"; [ "'${code}'" -eq 2 ]" || fail "unexpected exit code returned by helmfile diff"
info "Templating ${dir}/happypath.yaml"
${helmfile} -f ${dir}/happypath.yaml template
code=$?
[ ${code} -eq 0 ] || fail "unexpected exit code returned by helmfile template: ${code}"
info "Syncing ${dir}/happypath.yaml"
${helmfile} -f ${dir}/happypath.yaml sync
wait_deploy_ready httpbin-httpbin
retry 5 "curl --fail $(minikube service --url --namespace=${test_ns} httpbin-httpbin)/status/200"
[ ${retry_result} -eq 0 ] || fail "httpbin failed to return 200 OK"
info "Applying ${dir}/happypath.yaml"
${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"
info "Ensuring \"helmfile delete\" doesn't fail when no releases installed"
${helmfile} -f ${dir}/happypath.yaml delete || fail "\"helmfile delete\" shouldn't fail when there are no installed releases"
test_pass "happypath"
+3 -2
View File
@@ -1,6 +1,7 @@
package tmpl
type Context struct {
basePath string
readFile func(string) ([]byte, error)
preRender bool
basePath string
readFile func(string) ([]byte, error)
}
+108 -2
View File
@@ -3,8 +3,11 @@ package tmpl
import (
"fmt"
"gopkg.in/yaml.v2"
"io"
"os"
"os/exec"
"path/filepath"
"reflect"
"strings"
"text/template"
)
@@ -12,17 +15,112 @@ import (
type Values = map[string]interface{}
func (c *Context) createFuncMap() template.FuncMap {
return template.FuncMap{
funcMap := template.FuncMap{
"exec": c.Exec,
"readFile": c.ReadFile,
"toYaml": ToYaml,
"fromYaml": FromYaml,
"setValueAtPath": SetValueAtPath,
"requiredEnv": RequiredEnv,
"get": get,
"getOrNil": getOrNil,
"tpl": c.Tpl,
}
if c.preRender {
// disable potential side-effect template calls
funcMap["exec"] = func(string, []interface{}, ...string) (string, error) {
return "", nil
}
funcMap["readFile"] = func(string) (string, error) {
return "", nil
}
}
return funcMap
}
func (c *Context) Exec(command string, args []interface{}, inputs ...string) (string, error) {
var input string
if len(inputs) > 0 {
input = inputs[0]
}
strArgs := make([]string, len(args))
for i, a := range args {
switch a.(type) {
case string:
strArgs[i] = a.(string)
default:
return "", fmt.Errorf("unexpected type of arg \"%s\" in args %v at index %d", reflect.TypeOf(a), args, i)
}
}
cmd := exec.Command(command, strArgs...)
cmd.Dir = c.basePath
writeErrs := make(chan error)
cmdErrs := make(chan error)
cmdOuts := make(chan []byte)
if len(input) > 0 {
stdin, err := cmd.StdinPipe()
if err != nil {
return "", err
}
go func(input string, stdin io.WriteCloser) {
defer stdin.Close()
defer close(writeErrs)
size := len(input)
var n int
var err error
i := 0
for {
n, err = io.WriteString(stdin, input[i:])
if err != nil {
writeErrs <- fmt.Errorf("failed while writing %d bytes to stdin of \"%s\": %v", len(input), command, err)
break
}
i += n
if n == size {
break
}
}
}(input, stdin)
}
go func() {
defer close(cmdOuts)
defer close(cmdErrs)
bytes, err := cmd.Output()
if err != nil {
cmdErrs <- fmt.Errorf("exec cmd=%s args=[%s] failed: %v", command, strings.Join(strArgs, ", "), err)
} else {
cmdOuts <- bytes
}
}()
for {
select {
case bytes := <-cmdOuts:
return string(bytes), nil
case err := <-cmdErrs:
return "", err
case err := <-writeErrs:
return "", err
}
}
}
func (c *Context) ReadFile(filename string) (string, error) {
path := filepath.Join(c.basePath, filename)
var path string
if filepath.IsAbs(filename) {
path = filename
} else {
path = filepath.Join(c.basePath, filename)
}
bytes, err := c.readFile(path)
if err != nil {
@@ -31,6 +129,14 @@ func (c *Context) ReadFile(filename string) (string, error) {
return string(bytes), nil
}
func (c *Context) Tpl(text string, data interface{}) (string, error) {
buf, err := c.RenderTemplateToBuffer(text, data)
if err != nil {
return "", err
}
return buf.String(), nil
}
func ToYaml(v interface{}) (string, error) {
data, err := yaml.Marshal(v)
if err != nil {
@@ -2,6 +2,7 @@ package tmpl
import (
"fmt"
"path/filepath"
"reflect"
"testing"
)
@@ -11,7 +12,27 @@ func TestReadFile(t *testing.T) {
bar: BAR
`
expectedFilename := "values.yaml"
ctx := &Context{readFile: func(filename string) ([]byte, error) {
ctx := &Context{basePath: ".", readFile: func(filename string) ([]byte, error) {
if filename != expectedFilename {
return nil, fmt.Errorf("unexpected filename: expected=%v, actual=%s", expectedFilename, filename)
}
return []byte(expected), nil
}}
actual, err := ctx.ReadFile(expectedFilename)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if !reflect.DeepEqual(actual, expected) {
t.Errorf("unexpected result: expected=%v, actual=%v", expected, actual)
}
}
func TestReadFile_PassAbsPath(t *testing.T) {
expected := `foo:
bar: BAR
`
expectedFilename, _ := filepath.Abs("values.yaml")
ctx := &Context{basePath: ".", readFile: func(filename string) ([]byte, error) {
if filename != expectedFilename {
return nil, fmt.Errorf("unexpected filename: expected=%v, actual=%s", expectedFilename, filename)
}
@@ -97,3 +118,18 @@ func TestSetValueAtPath_TwoComponents(t *testing.T) {
t.Errorf("unexpected result: expected=%v, actual=%v", expected, actual)
}
}
func TestTpl(t *testing.T) {
text := `foo: {{ .foo }}
`
expected := `foo: FOO
`
ctx := &Context{basePath: "."}
actual, err := ctx.Tpl(text, map[string]interface{}{"foo": "FOO"})
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if !reflect.DeepEqual(actual, expected) {
t.Errorf("unexpected result: expected=%v, actual=%v", expected, actual)
}
}
+8 -2
View File
@@ -11,7 +11,13 @@ func (c *Context) stringTemplate() *template.Template {
for name, f := range c.createFuncMap() {
funcMap[name] = f
}
return template.New("stringTemplate").Funcs(funcMap)
tmpl := template.New("stringTemplate").Funcs(funcMap)
if c.preRender {
tmpl.Option("missingkey=zero")
} else {
tmpl.Option("missingkey=error")
}
return tmpl
}
func (c *Context) RenderTemplateToBuffer(s string, data ...interface{}) (*bytes.Buffer, error) {
@@ -28,7 +34,7 @@ func (c *Context) RenderTemplateToBuffer(s string, data ...interface{}) (*bytes.
var execErr = t.Execute(&tplString, d)
if execErr != nil {
return nil, execErr
return &tplString, execErr
}
return &tplString, nil
@@ -60,11 +60,61 @@ func TestRenderTemplate_WithData(t *testing.T) {
}
}
func renderTemplateToString(s string) (string, error) {
func TestRenderTemplate_AccessingMissingKeyWithGetOrNil(t *testing.T) {
valuesYamlContent := `foo:
bar: {{ . | getOrNil "foo.bar" }}
`
expected := `foo:
bar: <no value>
`
expectedFilename := "values.yaml"
data := map[string]interface{}{}
ctx := &Context{readFile: func(filename string) ([]byte, error) {
if filename != expectedFilename {
return nil, fmt.Errorf("unexpected filename: expected=%v, actual=%s", expectedFilename, filename)
}
return []byte(valuesYamlContent), nil
}}
buf, err := ctx.RenderTemplateToBuffer(valuesYamlContent, data)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
actual := buf.String()
if !reflect.DeepEqual(actual, expected) {
t.Errorf("unexpected result: expected=%v, actual=%v", expected, actual)
}
}
func TestRenderTemplate_Defaulting(t *testing.T) {
valuesYamlContent := `foo:
bar: {{ . | getOrNil "foo.bar" | default "DEFAULT" }}
`
expected := `foo:
bar: DEFAULT
`
expectedFilename := "values.yaml"
data := map[string]interface{}{}
ctx := &Context{readFile: func(filename string) ([]byte, error) {
if filename != expectedFilename {
return nil, fmt.Errorf("unexpected filename: expected=%v, actual=%s", expectedFilename, filename)
}
return []byte(valuesYamlContent), nil
}}
buf, err := ctx.RenderTemplateToBuffer(valuesYamlContent, data)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
actual := buf.String()
if !reflect.DeepEqual(actual, expected) {
t.Errorf("unexpected result: expected=%v, actual=%v", expected, actual)
}
}
func renderTemplateToString(s string, data ...interface{}) (string, error) {
ctx := &Context{readFile: func(filename string) ([]byte, error) {
return nil, fmt.Errorf("unexpected call to readFile: filename=%s", filename)
}}
tplString, err := ctx.RenderTemplateToBuffer(s)
tplString, err := ctx.RenderTemplateToBuffer(s, data...)
if err != nil {
return "", err
}
@@ -75,6 +125,7 @@ func Test_renderTemplateToString(t *testing.T) {
type args struct {
s string
envs map[string]string
data interface{}
}
tests := []struct {
name string
@@ -127,6 +178,18 @@ func Test_renderTemplateToString(t *testing.T) {
want: "7",
wantErr: false,
},
{
name: "get",
args: args{
s: `{{ . | get "Foo" }}, {{ . | get "Bar" "2" }}`,
envs: map[string]string{},
data: map[string]interface{}{
"Foo": "1",
},
},
want: "1, 2",
wantErr: false,
},
{
name: "env var not set",
args: args{
@@ -175,7 +238,7 @@ func Test_renderTemplateToString(t *testing.T) {
t.Error("renderTemplateToString() could not set env var for testing")
}
}
got, err := renderTemplateToString(tt.args.s)
got, err := renderTemplateToString(tt.args.s, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("renderTemplateToString() for %s error = %v, wantErr %v", tt.name, err, tt.wantErr)
return
-43
View File
@@ -1,43 +0,0 @@
package tmpl
import (
"bytes"
"github.com/roboll/helmfile/environment"
)
type templateFileRenderer struct {
ReadFile func(string) ([]byte, error)
Context *Context
Data TemplateData
}
type TemplateData struct {
// Environment is accessible as `.Environment` from any template executed by the renderer
Environment environment.Environment
}
type FileRenderer interface {
RenderTemplateFileToBuffer(file string) (*bytes.Buffer, error)
}
func NewFileRenderer(readFile func(filename string) ([]byte, error), basePath string, env environment.Environment) *templateFileRenderer {
return &templateFileRenderer{
ReadFile: readFile,
Context: &Context{
basePath: basePath,
readFile: readFile,
},
Data: TemplateData{
Environment: env,
},
}
}
func (r *templateFileRenderer) RenderTemplateFileToBuffer(file string) (*bytes.Buffer, error) {
content, err := r.ReadFile(file)
if err != nil {
return nil, err
}
return r.Context.RenderTemplateToBuffer(string(content), r.Data)
}
+78
View File
@@ -0,0 +1,78 @@
package tmpl
import (
"bytes"
"io/ioutil"
"fmt"
"strings"
)
type FileRenderer struct {
ReadFile func(string) ([]byte, error)
Context *Context
Data interface{}
}
func NewFileRenderer(readFile func(filename string) ([]byte, error), basePath string, data interface{}) *FileRenderer {
return &FileRenderer{
ReadFile: readFile,
Context: &Context{
basePath: basePath,
readFile: readFile,
},
Data: data,
}
}
func NewFirstPassRenderer(basePath string, data interface{}) *FileRenderer {
return &FileRenderer{
ReadFile: ioutil.ReadFile,
Context: &Context{
preRender: true,
basePath: basePath,
readFile: ioutil.ReadFile,
},
Data: data,
}
}
func (r *FileRenderer) RenderTemplateFileToBuffer(file string) (*bytes.Buffer, error) {
content, err := r.ReadFile(file)
if err != nil {
return nil, err
}
return r.RenderTemplateContentToBuffer(content)
}
func (r *FileRenderer) RenderToBytes(path string) ([]byte, error) {
var yamlBytes []byte
splits := strings.Split(path, ".")
if len(splits) > 0 && splits[len(splits)-1] == "gotmpl" {
yamlBuf, err := r.RenderTemplateFileToBuffer(path)
if err != nil {
return nil, fmt.Errorf("failed to render [%s], because of %v", path, err)
}
yamlBytes = yamlBuf.Bytes()
} else {
var err error
yamlBytes, err = r.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to load [%s]: %v", path, err)
}
}
return yamlBytes, nil
}
func (r *FileRenderer) RenderTemplateContentToBuffer(content []byte) (*bytes.Buffer, error) {
return r.Context.RenderTemplateToBuffer(string(content), r.Data)
}
func (r *FileRenderer) RenderTemplateContentToString(content []byte) (string, error) {
buf, err := r.Context.RenderTemplateToBuffer(string(content), r.Data)
if err != nil {
return "", err
}
return buf.String(), nil
}
@@ -1,4 +1,4 @@
package valuesfile
package tmpl
import (
"fmt"
@@ -7,6 +7,11 @@ import (
"testing"
)
var emptyEnvTmplData = map[string]interface{}{
"Environment": environment.EmptyEnvironment,
"Namespace": "",
}
func TestRenderToBytes_Gotmpl(t *testing.T) {
valuesYamlTmplContent := `foo:
bar: '{{ readFile "data.txt" }}'
@@ -17,7 +22,7 @@ func TestRenderToBytes_Gotmpl(t *testing.T) {
`
dataFile := "data.txt"
valuesTmplFile := "values.yaml.gotmpl"
r := NewRenderer(func(filename string) ([]byte, error) {
r := NewFileRenderer(func(filename string) ([]byte, error) {
switch filename {
case valuesTmplFile:
return []byte(valuesYamlTmplContent), nil
@@ -25,7 +30,7 @@ func TestRenderToBytes_Gotmpl(t *testing.T) {
return []byte(dataFileContent), nil
}
return nil, fmt.Errorf("unexpected filename: expected=%v or %v, actual=%s", dataFile, valuesTmplFile, filename)
}, "", environment.EmptyEnvironment)
}, "", emptyEnvTmplData)
buf, err := r.RenderToBytes(valuesTmplFile)
if err != nil {
t.Errorf("unexpected error: %v", err)
@@ -44,13 +49,13 @@ func TestRenderToBytes_Yaml(t *testing.T) {
bar: '{{ readFile "data.txt" }}'
`
valuesFile := "values.yaml"
r := NewRenderer(func(filename string) ([]byte, error) {
r := NewFileRenderer(func(filename string) ([]byte, error) {
switch filename {
case valuesFile:
return []byte(valuesYamlContent), nil
}
return nil, fmt.Errorf("unexpected filename: expected=%v, actual=%s", valuesFile, filename)
}, "", environment.EmptyEnvironment)
}, "", emptyEnvTmplData)
buf, err := r.RenderToBytes(valuesFile)
if err != nil {
t.Errorf("unexpected error: %v", err)
+91
View File
@@ -0,0 +1,91 @@
package tmpl
import (
"fmt"
"reflect"
"strings"
)
type noValueError struct {
msg string
}
func (e *noValueError) Error() string {
return e.msg
}
func get(path string, varArgs ...interface{}) (interface{}, error) {
var defSet bool
var def interface{}
var obj interface{}
switch len(varArgs) {
case 1:
defSet = false
def = nil
obj = varArgs[0]
case 2:
defSet = true
def = varArgs[0]
obj = varArgs[1]
default:
return nil, fmt.Errorf("unexpected number of args pased to the template function get(path, [def, ]obj): expected 1 or 2, got %d, args was %v", len(varArgs), varArgs)
}
if path == "" {
return obj, nil
}
keys := strings.Split(path, ".")
var v interface{}
var ok bool
switch typedObj := obj.(type) {
case map[string]interface{}:
v, ok = typedObj[keys[0]]
if !ok {
if defSet {
return def, nil
}
return nil, &noValueError{fmt.Sprintf("no value exist for key \"%s\" in %v", keys[0], typedObj)}
}
case map[interface{}]interface{}:
v, ok = typedObj[keys[0]]
if !ok {
if defSet {
return def, nil
}
return nil, &noValueError{fmt.Sprintf("no value exist for key \"%s\" in %v", keys[0], typedObj)}
}
default:
maybeStruct := reflect.ValueOf(typedObj)
if maybeStruct.Kind() != reflect.Struct {
return nil, &noValueError{fmt.Sprintf("unexpected type(%v) of value for key \"%s\": it must be either map[string]interface{} or any struct", reflect.TypeOf(obj), keys[0])}
} else if maybeStruct.NumField() < 1 {
return nil, &noValueError{fmt.Sprintf("no accessible struct fields for key \"%s\"", keys[0])}
}
f := maybeStruct.FieldByName(keys[0])
if !f.IsValid() {
if defSet {
return def, nil
}
return nil, &noValueError{fmt.Sprintf("no field named \"%s\" exist in %v", keys[0], typedObj)}
}
v = f.Interface()
}
if defSet {
return get(strings.Join(keys[1:], "."), def, v)
}
return get(strings.Join(keys[1:], "."), v)
}
func getOrNil(path string, o interface{}) (interface{}, error) {
v, err := get(path, o)
if err != nil {
switch err.(type) {
case *noValueError:
return nil, nil
default:
return nil, err
}
}
return v, nil
}
+128
View File
@@ -0,0 +1,128 @@
package tmpl
import (
"testing"
)
type EmptyStruct struct {
}
func TestGetStruct(t *testing.T) {
type Foo struct{ Bar string }
obj := struct{ Foo }{Foo{Bar: "Bar"}}
v1, err := get("Foo.Bar", obj)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if v1 != "Bar" {
t.Errorf("unexpected value for path Foo.Bar in %v: expected=Bar, actual=%v", obj, v1)
}
_, err = get("Foo.baz", obj)
if err == nil {
t.Errorf("expected error but was not occurred")
}
_, err = get("foo", EmptyStruct{})
if err == nil {
t.Errorf("expected error but was not occurred")
}
}
func TestGetMap(t *testing.T) {
obj := map[string]interface{}{"Foo": map[string]interface{}{"Bar": "Bar"}}
v1, err := get("Foo.Bar", obj)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if v1 != "Bar" {
t.Errorf("unexpected value for path Foo.Bar in %v: expected=Bar, actual=%v", obj, v1)
}
_, err = get("Foo.baz", obj)
if err == nil {
t.Errorf("expected error but was not occurred")
}
}
func TestGet_Default(t *testing.T) {
obj := map[string]interface{}{"Foo": map[string]interface{}{}, "foo": 1}
v1, err := get("Foo.Bar", "Bar", obj)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if v1 != "Bar" {
t.Errorf("unexpected value for path Foo.Bar in %v: expected=Bar, actual=%v", obj, v1)
}
v2, err := get("Baz", "Baz", obj)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if v2 != "Baz" {
t.Errorf("unexpected value for path Baz in %v: expected=Baz, actual=%v", obj, v2)
}
_, err = get("foo.Bar", "fooBar", obj)
if err == nil {
t.Errorf("expected error but was not occurred")
}
}
func TestGetOrNilStruct(t *testing.T) {
type Foo struct{ Bar string }
obj := struct{ Foo }{Foo{Bar: "Bar"}}
v1, err := getOrNil("Foo.Bar", obj)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if v1 != "Bar" {
t.Errorf("unexpected value for path Foo.Bar in %v: expected=Bar, actual=%v", obj, v1)
}
v2, err := getOrNil("Foo.baz", obj)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if v2 != nil {
t.Errorf("unexpected value for path Foo.baz in %v: expected=nil, actual=%v", obj, v2)
}
}
func TestGetOrNilMap(t *testing.T) {
obj := map[string]interface{}{"Foo": map[string]interface{}{"Bar": "Bar"}}
v1, err := getOrNil("Foo.Bar", obj)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if v1 != "Bar" {
t.Errorf("unexpected value for path Foo.Bar in %v: expected=Bar, actual=%v", obj, v1)
}
v2, err := getOrNil("Foo.baz", obj)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if v2 != nil {
t.Errorf("unexpected value for path Foo.baz in %v: expected=nil, actual=%v", obj, v2)
}
}
+30
View File
@@ -0,0 +1,30 @@
package tmpl
type templateTextRenderer struct {
ReadText func(string) ([]byte, error)
Context *Context
Data interface{}
}
type TextRenderer interface {
RenderTemplateText(text string) (string, error)
}
func NewTextRenderer(readFile func(filename string) ([]byte, error), basePath string, data interface{}) *templateTextRenderer {
return &templateTextRenderer{
ReadText: readFile,
Context: &Context{
basePath: basePath,
readFile: readFile,
},
Data: data,
}
}
func (r *templateTextRenderer) RenderTemplateText(text string) (string, error) {
buf, err := r.Context.RenderTemplateToBuffer(text, r.Data)
if err != nil {
return "", err
}
return buf.String(), nil
}
-39
View File
@@ -1,39 +0,0 @@
package valuesfile
import (
"fmt"
"github.com/roboll/helmfile/environment"
"github.com/roboll/helmfile/tmpl"
"strings"
)
type renderer struct {
readFile func(string) ([]byte, error)
tmplFileRenderer tmpl.FileRenderer
}
func NewRenderer(readFile func(filename string) ([]byte, error), basePath string, env environment.Environment) *renderer {
return &renderer{
readFile: readFile,
tmplFileRenderer: tmpl.NewFileRenderer(readFile, basePath, env),
}
}
func (r *renderer) RenderToBytes(path string) ([]byte, error) {
var yamlBytes []byte
splits := strings.Split(path, ".")
if len(splits) > 0 && splits[len(splits)-1] == "gotmpl" {
yamlBuf, err := r.tmplFileRenderer.RenderTemplateFileToBuffer(path)
if err != nil {
return nil, fmt.Errorf("failed to render [%s], because of %v", path, err)
}
yamlBytes = yamlBuf.Bytes()
} else {
var err error
yamlBytes, err = r.readFile(path)
if err != nil {
return nil, fmt.Errorf("failed to load [%s]: %v", path, err)
}
}
return yamlBytes, nil
}
-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

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