Skip to content

Commit 9fbdfe7

Browse files
authored
feat(jsonnet): Environment vendor (#198)
* feat(jsonnet): tkrc.yaml as root marker Adds new root marker (`tkrc.yaml`) alongside `jsonnetfile.json` to be able to mark the actual project root when environment-local vendoring is used. * doc: document vendor overriding
1 parent 874e6eb commit 9fbdfe7

File tree

7 files changed

+281
-46
lines changed

7 files changed

+281
-46
lines changed

docs/docs/directory-structure.md

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ route: /directory-structure
88
Tanka uses the following directories and special files:
99

1010
```bash
11-
. # the project
11+
. # the project (<rootDir>)
1212
├── environments # code defining clusters
13-
│   └── default
13+
│   └── default # <baseDir>
1414
│   ├── main.jsonnet # starting point of the Jsonnet compilation
1515
│   └── spec.json # environment's config
1616
├── jsonnetfile.json # direct dependencies
@@ -26,51 +26,55 @@ Tanka uses the following directories and special files:
2626
```
2727

2828
## Environments
29+
2930
Tanka organizes configuration in environments. For the rationale behind this,
3031
see the [section in the tutorial](/tutorial/environments).
3132

3233
An environment consists of at least two files:
3334

3435
#### spec.json
36+
3537
This file configures environment properties such as cluster connection
3638
(`spec.apiServer`), default namespace (`spec.namespace`), etc.
3739

3840
For the full set of options, see the [Golang source
3941
code](https://github.com/grafana/tanka/blob/master/pkg/spec/v1alpha1/config.go).
4042

4143
#### main.jsonnet
44+
4245
Like other programming languages, Jsonnet needs an entrypoint into the
4346
evaluation, something to begin with. `main.jsonnet` is exactly this: The very
4447
first file being evaluated, importing or directly specifying everything required
4548
for this specific environment.
4649

50+
## Root and Base
4751

48-
## Libraries
49-
Tanka builds on code-reuse, by refactoring common pieces into libraries, which can be imported from two locations:
52+
When talking about directories, Tanka uses the following terms:
5053

51-
### lib
52-
The `lib/` folder is for libraries that are meant for only this single project.
53-
If you intend to deploy your custom e-commerce stack, you could for example have
54-
libraries for the `auth`, `bookings`, `billing` and `inventory` here.
54+
| Term | Description | Identifier file |
55+
| --------- | ---------------------------------------- | --------------------------------- |
56+
| `baseDir` | The root of your project | `jsonnetfile.json` or `tkrc.yaml` |
57+
| `rootDir` | The directory of the current environment | `main.jsonnet` |
5558

56-
They are not intended to be shared and thus are a good fit for `lib/`
59+
Regardless what subdirectory of the project you are in, Tanka will always be
60+
able to identify both directories, by searching for the identifier files in the
61+
parent directories.
62+
Tanka needs these for correctly setting up the [import paths](/libraries/import-paths).
5763

58-
> **Note:** Opposing to `vendor/`, `lib/` is entirely your realm. You manage the
59-
> contents and Tanka won't ever mess with this after `tk init`.
64+
This is similar to how `git` always works, by looking for the `.git` directory.
6065

61-
### vendor
62-
Some libraries can be useful to many projects (for example ones for
63-
[Prometheus](https://prometheus.io), [Loki](https://grafana.com/loki), etc).
66+
## Libraries
67+
68+
Tanka relies heavily on code-reuse, so libraries are a natural thing. Roughly
69+
spoken, they can be imported from two paths:
6470

65-
These are usually published on GitHub. To use them in your project, [install
66-
them using `jb`](/libraries/install-publish#install-a-library). This will store
67-
a copy of the source code on the remote in the `vendor/` directory. Note that
68-
this folder belongs to `jb` and all files not recorded in
69-
`jsonnetfile.lock.json` will be removed on the next run. Also don't edit files
70-
in here (your changes would be reverted anyways), use
71-
[Shadowing](/libraries/import-paths#shadowing) instead.
71+
- `/lib`: Project local libraries
72+
- `/vendor` External libraries
73+
74+
For more details consider the [import paths](/libraries/import-paths).
7275

7376
### jsonnetfile.json and the lock
77+
7478
`jb` records all external packages installed in a file called
7579
`jsonnetfile.json`. This file is the source of truth about what should be
7680
included in `vendor/`. However, it should only include what is really directly

docs/docs/libraries/import-paths.md

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,14 @@ menu: Libraries
99
When using `import` or `importstr`, Tanka considers the following directories to
1010
find a suitable file for that specific import:
1111

12-
1. `<baseDir>`: The directory of your environment (`/environments/default`,
13-
etc). Put things that only belong to a single environment here.
14-
2. `/lib`: Libraries created for this very project, not meant to be shared
15-
otherwise. Put everything you need across multiple environments here.
16-
3. `/vendor`: Shared libraries installed using `jsonnet-bundler`. Do not modify
17-
this folder by hand, your changes will be overwritten by `jb` anways.
12+
| Rank | Path | Purpose |
13+
| ---- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
14+
| 4 | `<baseDir>` | The directory of your environment, e.g. `/environments/default`.<br /> Put things that belong to this very environment here. |
15+
| 3 | `/lib` | Project-global libraries, that are used in multiple environments, but are specific to this project. |
16+
| 2 | `<baseDir>/vendor` | Per-environment vendor, can be used for [`vendor` overriding](/libraries/overriding#per-environment) |
17+
| 1 | `/vendor` | Global vendor, holds external libraries installed using `jb`. |
1818

19-
> **Note**: The directories are visited in the above order. For example, when a
20-
> file is present in both, `/lib` and `/vendor`, the one from `/lib` will be
21-
> taken, as it occurs higher in the list.
22-
23-
### Shadowing
24-
It is possible to shadow certain files (overlay them with another version), by
25-
putting a file with the exact same name and into a higher ranked import path.
26-
This can be handy if you need to do temporary changes to a vendored library by
27-
overlaying the to-be-changed files using new ones in `lib/`.
28-
29-
For example, to shadow `/vendor/my/lib/file.libsonnet`, copy it to
30-
`/lib/my/lib/file.libsonnet` and do your changes. Tanka will take the file in
31-
`lib/` instead of `vendor/` from now on.
19+
> **Note**:
20+
>
21+
> - If a file occurs in multiple paths, the one with the highest rank will be chosen.
22+
> - `/` in above table means `<rootDir>`, which is your project root.

docs/docs/libraries/overriding.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
---
2+
name: Overriding
3+
route: /libraries/overriding
4+
menu: Libraries
5+
---
6+
7+
# Overriding vendor
8+
9+
The `vendor` directory is immutable in it's nature. You can't and should never
10+
modify any files inside of it, `jb` will revert those changes on the next run anyways.
11+
12+
Nevertheless, it can sometimes become required to to changes there, e.g. if an
13+
upstream library contains a bug that needs to be fixed immediately, without
14+
waiting for the upstream maintainer to review it.
15+
16+
## Shadowing
17+
18+
Because [import paths](/libraries/import-paths) are ranked in Tanka, you can use
19+
a technique called shadowing: By putting a file with the exact same name in a
20+
higher ranked path, Tanka will prefer that file instead of the original in
21+
`vendor`, which has the lowest possible rank of 1.
22+
23+
For example, if `/vendor/foo/bar.libsonnet` contained an error, you could create
24+
`/lib/foo/bar.libsonnet` and fix it there.
25+
26+
> **Tip:** Instead of copying the file to the new location and making the edits,
27+
> use a relative import and [patching](/tutorial/environments#patching):
28+
>
29+
> ```jsonnet
30+
> // in /lib/foo/bar.libsonnet:
31+
> import "../vendor/foo/bar.libsonnet" + {
32+
> foo+: {
33+
> bar: "fixed"
34+
> }
35+
> }
36+
> ```
37+
38+
## Per environment
39+
40+
Another common case is overriding the entire `vendor` bundle per environment.
41+
42+
This is handy, when you for example want to test a change of an upstream
43+
library which is used in many environments (including `prod`) in a single one,
44+
without affecting all the others.
45+
46+
For this, Tanka let's you have a separate `vendor`, `jsonnetfile.json` and
47+
`jsonnetfile.lock.json` per environment. To do so:
48+
49+
#### Create `tkrc.yaml`
50+
51+
Tanka normally uses the `jsonnetfile.json` from your project to find it's root.
52+
As we are going to create another one of that down the tree in the next step, we
53+
need another marker for `<rootDir>`.
54+
55+
For that, create an empty file called `tkrc.yaml` in your project's root,
56+
alongside the original `jsonnetfile.json`.
57+
58+
> **Info**: While the name suggests that `tkrc.yaml` could be used for setting
59+
> parameters, this is not case yet.
60+
> It might however be repurposed later, in case we need such functionality
61+
62+
#### Add a `vendor` to your environment
63+
64+
In your environments folder (e.g. `/environments/default`):
65+
66+
```bash
67+
# init jsonnet bundler (creates jsonnetfile.json)
68+
$ jb init
69+
70+
# install the updated dependency
71+
$ jb init github.com/foo/bar@v2
72+
```
73+
74+
> **Tip**: You don't need to install everything into the new `vendor/`, as
75+
> packages not present there can still be imported from the global `/vendor`.

docs/doczrc.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export default {
4040
// "Using libraries",
4141
// "Creating and structure",
4242
"Installing and publishing",
43+
"Overriding",
4344
],
4445
},
4546
"Command-line completion",

pkg/jsonnet/jpath/dirs_test.go

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package jpath
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
8+
"io/ioutil"
9+
10+
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
type scenario struct {
15+
name string
16+
testdata []string
17+
environment string
18+
19+
// expected results
20+
baseDir string
21+
rootDir string
22+
err error
23+
}
24+
25+
// TestFindRoot asserts that baseDir and rootDir can be correctly resolved.
26+
//
27+
// To do so, Tanka searches the directory tree from the passed directory up twice:
28+
// - for main.jsonnet to find the baseDir
29+
// - for jsonnetfile.json (or tkrc.yaml) to find the rootDir
30+
//
31+
// This enables a git-like behaviour (regardless how deep you are in the
32+
// project, it works)
33+
func TestFindRoot(t *testing.T) {
34+
scenarios := []scenario{
35+
// Scenario: Missing base pointerfile. We expect an ErrorNoBase.
36+
{
37+
name: "missing-basePointer",
38+
environment: "environments/default",
39+
testdata: []string{"jsonnetfile.json", "environments/default/"},
40+
err: ErrorNoBase,
41+
},
42+
// Scenario: Missing root pointerfile. We expect an ErrorNoRoot.
43+
{
44+
name: "missing-rootPointer",
45+
environment: "environments/default",
46+
testdata: []string{"environments/default/main.jsonnet"},
47+
err: ErrorNoRoot,
48+
},
49+
50+
// Make sure jsonnetfile.json works as a pointer
51+
scenarioPointer("jsonnetfile.json"),
52+
// Make sure tkrc.yaml works as a pointer
53+
scenarioPointer("tkrc.yaml"),
54+
55+
// Per-environment vendoring is tricky, because environments get their
56+
// own `jsonnetfile.json`, so `rootDir` would yield the same thing as
57+
// `baseDir`, which is usually not wanted.
58+
//
59+
// Scenario 1: No tkrc.yaml to mark the actual root. `baseDir` and
60+
// `rootDir` should be equal
61+
scenarioLocalVendor(false),
62+
// Scenario 2: A tkrc.yaml is added o the actual root. `rootDir` should
63+
// be the actual root, `baseDir` the environment.
64+
scenarioLocalVendor(true),
65+
}
66+
67+
for _, s := range scenarios {
68+
require.NotZero(t, s.environment)
69+
70+
t.Run(s.name, func(t *testing.T) {
71+
dir := makeTestdata(t, s.testdata)
72+
defer os.RemoveAll(dir)
73+
74+
_, base, root, err := Resolve(filepath.Join(dir, s.environment))
75+
assert.Equal(t, s.err, err)
76+
77+
if err == nil {
78+
assert.Equal(t, filepath.Join(dir, s.baseDir), base)
79+
assert.Equal(t, filepath.Join(dir, s.rootDir), root)
80+
}
81+
})
82+
}
83+
}
84+
85+
func scenarioLocalVendor(tkrc bool) scenario {
86+
name := "localvendor"
87+
td := []string{
88+
"jsonnetfile.json",
89+
"environments/default/main.jsonnet",
90+
"environments/default/jsonnetfile.json",
91+
}
92+
// first jsonnetfile.json is in baseDir, so it will become rootDir as well
93+
root := "environments/default"
94+
95+
if tkrc {
96+
name += "-with-tkrc"
97+
td = append(td, "tkrc.yaml") // add tkrc
98+
// now root should be project_root instead
99+
root = "/"
100+
}
101+
102+
return scenario{
103+
name: name,
104+
environment: "environments/default",
105+
testdata: td,
106+
107+
rootDir: root,
108+
baseDir: "environments/default",
109+
}
110+
}
111+
112+
func scenarioPointer(ptr string) scenario {
113+
return scenario{
114+
name: "pointer-" + ptr,
115+
environment: "environments/default",
116+
117+
testdata: []string{
118+
"environments/default/main.jsonnet",
119+
ptr,
120+
},
121+
122+
baseDir: "environments/default",
123+
rootDir: "/",
124+
}
125+
}
126+
127+
func makeTestdata(t *testing.T, td []string) string {
128+
t.Helper()
129+
130+
tmp, err := ioutil.TempDir("", "tk-dirsTest")
131+
require.NoError(t, err)
132+
133+
for _, f := range td {
134+
dir, file := filepath.Split(f)
135+
if dir != "" {
136+
err := os.MkdirAll(filepath.Join(tmp, dir), os.ModePerm)
137+
require.NoError(t, err)
138+
}
139+
if file != "" {
140+
_, err := os.Create(filepath.Join(tmp, dir, file))
141+
require.NoError(t, err)
142+
}
143+
}
144+
return tmp
145+
}

0 commit comments

Comments
 (0)