├── internal
├── filematcher
│ ├── testdata
│ │ ├── env.yaml
│ │ ├── .env.yaml
│ │ ├── 1env.yaml
│ │ ├── env1.yaml
│ │ └── testDirForGlobPatterns
│ │ │ ├── .keep
│ │ │ └── childDir
│ │ │ └── .keep
│ ├── match.go
│ └── match_test.go
├── fswalk
│ └── testdata
│ │ ├── 0.txt
│ │ ├── dir2
│ │ └── 2.jsonnet
│ │ └── dir1
│ │ ├── dir1a
│ │ └── 1a.libsonnet
│ │ └── 1.jsonnet
├── eval
│ └── testdata
│ │ ├── components
│ │ ├── d
│ │ │ ├── index.yaml
│ │ │ ├── subdir-cm.yaml
│ │ │ └── subdir-cm2.json
│ │ ├── bad.yaml
│ │ ├── bad-prep.xsonnet
│ │ ├── bad-objects.yaml
│ │ ├── bad.json
│ │ ├── bad-pp.libsonnet
│ │ ├── pp
│ │ │ ├── annotations.libsonnet
│ │ │ ├── pp2.jsonnet
│ │ │ └── pp.jsonnet
│ │ ├── b.yaml
│ │ ├── bad-metadata.yaml
│ │ ├── a.json
│ │ ├── c.jsonnet
│ │ └── tla.jsonnet
│ │ ├── params.invalid.libsonnet
│ │ ├── params.non-object.libsonnet
│ │ ├── bad-components
│ │ ├── e1.jsonnet
│ │ ├── e2.jsonnet
│ │ ├── e3.jsonnet
│ │ ├── e4.jsonnet
│ │ └── e5.jsonnet
│ │ ├── good-components
│ │ ├── g1.jsonnet
│ │ ├── g2.jsonnet
│ │ ├── g3.jsonnet
│ │ ├── g4.jsonnet
│ │ └── g5.jsonnet
│ │ └── params.libsonnet
├── model
│ └── testdata
│ │ ├── bad-app
│ │ ├── bad-comps
│ │ │ ├── a.json
│ │ │ └── a.yaml
│ │ ├── bad-yaml.yaml
│ │ ├── components
│ │ │ ├── a.json
│ │ │ ├── b.json
│ │ │ └── c.json
│ │ ├── malformed-env.yaml
│ │ ├── bad-no-envs.yaml
│ │ ├── bad-invalid-env-file.yaml
│ │ ├── bad-malformed-env-file.yaml
│ │ ├── bad-env-config.yaml
│ │ ├── bad-env-config3.yaml
│ │ ├── bad-missing-env-file.yaml
│ │ ├── dev2.yaml
│ │ ├── bad-app-name.yaml
│ │ ├── bad-env-name.yaml
│ │ ├── invalid-env.yaml
│ │ ├── bad-comps.yaml
│ │ ├── bad-env-config2.yaml
│ │ ├── bad-comp-exclude.yaml
│ │ ├── bad-env-exclude.yaml
│ │ ├── bad-env-include.yaml
│ │ ├── bad-baseline-env.yaml
│ │ ├── bad-dup-preproc.yaml
│ │ ├── envs
│ │ │ └── override-dev.yaml
│ │ ├── bad-dup-postproc.yaml
│ │ ├── bad-dup-ext.yaml
│ │ ├── bad-env-include-exclude.yaml
│ │ ├── bad-dup-tla.yaml
│ │ ├── bad-computed.yaml
│ │ └── app-warn.yaml
│ │ ├── http-app
│ │ ├── .gitignore
│ │ ├── components
│ │ │ └── service.jsonnet
│ │ ├── qbec-template.yaml
│ │ └── envs.yaml
│ │ ├── subdir-app
│ │ ├── components
│ │ │ ├── comp2
│ │ │ │ ├── index.yaml
│ │ │ │ ├── cm1.yaml
│ │ │ │ ├── cm2.json
│ │ │ │ └── level2
│ │ │ │ │ └── cm3.json
│ │ │ ├── ignored
│ │ │ │ ├── index.yaml
│ │ │ │ │ └── preserve.txt
│ │ │ │ └── cm4.json
│ │ │ └── comp1
│ │ │ │ ├── index.jsonnet
│ │ │ │ └── cm.jsonnet
│ │ └── qbec.yaml
│ │ ├── label-app
│ │ ├── components
│ │ │ ├── index.jsonnet
│ │ │ └── cm.jsonnet
│ │ └── qbec.yaml
│ │ ├── multi-dir-app
│ │ ├── components
│ │ │ ├── README.md
│ │ │ ├── dir1
│ │ │ │ └── a.jsonnet
│ │ │ └── dir2
│ │ │ │ └── b
│ │ │ │ └── index.jsonnet
│ │ └── qbec.yaml
│ │ └── no-dirs-app
│ │ └── qbec.yaml
├── cmd
│ ├── testdata
│ │ ├── lib
│ │ │ └── baz.libsonnet
│ │ ├── script
│ │ │ └── data-source.sh
│ │ ├── comp-file.jsonnet
│ │ ├── extra-env.yaml
│ │ ├── components
│ │ │ ├── c2.jsonnet
│ │ │ └── c1.jsonnet
│ │ ├── qbec-bad-ds1.yaml
│ │ ├── qbec-bad.yaml
│ │ ├── qbec-bad2.yaml
│ │ ├── qbec-bad-ds2.yaml
│ │ └── qbec.yaml
│ ├── errors_test.go
│ ├── lifecycle_test.go
│ ├── profile_test.go
│ ├── errors.go
│ ├── utils_test.go
│ └── lifecycle.go
├── commands
│ ├── testdata
│ │ ├── test.libsonnet
│ │ ├── projects
│ │ │ ├── lint-app
│ │ │ │ ├── files
│ │ │ │ │ ├── one.libsonnet
│ │ │ │ │ └── two.libsonnet
│ │ │ │ ├── lib
│ │ │ │ │ ├── bar.libsonnet
│ │ │ │ │ └── foo.libsonnet
│ │ │ │ ├── components
│ │ │ │ │ └── foo.jsonnet
│ │ │ │ └── qbec.yaml
│ │ │ ├── lazy-resources
│ │ │ │ ├── lib
│ │ │ │ │ └── externals.libsonnet
│ │ │ │ ├── components
│ │ │ │ │ ├── custom-resources
│ │ │ │ │ │ ├── res.yaml
│ │ │ │ │ │ └── index.jsonnet
│ │ │ │ │ └── crds
│ │ │ │ │ │ ├── index.jsonnet
│ │ │ │ │ │ └── crd.yaml
│ │ │ │ └── qbec.yaml
│ │ │ ├── policies
│ │ │ │ ├── components
│ │ │ │ │ ├── ns.jsonnet
│ │ │ │ │ ├── cm.jsonnet
│ │ │ │ │ └── secret.jsonnet
│ │ │ │ └── qbec.yaml
│ │ │ ├── string-secrets
│ │ │ │ ├── components
│ │ │ │ │ └── secret.jsonnet
│ │ │ │ └── qbec.yaml
│ │ │ ├── multi-ns
│ │ │ │ ├── qbec.yaml
│ │ │ │ └── components
│ │ │ │ │ ├── first
│ │ │ │ │ └── index.jsonnet
│ │ │ │ │ └── second
│ │ │ │ │ └── index.jsonnet
│ │ │ ├── wait
│ │ │ │ ├── qbec.yaml
│ │ │ │ └── components
│ │ │ │ │ └── deployment
│ │ │ │ │ └── index.jsonnet
│ │ │ └── simple-service
│ │ │ │ ├── qbec.yaml
│ │ │ │ ├── pp.libsonnet
│ │ │ │ └── components
│ │ │ │ └── simple-service
│ │ │ │ └── index.yaml
│ │ ├── test.libsonnet.formatted
│ │ ├── dups
│ │ │ ├── components
│ │ │ │ ├── y.yaml
│ │ │ │ └── x.yaml
│ │ │ └── qbec.yaml
│ │ ├── test.yml
│ │ ├── test.yml.formatted
│ │ ├── extra-env.yaml
│ │ ├── components
│ │ │ ├── c2.jsonnet
│ │ │ └── c1.jsonnet
│ │ ├── qbec.yaml
│ │ └── test.json
│ │ │ ├── test.json
│ │ │ └── test.json.formatted
│ └── alpha.go
├── vmexternals
│ └── testdata
│ │ ├── extCode.libsonnet
│ │ ├── vars.txt
│ │ ├── lib1
│ │ └── libcode1.libsonnet
│ │ └── lib2
│ │ └── libcode2.libsonnet
├── remote
│ ├── k8smeta
│ │ ├── testdata
│ │ │ ├── swagger-2.0.0.pb-v1
│ │ │ ├── ns-good.json
│ │ │ └── ns-bad.json
│ │ └── schema_test.go
│ ├── testdata
│ │ └── pristine
│ │ │ ├── input.yaml
│ │ │ ├── kc-created.yaml
│ │ │ ├── kc-applied.yaml
│ │ │ └── qbec-applied.yaml
│ ├── auth-plugins.go
│ └── pool.go
├── types
│ ├── doc.go
│ ├── secrets_test.go
│ └── testdata
│ │ ├── daemonset
│ │ ├── not-rolling-update.json
│ │ ├── bad-object.json
│ │ ├── fewer-pods-available.json
│ │ └── fewer-pods-updated.json
│ │ └── deploy
│ │ ├── bad-object.json
│ │ ├── zero-of-four.json
│ │ ├── wait-observe.json
│ │ ├── missing-rev.json
│ │ ├── bad-rev.json
│ │ ├── pend-term.json
│ │ ├── fewer-updated.json
│ │ ├── not-fully-available.json
│ │ ├── diff-rev.json
│ │ ├── three-of-four.json
│ │ └── success.json
└── testutil
│ └── testutil.go
├── examples
├── test-app
│ ├── compute.jsonnet
│ ├── components
│ │ ├── ignored
│ │ │ └── ignored-component.json
│ │ ├── service1.jsonnet
│ │ ├── test-job.yaml
│ │ ├── service2.jsonnet
│ │ └── cluster-objects.yaml
│ ├── misc
│ │ ├── simple-ds.xsonnet
│ │ ├── simple.jsonnet
│ │ ├── tla.jsonnet
│ │ ├── vars.jsonnet
│ │ └── qbec.jsonnet
│ ├── stage-env.yaml
│ ├── environments
│ │ ├── dev.libsonnet
│ │ ├── prod.libsonnet
│ │ └── _.libsonnet
│ ├── pp.jsonnet
│ ├── prod-env.yaml
│ ├── params.libsonnet
│ ├── lib
│ │ ├── objects.libsonnet
│ │ └── globutil.libsonnet
│ └── qbec.yaml
├── helm3
│ ├── components
│ │ ├── apache
│ │ │ ├── index.jsonnet
│ │ │ └── datasource.libsonnet
│ │ └── victoria-metrics
│ │ │ ├── index.jsonnet
│ │ │ └── datasource.libsonnet
│ ├── environments
│ │ ├── base.libsonnet
│ │ └── default.libsonnet
│ ├── params.libsonnet
│ └── qbec.yaml
└── external-data-app
│ ├── components
│ └── my-config-map.jsonnet
│ ├── config-map.sh
│ ├── qbec.yaml
│ └── README.md
├── vm
├── internal
│ ├── importers
│ │ ├── testdata
│ │ │ ├── example1
│ │ │ │ ├── caller2
│ │ │ │ │ └── keep-dir.txt
│ │ │ │ ├── caller
│ │ │ │ │ ├── inner
│ │ │ │ │ │ └── keep-dir.txt
│ │ │ │ │ ├── import-a.jsonnet
│ │ │ │ │ └── import-all-json.jsonnet
│ │ │ │ ├── z.json
│ │ │ │ ├── a.json
│ │ │ │ └── b.json
│ │ │ └── example2
│ │ │ │ ├── inc1
│ │ │ │ ├── a.json
│ │ │ │ └── subdir
│ │ │ │ │ └── a.json
│ │ │ │ └── inc2
│ │ │ │ └── a.json
│ │ ├── api.go
│ │ ├── file.go
│ │ ├── composite.go
│ │ ├── composite_test.go
│ │ └── data-source_test.go
│ ├── ds
│ │ ├── exec
│ │ │ ├── testdata
│ │ │ │ ├── exec-bit-set.sh
│ │ │ │ └── exec-bit-not-set.sh
│ │ │ └── runner.go
│ │ ├── api.go
│ │ └── factory
│ │ │ ├── datasource_test.go
│ │ │ ├── lazy.go
│ │ │ └── datasource.go
│ └── natives
│ │ ├── testdata
│ │ ├── charts
│ │ │ └── foobar
│ │ │ │ ├── values.yaml
│ │ │ │ ├── Chart.yaml
│ │ │ │ └── templates
│ │ │ │ ├── secret.yaml
│ │ │ │ └── config-map.yaml
│ │ ├── bad-relative.jsonnet
│ │ └── consumer.jsonnet
│ │ ├── json.go
│ │ └── yaml.go
├── testdata
│ ├── parallel-tla-vars.jsonnet
│ ├── parallel-ext-vars.jsonnet
│ ├── vars.txt
│ ├── data-sources
│ │ ├── replay.jsonnet
│ │ └── replay2.jsonnet
│ ├── extCode.libsonnet
│ ├── lib1
│ │ └── libcode1.libsonnet
│ ├── lib2
│ │ └── libcode2.libsonnet
│ ├── vmtest.jsonnet
│ └── vmlib
│ │ └── foobar.libsonnet
├── example_test.go
├── datasource
│ └── api.go
└── vmutil
│ └── vmutil.go
├── cmd
├── jsonnet-qbec
│ ├── testdata
│ │ ├── fail.jsonnet
│ │ ├── slow.jsonnet
│ │ └── basic.jsonnet
│ └── main.go
├── changelog-extractor
│ ├── testdata
│ │ └── abc.txt
│ ├── main_test.go
│ └── main.go
└── gen-qbec-swagger
│ └── main.go
├── site
├── .gitignore
├── static
│ └── images
│ │ └── favicon.png
├── content
│ ├── userguide
│ │ ├── usage
│ │ │ ├── _index.md
│ │ │ ├── common-metadata.md
│ │ │ ├── tips-and-tricks.md
│ │ │ └── basic.md
│ │ └── _index.md
│ ├── reference
│ │ ├── _index.md
│ │ ├── jsonnet-vars.md
│ │ ├── directives.md
│ │ ├── gen-metadata.md
│ │ └── diffs-and-patches.md
│ ├── getting-started
│ │ └── _index.md
│ └── _index.md
├── config.yaml
└── layouts
│ └── partials
│ ├── menu-footer.html
│ ├── logo.html
│ └── custom-header.html
├── .gitattributes
├── .gitignore
├── .github
├── RELEASE.md
└── workflows
│ └── release.yaml
├── test.sh
├── publish-docs.sh
├── prepare-release.sh
├── .golangci.yml
├── Makefile.tools
├── README.md
├── licenselint.sh
├── cmdtest
└── qbec-replay-exec
│ └── main.go
└── main.go
/internal/filematcher/testdata/env.yaml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/internal/fswalk/testdata/0.txt:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/internal/filematcher/testdata/.env.yaml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/internal/filematcher/testdata/1env.yaml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/internal/filematcher/testdata/env1.yaml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/internal/eval/testdata/components/d/index.yaml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/internal/fswalk/testdata/dir2/2.jsonnet:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/internal/eval/testdata/components/bad.yaml:
--------------------------------------------------------------------------------
1 | foo: {
2 |
--------------------------------------------------------------------------------
/internal/eval/testdata/params.invalid.libsonnet:
--------------------------------------------------------------------------------
1 | {
2 |
--------------------------------------------------------------------------------
/internal/eval/testdata/components/bad-prep.xsonnet:
--------------------------------------------------------------------------------
1 | {
2 |
--------------------------------------------------------------------------------
/internal/eval/testdata/params.non-object.libsonnet:
--------------------------------------------------------------------------------
1 | []
2 |
--------------------------------------------------------------------------------
/internal/filematcher/testdata/testDirForGlobPatterns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/internal/model/testdata/bad-app/bad-comps/a.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/internal/model/testdata/bad-app/bad-yaml.yaml:
--------------------------------------------------------------------------------
1 | foo: {
2 |
--------------------------------------------------------------------------------
/internal/model/testdata/bad-app/components/a.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/internal/model/testdata/bad-app/components/b.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/internal/model/testdata/bad-app/components/c.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/internal/model/testdata/http-app/.gitignore:
--------------------------------------------------------------------------------
1 | qbec.yaml
2 |
--------------------------------------------------------------------------------
/examples/test-app/compute.jsonnet:
--------------------------------------------------------------------------------
1 | {
2 | foo: 'bar',
3 | }
4 |
--------------------------------------------------------------------------------
/internal/eval/testdata/components/bad-objects.yaml:
--------------------------------------------------------------------------------
1 | foo: bar
2 |
--------------------------------------------------------------------------------
/internal/model/testdata/bad-app/malformed-env.yaml:
--------------------------------------------------------------------------------
1 | { foo:
2 |
--------------------------------------------------------------------------------
/internal/model/testdata/subdir-app/components/comp2/index.yaml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vm/internal/importers/testdata/example1/caller2/keep-dir.txt:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vm/testdata/parallel-tla-vars.jsonnet:
--------------------------------------------------------------------------------
1 | function (foo) foo
2 |
--------------------------------------------------------------------------------
/examples/test-app/components/ignored/ignored-component.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/internal/filematcher/testdata/testDirForGlobPatterns/childDir/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vm/internal/importers/testdata/example1/caller/inner/keep-dir.txt:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vm/testdata/parallel-ext-vars.jsonnet:
--------------------------------------------------------------------------------
1 | std.extVar('foo')
2 |
3 |
--------------------------------------------------------------------------------
/vm/testdata/vars.txt:
--------------------------------------------------------------------------------
1 | listVar1=l1
2 |
3 | listVar2
4 |
5 |
6 |
--------------------------------------------------------------------------------
/cmd/jsonnet-qbec/testdata/fail.jsonnet:
--------------------------------------------------------------------------------
1 | import 'data://replay/fail'
2 |
--------------------------------------------------------------------------------
/cmd/jsonnet-qbec/testdata/slow.jsonnet:
--------------------------------------------------------------------------------
1 | import 'data://replay/slow'
2 |
--------------------------------------------------------------------------------
/internal/eval/testdata/components/bad.json:
--------------------------------------------------------------------------------
1 | {
2 | foo: 'bar'
3 | }
4 |
--------------------------------------------------------------------------------
/internal/model/testdata/bad-app/bad-comps/a.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | foo: bar
3 |
--------------------------------------------------------------------------------
/cmd/jsonnet-qbec/testdata/basic.jsonnet:
--------------------------------------------------------------------------------
1 | import 'data://replay/test/path'
2 |
--------------------------------------------------------------------------------
/internal/cmd/testdata/lib/baz.libsonnet:
--------------------------------------------------------------------------------
1 | {
2 | baz: 'libbaz',
3 | }
4 |
--------------------------------------------------------------------------------
/internal/commands/testdata/test.libsonnet:
--------------------------------------------------------------------------------
1 | {a::{
2 | a:1,
3 | }
4 | }
5 |
--------------------------------------------------------------------------------
/internal/model/testdata/subdir-app/components/ignored/index.yaml/preserve.txt:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vm/internal/importers/testdata/example2/inc1/a.json:
--------------------------------------------------------------------------------
1 | {
2 | "a": "a"
3 | }
4 |
--------------------------------------------------------------------------------
/vm/testdata/data-sources/replay.jsonnet:
--------------------------------------------------------------------------------
1 | import 'data://replay/foo/bar'
2 |
--------------------------------------------------------------------------------
/examples/test-app/misc/simple-ds.xsonnet:
--------------------------------------------------------------------------------
1 | { foo: importstr 'data://simple-ds' }
2 |
--------------------------------------------------------------------------------
/internal/commands/testdata/projects/lint-app/files/one.libsonnet:
--------------------------------------------------------------------------------
1 | { one: 1 }
2 |
--------------------------------------------------------------------------------
/internal/commands/testdata/projects/lint-app/files/two.libsonnet:
--------------------------------------------------------------------------------
1 | { two: 2 }
2 |
--------------------------------------------------------------------------------
/internal/vmexternals/testdata/extCode.libsonnet:
--------------------------------------------------------------------------------
1 | { foo: 'ec1foo', bar: 'ec1bar'}
2 |
--------------------------------------------------------------------------------
/internal/vmexternals/testdata/vars.txt:
--------------------------------------------------------------------------------
1 | listVar1=l1
2 |
3 | listVar2
4 |
5 |
6 |
--------------------------------------------------------------------------------
/vm/internal/ds/exec/testdata/exec-bit-set.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | echo "{}"
4 |
--------------------------------------------------------------------------------
/vm/testdata/extCode.libsonnet:
--------------------------------------------------------------------------------
1 | {
2 | foo: 'ec1foo',
3 | bar: 'ec1bar',
4 | }
--------------------------------------------------------------------------------
/examples/test-app/misc/simple.jsonnet:
--------------------------------------------------------------------------------
1 | {
2 | foo: 'str',
3 | bar: true,
4 | }
5 |
--------------------------------------------------------------------------------
/internal/cmd/testdata/script/data-source.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | echo "hello world"
4 |
--------------------------------------------------------------------------------
/internal/model/testdata/label-app/components/index.jsonnet:
--------------------------------------------------------------------------------
1 | import './cm.jsonnet'
2 |
--------------------------------------------------------------------------------
/vm/internal/importers/testdata/example1/caller/import-a.jsonnet:
--------------------------------------------------------------------------------
1 | import '../a.json'
2 |
--------------------------------------------------------------------------------
/vm/internal/importers/testdata/example2/inc2/a.json:
--------------------------------------------------------------------------------
1 | {
2 | "a": "long form a"
3 | }
4 |
--------------------------------------------------------------------------------
/vm/testdata/lib1/libcode1.libsonnet:
--------------------------------------------------------------------------------
1 | {
2 | foo: 'lc1foo',
3 | bar: 'lc1bar',
4 | }
--------------------------------------------------------------------------------
/vm/testdata/lib2/libcode2.libsonnet:
--------------------------------------------------------------------------------
1 | {
2 | foo: 'lc2foo',
3 | bar: 'lc2bar',
4 | }
--------------------------------------------------------------------------------
/examples/helm3/components/apache/index.jsonnet:
--------------------------------------------------------------------------------
1 | (import 'datasource.libsonnet').objects
2 |
--------------------------------------------------------------------------------
/internal/model/testdata/subdir-app/components/comp1/index.jsonnet:
--------------------------------------------------------------------------------
1 | import './cm.jsonnet'
2 |
--------------------------------------------------------------------------------
/site/.gitignore:
--------------------------------------------------------------------------------
1 | public/
2 | rebuild.sh
3 | deploy.yaml
4 | themes/
5 | Dockerfile
6 |
7 |
--------------------------------------------------------------------------------
/vm/internal/ds/exec/testdata/exec-bit-not-set.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | echo hello world
4 |
--------------------------------------------------------------------------------
/vm/internal/importers/testdata/example2/inc1/subdir/a.json:
--------------------------------------------------------------------------------
1 | {
2 | "a": "inner a"
3 | }
4 |
--------------------------------------------------------------------------------
/examples/test-app/misc/tla.jsonnet:
--------------------------------------------------------------------------------
1 | function(foo, bar) {
2 | foo: foo,
3 | bar: bar,
4 | }
5 |
--------------------------------------------------------------------------------
/internal/commands/testdata/test.libsonnet.formatted:
--------------------------------------------------------------------------------
1 | {
2 | a:: {
3 | a: 1,
4 | },
5 | }
6 |
--------------------------------------------------------------------------------
/internal/eval/testdata/components/bad-pp.libsonnet:
--------------------------------------------------------------------------------
1 | function (object) (
2 | [ object ]
3 | )
4 |
--------------------------------------------------------------------------------
/vm/internal/natives/testdata/charts/foobar/values.yaml:
--------------------------------------------------------------------------------
1 | foo: bar
2 | secret: Y2hhbmdlbWUK
3 |
4 |
--------------------------------------------------------------------------------
/examples/helm3/components/victoria-metrics/index.jsonnet:
--------------------------------------------------------------------------------
1 | (import 'datasource.libsonnet').objects
2 |
--------------------------------------------------------------------------------
/internal/commands/testdata/projects/lint-app/lib/bar.libsonnet:
--------------------------------------------------------------------------------
1 | local quux = 10;
2 | { bar: 10 }
3 |
--------------------------------------------------------------------------------
/vm/internal/importers/testdata/example1/z.json:
--------------------------------------------------------------------------------
1 | {
2 | "z": "the last letter of the alphabet"
3 | }
4 |
--------------------------------------------------------------------------------
/examples/test-app/misc/vars.jsonnet:
--------------------------------------------------------------------------------
1 | {
2 | foo: std.extVar('foo'),
3 | bar: std.extVar('bar'),
4 | }
5 |
--------------------------------------------------------------------------------
/internal/vmexternals/testdata/lib1/libcode1.libsonnet:
--------------------------------------------------------------------------------
1 | {
2 | foo: 'lc1foo',
3 | bar: 'lc1bar',
4 | }
--------------------------------------------------------------------------------
/internal/vmexternals/testdata/lib2/libcode2.libsonnet:
--------------------------------------------------------------------------------
1 | {
2 | foo: 'lc2foo',
3 | bar: 'lc2bar',
4 | }
--------------------------------------------------------------------------------
/site/static/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/splunk/qbec/HEAD/site/static/images/favicon.png
--------------------------------------------------------------------------------
/vm/internal/importers/testdata/example1/a.json:
--------------------------------------------------------------------------------
1 | {
2 | "a": "the first letter of the alphabet"
3 | }
4 |
--------------------------------------------------------------------------------
/vm/internal/importers/testdata/example1/b.json:
--------------------------------------------------------------------------------
1 | {
2 | "b": "the second letter of the alphabet"
3 | }
4 |
--------------------------------------------------------------------------------
/vm/internal/importers/testdata/example1/caller/import-all-json.jsonnet:
--------------------------------------------------------------------------------
1 | import 'glob-import:../*.json'
2 |
--------------------------------------------------------------------------------
/vm/internal/natives/testdata/charts/foobar/Chart.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | name: foobar
3 | version: 1.0
4 |
--------------------------------------------------------------------------------
/internal/eval/testdata/components/pp/annotations.libsonnet:
--------------------------------------------------------------------------------
1 | {
2 | team: 'service2',
3 | slack: '#svc2',
4 | }
--------------------------------------------------------------------------------
/internal/commands/testdata/projects/lazy-resources/lib/externals.libsonnet:
--------------------------------------------------------------------------------
1 | {
2 | suffix: std.extVar('suffix'),
3 | }
4 |
--------------------------------------------------------------------------------
/internal/eval/testdata/components/pp/pp2.jsonnet:
--------------------------------------------------------------------------------
1 | function (object) object + { metadata +: { labels +: { foo: 'bar' } } }
2 |
--------------------------------------------------------------------------------
/vm/testdata/vmtest.jsonnet:
--------------------------------------------------------------------------------
1 | local lib = import 'foobar.libsonnet';
2 | lib.makeFooBar(std.extVar('foo'), std.extVar('bar'))
3 |
--------------------------------------------------------------------------------
/examples/helm3/environments/base.libsonnet:
--------------------------------------------------------------------------------
1 | // this file has the baseline default parameters
2 | {
3 | components: {
4 | },
5 | }
6 |
--------------------------------------------------------------------------------
/internal/cmd/testdata/comp-file.jsonnet:
--------------------------------------------------------------------------------
1 | {
2 | cfoo: std.extVar('compFoo'),
3 | baz: (import 'lib/baz.libsonnet').baz,
4 | }
5 |
--------------------------------------------------------------------------------
/internal/fswalk/testdata/dir1/dir1a/1a.libsonnet:
--------------------------------------------------------------------------------
1 | local foo = 10;
2 | local bar = 20;
3 | {
4 | foo: foo,
5 | bar: bar,
6 | }
7 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Ensure that git would checkout files with lf instead of crlf on windows
2 | *.formatted text eol=lf
3 | *.yml text eol=lf
4 |
--------------------------------------------------------------------------------
/examples/test-app/misc/qbec.jsonnet:
--------------------------------------------------------------------------------
1 | {
2 | foo: std.extVar('qbec.io/env'),
3 | bar: std.extVar('qbec.io/envProperties').envType,
4 | }
5 |
--------------------------------------------------------------------------------
/internal/commands/testdata/dups/components/y.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: ConfigMap
3 | metadata:
4 | name: cm1
5 | data:
6 | foo: bar
7 |
--------------------------------------------------------------------------------
/internal/eval/testdata/bad-components/e1.jsonnet:
--------------------------------------------------------------------------------
1 | {
2 | apiVersion: 'v1, //deliberate syntax error
3 | kind: 'ConfigMap',
4 | }
5 |
6 |
--------------------------------------------------------------------------------
/internal/eval/testdata/bad-components/e2.jsonnet:
--------------------------------------------------------------------------------
1 | {
2 | apiVersion: 'v1, //deliberate syntax error
3 | kind: 'ConfigMap',
4 | }
5 |
6 |
--------------------------------------------------------------------------------
/internal/eval/testdata/bad-components/e3.jsonnet:
--------------------------------------------------------------------------------
1 | {
2 | apiVersion: 'v1, //deliberate syntax error
3 | kind: 'ConfigMap',
4 | }
5 |
6 |
--------------------------------------------------------------------------------
/internal/eval/testdata/bad-components/e4.jsonnet:
--------------------------------------------------------------------------------
1 | {
2 | apiVersion: 'v1, //deliberate syntax error
3 | kind: 'ConfigMap',
4 | }
5 |
6 |
--------------------------------------------------------------------------------
/internal/eval/testdata/bad-components/e5.jsonnet:
--------------------------------------------------------------------------------
1 | {
2 | apiVersion: 'v1, //deliberate syntax error
3 | kind: 'ConfigMap',
4 | }
5 |
6 |
--------------------------------------------------------------------------------
/internal/fswalk/testdata/dir1/1.jsonnet:
--------------------------------------------------------------------------------
1 | local foo = import 'dir1a/1a.libsonnet';
2 | {
3 | foo: foo,
4 | extra: 'hello world',
5 | }
6 |
--------------------------------------------------------------------------------
/internal/model/testdata/bad-app/bad-no-envs.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: qbec.io/v1alpha1
2 | kind: App
3 | metadata:
4 | name: test-app
5 | spec: {}
6 |
7 |
--------------------------------------------------------------------------------
/vm/testdata/data-sources/replay2.jsonnet:
--------------------------------------------------------------------------------
1 | local r1 = import 'data://replay/foo/bar';
2 | local r2 = import 'data://replay2/bar/baz';
3 | [r1, r2]
4 |
--------------------------------------------------------------------------------
/internal/commands/testdata/dups/components/x.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: ConfigMap
3 | metadata:
4 | name: cm1
5 | data:
6 | foo: bar
7 |
8 |
9 |
--------------------------------------------------------------------------------
/internal/commands/testdata/projects/lint-app/lib/foo.libsonnet:
--------------------------------------------------------------------------------
1 | local bar = '10';
2 | local baz = 15;
3 | import 'glob-import:../files/*libsonnet'
4 |
--------------------------------------------------------------------------------
/internal/model/testdata/multi-dir-app/components/README.md:
--------------------------------------------------------------------------------
1 | ### multi dirs
2 |
3 | A multi dir app is allowed to have files that are ignored.
4 |
5 |
--------------------------------------------------------------------------------
/internal/remote/k8smeta/testdata/swagger-2.0.0.pb-v1:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/splunk/qbec/HEAD/internal/remote/k8smeta/testdata/swagger-2.0.0.pb-v1
--------------------------------------------------------------------------------
/internal/eval/testdata/components/b.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: v1
3 | kind: ConfigMap
4 | metadata:
5 | name: yaml-config-map
6 | data:
7 | foo: bar
8 |
--------------------------------------------------------------------------------
/internal/model/testdata/subdir-app/components/comp2/cm1.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: v1
3 | kind: ConfigMap
4 | metadata:
5 | name: cm1
6 | data:
7 | foo: bar
8 |
--------------------------------------------------------------------------------
/internal/eval/testdata/components/d/subdir-cm.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: v1
3 | kind: ConfigMap
4 | metadata:
5 | name: subdir-config-map1
6 | data:
7 | foo: bar
8 |
--------------------------------------------------------------------------------
/internal/model/testdata/http-app/components/service.jsonnet:
--------------------------------------------------------------------------------
1 | {
2 | apiVersion: 'v1',
3 | kind: 'ConfigMap',
4 | data: {
5 | foo: bar,
6 | },
7 | }
8 |
--------------------------------------------------------------------------------
/internal/remote/k8smeta/testdata/ns-good.json:
--------------------------------------------------------------------------------
1 | {
2 | "kind": "Namespace",
3 | "apiVersion": "v1",
4 | "metadata": {
5 | "name" : "foobar"
6 | }
7 | }
8 |
9 |
--------------------------------------------------------------------------------
/vm/testdata/vmlib/foobar.libsonnet:
--------------------------------------------------------------------------------
1 | local makeFooBar = function (foo, bar) {
2 | foo: foo,
3 | bar: bar,
4 | };
5 |
6 | {
7 | makeFooBar:: makeFooBar,
8 | }
9 |
--------------------------------------------------------------------------------
/examples/test-app/stage-env.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: qbec.io/v1alpha1
2 | kind: EnvironmentMap
3 | spec:
4 | environments:
5 | stage:
6 | server: https://stage-server
7 |
--------------------------------------------------------------------------------
/internal/model/testdata/bad-app/bad-invalid-env-file.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: qbec.io/v1alpha1
2 | kind: App
3 | metadata:
4 | name: test-app
5 | spec:
6 | envFiles:
7 | - invalid-env.yaml
8 |
--------------------------------------------------------------------------------
/internal/model/testdata/bad-app/bad-malformed-env-file.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: qbec.io/v1alpha1
2 | kind: App
3 | metadata:
4 | name: test-app
5 | spec:
6 | envFiles:
7 | - malformed-env.yaml
8 |
--------------------------------------------------------------------------------
/site/content/userguide/usage/_index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Using qbec
3 | chapter: true
4 | ---
5 |
6 |
Using qbec
7 |
8 | We describe how to use qbec effectively to get the most bang for buck.
--------------------------------------------------------------------------------
/examples/test-app/environments/dev.libsonnet:
--------------------------------------------------------------------------------
1 | local base = import '_.libsonnet';
2 |
3 | base {
4 | components+: {
5 | service2+: {
6 | cpu: '50m',
7 | },
8 | },
9 | }
10 |
--------------------------------------------------------------------------------
/internal/commands/testdata/projects/lazy-resources/components/custom-resources/res.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: test.qbec.io/v1alpha1
2 | kind: Foo
3 | metadata:
4 | name: foo
5 | spec:
6 | bar: baz
7 |
8 |
--------------------------------------------------------------------------------
/internal/remote/k8smeta/testdata/ns-bad.json:
--------------------------------------------------------------------------------
1 | {
2 | "kind": "Namespace",
3 | "apiVersion": "v1",
4 | "metadata": {
5 | "name" : "foobar",
6 | "foo" : "bar"
7 | }
8 | }
9 |
10 |
--------------------------------------------------------------------------------
/internal/commands/testdata/test.yml:
--------------------------------------------------------------------------------
1 | a: 1
2 | # My super important comment
3 | b:
4 | - c:
5 | e: 4
6 | a: 4
7 | e:
8 | - f: 2
9 | g: 4
10 |
11 | ---
12 | b: c
13 | ---
14 | c: d
15 |
--------------------------------------------------------------------------------
/internal/model/testdata/bad-app/bad-env-config.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: qbec.io/v1alpha1
2 | kind: App
3 | metadata:
4 | name: test-app
5 | spec:
6 | environments:
7 | foo:
8 | defaultNamespace: foo
9 |
--------------------------------------------------------------------------------
/internal/model/testdata/bad-app/bad-env-config3.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: qbec.io/v1alpha1
2 | kind: App
3 | metadata:
4 | name: test-app
5 | spec:
6 | environments:
7 | foo:
8 | context: __current__
9 |
--------------------------------------------------------------------------------
/examples/helm3/environments/default.libsonnet:
--------------------------------------------------------------------------------
1 | // this file has the param overrides for the default environment
2 | local base = import './base.libsonnet';
3 |
4 | base {
5 | components+: {
6 | },
7 | }
8 |
--------------------------------------------------------------------------------
/internal/cmd/testdata/extra-env.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: qbec.io/v1alpha1
2 | kind: EnvironmentMap
3 | spec:
4 | environments:
5 | prod:
6 | server: https://prod-server
7 | defaultNamespace: prodfoo
8 |
--------------------------------------------------------------------------------
/internal/commands/testdata/projects/policies/components/ns.jsonnet:
--------------------------------------------------------------------------------
1 | {
2 | apiVersion: 'v1',
3 | kind: 'Namespace',
4 | metadata: {
5 | name: std.extVar('qbec.io/defaultNs'),
6 | },
7 | }
8 |
--------------------------------------------------------------------------------
/internal/commands/testdata/test.yml.formatted:
--------------------------------------------------------------------------------
1 | a: 1
2 | # My super important comment
3 | b:
4 | - c:
5 | e: 4
6 | a: 4
7 | e:
8 | - f: 2
9 | g: 4
10 | ---
11 | b: c
12 | ---
13 | c: d
14 |
--------------------------------------------------------------------------------
/internal/model/testdata/bad-app/bad-missing-env-file.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: qbec.io/v1alpha1
2 | kind: App
3 | metadata:
4 | name: test-app
5 | spec:
6 | envFiles:
7 | - missing-env.yaml
8 |
9 |
10 |
--------------------------------------------------------------------------------
/internal/commands/testdata/extra-env.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: qbec.io/v1alpha1
2 | kind: EnvironmentMap
3 | spec:
4 | environments:
5 | prod:
6 | server: https://prod-server
7 | defaultNamespace: prodfoo
8 |
--------------------------------------------------------------------------------
/internal/eval/testdata/components/bad-metadata.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: v1
3 | kind: ConfigMap
4 | metadata:
5 | name: subdir-config-map1
6 | annotations:
7 | do-it: true
8 | data:
9 | foo: bar
10 |
--------------------------------------------------------------------------------
/internal/model/testdata/bad-app/dev2.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: qbec.io/v1alpha1
2 | kind: EnvironmentMap
3 | spec:
4 | environments:
5 | dev:
6 | server: https://dev-server2
7 | includes:
8 | - b
9 |
--------------------------------------------------------------------------------
/internal/eval/testdata/good-components/g1.jsonnet:
--------------------------------------------------------------------------------
1 | {
2 | apiVersion: 'v1',
3 | kind: 'ConfigMap',
4 | metadata: {
5 | name: 'g1',
6 | }
7 | data: {
8 | foo: 'bar',
9 | },
10 | }
11 |
12 |
13 |
--------------------------------------------------------------------------------
/internal/eval/testdata/good-components/g2.jsonnet:
--------------------------------------------------------------------------------
1 | {
2 | apiVersion: 'v1',
3 | kind: 'ConfigMap',
4 | metadata: {
5 | name: 'g2',
6 | }
7 | data: {
8 | foo: 'bar',
9 | },
10 | }
11 |
12 |
13 |
--------------------------------------------------------------------------------
/internal/eval/testdata/good-components/g3.jsonnet:
--------------------------------------------------------------------------------
1 | {
2 | apiVersion: 'v1',
3 | kind: 'ConfigMap',
4 | metadata: {
5 | name: 'g3',
6 | }
7 | data: {
8 | foo: 'bar',
9 | },
10 | }
11 |
12 |
13 |
--------------------------------------------------------------------------------
/internal/eval/testdata/good-components/g4.jsonnet:
--------------------------------------------------------------------------------
1 | {
2 | apiVersion: 'v1',
3 | kind: 'ConfigMap',
4 | metadata: {
5 | name: 'g4',
6 | }
7 | data: {
8 | foo: 'bar',
9 | },
10 | }
11 |
12 |
13 |
--------------------------------------------------------------------------------
/internal/eval/testdata/good-components/g5.jsonnet:
--------------------------------------------------------------------------------
1 | {
2 | apiVersion: 'v1',
3 | kind: 'ConfigMap',
4 | metadata: {
5 | name: 'g5',
6 | }
7 | data: {
8 | foo: 'bar',
9 | },
10 | }
11 |
12 |
13 |
--------------------------------------------------------------------------------
/internal/eval/testdata/components/a.json:
--------------------------------------------------------------------------------
1 | {
2 | "apiVersion": "v1",
3 | "kind": "ConfigMap",
4 | "metadata": {
5 | "name": "json-config-map"
6 | },
7 | "data": {
8 | "foo": "bar"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/internal/eval/testdata/components/c.jsonnet:
--------------------------------------------------------------------------------
1 | {
2 | apiVersion: 'v1',
3 | kind: 'ConfigMap',
4 | metadata: {
5 | name: 'foobar',
6 | },
7 | data: {
8 | foo: std.extVar('qbec.io/env'),
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/internal/model/testdata/subdir-app/qbec.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: qbec.io/v1alpha1
3 | kind: App
4 | metadata:
5 | name: subdir-app
6 | spec:
7 | environments:
8 | dev:
9 | server: https://dev-server
10 |
--------------------------------------------------------------------------------
/internal/model/testdata/bad-app/bad-app-name.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: qbec.io/v1alpha1
2 | kind: App
3 | metadata:
4 | name: test-app/foo
5 | spec:
6 | environments:
7 | dev:
8 | server: https://dev-server
9 |
10 |
--------------------------------------------------------------------------------
/internal/model/testdata/bad-app/bad-env-name.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: qbec.io/v1alpha1
2 | kind: App
3 | metadata:
4 | name: test-app
5 | spec:
6 | environments:
7 | "foo/bar":
8 | server: https://dev-server
9 |
10 |
--------------------------------------------------------------------------------
/vm/internal/natives/testdata/charts/foobar/templates/secret.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Secret
3 | metadata:
4 | namespace: {{ .Release.Namespace }}
5 | name: {{ .Release.Name }}
6 | data:
7 | secret: {{.Values.secret}}
8 |
--------------------------------------------------------------------------------
/examples/test-app/components/service1.jsonnet:
--------------------------------------------------------------------------------
1 | local objects = import 'objects.libsonnet';
2 | local fooValue = std.extVar('externalFoo');
3 |
4 | {
5 | configMap: objects.configmap('foo-system', 'svc1-cm', { foo: fooValue }),
6 | }
7 |
--------------------------------------------------------------------------------
/internal/eval/testdata/components/d/subdir-cm2.json:
--------------------------------------------------------------------------------
1 | {
2 | "apiVersion": "v1",
3 | "kind": "ConfigMap",
4 | "metadata": {
5 | "name": "subdir-config-map2"
6 | },
7 | "data": {
8 | "foo": "bar"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/internal/model/testdata/label-app/components/cm.jsonnet:
--------------------------------------------------------------------------------
1 | {
2 | apiVersion: "v1",
3 | kind: "ConfigMap",
4 | metadata: {
5 | name: "cm0"
6 | },
7 | data: {
8 | foo: "bar",
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/internal/model/testdata/subdir-app/components/comp2/cm2.json:
--------------------------------------------------------------------------------
1 | {
2 | "apiVersion": "v1",
3 | "kind": "ConfigMap",
4 | "metadata": {
5 | "name": "cm2"
6 | },
7 | "data": {
8 | "foo": "bar"
9 | }
10 | }
11 |
12 |
--------------------------------------------------------------------------------
/internal/model/testdata/multi-dir-app/components/dir1/a.jsonnet:
--------------------------------------------------------------------------------
1 | {
2 | apiVersion: 'v1',
3 | kind: 'ConfigMap',
4 | metadata: {
5 | name: 'cm-a',
6 | },
7 | data: {
8 | foo: 'bar'
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/internal/model/testdata/subdir-app/components/comp1/cm.jsonnet:
--------------------------------------------------------------------------------
1 | {
2 | apiVersion: "v1",
3 | kind: "ConfigMap",
4 | metadata: {
5 | name: "cm0"
6 | },
7 | data: {
8 | foo: "bar",
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/internal/commands/testdata/components/c2.jsonnet:
--------------------------------------------------------------------------------
1 | {
2 | apiVersion: 'v1',
3 | kind: 'ConfigMap',
4 | metadata: {
5 | name: 'cm2',
6 | },
7 | data: {
8 | foo: std.extVar('extFoo'),
9 | },
10 | }
11 |
--------------------------------------------------------------------------------
/internal/eval/testdata/components/pp/pp.jsonnet:
--------------------------------------------------------------------------------
1 | local annotations = import './annotations.libsonnet';
2 |
3 | function (object) object + if std.extVar('qbec.io/cleanMode') == 'on' then {} else { metadata +: { annotations +: annotations } }
4 |
--------------------------------------------------------------------------------
/internal/model/testdata/bad-app/invalid-env.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: qbec.io/v1alpha1
2 | kind: EnvironmentMap
3 | spec:
4 | environments:
5 | dev:
6 | server: https://dev-server2
7 | includes:
8 | - b
9 | foo: bar
10 |
--------------------------------------------------------------------------------
/internal/model/testdata/bad-app/bad-comps.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: qbec.io/v1alpha1
2 | kind: App
3 | metadata:
4 | name: test-app
5 | spec:
6 | componentsDir: bad-comps
7 | environments:
8 | dev:
9 | server: https://dev-server
10 |
--------------------------------------------------------------------------------
/internal/model/testdata/bad-app/bad-env-config2.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: qbec.io/v1alpha1
2 | kind: App
3 | metadata:
4 | name: test-app
5 | spec:
6 | environments:
7 | foo:
8 | server: https://dev-server
9 | context: minikube
10 |
--------------------------------------------------------------------------------
/internal/commands/testdata/dups/qbec.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: qbec.io/v1alpha1
3 | kind: App
4 | metadata:
5 | name: app2
6 | spec:
7 | environments:
8 | dev:
9 | server: https://dev-server
10 | defaultNamespace: kube-system
11 |
--------------------------------------------------------------------------------
/internal/model/testdata/bad-app/bad-comp-exclude.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: qbec.io/v1alpha1
2 | kind: App
3 | metadata:
4 | name: test-app
5 | spec:
6 | excludes:
7 | - d
8 | environments:
9 | dev:
10 | server: https://dev-server
11 |
--------------------------------------------------------------------------------
/internal/model/testdata/bad-app/bad-env-exclude.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: qbec.io/v1alpha1
2 | kind: App
3 | metadata:
4 | name: test-app
5 | spec:
6 | environments:
7 | dev:
8 | server: https://dev-server
9 | excludes:
10 | - d
--------------------------------------------------------------------------------
/internal/model/testdata/bad-app/bad-env-include.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: qbec.io/v1alpha1
2 | kind: App
3 | metadata:
4 | name: test-app
5 | spec:
6 | environments:
7 | dev:
8 | server: https://dev-server
9 | includes:
10 | - d
--------------------------------------------------------------------------------
/internal/model/testdata/label-app/qbec.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: qbec.io/v1alpha1
3 | kind: App
4 | metadata:
5 | name: label-app
6 | spec:
7 | addComponentLabel: true
8 | environments:
9 | dev:
10 | server: https://dev-server
11 |
--------------------------------------------------------------------------------
/internal/model/testdata/multi-dir-app/components/dir2/b/index.jsonnet:
--------------------------------------------------------------------------------
1 | {
2 | apiVersion: 'v1',
3 | kind: 'ConfigMap',
4 | metadata: {
5 | name: 'cm-b',
6 | },
7 | data: {
8 | bar: 'baz'
9 | }
10 | }
11 |
12 |
--------------------------------------------------------------------------------
/examples/test-app/environments/prod.libsonnet:
--------------------------------------------------------------------------------
1 | local base = import '_.libsonnet';
2 |
3 | base {
4 | components+: {
5 | service1+: {
6 | cpu: '1',
7 | },
8 | service2+: {
9 | memory: '16Gi',
10 | },
11 | },
12 | }
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | .DS_Store
3 | tmp/
4 | vendor/
5 | cover.out
6 | coverage.txt
7 | dist/
8 | helm.tar.gz
9 | linux-amd64/
10 | darwin-amd64/
11 | bin/
12 | .release-notes.md
13 | .tools/
14 | .vscode/
15 | *.generated
16 | .gitconfig
17 | .githooks/
18 |
--------------------------------------------------------------------------------
/examples/test-app/pp.jsonnet:
--------------------------------------------------------------------------------
1 | // the post processor jsonnet must return a function taking exactly one parameter
2 | // called "object" and returning its decorated version.
3 |
4 | function(object) object { metadata+: { annotations+: { slack: '#my-channel' } } }
5 |
--------------------------------------------------------------------------------
/internal/commands/testdata/projects/string-secrets/components/secret.jsonnet:
--------------------------------------------------------------------------------
1 | {
2 | apiVersion: 'v1',
3 | kind: 'Secret',
4 | metadata: {
5 | name: 'my-secret',
6 | },
7 | stringData: {
8 | foo: std.extVar('secretValue'),
9 | },
10 | }
11 |
--------------------------------------------------------------------------------
/internal/model/testdata/multi-dir-app/qbec.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: qbec.io/v1alpha1
3 | kind: App
4 | metadata:
5 | name: multi-dir-app
6 | spec:
7 | componentsDir: components/*
8 | environments:
9 | dev:
10 | server: https://dev-server
11 |
--------------------------------------------------------------------------------
/internal/model/testdata/no-dirs-app/qbec.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: qbec.io/v1alpha1
3 | kind: App
4 | metadata:
5 | name: no-dirs-app
6 | spec:
7 | componentsDir: components/dir*
8 | environments:
9 | dev:
10 | server: https://dev-server
11 |
--------------------------------------------------------------------------------
/internal/remote/testdata/pristine/input.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: v1
3 | kind: PersistentVolumeClaim
4 | metadata:
5 | name: storage
6 | spec:
7 | accessModes:
8 | - ReadWriteOnce
9 | resources:
10 | requests:
11 | storage: 1Mi
12 |
13 |
--------------------------------------------------------------------------------
/examples/test-app/prod-env.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: qbec.io/v1alpha1
2 | kind: EnvironmentMap
3 | spec:
4 | environments:
5 | prod:
6 | server: https://prod-server
7 | includes:
8 | - service2
9 | properties:
10 | envType: prod
11 |
--------------------------------------------------------------------------------
/internal/model/testdata/http-app/qbec-template.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: qbec.io/v1alpha1
3 | kind: App
4 | metadata:
5 | name: subdir-app
6 | spec:
7 | environments:
8 | dev:
9 | server: https://dev-server
10 | envFiles:
11 | - '{{.URL}}'
12 |
--------------------------------------------------------------------------------
/vm/internal/natives/testdata/charts/foobar/templates/config-map.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: ConfigMap
3 | metadata:
4 | namespace: {{ .Release.Namespace }}
5 | name: {{ .Release.Name }}
6 | data:
7 | foo: {{.Values.foo}}
8 | bar: {{default "baz" .Values.bar}}
9 |
--------------------------------------------------------------------------------
/internal/eval/testdata/components/tla.jsonnet:
--------------------------------------------------------------------------------
1 | function (foo, bar) {
2 | apiVersion: "v1",
3 | kind: "ConfigMap",
4 | metadata: {
5 | name: "tla-config-map"
6 | },
7 | data: {
8 | foo: foo,
9 | bar: if bar then 'yes' else 'no',
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/internal/model/testdata/bad-app/bad-baseline-env.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: qbec.io/v1alpha1
2 | kind: App
3 | metadata:
4 | name: test-app
5 | spec:
6 | environments:
7 | _:
8 | server: http://baseline-server
9 | dev:
10 | server: https://dev-server
11 |
--------------------------------------------------------------------------------
/internal/model/testdata/subdir-app/components/comp2/level2/cm3.json:
--------------------------------------------------------------------------------
1 | {
2 | "apiVersion": "v1",
3 | "kind": "ConfigMap",
4 | "metadata": {
5 | "name": "cm-should-not-load"
6 | },
7 | "data": {
8 | "foo": "bar",
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/internal/model/testdata/subdir-app/components/ignored/cm4.json:
--------------------------------------------------------------------------------
1 | {
2 | "apiVersion": "v1",
3 | "kind": "ConfigMap",
4 | "metadata": {
5 | "name": "cm-should-not-load-either"
6 | },
7 | "data": {
8 | "foo": "bar",
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/internal/cmd/testdata/components/c2.jsonnet:
--------------------------------------------------------------------------------
1 | {
2 | apiVersion: 'v1',
3 | kind: 'ConfigMap',
4 | metadata: {
5 | name: 'cm2',
6 | },
7 | data: {
8 | foo: std.extVar('extFoo'),
9 | bar: importstr 'data://myds',
10 | },
11 | }
12 |
--------------------------------------------------------------------------------
/internal/model/testdata/bad-app/bad-dup-preproc.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: qbec.io/v1alpha1
2 | kind: App
3 | metadata:
4 | name: bad-pre-proc
5 | spec:
6 | preProcessor: lib/foo.jsonnet:lib2/foo.jsonnet
7 | environments:
8 | prod:
9 | server: http://baseline-server
10 |
11 |
--------------------------------------------------------------------------------
/internal/model/testdata/bad-app/envs/override-dev.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: qbec.io/v1alpha1
2 | kind: EnvironmentMap
3 | spec:
4 | environments:
5 | dev:
6 | server: https://dev-server
7 | properties:
8 | envType: overridden
9 | includes:
10 | - b
11 |
--------------------------------------------------------------------------------
/site/content/userguide/_index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: User guide
3 | chapter: true
4 | weight: 10
5 | ---
6 |
7 | User guide
8 |
9 | After a quick qbec tour, we describe core concepts, how to organize your files and folders,
10 | create components, and use qbec to manage them.
--------------------------------------------------------------------------------
/examples/test-app/environments/_.libsonnet:
--------------------------------------------------------------------------------
1 | {
2 | components: {
3 | service1: {
4 | cpu: '10m',
5 | memory: '4Gi',
6 | },
7 | service2: {
8 | cpu: '100m',
9 | memory: '8Gi',
10 | longVal: 'a really long value',
11 | },
12 | },
13 | }
14 |
--------------------------------------------------------------------------------
/internal/model/testdata/bad-app/bad-dup-postproc.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: qbec.io/v1alpha1
2 | kind: App
3 | metadata:
4 | name: bad-post-proc
5 | spec:
6 | postProcessor: lib/foo.jsonnet:lib2/foo.jsonnet
7 | environments:
8 | prod:
9 | server: http://baseline-server
10 |
11 |
--------------------------------------------------------------------------------
/internal/cmd/testdata/qbec-bad-ds1.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: qbec.io/v1alpha1
3 | kind: App
4 | metadata:
5 | name: bad-ds1
6 | spec:
7 | dataSources:
8 | - exec://foo
9 | environments:
10 | dev:
11 | server: https://dev-server
12 | defaultNamespace: kube-system
13 |
--------------------------------------------------------------------------------
/internal/model/testdata/http-app/envs.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: qbec.io/v1alpha1
2 | kind: EnvironmentMap
3 | spec:
4 | environments:
5 | stage:
6 | server: https://stage-server
7 | dev:
8 | server: https://new-dev-server
9 | prod:
10 | server: https://prod-server
11 |
--------------------------------------------------------------------------------
/internal/cmd/testdata/components/c1.jsonnet:
--------------------------------------------------------------------------------
1 | function (tlaFoo = 'bar') (
2 | {
3 | apiVersion: 'v1',
4 | kind: 'ConfigMap',
5 | metadata: {
6 | name: 'cm1',
7 | },
8 | data: {
9 | foo: tlaFoo,
10 | },
11 | }
12 | )
13 |
--------------------------------------------------------------------------------
/internal/model/testdata/bad-app/bad-dup-ext.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: qbec.io/v1alpha1
2 | kind: App
3 | metadata:
4 | name: test-app
5 | spec:
6 | vars:
7 | external:
8 | - name: foo
9 | - name: foo
10 | environments:
11 | prod:
12 | server: http://baseline-server
13 |
14 |
--------------------------------------------------------------------------------
/internal/commands/testdata/components/c1.jsonnet:
--------------------------------------------------------------------------------
1 | function (tlaFoo = 'bar') (
2 | {
3 | apiVersion: 'v1',
4 | kind: 'ConfigMap',
5 | metadata: {
6 | name: 'cm1',
7 | },
8 | data: {
9 | foo: tlaFoo,
10 | },
11 | }
12 | )
13 |
--------------------------------------------------------------------------------
/internal/commands/testdata/projects/multi-ns/qbec.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: qbec.io/v1alpha1
3 | kind: App
4 | metadata:
5 | name: multi-ns
6 | spec:
7 | libPaths:
8 | - lib
9 | clusterScopedLists: true
10 | environments:
11 | local:
12 | context: kind-kind
13 | defaultNamespace: second
14 |
--------------------------------------------------------------------------------
/examples/helm3/components/victoria-metrics/datasource.libsonnet:
--------------------------------------------------------------------------------
1 | {
2 | objects: import 'data://helm/github.com/VictoriaMetrics/helm-charts/raw/347d4558d9c25cd341718bf5a2ee167da042c080/packages/victoria-metrics-cluster-0.9.6.tgz?config-from=victoria-config',
3 | config: {
4 | options: {},
5 | values: {},
6 | },
7 | }
8 |
--------------------------------------------------------------------------------
/internal/cmd/testdata/qbec-bad.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: qbec.io/v1alpha1
3 | kind: App
4 | metadata:
5 | name: bad-app
6 | spec:
7 | vars:
8 | computed:
9 | - name: compFoo
10 | code: '{'
11 | environments:
12 | dev:
13 | server: https://dev-server
14 | defaultNamespace: kube-system
15 |
--------------------------------------------------------------------------------
/internal/model/testdata/bad-app/bad-env-include-exclude.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: qbec.io/v1alpha1
2 | kind: App
3 | metadata:
4 | name: test-app
5 | spec:
6 | environments:
7 | dev:
8 | server: https://dev-server
9 | includes:
10 | - a
11 | - c
12 | excludes:
13 | - b
14 | - c
--------------------------------------------------------------------------------
/examples/test-app/params.libsonnet:
--------------------------------------------------------------------------------
1 | local globutil = import 'globutil.libsonnet';
2 | local p = globutil.transform(import 'glob-import:environments/*.libsonnet', globutil.nameOnly);
3 |
4 | local key = std.extVar('qbec.io/env');
5 | if std.objectHas(p, key) then p[key] else error 'Environment ' + key + ' not defined in environments/'
6 |
--------------------------------------------------------------------------------
/internal/eval/testdata/params.libsonnet:
--------------------------------------------------------------------------------
1 | {
2 | components: {
3 | base: {
4 | env: std.extVar('qbec.io/env'),
5 | ns: std.extVar('qbec.io/defaultNs'),
6 | tag: std.extVar('qbec.io/tag'),
7 | foo: std.extVar('qbec.io/envProperties').foo,
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/vm/internal/natives/testdata/bad-relative.jsonnet:
--------------------------------------------------------------------------------
1 | local expandHelmTemplate = std.native('expandHelmTemplate');
2 |
3 | expandHelmTemplate(
4 | './charts/foobar',
5 | {
6 | foo: 'barbar',
7 | },
8 | {
9 | namespace: 'my-ns',
10 | name: 'my-name',
11 | verbose: true,
12 | }
13 | )
14 |
--------------------------------------------------------------------------------
/internal/commands/testdata/projects/wait/qbec.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: qbec.io/v1alpha1
3 | kind: App
4 | metadata:
5 | name: wait
6 | spec:
7 | environments:
8 | local:
9 | context: kind-kind
10 | defaultNamespace: default
11 | vars:
12 | external:
13 | - name: wait
14 | default: true
15 |
16 |
--------------------------------------------------------------------------------
/cmd/changelog-extractor/testdata/abc.txt:
--------------------------------------------------------------------------------
1 | This
2 | is
3 | a
4 | file
5 | contaning
6 | the
7 | changelog
8 | separated by ## vX.X.X
9 | at
10 | the
11 | beginning
12 | of
13 | line
14 | ## v0.0.1
15 | This goes in the published release notes
16 | this too
17 | ## v0.0.1-older
18 | This does not
19 | ## v0.0.1-oldest
20 | Neither does this
21 |
--------------------------------------------------------------------------------
/internal/commands/testdata/projects/string-secrets/qbec.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: qbec.io/v1alpha1
2 | kind: App
3 | metadata:
4 | name: string-secrets
5 | spec:
6 | environments:
7 | local:
8 | context: kind-kind
9 | defaultNamespace: default
10 | vars:
11 | external:
12 | - name: secretValue
13 | default: foobar
14 |
--------------------------------------------------------------------------------
/examples/test-app/components/test-job.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: batch/v1
2 | kind: Job
3 | metadata:
4 | generateName: tj-
5 | spec:
6 | template:
7 | spec:
8 | containers:
9 | - name: pi
10 | image: perl
11 | command: ["perl"]
12 | args: ["-Mbignum=bpi", "-wle", "print bpi(2000)"]
13 | restartPolicy: Never
14 |
--------------------------------------------------------------------------------
/internal/model/testdata/bad-app/bad-dup-tla.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: qbec.io/v1alpha1
2 | kind: App
3 | metadata:
4 | name: test-app
5 | spec:
6 | vars:
7 | topLevel:
8 | - name: foo
9 | components: [ 'a' ]
10 | - name: foo
11 | components: [ 'a' ]
12 | environments:
13 | prod:
14 | server: http://baseline-server
15 |
16 |
--------------------------------------------------------------------------------
/site/content/reference/_index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Reference
3 | chapter: true
4 | weight: 15
5 | ---
6 |
7 | Reference
8 |
9 | We describe qbec interfaces as well as some high-level implementation information.
10 | We expect this will help users create a mental model of how qbec works
11 | and understand what is _supposed_ to happen for various operations.
12 |
--------------------------------------------------------------------------------
/vm/internal/natives/testdata/consumer.jsonnet:
--------------------------------------------------------------------------------
1 | local expandHelmTemplate = std.native('expandHelmTemplate');
2 |
3 | expandHelmTemplate(
4 | './charts/foobar',
5 | {
6 | foo: 'barbar',
7 | },
8 | {
9 | namespace: 'my-ns',
10 | nameTemplate: 'my-name',
11 | thisFile: std.thisFile,
12 | verbose: true,
13 | }
14 | )
15 |
--------------------------------------------------------------------------------
/examples/helm3/components/apache/datasource.libsonnet:
--------------------------------------------------------------------------------
1 | {
2 | objects: import 'data://helm/apache?config-from=apache-config',
3 | config: {
4 | name: 'mock-release',
5 | options: {
6 | repo: 'https://charts.bitnami.com/bitnami',
7 | version: '11.2.17',
8 | namespace: 'foobar',
9 | },
10 | values: {
11 | key: 'value',
12 | },
13 | },
14 | }
15 |
--------------------------------------------------------------------------------
/internal/commands/testdata/projects/lazy-resources/qbec.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: qbec.io/v1alpha1
3 | kind: App
4 | metadata:
5 | name: lazy-resources
6 | spec:
7 | libPaths:
8 | - lib
9 | environments:
10 | local:
11 | context: kind-kind
12 | defaultNamespace: default
13 | vars:
14 | external:
15 | - name: suffix
16 | default: '001'
17 |
18 |
--------------------------------------------------------------------------------
/internal/model/testdata/bad-app/bad-computed.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: qbec.io/v1alpha1
2 | kind: App
3 | metadata:
4 | name: test-app
5 | spec:
6 | vars:
7 | external:
8 | - name: foo
9 | default: 'abc'
10 | computed:
11 | - name: foo
12 | code: |
13 | { foo: 'abc' }
14 | environments:
15 | dev:
16 | server: https://dev-server
17 |
18 |
19 |
--------------------------------------------------------------------------------
/examples/test-app/components/service2.jsonnet:
--------------------------------------------------------------------------------
1 | local objects = import 'objects.libsonnet';
2 |
3 | function(tlaFoo='bar') (
4 | {
5 | configMap: objects.configmap('bar-system', 'svc2-cm', { foo: tlaFoo }),
6 | secret: objects.secret('bar-system', 'svc2-secret', { foo: std.base64('bar') }),
7 | deployment: objects.deployment('bar-system', 'svc2-deploy', 'nginx:latest'),
8 | }
9 | )
10 |
--------------------------------------------------------------------------------
/internal/commands/testdata/projects/policies/components/cm.jsonnet:
--------------------------------------------------------------------------------
1 | local foo = std.extVar('foo');
2 | local cm = {
3 | apiVersion: 'v1',
4 | kind: 'ConfigMap',
5 | metadata: {
6 | name: 'cm1',
7 | annotations: {
8 | 'directives.qbec.io/update-policy': 'never',
9 | },
10 | },
11 | data: {
12 | foo: foo,
13 | },
14 | };
15 | cm
16 |
17 |
--------------------------------------------------------------------------------
/internal/commands/testdata/projects/lazy-resources/components/custom-resources/index.jsonnet:
--------------------------------------------------------------------------------
1 | local parseYaml = std.native('parseYaml');
2 | local externals = import 'externals.libsonnet';
3 | local suffix = externals.suffix;
4 | local params = { foo: 'foo' + suffix, Foo: 'Foo' + suffix };
5 | local resources = parseYaml(importstr './res.yaml');
6 |
7 | std.map(function (obj) obj + { kind: '%(Foo)s' % params }, resources)
8 |
--------------------------------------------------------------------------------
/internal/commands/testdata/projects/policies/qbec.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: qbec.io/v1alpha1
3 | kind: App
4 | metadata:
5 | name: policies
6 | spec:
7 | libPaths:
8 | - lib
9 | environments:
10 | local:
11 | context: kind-kind
12 | defaultNamespace: foobar
13 | vars:
14 | external:
15 | - name: foo
16 | default: foobar
17 | - name: hide
18 | default: false
19 |
--------------------------------------------------------------------------------
/internal/commands/testdata/projects/multi-ns/components/first/index.jsonnet:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | apiVersion: 'v1',
4 | kind: 'Namespace',
5 | metadata: {
6 | name: 'first',
7 | },
8 | },
9 | {
10 | apiVersion: 'v1',
11 | kind: 'ConfigMap',
12 | metadata: {
13 | name: 'first-cm',
14 | namespace: 'first',
15 | },
16 | data: {
17 | foo: 'bar',
18 | },
19 | },
20 | ]
21 |
--------------------------------------------------------------------------------
/internal/commands/testdata/projects/policies/components/secret.jsonnet:
--------------------------------------------------------------------------------
1 | local secret = {
2 | apiVersion: 'v1',
3 | kind: 'Secret',
4 | metadata: {
5 | name: 's1',
6 | annotations: {
7 | 'directives.qbec.io/delete-policy': 'never',
8 | },
9 | },
10 | stringData: {
11 | foo: 'bar',
12 | },
13 | };
14 | if std.extVar('hide') then null else secret
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/.github/RELEASE.md:
--------------------------------------------------------------------------------
1 | # Release process
2 |
3 | 1. Update [CHANGELOG.md](../CHANGELOG.md) and [Makefile](../Makefile) directly on main and commit locally with "update changelog, up version" comment
4 | * Bump minor version when upgrading marquee dependencies (jsonnet, k8s client, golang, etc.) or when making incompatible changes
5 | 1. Run `./prepare-release.sh`
6 | 1. Push to main with following command: `git push --atomic upstream main `
7 |
--------------------------------------------------------------------------------
/internal/model/testdata/bad-app/app-warn.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: qbec.io/v1alpha1
2 | kind: App
3 | metadata:
4 | name: test-app
5 | spec:
6 | namespaceTagSuffix: true
7 | excludes:
8 | - a
9 | environments:
10 | prod:
11 | server: http://baseline-server
12 | excludes:
13 | - a
14 | dev:
15 | server: https://dev-server
16 | includes:
17 | - b
18 | envFiles:
19 | - dev2.yaml
20 |
21 |
22 |
--------------------------------------------------------------------------------
/internal/commands/testdata/projects/simple-service/qbec.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: qbec.io/v1alpha1
3 | kind: App
4 | metadata:
5 | name: simple-service
6 | spec:
7 | postProcessor: pp.libsonnet
8 | environments:
9 | local:
10 | context: kind-kind
11 | defaultNamespace: default
12 | vars:
13 | external:
14 | - name: replicas
15 | default: 1
16 | - name: cmContent
17 | default: 'hello world'
18 |
19 |
--------------------------------------------------------------------------------
/examples/helm3/params.libsonnet:
--------------------------------------------------------------------------------
1 | // this file returns the params for the current qbec environment
2 | local env = std.extVar('qbec.io/env');
3 | local paramsMap = import 'glob-import:environments/*.libsonnet';
4 | local baseFile = if env == '_' then 'base' else env;
5 | local key = 'environments/%s.libsonnet' % baseFile;
6 |
7 | if std.objectHas(paramsMap, key)
8 | then paramsMap[key]
9 | else error 'no param file %s found for environment %s' % [key, env]
10 |
--------------------------------------------------------------------------------
/internal/commands/testdata/projects/simple-service/pp.libsonnet:
--------------------------------------------------------------------------------
1 | function(object) (
2 | // please don't do things like this for real. This is just to make my testing easy :)
3 | local replicas = std.extVar('replicas');
4 | local cmContent = std.extVar('cmContent');
5 | if object.kind == 'Deployment' then
6 | object { spec+: { replicas: replicas } }
7 | else if object.kind == 'ConfigMap' then
8 | object { data+: { 'index.html': cmContent } }
9 | else
10 | object
11 | )
12 |
--------------------------------------------------------------------------------
/internal/cmd/testdata/qbec-bad2.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: qbec.io/v1alpha1
3 | kind: App
4 | metadata:
5 | name: bad-app
6 | spec:
7 | vars:
8 | computed:
9 | - name: compFoo
10 | code: |
11 | {
12 | bar: std.extVar('compBar'),
13 | }
14 | - name: compBar
15 | code: |
16 | {
17 | baz: 10,
18 | }
19 | environments:
20 | dev:
21 | server: https://dev-server
22 | defaultNamespace: kube-system
23 |
--------------------------------------------------------------------------------
/examples/external-data-app/components/my-config-map.jsonnet:
--------------------------------------------------------------------------------
1 | // we pull in the generated config map as a string usiong importstr
2 | // the syntax for the import is:
3 | // data://[/optional/path]
4 | // in this data source implementation the path appears in the config map
5 | // when used for real it can contain information such as the path to a vault KV entry,
6 | // a path to a helm chart and so on.
7 | local cmYaml = importstr 'data://config-map/some/path';
8 | std.native('parseYaml')(cmYaml)
9 |
--------------------------------------------------------------------------------
/internal/cmd/testdata/qbec-bad-ds2.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: qbec.io/v1alpha1
3 | kind: App
4 | metadata:
5 | name: bad-ds1
6 | spec:
7 | vars:
8 | computed:
9 | - name: c1
10 | code: |
11 | import 'data://foo'
12 | - name: c2
13 | code: |
14 | {
15 | command: 'cat',
16 | }
17 | dataSources:
18 | - exec://foo?configVar=c2
19 | environments:
20 | dev:
21 | server: https://dev-server
22 | defaultNamespace: kube-system
23 |
--------------------------------------------------------------------------------
/internal/commands/testdata/projects/lint-app/components/foo.jsonnet:
--------------------------------------------------------------------------------
1 | local foo = import 'foo.libsonnet';
2 | local today = importstr 'data://today';
3 | local s = import 'data://jsonstr';
4 | local o = import 'data://object';
5 | local a = import 'data://array';
6 |
7 | {
8 | apiVersion: "v1",
9 | kind: "ConfigMap",
10 | metadata: {
11 | name: 'lint-test'
12 | },
13 | data: {
14 | foo: std.toString(foo),
15 | today: today,
16 | str: s,
17 | bar: o.bar,
18 | a: std.map(function (item) 'x:' + item, a),
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/examples/helm3/qbec.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: qbec.io/v1alpha1
2 | kind: App
3 | metadata:
4 | name: helm3
5 | spec:
6 | environments:
7 | default:
8 | defaultNamespace: charts
9 | context: kind
10 | vars:
11 | computed:
12 | - name: helmSetup
13 | code: |
14 | {}
15 | - name: victoria-config
16 | code: |
17 | (import 'components/victoria-metrics/datasource.libsonnet').config
18 | - name: apache-config
19 | code: |
20 | (import 'components/apache/datasource.libsonnet').config
21 | dataSources:
22 | - helm3://helm?configVar=helmSetup
23 |
--------------------------------------------------------------------------------
/site/config.yaml:
--------------------------------------------------------------------------------
1 | baseURL: http://qbec.io
2 | languageCode: en-us
3 | title: Qbec
4 | theme: learn
5 | params:
6 | copyright: "© 2019 - the qbec authors"
7 | description: a tool to configure and create Kubernetes objects on multiple environments
8 | disableAssetsBusting: true
9 | disableSearch: true
10 | disableInlineCopyToClipBoard: true
11 | editURL: https://github.com/splunk/qbec/edit/main/site/content/
12 | themeVariant: blue
13 | menu:
14 | shortcuts:
15 | - name: " Github repo"
16 | identifier: "ds"
17 | url: "https://github.com/splunk/qbec"
18 | weight: 10
19 |
--------------------------------------------------------------------------------
/test.sh:
--------------------------------------------------------------------------------
1 | # Copyright 2025 Splunk Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 |
--------------------------------------------------------------------------------
/internal/commands/testdata/qbec.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: qbec.io/v1alpha1
3 | kind: App
4 | metadata:
5 | name: app1
6 | spec:
7 | libPaths:
8 | - lib
9 | namespaceTagSuffix: true
10 | vars:
11 | topLevel:
12 | - name: tlaFoo
13 | components: [ 'c1' ]
14 | external:
15 | - name: extFoo
16 | default: 'baz'
17 | - name: extBar
18 | default: { bar: 'quux' }
19 | - name: noDefault
20 | environments:
21 | minikube:
22 | context: minikube
23 | defaultNamespace: kube-public
24 | dev:
25 | server: https://dev-server
26 | defaultNamespace: kube-system
27 |
--------------------------------------------------------------------------------
/internal/commands/testdata/projects/multi-ns/components/second/index.jsonnet:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | apiVersion: 'v1',
4 | kind: 'Namespace',
5 | metadata: {
6 | name: 'second',
7 | },
8 | },
9 | {
10 | apiVersion: 'v1',
11 | kind: 'ConfigMap',
12 | metadata: {
13 | name: 'second-cm', // don't specify a namespace, use default
14 | },
15 | data: {
16 | foo: 'bar',
17 | },
18 | },
19 | {
20 | apiVersion: 'v1',
21 | kind: 'Secret',
22 | metadata: {
23 | name: 'second-secret',
24 | namespace: 'second',
25 | },
26 | stringData: {
27 | foo: 'bar',
28 | },
29 | },
30 | ]
31 |
--------------------------------------------------------------------------------
/site/content/getting-started/_index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Getting started
3 | weight: 1
4 | ---
5 |
6 | ## Install manually
7 |
8 | * Download a release binary for your operating system from the [github release page](https://github.com/splunk/qbec/releases).
9 | * unzip the archive. This should have 2 executables `qbec` and `jsonnet-qbec`
10 | * install the `qbec` executable somewhere under your `PATH`.
11 |
12 | ## Install using brew
13 |
14 | If you have homebrew installed, you can install `qbec` with the following commands:
15 | ```
16 | $ brew tap splunk/tap
17 | $ brew install qbec
18 | ```
19 |
20 |
21 | You are now ready to start [the tour](../userguide/tour/).
22 |
--------------------------------------------------------------------------------
/site/layouts/partials/menu-footer.html:
--------------------------------------------------------------------------------
1 |
16 |
17 |
--------------------------------------------------------------------------------
/internal/commands/testdata/projects/lazy-resources/components/crds/index.jsonnet:
--------------------------------------------------------------------------------
1 | local parseYaml = std.native('parseYaml');
2 | local externals = import 'externals.libsonnet';
3 | local suffix = externals.suffix;
4 | local params = { foo: 'foo' + suffix, Foo: 'Foo' + suffix };
5 | local crd = parseYaml(importstr './crd.yaml')[0];
6 |
7 | crd + {
8 | metadata +: {
9 | name: '%(foo)ss.test.qbec.io' % params,
10 | },
11 | spec +: {
12 | names +: {
13 | kind: '%(Foo)s' % params,
14 | listKind: '%(Foo)sList' % params,
15 | plural: '%(foo)ss' % params,
16 | singular: '%(foo)s' % params,
17 | }
18 | }
19 | }
20 |
21 |
--------------------------------------------------------------------------------
/internal/types/doc.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 Splunk Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | // Package types contains all type specific processing.
16 | package types
17 |
--------------------------------------------------------------------------------
/site/layouts/partials/logo.html:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/internal/remote/auth-plugins.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 Splunk Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package remote
16 |
17 | import (
18 | _ "k8s.io/client-go/plugin/pkg/client/auth" // register all auth plugins
19 | )
20 |
--------------------------------------------------------------------------------
/internal/commands/alpha.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 Splunk Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package commands
16 |
17 | import "github.com/spf13/cobra"
18 |
19 | func newAlphaCommand() *cobra.Command {
20 | cmd := &cobra.Command{
21 | Use: "alpha",
22 | Short: "experimental qbec commands",
23 | }
24 | return cmd
25 | }
26 |
--------------------------------------------------------------------------------
/internal/remote/testdata/pristine/kc-created.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: PersistentVolumeClaim
3 | metadata:
4 | annotations:
5 | pv.kubernetes.io/bind-completed: "yes"
6 | pv.kubernetes.io/bound-by-controller: "yes"
7 | volume.beta.kubernetes.io/storage-provisioner: kubernetes.io/aws-ebs
8 | creationTimestamp: 2019-07-28T17:39:09Z
9 | finalizers:
10 | - kubernetes.io/pvc-protection
11 | name: storage
12 | namespace: istio-test
13 | resourceVersion: "41064230"
14 | selfLink: /api/v1/namespaces/istio-test/persistentvolumeclaims/storage
15 | uid: 9b0bfad7-b15e-11e9-bd54-0ace00d90692
16 | spec:
17 | accessModes:
18 | - ReadWriteOnce
19 | resources:
20 | requests:
21 | storage: 1Mi
22 | storageClassName: gp2
23 | volumeName: pvc-9b0bfad7-b15e-11e9-bd54-0ace00d90692
24 | status:
25 | accessModes:
26 | - ReadWriteOnce
27 | capacity:
28 | storage: 1Gi
29 | phase: Bound
30 |
--------------------------------------------------------------------------------
/cmd/changelog-extractor/main_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 Splunk Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package main
16 |
17 | // Example shows usage of printReleaseNotes function
18 | func Example() {
19 | err := printReleaseNotes("testdata/abc.txt")
20 | if err != nil {
21 | panic(err)
22 | }
23 | // Output: This goes in the published release notes
24 | // this too
25 | }
26 |
--------------------------------------------------------------------------------
/internal/commands/testdata/projects/wait/components/deployment/index.jsonnet:
--------------------------------------------------------------------------------
1 | local wait = std.extVar('wait');
2 | local annotations = if !wait then { 'directives.qbec.io/wait-policy': 'never' } else {};
3 |
4 | {
5 | apiVersion: 'apps/v1',
6 | kind: 'Deployment',
7 | metadata: {
8 | annotations: annotations,
9 | labels: {
10 | app: 'nginx',
11 | },
12 | name: 'nginx-by-wait',
13 | },
14 | spec: {
15 | replicas: 1,
16 | selector: {
17 | matchLabels: {
18 | app: 'nginx',
19 | },
20 | },
21 | strategy: {
22 | type: 'RollingUpdate',
23 | },
24 | template: {
25 | metadata: {
26 | labels: {
27 | app: 'nginx',
28 | },
29 | },
30 | spec: {
31 | containers: [
32 | {
33 | image: 'nginx',
34 | imagePullPolicy: 'Always',
35 | name: 'nginx',
36 | },
37 | ],
38 | },
39 | },
40 | },
41 | }
42 |
--------------------------------------------------------------------------------
/vm/internal/importers/api.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 Splunk Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package importers
16 |
17 | import "github.com/google/go-jsonnet"
18 |
19 | // ExtendedImporter extends the jsonnet importer interface to add a new method that can determine whether
20 | // an importer can be used for a path.
21 | type ExtendedImporter interface {
22 | jsonnet.Importer
23 | CanProcess(path string) bool
24 | }
25 |
--------------------------------------------------------------------------------
/site/layouts/partials/custom-header.html:
--------------------------------------------------------------------------------
1 |
16 |
17 |
32 |
--------------------------------------------------------------------------------
/examples/external-data-app/config-map.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # Copyright 2025 Splunk Inc.
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 |
17 | # the implementation of the config-map data source that shows that you can pull data out
18 | # of arguments, the environment and stdin
19 | cat <&2
22 | exit 1
23 | fi
24 |
25 | COMMIT=$(git rev-parse --short HEAD)
26 |
27 | make site
28 | rsync -rvtl ./site/public/ ../qbec-docs/
29 | cd ../qbec-docs
30 | git add .
31 | git commit -m "update docs from $COMMIT"
32 | git push origin gh-pages
33 |
34 |
--------------------------------------------------------------------------------
/internal/testutil/testutil.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 Splunk Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package testutil
16 |
17 | import (
18 | "runtime"
19 | )
20 |
21 | // FileNotFoundMessage is the string to be used in test code for comparing to file not found error messages.
22 | var FileNotFoundMessage = "no such file or directory"
23 |
24 | func init() {
25 | if runtime.GOOS == "windows" {
26 | FileNotFoundMessage = "The system cannot find the file specified."
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/vm/internal/natives/json.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 Splunk Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package natives
16 |
17 | import (
18 | "encoding/json"
19 | "io"
20 | )
21 |
22 | // ParseJSON parses the contents of the reader into a data object and returns it.
23 | func ParseJSON(reader io.Reader) (interface{}, error) {
24 | dec := json.NewDecoder(reader)
25 | var data interface{}
26 | if err := dec.Decode(&data); err != nil {
27 | return nil, err
28 | }
29 | return data, nil
30 | }
31 |
--------------------------------------------------------------------------------
/internal/commands/testdata/projects/lazy-resources/components/crds/crd.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: apiextensions.k8s.io/v1beta1
3 | kind: CustomResourceDefinition
4 | metadata:
5 | name: foo.test.qbec.io
6 | spec:
7 | group: test.qbec.io
8 | names:
9 | kind: Foo
10 | listKind: FooList
11 | plural: foos
12 | singular: foo
13 | scope: Namespaced
14 | validation:
15 | openAPIV3Schema:
16 | description: Foo is a foo.
17 | properties:
18 | apiVersion:
19 | type: string
20 | kind:
21 | type: string
22 | metadata:
23 | type: object
24 | spec:
25 | properties:
26 | bar:
27 | description: the bar for the foo
28 | type: string
29 | required:
30 | - bar
31 | type: object
32 | status:
33 | type: object
34 | required:
35 | - metadata
36 | - spec
37 | type: object
38 | version: v1alpha1
39 | versions:
40 | - name: v1alpha1
41 | served: true
42 | storage: true
43 |
--------------------------------------------------------------------------------
/vm/internal/ds/api.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 Splunk Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package ds
16 |
17 | import (
18 | "io"
19 |
20 | "github.com/splunk/qbec/vm/datasource"
21 | )
22 |
23 | // DataSourceWithLifecycle represents an external data source that implements the methods needed by the data source
24 | // as well as lifecycle methods to handle initialization and clean up.
25 | type DataSourceWithLifecycle interface {
26 | datasource.DataSource
27 | Init(c datasource.ConfigProvider) error
28 | io.Closer
29 | }
30 |
--------------------------------------------------------------------------------
/examples/test-app/lib/objects.libsonnet:
--------------------------------------------------------------------------------
1 | {
2 | configmap(namespace, name, vars={}):: {
3 | apiVersion: 'v1',
4 | kind: 'ConfigMap',
5 | metadata: {
6 | namespace: namespace,
7 | name: name,
8 | },
9 | data: vars,
10 | },
11 | secret(namespace, name, vars={}):: {
12 | apiVersion: 'v1',
13 | kind: 'Secret',
14 | metadata: {
15 | namespace: namespace,
16 | name: name,
17 | },
18 | data: vars,
19 | },
20 | deployment(namespace, name, image):: {
21 | apiVersion: 'apps/v1',
22 | kind: 'Deployment',
23 | metadata: {
24 | namespace: namespace,
25 | name: name,
26 | },
27 | spec: {
28 | selector: {
29 | matchLabels: {
30 | app: name,
31 | },
32 | },
33 | template: {
34 | metadata: {
35 | labels: { app: name },
36 | },
37 | spec: {
38 | containers: [
39 | {
40 | name: 'main',
41 | image: image,
42 | },
43 | ],
44 | },
45 | },
46 | },
47 | },
48 | }
49 |
--------------------------------------------------------------------------------
/site/content/userguide/usage/common-metadata.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Common object metadata
3 | weight: 18
4 | ---
5 |
6 | qbec provides an easy mechanism to set up common metadata like annotations for all objects
7 | produced. For example, you may want to set up a `team` annotation for all objects.
8 |
9 | You do this by defining a post-processor. A post-processor is a jsonnet file that contains
10 | a single function like so:
11 |
12 | ```
13 | // the post processor jsonnet must return a function taking exactly one parameter
14 | // called "object" and returning its decorated version.
15 |
16 | function (object) object {
17 | metadata +: {
18 | annotations +: {
19 | team: 'my-team',
20 | },
21 | },
22 | }
23 | ```
24 |
25 | Note that the argument of the function *must* be called `object`.
26 |
27 | You then set the `postProcessor` attribute in `qbec.yaml` set to the path of this file.
28 |
29 | **Note:** It is possible to abuse this feature to do a lot more than adding metadata since it
30 | is a hook that allows you to do almost anything to the supplied object. Abuse with care :)
31 |
--------------------------------------------------------------------------------
/internal/commands/testdata/test.json/test.json:
--------------------------------------------------------------------------------
1 | {"groups": {"kind": "APIGroupList",
2 | "apiVersion": "v1",
3 | "num": 1,"bool": true,"float": 1.12,
4 | "groups": [
5 | {
6 | "name": "",
7 | "versions": [{
8 | "groupVersion": "v1", "version": "v1"
9 | }
10 | ],"preferredVersion": {
11 | "groupVersion": "v1",
12 | "version": "v1"
13 | },
14 | "serverAddressByClientCIDRs": null
15 | }
16 | ]
17 | },
18 | "resourceLists": {
19 | ":v1": {
20 | "kind": "APIResourceList",
21 | "groupVersion": "v1","resources": [
22 | {
23 | "name": "bindings",
24 | "singularName": "",
25 | "namespaced": true,
26 | "kind": "Binding",
27 | "verbs": ["create"]
28 | },
29 | {
30 | "name": "resourcequotas/status",
31 | "singularName": "",
32 | "namespaced": true,
33 | "kind": "ResourceQuota",
34 | "verbs": [
35 | "get", "patch",
36 | "update"
37 | ]
38 | }]
39 | }}}
40 |
--------------------------------------------------------------------------------
/examples/external-data-app/qbec.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: qbec.io/v1alpha1
2 | kind: App
3 | metadata:
4 | name: external-data
5 | spec:
6 | vars:
7 | external:
8 | - name: cm-value
9 | default: def
10 | computed:
11 | # the variable below is the configuration of the config-map data source where
12 | # you can specify a command, arguments to the program, environment variables,
13 | # and standard input
14 | - name: cmdConfig
15 | code: |
16 | {
17 | command: './config-map.sh',
18 | args: [ std.extVar('cm-value') ],
19 | env: {
20 | qbec_env: std.extVar('qbec.io/env'),
21 | },
22 | stdin: 'now is the time for all good men to come to the aid of the party',
23 | }
24 | dataSources:
25 | # data sources are declared to be of the form
26 | # ://?configVar=
27 | # in this case kind=exec, name=config-map, and config var is cmdConfig defined above
28 | - exec://config-map?configVar=cmdConfig
29 | environments:
30 | local:
31 | context: minikube
32 |
--------------------------------------------------------------------------------
/prepare-release.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Copyright 2025 Splunk Inc.
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 |
17 | set -euo pipefail
18 |
19 | version_num=$(grep ^VERSION Makefile | awk -F= '{print $2}' | sed 's/ //')
20 |
21 | if [[ -z "${version_num}" ]]
22 | then
23 | echo "unable to derive version, abort" >&2
24 | exit 1
25 | fi
26 |
27 | version="v${version_num}"
28 | echo "publish version ${version}"
29 |
30 | if [[ ! -z "$(git tag -l ${version})" ]]
31 | then
32 | echo "tag ${version} already exists, abort" >&2
33 | exit 1
34 | fi
35 |
36 | git tag -s -m "${version} release" ${version}
37 |
38 |
--------------------------------------------------------------------------------
/.golangci.yml:
--------------------------------------------------------------------------------
1 | # Copyright 2025 Splunk Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | run:
16 | timeout: 3m
17 | tests: true
18 |
19 | linters-settings:
20 | exhaustive:
21 | default-signifies-exhaustive: false
22 | golint:
23 | min-confidence: 0
24 |
25 | linters:
26 | disable-all: true
27 | enable:
28 | - dogsled
29 | - errcheck
30 | - goconst
31 | - gocyclo
32 | - gofmt
33 | - goimports
34 | - revive
35 | - gosimple
36 | - govet
37 | - ineffassign
38 | - staticcheck
39 | - stylecheck
40 | - typecheck
41 | - unconvert
42 |
--------------------------------------------------------------------------------
/site/content/reference/jsonnet-vars.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Standard jsonnet variables
3 | weight: 110
4 | ---
5 |
6 | qbec exposes the following standard jsonnet variables whenever it evaluates components.
7 |
8 | * `qbec.io/env` - the name of the environment for which processing occurs.
9 | * `qbec.io/envProperties` - the properties associated with the environment if present or an empty object. For the
10 | baseline environment (`_`), this is set to the `baseProperties` object define in `qbec.yaml`.
11 | * `qbec.io/tag` - the tag specified for the command using the `--app-tag` option.
12 | * `qbec.io/defaultNs` - the default namespace in use. This is typically picked from the environment definition,
13 | possibly changed for app tags, or the value forced from the command line using the `--force:k8s-namespace` option.
14 | * `qbec.io/cleanMode` - has the `off` or `on`. The `on` value is only set for the `show --clean` command.
15 |
16 | In addition to the above, qbec will also set the default values for declared external variables and
17 | override them from command line arguments.
18 |
19 | See the [component evaluation page](../component-evaluation) for the gory details.
20 |
--------------------------------------------------------------------------------
/examples/external-data-app/README.md:
--------------------------------------------------------------------------------
1 | external-data-app
2 | ---
3 |
4 | Possibly the world's most stupid way to create a config map.
5 |
6 | The point of this example is to show how to run an external program as part of qbec component evaluation.
7 |
8 | A real example would run something more meaningful like `helm`, `istioctl`, or a script that talks to vault,
9 | but that would need to pull in too many dependencies for our little demo.
10 |
11 | The things to look for are:
12 |
13 | * the computed variable called `cmdConfig` in [qbec.yaml](qbec.yaml) that shows the JSON that can be used to
14 | configure how an external program is invoked with arguments, environment variables and standard input.
15 | * the data source definition in [qbec.yaml](qbec.yaml) that defines a data source called `config-map` and associates the
16 | command configuration with it.
17 | * the script [config-map.sh](config-map.sh) that uses its inputs to dump a config map object in YAML form
18 | * the import in [components/my-config-map.jsonnet](components/my-config-map.jsonnet) that imports the data as a
19 | string and returns the parsed YAML.
20 |
21 | Run `qbec show local` to see the output.
22 |
--------------------------------------------------------------------------------
/internal/remote/testdata/pristine/kc-applied.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: PersistentVolumeClaim
3 | metadata:
4 | annotations:
5 | kubectl.kubernetes.io/last-applied-configuration: |
6 | {"apiVersion":"v1","kind":"PersistentVolumeClaim","metadata":{"annotations":{},"name":"storage","namespace":"istio-test"},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"1Mi"}}}}
7 | pv.kubernetes.io/bind-completed: "yes"
8 | pv.kubernetes.io/bound-by-controller: "yes"
9 | volume.beta.kubernetes.io/storage-provisioner: kubernetes.io/aws-ebs
10 | creationTimestamp: 2019-07-28T17:39:09Z
11 | finalizers:
12 | - kubernetes.io/pvc-protection
13 | name: storage
14 | namespace: istio-test
15 | resourceVersion: "41064362"
16 | selfLink: /api/v1/namespaces/istio-test/persistentvolumeclaims/storage
17 | uid: 9b0bfad7-b15e-11e9-bd54-0ace00d90692
18 | spec:
19 | accessModes:
20 | - ReadWriteOnce
21 | resources:
22 | requests:
23 | storage: 1Mi
24 | storageClassName: gp2
25 | volumeName: pvc-9b0bfad7-b15e-11e9-bd54-0ace00d90692
26 | status:
27 | accessModes:
28 | - ReadWriteOnce
29 | capacity:
30 | storage: 1Gi
31 | phase: Bound
32 |
--------------------------------------------------------------------------------
/internal/cmd/testdata/qbec.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: qbec.io/v1alpha1
3 | kind: App
4 | metadata:
5 | name: app1
6 | spec:
7 | libPaths:
8 | - lib
9 | namespaceTagSuffix: true
10 | vars:
11 | topLevel:
12 | - name: tlaFoo
13 | components: [ 'c1' ]
14 | external:
15 | - name: extFoo
16 | default: 'baz'
17 | - name: extBar
18 | default: { bar: 'quux' }
19 | - name: noDefault
20 | computed:
21 | - name: compFoo
22 | code: |
23 | {
24 | foo: std.extVar('extFoo'),
25 | bar: std.extVar('extBar'),
26 | baz: (import 'lib/baz.libsonnet').baz,
27 | env: std.extVar('qbec.io/env'),
28 | }
29 | - name: compBar
30 | code: |
31 | import 'comp-file.jsonnet'
32 | - name: dsconfig
33 | code: |
34 | {
35 | command: './script/data-source.sh',
36 | }
37 | dataSources:
38 | - exec://myds?configVar=dsconfig
39 | environments:
40 | minikube:
41 | context: minikube
42 | defaultNamespace: kube-public
43 | dev:
44 | server: https://dev-server
45 | defaultNamespace: kube-system
46 |
--------------------------------------------------------------------------------
/Makefile.tools:
--------------------------------------------------------------------------------
1 | # Copyright 2025 Splunk Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | .tools/jb: JB_VERSION := v0.4.0
16 | .tools/jb: JB_PLATFORM := $(shell uname | tr '[:upper:]' '[:lower:]')-$(shell uname -m)
17 | .tools/jb:
18 | mkdir -p .tools
19 | curl -sSL -o .tools/jb https://github.com/jsonnet-bundler/jsonnet-bundler/releases/download/$(JB_VERSION)/jb-$(JB_PLATFORM)
20 | chmod +x .tools/jb
21 |
22 | .tools/kind: KIND_VERSION := v0.11.1
23 | .tools/kind: KIND_PLATFORM := $(shell uname)-$(shell uname -m)
24 | .tools/kind:
25 | mkdir -p .tools
26 | curl -o $@ -sSL https://kind.sigs.k8s.io/dl/$(KIND_VERSION)/kind-$(KIND_PLATFORM)
27 | chmod +x $@
28 |
29 | tools: .tools/jb .tools/kind
30 |
31 |
--------------------------------------------------------------------------------
/internal/commands/testdata/projects/simple-service/components/simple-service/index.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: v1
3 | kind: ConfigMap
4 | metadata:
5 | name: nginx
6 | data:
7 | 'index.html': 'Hello world'
8 | ---
9 | apiVersion: apps/v1
10 | kind: Deployment
11 | metadata:
12 | labels:
13 | app: nginx
14 | name: nginx
15 | spec:
16 | progressDeadlineSeconds: 600
17 | replicas: 1
18 | revisionHistoryLimit: 10
19 | selector:
20 | matchLabels:
21 | app: nginx
22 | strategy:
23 | rollingUpdate:
24 | maxSurge: 25%
25 | maxUnavailable: 25%
26 | type: RollingUpdate
27 | template:
28 | metadata:
29 | labels:
30 | app: nginx
31 | spec:
32 | containers:
33 | - image: nginx
34 | imagePullPolicy: Always
35 | name: nginx
36 | volumeMounts:
37 | - mountPath: /usr/share/nginx/html
38 | name: content
39 | volumes:
40 | - configMap:
41 | name: nginx
42 | name: content
43 | ---
44 | apiVersion: v1
45 | kind: Service
46 | metadata:
47 | labels:
48 | app: nginx
49 | name: nginx
50 | spec:
51 | ports:
52 | - name: http
53 | port: 80
54 | protocol: TCP
55 | selector:
56 | app: nginx
57 |
--------------------------------------------------------------------------------
/vm/internal/importers/file.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 Splunk Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package importers
16 |
17 | import "github.com/google/go-jsonnet"
18 |
19 | // NewFileImporter creates an extended file importer, wrapping the one supplied.
20 | func NewFileImporter(jfi *jsonnet.FileImporter) *ExtendedFileImporter {
21 | return &ExtendedFileImporter{FileImporter: jfi}
22 | }
23 |
24 | // ExtendedFileImporter wraps a file importer and declares that it can import any path.
25 | type ExtendedFileImporter struct {
26 | *jsonnet.FileImporter
27 | }
28 |
29 | // CanProcess implements the interface method.
30 | func (e *ExtendedFileImporter) CanProcess(_ string) bool {
31 | return true
32 | }
33 |
--------------------------------------------------------------------------------
/internal/commands/testdata/projects/lint-app/qbec.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: qbec.io/v1alpha1
3 | kind: App
4 | metadata:
5 | name: lint-app
6 | spec:
7 | libPaths:
8 | - lib
9 | environments:
10 | local:
11 | context: kind-kind
12 | defaultNamespace: foobar
13 | vars:
14 | computed:
15 | - name: get-date
16 | code: |
17 | {
18 | command: 'date',
19 | args: [ '-u', '+%Y-%m-%d' ],
20 | }
21 | - name: get-object
22 | code: |
23 | {
24 | command: 'echo',
25 | args: [ '{ "bar": "baz" }' ]
26 | }
27 | - name: get-array
28 | code: |
29 | {
30 | command: 'echo',
31 | args: [ '[ "foo", "bar" ]' ]
32 | }
33 | - name: get-jsonstr
34 | code: |
35 | {
36 | command: 'echo',
37 | args: [ '"foobar"' ]
38 | }
39 | dataSources:
40 | - exec://today?configVar=get-date
41 | - exec://object?configVar=get-object
42 | - exec://array?configVar=get-array
43 | - exec://jsonstr?configVar=get-jsonstr
44 | dsExamples:
45 | today: '2021-01-01'
46 | object:
47 | bar: 'baz'
48 | array:
49 | - foo
50 | - bar
51 | jsonstr: '"foobar"'
52 |
--------------------------------------------------------------------------------
/internal/commands/testdata/test.json/test.json.formatted:
--------------------------------------------------------------------------------
1 | {
2 | "groups": {
3 | "kind": "APIGroupList",
4 | "apiVersion": "v1",
5 | "num": 1,
6 | "bool": true,
7 | "float": 1.12,
8 | "groups": [
9 | {
10 | "name": "",
11 | "versions": [
12 | {
13 | "groupVersion": "v1",
14 | "version": "v1"
15 | }
16 | ],
17 | "preferredVersion": {
18 | "groupVersion": "v1",
19 | "version": "v1"
20 | },
21 | "serverAddressByClientCIDRs": null
22 | }
23 | ]
24 | },
25 | "resourceLists": {
26 | ":v1": {
27 | "kind": "APIResourceList",
28 | "groupVersion": "v1",
29 | "resources": [
30 | {
31 | "name": "bindings",
32 | "singularName": "",
33 | "namespaced": true,
34 | "kind": "Binding",
35 | "verbs": [
36 | "create"
37 | ]
38 | },
39 | {
40 | "name": "resourcequotas/status",
41 | "singularName": "",
42 | "namespaced": true,
43 | "kind": "ResourceQuota",
44 | "verbs": [
45 | "get",
46 | "patch",
47 | "update"
48 | ]
49 | }
50 | ]
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/examples/test-app/qbec.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: qbec.io/v1alpha1
2 | kind: App
3 | metadata:
4 | name: example1
5 | spec:
6 | # componentsDir: components
7 | # paramsFile: params.libsonnet
8 | postProcessor: pp.jsonnet
9 | libPaths:
10 | - lib
11 | excludes:
12 | - service2
13 | baseProperties:
14 | core: no-override
15 | envType: unknown
16 | extra:
17 | foo: bar
18 | bar: baz
19 | vars:
20 | topLevel:
21 | - name: tlaFoo
22 | components: ['service2']
23 | external:
24 | - name: externalFoo
25 | default: 'bar'
26 | computed:
27 | - name: c1
28 | code: |
29 | {
30 | env: std.extVar('qbec.io/env'),
31 | }
32 | - name: c2
33 | code: |
34 | {
35 | vars: std.extVar('c1'),
36 | }
37 | - name: c3
38 | code: |
39 | import 'compute.jsonnet'
40 | environments:
41 | local:
42 | context: minikube
43 | properties:
44 | envType: local
45 | dev:
46 | server: https://dev-server
47 | includes:
48 | - service2
49 | excludes:
50 | - service1
51 | properties:
52 | envType: development
53 | extra:
54 | foo: baz
55 | bar: null
56 | envFiles:
57 | - '*-env.yaml'
58 |
--------------------------------------------------------------------------------
/internal/remote/testdata/pristine/qbec-applied.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: PersistentVolumeClaim
3 | metadata:
4 | annotations:
5 | pv.kubernetes.io/bind-completed: "yes"
6 | pv.kubernetes.io/bound-by-controller: "yes"
7 | qbec.io/component: storage
8 | qbec.io/last-applied: H4sIAAAAAAAA/1SOMWvDQAxG9/6Mb3ZTst7aObR0SIfSQTmLVuROupzkQDH+78UOBDJKPN73ZlCTI3cXUyRc9xhwFh2R8L5+PVjjaGWq/FpIKgZUDhopCGkGqVpQiKmv5+XEeSf2kq02U9ZAgod1+mEsAwqduDyA1FqRvAmQcO7iv0q6Ew+x52APx3CHWa/STevNe2Naob/96laq/LjmjfPWmDO7H2xkR/rCB9P42SX4TTPje0Bnt6ln3sI6X6ZtNs13V8L+IFiWZXn6BwAA//8BAAD//5YWR/4vAQAA
9 | volume.beta.kubernetes.io/storage-provisioner: kubernetes.io/aws-ebs
10 | creationTimestamp: 2019-07-28T17:39:09Z
11 | finalizers:
12 | - kubernetes.io/pvc-protection
13 | labels:
14 | qbec.io/application: krishnan.istio-tests
15 | qbec.io/environment: istio-play1
16 | name: storage
17 | namespace: istio-test
18 | resourceVersion: "41064645"
19 | selfLink: /api/v1/namespaces/istio-test/persistentvolumeclaims/storage
20 | uid: 9b0bfad7-b15e-11e9-bd54-0ace00d90692
21 | spec:
22 | accessModes:
23 | - ReadWriteOnce
24 | resources:
25 | requests:
26 | storage: 1Mi
27 | storageClassName: gp2
28 | volumeName: pvc-9b0bfad7-b15e-11e9-bd54-0ace00d90692
29 | status:
30 | accessModes:
31 | - ReadWriteOnce
32 | capacity:
33 | storage: 1Gi
34 | phase: Bound
35 |
--------------------------------------------------------------------------------
/vm/example_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 Splunk Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package vm_test
16 |
17 | import (
18 | "fmt"
19 |
20 | "github.com/splunk/qbec/vm"
21 | )
22 |
23 | func Example() {
24 | jvm := vm.New(vm.Config{})
25 |
26 | code := `
27 | function (str, num) {
28 | foo: str,
29 | bar: num,
30 | baz: std.extVar('baz'),
31 | }
32 | `
33 | vs := vm.VariableSet{}.
34 | WithTopLevelVars(
35 | vm.NewVar("str", "hello"),
36 | vm.NewCodeVar("num", "10"),
37 | ).
38 | WithVars(
39 | vm.NewVar("baz", "world"),
40 | )
41 |
42 | out, err := jvm.EvalCode("inline-code.jsonnet", vm.MakeCode(code), vs)
43 | if err != nil {
44 | panic(err)
45 | }
46 |
47 | fmt.Println(out)
48 | // Output:
49 | // {
50 | // "bar": 10,
51 | // "baz": "world",
52 | // "foo": "hello"
53 | // }
54 | }
55 |
--------------------------------------------------------------------------------
/internal/cmd/errors_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 Splunk Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package cmd
16 |
17 | import (
18 | "errors"
19 | "testing"
20 |
21 | "github.com/stretchr/testify/assert"
22 | )
23 |
24 | func TestUsageError(t *testing.T) {
25 | ue := NewUsageError("foobar")
26 | a := assert.New(t)
27 | a.True(IsUsageError(ue))
28 | a.Equal("foobar", ue.Error())
29 | }
30 |
31 | func TestRuntimeError(t *testing.T) {
32 | re := NewRuntimeError(errors.New("foobar"))
33 | a := assert.New(t)
34 | a.True(IsRuntimeError(re))
35 | a.False(IsUsageError(re))
36 | a.Equal("foobar", re.Error())
37 | }
38 |
39 | func TestWrapError(t *testing.T) {
40 | ue := NewUsageError("foobar")
41 | a := assert.New(t)
42 | a.Nil(WrapError(nil))
43 | a.True(IsUsageError(WrapError(ue)))
44 | a.True(IsRuntimeError(WrapError(errors.New("foobar"))))
45 | }
46 |
--------------------------------------------------------------------------------
/.github/workflows/release.yaml:
--------------------------------------------------------------------------------
1 | # Copyright 2025 Splunk Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | name: Release
16 | on:
17 | push:
18 | branches:
19 | - "!*"
20 | tags:
21 | - "v*.*.*"
22 | permissions:
23 | contents: write
24 | jobs:
25 | build:
26 | runs-on: ubuntu-latest
27 | name: goreleaser
28 | steps:
29 | - uses: actions/setup-go@v6
30 | with:
31 | go-version: 1.24
32 | id: go
33 | - uses: actions/checkout@v2
34 | - name: Install package dependencies
35 | run: |
36 | echo "GO_VERSION=$(go version | awk '{ print $3}' | sed 's/^go//')" >> $GITHUB_ENV
37 | make get
38 | - name: Release via goreleaser
39 | uses: goreleaser/goreleaser-action@v2
40 | with:
41 | args: release --release-notes .release-notes.md
42 | env:
43 | GITHUB_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
44 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | [](https://github.com/splunk/qbec/actions)
4 | [](https://goreportcard.com/report/github.com/splunk/qbec)
5 | [](https://codecov.io/gh/splunk/qbec)
6 |
7 |
8 | Qbec (pronounced like the [Canadian province](https://en.wikipedia.org/wiki/Quebec)) is a CLI tool that
9 | allows you to create Kubernetes objects on multiple Kubernetes clusters or namespaces configured correctly for
10 | the target environment in question.
11 |
12 | It is based on [jsonnet](https://jsonnet.org) and is similar to other tools in the same space like
13 | [kubecfg](https://github.com/ksonnet/kubecfg) and [ksonnet](https://ksonnet.io/).
14 |
15 | For more info, [read the docs](https://qbec.io/)
16 |
17 | ### Installing
18 |
19 | Use a prebuilt binary [from the releases page](https://github.com/splunk/qbec/releases) for your operating system.
20 |
21 | On MacOS, you can install qbec using homebrew:
22 |
23 | ```
24 | $ brew tap splunk/tap
25 | $ brew install qbec
26 | ```
27 |
28 | ### Building from source
29 |
30 | ```shell
31 | git clone git@github.com:splunk/qbec
32 | cd qbec
33 | make install # installs lint tools etc.
34 | make
35 | ```
36 |
37 | ### Sign the CLA
38 |
39 | Follow the steps here [cla-assistant](https://github.com/splunk/cla-agreement)
40 |
--------------------------------------------------------------------------------
/vm/datasource/api.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 Splunk Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | // Package datasource declares the data source interface.
16 | package datasource
17 |
18 | // DataSource is a named delegate that can resolve import paths. Multiple VMs may
19 | // access a single instance of a data source. Thus, data source implementations must
20 | // be safe for concurrent use.
21 | type DataSource interface {
22 | // Name returns the name of this data source and is used to determine if
23 | // an import path should be processed by the data source importer.
24 | Name() string
25 | // Resolve resolves the absolute path defined for the data source to a string.
26 | Resolve(path string) (string, error)
27 | }
28 |
29 | // ConfigProvider returns the value of the supplied variable as a JSON string.
30 | // A config provider is used at the time of data source creation to allow the data source to be
31 | // correctly configured.
32 | type ConfigProvider func(varName string) (string, error)
33 |
--------------------------------------------------------------------------------
/cmd/changelog-extractor/main.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 Splunk Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package main
16 |
17 | import (
18 | "bufio"
19 | "fmt"
20 | "log"
21 | "os"
22 | "strings"
23 | )
24 |
25 | func main() {
26 | log.SetOutput(os.Stderr)
27 | if len(os.Args) != 2 {
28 | log.Fatalln("Must pass the Changelog file name as the first argument")
29 | }
30 | filename := os.Args[1]
31 | if err := printReleaseNotes(filename); err != nil {
32 | log.Fatalln(err)
33 | }
34 |
35 | }
36 |
37 | func printReleaseNotes(filename string) error {
38 | f, err := os.Open(filename)
39 | if err != nil {
40 | return err
41 | }
42 | scanner := bufio.NewScanner(f)
43 | var startPrint bool
44 | tagLinePrefix := "## v"
45 | for scanner.Scan() {
46 | line := scanner.Text()
47 | if startPrint && !strings.HasPrefix(line, tagLinePrefix) {
48 | fmt.Println(scanner.Text())
49 | }
50 | if strings.HasPrefix(line, tagLinePrefix) {
51 | if startPrint {
52 | // Exit at the next matching tag
53 | break
54 | }
55 | startPrint = true
56 | }
57 | }
58 | return scanner.Err()
59 | }
60 |
--------------------------------------------------------------------------------
/internal/filematcher/match.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 Splunk Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package filematcher
16 |
17 | import (
18 | "fmt"
19 | "io/fs"
20 | "path/filepath"
21 | "sort"
22 | "strings"
23 | )
24 |
25 | // Match returns a sorted list of files and dirs matching a glob pattern
26 | func Match(pattern string) ([]string, error) {
27 | if IsRemoteFile(pattern) {
28 | return []string{pattern}, nil
29 | }
30 | files, err := filepath.Glob(pattern)
31 | if err != nil {
32 | return nil, err
33 | }
34 | // files is nil when pattern does not match any files
35 | if files == nil {
36 | return nil, fmt.Errorf("%s: %w", pattern, fs.ErrNotExist)
37 | }
38 | var envFiles []string
39 | for _, f := range files {
40 | abs, err := filepath.Abs(f)
41 | if err != nil {
42 | return nil, err
43 | }
44 | envFiles = append(envFiles, abs)
45 | }
46 | sort.Strings(envFiles)
47 | return envFiles, nil
48 | }
49 |
50 | // IsRemoteFile distinguishes remote files from local files
51 | func IsRemoteFile(file string) bool {
52 | return strings.HasPrefix(file, "http://") || strings.HasPrefix(file, "https://")
53 | }
54 |
--------------------------------------------------------------------------------
/cmd/gen-qbec-swagger/main.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 Splunk Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package main
16 |
17 | import (
18 | "encoding/json"
19 | "fmt"
20 | "io/ioutil"
21 | "log"
22 | "os"
23 | "time"
24 |
25 | "sigs.k8s.io/yaml"
26 | )
27 |
28 | func main() {
29 | if len(os.Args) != 3 {
30 | log.Fatalln("Usage: gen-qbec-swagger ")
31 | }
32 |
33 | inFile := os.Args[1]
34 | outFile := os.Args[2]
35 |
36 | handle := func(e error) {
37 | if e != nil {
38 | log.Fatalln(e)
39 | }
40 | }
41 |
42 | b, err := ioutil.ReadFile(inFile)
43 | handle(err)
44 |
45 | var data interface{}
46 | err = yaml.Unmarshal(b, &data)
47 | handle(err)
48 |
49 | b, err = json.MarshalIndent(data, "", " ")
50 | handle(err)
51 |
52 | code := fmt.Sprintf(`package model
53 |
54 | // generated by gen-qbec-swagger from %s at %v
55 | // Do NOT edit this file by hand
56 |
57 | var swaggerJSON = %s
58 | %s
59 | %s
60 | `, inFile, time.Now().UTC(), "`", b, "`")
61 |
62 | err = ioutil.WriteFile(outFile, []byte(code), 0644)
63 | handle(err)
64 | log.Println("Successfully wrote", outFile, "from", inFile)
65 | }
66 |
--------------------------------------------------------------------------------
/vm/internal/ds/exec/runner.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 Splunk Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package exec
16 |
17 | import (
18 | "bytes"
19 | "context"
20 | "fmt"
21 | "os"
22 | "os/exec"
23 | )
24 |
25 | type runner struct {
26 | c *Config
27 | }
28 |
29 | func newRunner(c *Config) *runner {
30 | return &runner{c: c}
31 | }
32 |
33 | func (r *runner) runWithEnv(e map[string]string) (string, error) {
34 | ctx, cancel := context.WithTimeout(context.Background(), r.c.timeout)
35 | defer cancel()
36 |
37 | cmd := exec.CommandContext(ctx, r.c.Command, r.c.Args...)
38 | var env []string
39 | if r.c.InheritEnv {
40 | env = os.Environ()
41 | }
42 | for k, v := range r.c.Env {
43 | env = append(env, fmt.Sprintf("%s=%s", k, v))
44 | }
45 | for k, v := range e {
46 | env = append(env, fmt.Sprintf("%s=%s", k, v))
47 | }
48 | cmd.Env = env
49 |
50 | var capture bytes.Buffer
51 | cmd.Stdin = bytes.NewReader([]byte(r.c.Stdin))
52 | cmd.Stdout = &capture
53 | cmd.Stderr = os.Stderr
54 |
55 | if err := cmd.Run(); err != nil {
56 | return "", err
57 | }
58 | return capture.String(), nil
59 | }
60 |
61 | func (r *runner) close() error {
62 | return nil
63 | }
64 |
--------------------------------------------------------------------------------
/internal/cmd/lifecycle_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 Splunk Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package cmd
16 |
17 | import (
18 | "fmt"
19 | "testing"
20 |
21 | "github.com/stretchr/testify/assert"
22 | "github.com/stretchr/testify/require"
23 | )
24 |
25 | type cl struct {
26 | err error
27 | called bool
28 | }
29 |
30 | func (c *cl) Close() error {
31 | c.called = true
32 | return c.err
33 | }
34 |
35 | func TestCleanupSuccess(t *testing.T) {
36 | defer func() { cleanup = &closers{} }()
37 | c := &cl{err: nil}
38 | RegisterCleanupTask(c)
39 | err := Close()
40 | require.NoError(t, err)
41 | assert.True(t, c.called)
42 | }
43 |
44 | func TestCleanupError(t *testing.T) {
45 | defer func() { cleanup = &closers{} }()
46 | c1 := &cl{err: nil}
47 | c2 := &cl{err: fmt.Errorf("foobar")}
48 | c3 := &cl{err: fmt.Errorf("barbaz")}
49 | c4 := &cl{err: nil}
50 | RegisterCleanupTask(c1)
51 | RegisterCleanupTask(c2)
52 | RegisterCleanupTask(c3)
53 | RegisterCleanupTask(c4)
54 | err := Close()
55 | require.Error(t, err)
56 | a := assert.New(t)
57 | a.True(c1.called)
58 | a.True(c2.called)
59 | a.True(c3.called)
60 | a.True(c4.called)
61 | a.Equal("barbaz", err.Error())
62 | }
63 |
--------------------------------------------------------------------------------
/licenselint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Copyright 2025 Splunk Inc.
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 |
17 | set -euo pipefail
18 |
19 | YEAR="2025"
20 | OWNER="Splunk Inc."
21 | ERROR=0
22 |
23 | addlicense -c "${OWNER}" -l apache -check \
24 | $(find . -type f ! -path "*testdata*" ! -path "*examples*.yaml" -print0 | xargs -0) \
25 | || ( echo -e "\nRun 'make fmt-license' to fix missing license headers" && exit 1 )
26 |
27 | # array of file patterns to exclude from header check
28 | EXCLUDE_PATTERNS=( \
29 | "*.json" \
30 | "*.jsonnet" \
31 | "*.libsonnet" \
32 | "*.md" \
33 | "*.xsonnet" \
34 | "*testdata*" \
35 | ".git*" \
36 | "examples/*.yaml" \
37 | "go.mod" \
38 | "go.sum" \
39 | "LICENSE"
40 | "site/*" \
41 | )
42 |
43 | # check if the file matches any exclude pattern
44 | exclude_file() {
45 | for pattern in "${EXCLUDE_PATTERNS[@]}"; do
46 | if [[ "$1" == $pattern ]]; then
47 | return 0
48 | fi
49 | done
50 | return 1
51 | }
52 |
53 | for file in $(git ls-files); do
54 | if exclude_file "$file"; then
55 | continue
56 | fi
57 | if ! grep -q "Copyright $YEAR $OWNER" "$file"; then
58 | echo "Missing or incorrect license header in: $file"
59 | ERROR=1
60 | fi
61 | done
62 |
63 | exit $ERROR
--------------------------------------------------------------------------------
/cmdtest/qbec-replay-exec/main.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 Splunk Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | // cmd qbec-replay-exec implements an exec provider that replays what was given to it in JSON format
16 | package main
17 |
18 | import (
19 | "encoding/json"
20 | "fmt"
21 | "io/ioutil"
22 | "log"
23 | "os"
24 | "sort"
25 | "time"
26 | )
27 |
28 | func main() {
29 | b, err := ioutil.ReadAll(os.Stdin)
30 | if err != nil {
31 | log.Fatalln(err)
32 | }
33 | exe := os.Args[0]
34 | args := os.Args[1:]
35 | wd, err := os.Getwd()
36 | if err != nil {
37 | log.Fatalf("get wd: %v", err)
38 | }
39 | var env []string
40 | for _, v := range os.Environ() {
41 | env = append(env, v)
42 | }
43 | sort.Strings(env)
44 | data := map[string]interface{}{
45 | "dsName": os.Getenv("__DS_NAME__"),
46 | "command": exe,
47 | "args": args,
48 | "dir": wd,
49 | "env": env,
50 | "stdin": string(b),
51 | }
52 | b, err = json.MarshalIndent(data, "", " ")
53 | if err != nil {
54 | log.Fatalln(err)
55 | }
56 | if os.Getenv("__DS_PATH__") == "/fail" {
57 | log.Fatalln("failed data source lookup of path", os.Getenv("__DS_PATH__"))
58 | }
59 | if os.Getenv("__DS_PATH__") == "/slow" {
60 | time.Sleep(5 * time.Second)
61 | }
62 | fmt.Printf("%s\n", b)
63 | }
64 |
--------------------------------------------------------------------------------
/vm/vmutil/vmutil.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 Splunk Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | // Package vmutil exposes specific functions used in the native implementation of the VM for general purpose use.
16 | package vmutil
17 |
18 | import (
19 | "io"
20 |
21 | "github.com/splunk/qbec/vm/internal/natives"
22 | )
23 |
24 | // ParseJSON parses the contents of the reader into a data object and returns it.
25 | func ParseJSON(reader io.Reader) (interface{}, error) {
26 | return natives.ParseJSON(reader)
27 | }
28 |
29 | // ParseYAMLDocuments parses the contents of the reader into an array of
30 | // objects, one for each non-nil document in the input.
31 | func ParseYAMLDocuments(reader io.Reader) ([]interface{}, error) {
32 | return natives.ParseYAMLDocuments(reader)
33 | }
34 |
35 | // RenderYAMLDocuments renders the supplied data as a series of YAML documents if the input is an array
36 | // or a single document when it is not. Nils are excluded from output.
37 | // If the caller wants an array to be rendered as a single document,
38 | // they need to wrap it in an array first. Note that this function is not a drop-in replacement for
39 | // data that requires ghodss/yaml to be rendered correctly.
40 | func RenderYAMLDocuments(data interface{}, writer io.Writer) (retErr error) {
41 | return natives.RenderYAMLDocuments(data, writer)
42 | }
43 |
--------------------------------------------------------------------------------
/site/content/reference/directives.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Qbec directives
3 | weight: 105
4 | ---
5 | Annotations that you can use for your objects to control qbec behavior.
6 |
7 | #### `directives.qbec.io/apply-order`
8 |
9 | * Annotation source: local object
10 | * Allowed values: Any positive integer as a string (i.e. ensure that the value is quoted in YAML)
11 | * Default value: `"0"` (use qbec defaults)
12 |
13 | controls the order in which objects are applied. This allows you, for example, to move updates of a custom
14 | resource to after all other objects have been processed.
15 |
16 | #### `directives.qbec.io/delete-policy`
17 |
18 | * Annotation source: in-cluster object
19 | * Allowed values: `"default"`, `"never"`
20 | * Default value: `"default"`
21 |
22 | when set to `"never"`, indicates that the specific object should never be deleted. This applies to both explicit deletes as well as garbage collection.
23 | If you want qbec to delete this object, you need to remove the annotation from the in-cluster object. Changing the source
24 | object to remove this annotation will not work.
25 |
26 | #### `directives.qbec.io/update-policy`
27 |
28 | * Annotation source: in-cluster object.
29 | * Allowed values: `"default"`, `"never"`
30 | * Default value: `"default"`
31 |
32 | when set to `"never"`, indicates that the specific object should never be updated.
33 | If you want qbec to update this object, you need to remove the annotation from the in-cluster object. Changing the source
34 | object to remove this annotation will not work.
35 |
36 | #### `directives.qbec.io/wait-policy`
37 |
38 | * Annotation source: local object
39 | * Allowed values: `"default"`, `"never"`
40 | * Default value: `"default"`
41 |
42 | when set to `"never"` for deployments or daemonsets, indicates that qbec should not wait for that object even when
43 | the `--wait` or `--wait-all` flags are set for the `apply` command.
44 |
45 |
--------------------------------------------------------------------------------
/internal/cmd/profile_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 Splunk Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package cmd
16 |
17 | import (
18 | "bytes"
19 | "io/ioutil"
20 | "os"
21 | "path/filepath"
22 | "testing"
23 |
24 | "github.com/stretchr/testify/assert"
25 | "github.com/stretchr/testify/require"
26 | )
27 |
28 | func TestProfileSuccess(t *testing.T) {
29 | defer func() { cleanup = &closers{} }()
30 | tmpDir, err := ioutil.TempDir("", "")
31 | require.NoError(t, err)
32 | defer os.RemoveAll(tmpDir)
33 | cpuFile := filepath.Join(tmpDir, "cpu.prof")
34 | memFile := filepath.Join(tmpDir, "mem.prof")
35 | _ = getContext(t, Options{}, []string{
36 | "--pprof:cpu=" + cpuFile,
37 | "--pprof:memory=" + memFile,
38 | })
39 | // run some cycles create some garbage
40 | for i := 0; i < 10000; i++ {
41 | _ = bytes.NewBuffer(nil)
42 | }
43 | err = Close()
44 | require.NoError(t, err)
45 | s, err := os.Stat(cpuFile)
46 | require.NoError(t, err)
47 | assert.True(t, s.Size() > 0)
48 | s, err = os.Stat(memFile)
49 | require.NoError(t, err)
50 | assert.True(t, s.Size() > 0)
51 | }
52 |
53 | func TestProfileInitFail(t *testing.T) {
54 | p := &profiler{cpuProfile: "non-existent-path/foo.prof"}
55 | err := p.init()
56 | require.Error(t, err)
57 | }
58 |
59 | func TestProfileInitFail2(t *testing.T) {
60 | p := &profiler{memoryProfile: "non-existent-path/foo.prof"}
61 | err := p.init()
62 | require.Error(t, err)
63 | }
64 |
--------------------------------------------------------------------------------
/vm/internal/ds/factory/datasource_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 Splunk Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package factory
16 |
17 | import (
18 | "testing"
19 |
20 | "github.com/stretchr/testify/assert"
21 | "github.com/stretchr/testify/require"
22 | )
23 |
24 | func TestDataSourceSuccess(t *testing.T) {
25 | ds, err := Create("exec://foo?configVar=bar")
26 | require.NoError(t, err)
27 | assert.IsType(t, &lazySource{}, ds)
28 | }
29 |
30 | func TestNegativeCases(t *testing.T) {
31 | tests := []struct {
32 | name string
33 | uri string
34 | msg string
35 | }{
36 | {
37 | name: "bad-url",
38 | uri: "exec://bar\x00?configVar=test",
39 | msg: "parse URL",
40 | },
41 | {
42 | name: "bad-kind",
43 | uri: "exec-foo://bar?configVar=test",
44 | msg: "unsupported scheme 'exec-foo",
45 | },
46 | {
47 | name: "no-name",
48 | uri: "exec://?configVar=test",
49 | msg: "does not have a name",
50 | },
51 | {
52 | name: "forgot-slash",
53 | uri: "exec:foobar?configVar=test",
54 | msg: "did you forget the '//' after the ':'",
55 | },
56 | {
57 | name: "no-cfg-var",
58 | uri: "exec://foo?config=test",
59 | msg: "must have a configVar param",
60 | },
61 | }
62 | for _, test := range tests {
63 | t.Run(test.name, func(t *testing.T) {
64 | _, err := Create(test.uri)
65 | require.Error(t, err)
66 | assert.Contains(t, err.Error(), test.msg)
67 | })
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/site/content/reference/gen-metadata.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Metadata for K8s objects
3 | weight: 120
4 | ---
5 |
6 | ## Labels
7 |
8 | All Kubernetes objects produced by qbec have the following labels associated with them:
9 |
10 | * `qbec.io/application` - the app name from `qbec.yaml`.
11 | * `qbec.io/environment` - the environment name in `qbec.yaml` for which the object was created.
12 | * `qbec.io/tag` - the `--app-tag` parameter passed in on the command line. This label is only set when non-blank.
13 |
14 | The labels are used to efficiently find all cluster objects for a specific app and environment
15 | (and tag, if specified) for garbage collection.
16 |
17 | {{% notice note %}}
18 | If you rename an app, environment, or component, garbage collection for the next immediate run of `qbec apply` may
19 | not work correctly. Subsequent apply operations will then work as usual since the object labels will be updated with
20 | the new values.
21 | {{% /notice %}}
22 |
23 | ## Annotations
24 |
25 | All Kubernetes objects produced by qbec have the following annotation associated with them:
26 |
27 | * `qbec.io/last-applied` - this is the pristine version of the object stored for the purposes of diff and 3-way merge
28 | patches and plays the same role as the `kubectl.kubernetes.io/last-applied-configuration` annotation set by `kubectl apply`.
29 | * `qbec.io/component` - the component that created the object. This is derived from the file name of the component.
30 |
31 | The component annotation is used to respect component filters for `apply` and `delete` operations.
32 | Specifically, if `apply` is being run with component filters, only the extra remote objects matching the filter are
33 | garbage-collected.
34 |
35 | {{% notice note %}}
36 | If you are using qbec to update an object that was created by another tool, you may see strange diffs for the very first time when
37 | this annotation is missing. Once applied, the annotation will now be in place and subsequent updates will show cleaner
38 | diffs.
39 | {{% /notice %}}
40 |
41 |
42 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 Splunk Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package main
16 |
17 | import (
18 | "context"
19 | "fmt"
20 | "os"
21 | "strings"
22 | "time"
23 |
24 | "github.com/spf13/cobra"
25 | "github.com/splunk/qbec/internal/cmd"
26 | "github.com/splunk/qbec/internal/commands"
27 | "github.com/splunk/qbec/internal/sio"
28 | )
29 |
30 | var start = time.Now()
31 |
32 | func main() {
33 | longdesc := "\n" + strings.Trim(fmt.Sprintf(`
34 |
35 | %s provides a set of commands to manage kubernetes objects on multiple clusters.
36 |
37 | `, commands.Executable), "\n")
38 | root := &cobra.Command{
39 | Use: commands.Executable,
40 | Short: "Kubernetes cluster config tool",
41 | Long: longdesc,
42 | BashCompletionFunction: commands.BashCompletionFunc,
43 | }
44 | root.SilenceUsage = true
45 | root.SilenceErrors = true
46 | commands.Setup(root)
47 | c, err := root.ExecuteContextC(context.TODO())
48 |
49 | exit := func(code int) {
50 | duration := time.Since(start).Round(time.Second / 100)
51 | if duration > 100*time.Millisecond {
52 | sio.Debugln("command took", duration)
53 | }
54 | os.Exit(code)
55 | }
56 |
57 | switch {
58 | case err == nil:
59 | exit(0)
60 | case cmd.IsRuntimeError(err):
61 | default:
62 | sio.Println()
63 | c.Example = "" // do not print examples when there is a usage error
64 | _ = c.Usage()
65 | sio.Println()
66 | }
67 | sio.Errorln(err)
68 | exit(1)
69 | }
70 |
--------------------------------------------------------------------------------
/vm/internal/importers/composite.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 Splunk Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package importers
16 |
17 | import (
18 | "fmt"
19 |
20 | "github.com/google/go-jsonnet"
21 | )
22 |
23 | // CompositeImporter tries multiple extended importers in sequence for a given path
24 | type CompositeImporter struct {
25 | importers []ExtendedImporter
26 | }
27 |
28 | // NewCompositeImporter creates a composite importer with the supplied extended importers. Note that if
29 | // two importers could match the same path, the first one will be used so order is important.
30 | func NewCompositeImporter(importers ...ExtendedImporter) *CompositeImporter {
31 | return &CompositeImporter{importers: importers}
32 | }
33 |
34 | // Import implements the interface method by delegating to installed importers in sequence
35 | func (c *CompositeImporter) Import(importedFrom, importedPath string) (contents jsonnet.Contents, foundAt string, err error) {
36 | for _, importer := range c.importers {
37 | if importer.CanProcess(importedPath) {
38 | return importer.Import(importedFrom, importedPath)
39 | }
40 | }
41 | return contents, foundAt, fmt.Errorf("no importer for path %s", importedPath)
42 | }
43 |
44 | // CanProcess implements the interface method of the ExtendedImporter
45 | func (c *CompositeImporter) CanProcess(importedPath string) bool {
46 | for _, importer := range c.importers {
47 | if importer.CanProcess(importedPath) {
48 | return true
49 | }
50 | }
51 | return false
52 | }
53 |
--------------------------------------------------------------------------------
/vm/internal/importers/composite_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 Splunk Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package importers
16 |
17 | import (
18 | "testing"
19 |
20 | "github.com/google/go-jsonnet"
21 | "github.com/stretchr/testify/assert"
22 | "github.com/stretchr/testify/require"
23 | )
24 |
25 | func TestCompositeImporter(t *testing.T) {
26 | a := assert.New(t)
27 | vm := jsonnet.MakeVM()
28 | c := NewCompositeImporter(
29 | NewGlobImporter("import"),
30 | NewGlobImporter("importstr"),
31 | NewFileImporter(&jsonnet.FileImporter{}),
32 | )
33 | vm.Importer(c)
34 | a.True(c.CanProcess("glob-import:*.libsonnet"))
35 | a.True(c.CanProcess("glob-importstr:*.yaml"))
36 | a.True(c.CanProcess("a.yaml"))
37 |
38 | _, err := vm.EvaluateFile("testdata/example1/caller/import-a.jsonnet")
39 | require.NoError(t, err)
40 |
41 | _, err = vm.EvaluateFile("testdata/example1/caller/import-all-json.jsonnet")
42 | require.NoError(t, err)
43 |
44 | vm = jsonnet.MakeVM()
45 | c = NewCompositeImporter(
46 | NewGlobImporter("import"),
47 | NewGlobImporter("importstr"),
48 | )
49 | vm.Importer(c)
50 | a.True(c.CanProcess("glob-import:*.libsonnet"))
51 | a.True(c.CanProcess("glob-importstr:*.yaml"))
52 | a.False(c.CanProcess("a.yaml"))
53 |
54 | _, err = vm.EvaluateAnonymousSnippet("testdata/example1/caller/caller.jsonnet", `import '../bag-of-files/a.json'`)
55 | require.Error(t, err)
56 | assert.Contains(t, err.Error(), "RUNTIME ERROR: no importer for path ../bag-of-files/a.json")
57 | }
58 |
--------------------------------------------------------------------------------
/internal/filematcher/match_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 Splunk Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package filematcher_test
16 |
17 | import (
18 | "fmt"
19 | "path/filepath"
20 | "testing"
21 |
22 | "github.com/splunk/qbec/internal/filematcher"
23 | "github.com/stretchr/testify/assert"
24 | )
25 |
26 | func TestMatch(t *testing.T) {
27 | cwd, err := filepath.Abs(".")
28 | assert.Nil(t, err)
29 | var tests = []struct {
30 | pattern string
31 | expectedMatch bool
32 | expectedFiles []string
33 | }{
34 | {"testdata/*/env.yaml", false, nil},
35 | {"testdata/env.yaml", true, []string{filepath.Join(cwd, "testdata/env.yaml")}},
36 | {"testdata/non-existentenv.yaml", false, nil},
37 | {"testdata/*.yaml", true, []string{filepath.Join(cwd, "testdata/.env.yaml"), filepath.Join(cwd, "testdata/1env.yaml"), filepath.Join(cwd, "testdata/env.yaml"), filepath.Join(cwd, "testdata/env1.yaml")}},
38 | {"testdata", true, []string{filepath.Join(cwd, "testdata")}},
39 | {"https://testdata", true, []string{"https://testdata"}},
40 | {"testdata/testDirForGlobPatterns/*", true, []string{filepath.Join(cwd, "testdata/testDirForGlobPatterns/.keep"), filepath.Join(cwd, "testdata/testDirForGlobPatterns/childDir")}},
41 | }
42 | for i, test := range tests {
43 | t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
44 | files, err := filematcher.Match(test.pattern)
45 | assert.Equal(t, test.expectedMatch, err == nil)
46 | assert.Equal(t, test.expectedFiles, files)
47 | })
48 | }
49 |
50 | }
51 |
--------------------------------------------------------------------------------
/site/content/reference/diffs-and-patches.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Diffs and patches
3 | weight: 400
4 | ---
5 |
6 | qbec uses a 3-way merge patch similar to `kubectl/ksonnet apply`. The [Kubernetes documentation](https://kubernetes.io/docs/concepts/overview/object-management-kubectl/declarative-config/#how-apply-calculates-differences-and-merges-changes)
7 | describes how this works.
8 |
9 | For existing objects, the `qbec diff` command produces a diff between the last applied configuration stored
10 | on the server and the current configuration of the object loaded from source. This diff is "clean" in the
11 | sense of the remote object not having additional fields, default values and so on.
12 | It faithfully represents the change between the previous and current version of the object produced from
13 | source code.
14 |
15 | When `qbec apply` is run, it calculates the patch for existing objects. This calculation _does_ have to account for the
16 | shape of the object as stored by Kubernetes.
17 |
18 | In many if not most cases, if `qbec diff` does not report a diff for an object, `qbec apply` will also
19 | not try to update the object.
20 |
21 | This is not always true. Among other things:
22 |
23 | * the local object may have fields that are never stored on the server and every run of `qbec apply` will
24 | attempt to update these extra fields. This is particularly noticeable for custom resources and definitions.
25 |
26 | * the local object may represent a value differently from how the server stores it. For example a local
27 | CPU resource of `1000m` may be stored in the server as `1` instead. Every `qbec apply` will notice
28 | this difference and try to update the value back to `1000m`.
29 |
30 | ## Summary
31 |
32 | * Diffs and patches may not always agree on the number of objects that are different.
33 | * Spurious apply patches can appear in the output. These can be noisy but they're benign.
34 | One way to fix this would be to check the YAML output from the server and try to match the source
35 | code to have the same representation of the value.
36 |
37 |
38 |
--------------------------------------------------------------------------------
/internal/cmd/errors.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 Splunk Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package cmd
16 |
17 | import "errors"
18 |
19 | // usageError indicates that the user supplied incorrect arguments or flags to the command.
20 | type usageError struct {
21 | error
22 | }
23 |
24 | // NewUsageError returns a usage error
25 | func NewUsageError(msg string) error {
26 | return &usageError{
27 | error: errors.New(msg),
28 | }
29 | }
30 |
31 | // IsUsageError returns if the supplied error was caused due to incorrect command usage.
32 | func IsUsageError(err error) bool {
33 | _, ok := err.(*usageError)
34 | return ok
35 | }
36 |
37 | // runtimeError indicates that there were runtime issues with execution.
38 | type runtimeError struct {
39 | error
40 | }
41 |
42 | // NewRuntimeError returns a runtime error
43 | func NewRuntimeError(err error) error {
44 | return &runtimeError{
45 | error: err,
46 | }
47 | }
48 |
49 | // Unwrap returns the underlying error
50 | func (e *runtimeError) Unwrap() error {
51 | return e.error
52 | }
53 |
54 | // IsRuntimeError returns if the supplied error was a runtime error as opposed to an error arising out of user input.
55 | func IsRuntimeError(err error) bool {
56 | _, ok := err.(*runtimeError)
57 | return ok
58 | }
59 |
60 | // WrapError passes through usage errors and wraps all other errors with a runtime marker.
61 | func WrapError(err error) error {
62 | if err == nil {
63 | return nil
64 | }
65 | if IsUsageError(err) {
66 | return err
67 | }
68 | return NewRuntimeError(err)
69 | }
70 |
--------------------------------------------------------------------------------
/internal/cmd/utils_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 Splunk Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package cmd
16 |
17 | import (
18 | "os"
19 | "path/filepath"
20 | "testing"
21 |
22 | "github.com/spf13/cobra"
23 | "github.com/stretchr/testify/require"
24 | )
25 |
26 | func setPwd(t *testing.T, dir string) func() {
27 | wd, err := os.Getwd()
28 | require.NoError(t, err)
29 | p, err := filepath.Abs(dir)
30 | require.NoError(t, err)
31 | err = os.Chdir(p)
32 | require.NoError(t, err)
33 | return func() {
34 | err = os.Chdir(wd)
35 | require.NoError(t, err)
36 | }
37 | }
38 |
39 | func getContext(t *testing.T, opts Options, args []string) Context {
40 | var ctx Context
41 | var ctxMaker func() (Context, error)
42 | root := &cobra.Command{
43 | Use: "qbec-test",
44 | Short: "qbec test tool",
45 | RunE: func(c *cobra.Command, args []string) error {
46 | var err error
47 | ctx, err = ctxMaker()
48 | return err
49 | },
50 | }
51 | ctxMaker = NewContext(root, opts)
52 | root.SetArgs(args)
53 | err := root.Execute()
54 | require.NoError(t, err)
55 | return ctx
56 | }
57 |
58 | func getBadContext(t *testing.T, opts Options, args []string) error {
59 | var ctxMaker func() (Context, error)
60 | root := &cobra.Command{
61 | Use: "qbec-test",
62 | Short: "qbec test tool",
63 | RunE: func(c *cobra.Command, args []string) error {
64 | _, err := ctxMaker()
65 | return err
66 | },
67 | }
68 | ctxMaker = NewContext(root, opts)
69 | root.SetArgs(args)
70 | err := root.Execute()
71 | require.Error(t, err)
72 | return err
73 | }
74 |
--------------------------------------------------------------------------------
/internal/remote/k8smeta/schema_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 Splunk Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package k8smeta
16 |
17 | import (
18 | "context"
19 | "io/ioutil"
20 | "path/filepath"
21 | "testing"
22 |
23 | openapi_v2 "github.com/google/gnostic-models/openapiv2"
24 | "github.com/stretchr/testify/assert"
25 | "github.com/stretchr/testify/require"
26 | "google.golang.org/protobuf/proto"
27 | "k8s.io/apimachinery/pkg/runtime/schema"
28 | )
29 |
30 | type sd struct{}
31 |
32 | func (d sd) OpenAPISchema() (*openapi_v2.Document, error) {
33 | b, err := ioutil.ReadFile(filepath.Join("testdata", "swagger-2.0.0.pb-v1"))
34 | if err != nil {
35 | return nil, err
36 | }
37 | var doc openapi_v2.Document
38 | if err := proto.Unmarshal(b, &doc); err != nil {
39 | return nil, err
40 | }
41 | return &doc, nil
42 | }
43 |
44 | func TestMetadataValidator(t *testing.T) {
45 | a := assert.New(t)
46 | ss := NewServerSchema(sd{})
47 | ctx := context.TODO()
48 | v, err := ss.ValidatorFor(ctx, schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Namespace"})
49 | require.Nil(t, err)
50 | errs := v.Validate(loadObject(t, "ns-good.json").ToUnstructured())
51 | require.Nil(t, errs)
52 |
53 | errs = v.Validate(loadObject(t, "ns-bad.json").ToUnstructured())
54 | require.NotNil(t, errs)
55 | a.Equal(1, len(errs))
56 | a.Contains(errs[0].Error(), `unknown field "foo"`)
57 |
58 | _, err = ss.ValidatorFor(ctx, schema.GroupVersionKind{Group: "", Version: "v1", Kind: "FooBar"})
59 | require.NotNil(t, err)
60 | a.Equal(ErrSchemaNotFound, err)
61 |
62 | }
63 |
--------------------------------------------------------------------------------
/vm/internal/importers/data-source_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 Splunk Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package importers
16 |
17 | import (
18 | "encoding/json"
19 | "testing"
20 |
21 | "github.com/google/go-jsonnet"
22 | "github.com/stretchr/testify/assert"
23 | "github.com/stretchr/testify/require"
24 | )
25 |
26 | type replay struct{}
27 |
28 | func (r replay) Name() string { return "replay" }
29 | func (r replay) Resolve(path string) (string, error) { return path, nil }
30 |
31 | func TestDataSourceImporterBasic(t *testing.T) {
32 | imp := NewDataSourceImporter(replay{})
33 | vm := jsonnet.MakeVM()
34 | vm.Importer(NewCompositeImporter(imp))
35 | jsonCode, err := vm.EvaluateAnonymousSnippet("test.jsonnet", `{ foo: importstr 'data://replay/foo/bar' }`)
36 | require.NoError(t, err)
37 | var data struct {
38 | Foo string `json:"foo"`
39 | }
40 | err = json.Unmarshal([]byte(jsonCode), &data)
41 | require.NoError(t, err)
42 | assert.Equal(t, "/foo/bar", data.Foo)
43 | }
44 |
45 | func TestDataSourceImporterNoPathc(t *testing.T) {
46 | imp := NewDataSourceImporter(replay{})
47 | vm := jsonnet.MakeVM()
48 | vm.Importer(NewCompositeImporter(imp))
49 | jsonCode, err := vm.EvaluateAnonymousSnippet("test.jsonnet", `{ foo: importstr 'data://replay', bar: importstr 'data://replay' }`)
50 | require.NoError(t, err)
51 | var data struct {
52 | Foo string `json:"foo"`
53 | }
54 | err = json.Unmarshal([]byte(jsonCode), &data)
55 | require.NoError(t, err)
56 | assert.Equal(t, "/", data.Foo)
57 | assert.Equal(t, 1, len(imp.cache))
58 | }
59 |
--------------------------------------------------------------------------------
/vm/internal/ds/factory/lazy.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 Splunk Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package factory
16 |
17 | import (
18 | "sync"
19 |
20 | "github.com/splunk/qbec/vm/datasource"
21 | "github.com/splunk/qbec/vm/internal/ds"
22 | )
23 |
24 | // lazySource wraps a data source and defers initialization of its delegate until the first call to Resolve.
25 | // This allows data sources to be initialized before computed variables are, such that code in computed
26 | // variables can also refer to data sources.
27 | type lazySource struct {
28 | delegate ds.DataSourceWithLifecycle
29 | provider datasource.ConfigProvider
30 | l sync.Mutex
31 | once sync.Once
32 | initErr error
33 | }
34 |
35 | func makeLazy(delegate ds.DataSourceWithLifecycle) ds.DataSourceWithLifecycle {
36 | return &lazySource{
37 | delegate: delegate,
38 | }
39 | }
40 |
41 | func (l *lazySource) Init(c datasource.ConfigProvider) error {
42 | l.provider = c
43 | return nil
44 | }
45 |
46 | func (l *lazySource) Name() string {
47 | return l.delegate.Name()
48 | }
49 |
50 | func (l *lazySource) initOnce() error {
51 | l.l.Lock()
52 | defer l.l.Unlock()
53 | l.once.Do(func() {
54 | l.initErr = l.delegate.Init(l.provider)
55 | })
56 | return l.initErr
57 | }
58 |
59 | func (l *lazySource) Resolve(path string) (string, error) {
60 | if err := l.initOnce(); err != nil {
61 | return "", err
62 | }
63 | return l.delegate.Resolve(path)
64 | }
65 |
66 | func (l *lazySource) Close() error {
67 | return l.delegate.Close()
68 | }
69 |
70 | var _ ds.DataSourceWithLifecycle = &lazySource{}
71 |
--------------------------------------------------------------------------------
/internal/types/secrets_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 Splunk Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package types
16 |
17 | import (
18 | "encoding/base64"
19 | "fmt"
20 | "testing"
21 |
22 | "github.com/splunk/qbec/internal/model"
23 | "github.com/stretchr/testify/assert"
24 | "sigs.k8s.io/yaml"
25 | )
26 |
27 | var cm = `
28 | ---
29 | apiVersion: v1
30 | kind: ConfigMap
31 | metadata:
32 | namespace: ns1
33 | name: cm
34 | data:
35 | foo: bar
36 | `
37 |
38 | var b64 = base64.StdEncoding.EncodeToString([]byte("changeme"))
39 |
40 | var secret = fmt.Sprintf(`
41 | ---
42 | apiVersion: v1
43 | kind: Secret
44 | metadata:
45 | namespace: ns1
46 | name: s
47 | data:
48 | foo: %s
49 | `, b64)
50 |
51 | func toData(s string) map[string]interface{} {
52 | var data map[string]interface{}
53 | err := yaml.Unmarshal([]byte(s), &data)
54 | if err != nil {
55 | panic(err)
56 | }
57 | return data
58 | }
59 |
60 | func TestSecrets(t *testing.T) {
61 | cmObj := model.NewK8sLocalObject(toData(cm), model.LocalAttrs{App: "app1", Tag: "", Component: "c1", Env: "e1"})
62 | secretObj := model.NewK8sLocalObject(toData(secret), model.LocalAttrs{App: "app1", Component: "c1", Env: "e1"})
63 | a := assert.New(t)
64 | a.False(HasSensitiveInfo(cmObj.ToUnstructured()))
65 | a.True(HasSensitiveInfo(secretObj.ToUnstructured()))
66 | changed, ok := HideSensitiveLocalInfo(cmObj)
67 | a.Equal(cmObj, changed)
68 | a.False(ok)
69 | changed, ok = HideSensitiveLocalInfo(secretObj)
70 | a.NotEqual(secretObj, changed)
71 | a.True(ok)
72 | v := changed.ToUnstructured().Object["data"].(map[string]interface{})["foo"]
73 | a.NotEqual(b64, v)
74 | }
75 |
--------------------------------------------------------------------------------
/examples/test-app/lib/globutil.libsonnet:
--------------------------------------------------------------------------------
1 | local len = std.length;
2 | local split = std.split;
3 | local join = std.join;
4 |
5 | // keepDirs returns a key mapping function for the number of directories to be retained
6 | local keepDirs = function(num=0) function(s) (
7 | if num < 0
8 | then
9 | s
10 | else (
11 | local elems = split(s, '/');
12 | local preserveRight = num + 1;
13 | if len(elems) <= preserveRight
14 | then
15 | s
16 | else (
17 | local remove = len(elems) - preserveRight;
18 | join('/', elems[remove:])
19 | )
20 | )
21 | );
22 |
23 | // stripExtension is a key mapping function that strips the file extension from the key
24 | local stripExtension = function(s) (
25 | local parts = split(s, '/');
26 | local dirs = parts[:len(parts) - 1];
27 | local file = parts[len(parts) - 1];
28 | local fileParts = split(file, '.');
29 | local fixed = if len(fileParts) == 1 then file else join('.', fileParts[:len(fileParts) - 1]);
30 | join('/', dirs + [fixed])
31 | );
32 |
33 | // compose composes an array of map functions by applying them in sequence
34 | local compose = function(arr) function(s) std.foldl(function(prev, fn) fn(prev), arr, s);
35 |
36 | // transform transforms an object, mapping keys using the key mapper and values using the valueMapper.
37 | // It ensures that the key mapping does not produce duplicate keys.
38 | local transform = function(globObject, keyMapper=function(s) s, valueMapper=function(o) o) (
39 | local keys = std.objectFields(globObject);
40 | std.foldl(function(obj, key) (
41 | local mKey = keyMapper(key);
42 | local val = globObject[key];
43 | if std.objectHas(obj, mKey)
44 | then
45 | error 'multiple keys map to the same value: %s' % [mKey]
46 | else
47 | obj { [mKey]: valueMapper(val) }
48 | ), keys, {})
49 | );
50 |
51 | // nameOnly is a key mapper that removes all directories and strips extensions from file names,
52 | // syntax sugar for the common case.
53 | local nameOnly = compose([keepDirs(0), stripExtension]);
54 |
55 | {
56 | transform:: transform,
57 | keepDirs:: keepDirs,
58 | stripExtension:: stripExtension,
59 | compose:: compose,
60 | nameOnly:: nameOnly,
61 | }
62 |
--------------------------------------------------------------------------------
/site/content/_index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Home
3 | chapter: true
4 | ---
5 |
6 | # 
7 |
8 | _a tool to configure and create Kubernetes objects on multiple environments_
9 |
10 | qbec (pronounced like the [Canadian province](https://en.wikipedia.org/wiki/Quebec)) is a command line tool that
11 | allows you to create Kubernetes objects on multiple Kubernetes clusters and/ or namespaces configured for
12 | the target environment in question.
13 |
14 | It is based on [jsonnet](https://jsonnet.org) and is similar to other tools in the same space like
15 | [kubecfg](https://github.com/ksonnet/kubecfg) and [ksonnet](https://ksonnet.io/).
16 | If you already know what the other tools do, read [the comparison document](comparison-with-other-tools/) to understand
17 | how qbec is different from them. Otherwise read the [user guide](userguide/).
18 |
19 | ## Features
20 |
21 | * Deploy Kubernetes objects across multiple clusters and/ or namespaces.
22 | * Create transient objects like one-off `jobs` and `pods` with generated names.
23 | * Deploy cluster-scoped objects (useful for cluster admins)
24 | * Specify common metadata (e.g. annotations) for all objects in one place.
25 | * Track configurations with version control
26 | * Specify environment-specific component lists
27 | * Apply component and kind filters to commands
28 | * Automatic garbage collection for deleted and renamed objects.
29 | * Integrate with jsonnet external and top-level variables for late-bound configuration
30 | * Create differently named objects for branch builds and garbage collect in that limited scope.
31 | * Customize update and delete behavior using directives.
32 | * Usable, safe and secure by default.
33 | * Duplicate objects having the same kind, namespace, and name are detected and disallowed.
34 | * Remote commands that change cluster state require confirmation.
35 | * Secrets are automatically hidden and never appear in any output.
36 | * Performant (limited by the speed of the jsonnet libraries you use)
37 | * Ability to use qbec environment definitions in other unrelated commands and scripts so that the
38 | safety properties of qbec are carried over to those commands.
39 |
40 | [Get started](getting-started/).
41 |
--------------------------------------------------------------------------------
/internal/types/testdata/daemonset/not-rolling-update.json:
--------------------------------------------------------------------------------
1 | {
2 | "apiVersion": "extensions/v1beta1",
3 | "kind": "DaemonSet",
4 | "metadata": {
5 | "annotations": {
6 | "test/status": "skip rollout check for daemonset (strategy=OnDelete)",
7 | "test/done": "true"
8 | },
9 | "creationTimestamp": "2018-06-20T06:18:37Z",
10 | "generation": 12,
11 | "labels": {
12 | "app": "nginx"
13 | },
14 | "name": "nginx",
15 | "namespace": "default",
16 | "resourceVersion": "95296317",
17 | "selfLink": "/apis/extensions/v1beta1/namespaces/default/daemonsets/nginx",
18 | "uid": "c466b004-7451-11e8-9551-0a42ecfaa366"
19 | },
20 | "spec": {
21 | "revisionHistoryLimit": 10,
22 | "selector": {
23 | "matchLabels": {
24 | "app": "nginx"
25 | }
26 | },
27 | "template": {
28 | "metadata": {
29 | "labels": {
30 | "app": "nginx"
31 | }
32 | },
33 | "spec": {
34 | "containers": [
35 | {
36 | "image": "nginx",
37 | "name": "main",
38 | "ports": [
39 | {
40 | "containerPort": 8181,
41 | "hostPort": 8181,
42 | "name": "http",
43 | "protocol": "TCP"
44 | }
45 | ],
46 | "resources": {},
47 | "terminationMessagePath": "/dev/termination-log",
48 | "terminationMessagePolicy": "File"
49 | }
50 | ],
51 | "dnsPolicy": "ClusterFirst",
52 | "hostNetwork": true,
53 | "restartPolicy": "Always",
54 | "schedulerName": "default-scheduler",
55 | "securityContext": {},
56 | "serviceAccount": "default",
57 | "serviceAccountName": "default",
58 | "terminationGracePeriodSeconds": 30
59 | }
60 | },
61 | "templateGeneration": 11,
62 | "updateStrategy": {
63 | "type": "OnDelete"
64 | }
65 | },
66 | "status": {
67 | "currentNumberScheduled": 3,
68 | "desiredNumberScheduled": 3,
69 | "numberAvailable": 3,
70 | "numberMisscheduled": 0,
71 | "numberReady": 3,
72 | "observedGeneration": 12,
73 | "updatedNumberScheduled": 3
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/internal/types/testdata/daemonset/bad-object.json:
--------------------------------------------------------------------------------
1 | {
2 | "apiVersion": "extensions/v1beta1",
3 | "kind": "DaemonSet",
4 | "metadata": {
5 | "annotations": {
6 | "test/error": "/json unmarshal/"
7 | },
8 | "creationTimestamp": "2018-06-20T06:18:37Z",
9 | "generation": 12,
10 | "labels": {
11 | "app": "nginx"
12 | },
13 | "name": "nginx",
14 | "namespace": "default",
15 | "resourceVersion": "95296317",
16 | "selfLink": "/apis/extensions/v1beta1/namespaces/default/daemonsets/nginx",
17 | "uid": "c466b004-7451-11e8-9551-0a42ecfaa366"
18 | },
19 | "spec": {
20 | "revisionHistoryLimit": 10,
21 | "selector": {
22 | "matchLabels": {
23 | "app": "nginx"
24 | }
25 | },
26 | "template": {
27 | "metadata": {
28 | "labels": {
29 | "app": "nginx"
30 | }
31 | },
32 | "spec": {
33 | "containers": [
34 | {
35 | "image": "nginx",
36 | "name": "main",
37 | "ports": [
38 | {
39 | "containerPort": 8181,
40 | "hostPort": 8181,
41 | "name": "http",
42 | "protocol": "TCP"
43 | }
44 | ],
45 | "resources": {},
46 | "terminationMessagePath": "/dev/termination-log",
47 | "terminationMessagePolicy": "File"
48 | }
49 | ],
50 | "dnsPolicy": "ClusterFirst",
51 | "hostNetwork": true,
52 | "restartPolicy": "Always",
53 | "schedulerName": "default-scheduler",
54 | "securityContext": {},
55 | "serviceAccount": "default",
56 | "serviceAccountName": "default",
57 | "terminationGracePeriodSeconds": 30
58 | }
59 | },
60 | "templateGeneration": 11,
61 | "updateStrategy": {
62 | "rollingUpdate": {
63 | "maxUnavailable": 1
64 | },
65 | "type": "RollingUpdate"
66 | }
67 | },
68 | "status": {
69 | "currentNumberScheduled": 3,
70 | "desiredNumberScheduled": 3,
71 | "numberAvailable": "xxx",
72 | "numberMisscheduled": 0,
73 | "numberReady": 3,
74 | "observedGeneration": 12,
75 | "updatedNumberScheduled": 3
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/cmd/jsonnet-qbec/main.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 Splunk Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package main
16 |
17 | import (
18 | "fmt"
19 | "log"
20 | "os"
21 | "path/filepath"
22 |
23 | "github.com/pkg/errors"
24 | "github.com/spf13/cobra"
25 | "github.com/splunk/qbec/internal/cmd"
26 | "github.com/splunk/qbec/internal/vmexternals"
27 | "github.com/splunk/qbec/vm"
28 | )
29 |
30 | func run(file string, ext vmexternals.Externals) (string, error) {
31 | vs := ext.ToVariableSet()
32 | dataSources, closer, err := vm.CreateDataSources(ext.DataSources, vm.ConfigProviderFromVariables(vs))
33 | cmd.RegisterCleanupTask(closer)
34 | if err != nil {
35 | return "", err
36 | }
37 | cfg := vm.Config{
38 | LibPaths: ext.LibPaths,
39 | DataSources: dataSources,
40 | }
41 | jvm := vm.New(cfg)
42 | return jvm.EvalFile(file, vs)
43 | }
44 |
45 | func main() {
46 | var configInit func() (vmexternals.Externals, error)
47 | exe := filepath.Base(os.Args[0])
48 | root := &cobra.Command{
49 | Use: exe + " ",
50 | Short: "jsonnet with yaml support",
51 | RunE: func(c *cobra.Command, args []string) error {
52 | if len(args) != 1 {
53 | return fmt.Errorf("exactly one file argument is required")
54 | }
55 | ext, err := configInit()
56 | if err != nil {
57 | return errors.Wrap(err, "create VM ext")
58 | }
59 | s, err := run(args[0], ext)
60 | if err != nil {
61 | return err
62 | }
63 | fmt.Println(s)
64 | return nil
65 | },
66 | }
67 | cmd.RegisterSignalHandlers()
68 | defer cmd.Close()
69 | configInit = vmexternals.FromCommandParams(root, "", true)
70 | if err := root.Execute(); err != nil {
71 | log.Fatalln(err)
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/internal/types/testdata/daemonset/fewer-pods-available.json:
--------------------------------------------------------------------------------
1 | {
2 | "apiVersion": "extensions/v1beta1",
3 | "kind": "DaemonSet",
4 | "metadata": {
5 | "annotations": {
6 | "test/status": "2 of 3 updated pods are available"
7 | },
8 | "creationTimestamp": "2018-06-20T06:18:37Z",
9 | "generation": 12,
10 | "labels": {
11 | "app": "nginx"
12 | },
13 | "name": "nginx",
14 | "namespace": "default",
15 | "resourceVersion": "95296317",
16 | "selfLink": "/apis/extensions/v1beta1/namespaces/default/daemonsets/nginx",
17 | "uid": "c466b004-7451-11e8-9551-0a42ecfaa366"
18 | },
19 | "spec": {
20 | "revisionHistoryLimit": 10,
21 | "selector": {
22 | "matchLabels": {
23 | "app": "nginx"
24 | }
25 | },
26 | "template": {
27 | "metadata": {
28 | "labels": {
29 | "app": "nginx"
30 | }
31 | },
32 | "spec": {
33 | "containers": [
34 | {
35 | "image": "nginx",
36 | "name": "main",
37 | "ports": [
38 | {
39 | "containerPort": 8181,
40 | "hostPort": 8181,
41 | "name": "http",
42 | "protocol": "TCP"
43 | }
44 | ],
45 | "resources": {},
46 | "terminationMessagePath": "/dev/termination-log",
47 | "terminationMessagePolicy": "File"
48 | }
49 | ],
50 | "dnsPolicy": "ClusterFirst",
51 | "hostNetwork": true,
52 | "restartPolicy": "Always",
53 | "schedulerName": "default-scheduler",
54 | "securityContext": {},
55 | "serviceAccount": "default",
56 | "serviceAccountName": "default",
57 | "terminationGracePeriodSeconds": 30
58 | }
59 | },
60 | "templateGeneration": 11,
61 | "updateStrategy": {
62 | "rollingUpdate": {
63 | "maxUnavailable": 1
64 | },
65 | "type": "RollingUpdate"
66 | }
67 | },
68 | "status": {
69 | "currentNumberScheduled": 3,
70 | "desiredNumberScheduled": 3,
71 | "numberAvailable": 2,
72 | "numberMisscheduled": 0,
73 | "numberReady": 3,
74 | "observedGeneration": 12,
75 | "updatedNumberScheduled": 3
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/internal/types/testdata/daemonset/fewer-pods-updated.json:
--------------------------------------------------------------------------------
1 | {
2 | "apiVersion": "extensions/v1beta1",
3 | "kind": "DaemonSet",
4 | "metadata": {
5 | "annotations": {
6 | "test/status": "2 out of 3 new pods have been updated"
7 | },
8 | "creationTimestamp": "2018-06-20T06:18:37Z",
9 | "generation": 12,
10 | "labels": {
11 | "app": "nginx"
12 | },
13 | "name": "nginx",
14 | "namespace": "default",
15 | "resourceVersion": "95296317",
16 | "selfLink": "/apis/extensions/v1beta1/namespaces/default/daemonsets/nginx",
17 | "uid": "c466b004-7451-11e8-9551-0a42ecfaa366"
18 | },
19 | "spec": {
20 | "revisionHistoryLimit": 10,
21 | "selector": {
22 | "matchLabels": {
23 | "app": "nginx"
24 | }
25 | },
26 | "template": {
27 | "metadata": {
28 | "labels": {
29 | "app": "nginx"
30 | }
31 | },
32 | "spec": {
33 | "containers": [
34 | {
35 | "image": "nginx",
36 | "name": "main",
37 | "ports": [
38 | {
39 | "containerPort": 8181,
40 | "hostPort": 8181,
41 | "name": "http",
42 | "protocol": "TCP"
43 | }
44 | ],
45 | "resources": {},
46 | "terminationMessagePath": "/dev/termination-log",
47 | "terminationMessagePolicy": "File"
48 | }
49 | ],
50 | "dnsPolicy": "ClusterFirst",
51 | "hostNetwork": true,
52 | "restartPolicy": "Always",
53 | "schedulerName": "default-scheduler",
54 | "securityContext": {},
55 | "serviceAccount": "default",
56 | "serviceAccountName": "default",
57 | "terminationGracePeriodSeconds": 30
58 | }
59 | },
60 | "templateGeneration": 11,
61 | "updateStrategy": {
62 | "rollingUpdate": {
63 | "maxUnavailable": 1
64 | },
65 | "type": "RollingUpdate"
66 | }
67 | },
68 | "status": {
69 | "currentNumberScheduled": 3,
70 | "desiredNumberScheduled": 3,
71 | "numberAvailable": 3,
72 | "numberMisscheduled": 0,
73 | "numberReady": 3,
74 | "observedGeneration": 12,
75 | "updatedNumberScheduled": 2
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/internal/types/testdata/deploy/bad-object.json:
--------------------------------------------------------------------------------
1 | {
2 | "apiVersion": "extensions/v1beta1",
3 | "kind": "Deployment",
4 | "metadata": {
5 | "annotations": {
6 | "deployment.kubernetes.io/revision": "1",
7 | "test/error": "/json unmarshal:/"
8 | },
9 | "creationTimestamp": "2019-07-16T23:18:14Z",
10 | "generation": 1,
11 | "labels": {
12 | "run": "nginx"
13 | },
14 | "name": "nginx",
15 | "namespace": "default",
16 | "resourceVersion": "146445064",
17 | "selfLink": "/apis/extensions/v1beta1/namespaces/default/deployments/nginx",
18 | "uid": "fc9c5c4f-a81f-11e9-9cb0-02b049f8a858"
19 | },
20 | "spec": {
21 | "progressDeadlineSeconds": 2147483647,
22 | "replicas": 4,
23 | "revisionHistoryLimit": 10,
24 | "selector": {
25 | "matchLabels": {
26 | "run": "nginx"
27 | }
28 | },
29 | "strategy": {
30 | "rollingUpdate": {
31 | "maxSurge": "25%",
32 | "maxUnavailable": "25%"
33 | },
34 | "type": "RollingUpdate"
35 | },
36 | "template": {
37 | "metadata": {
38 | "labels": {
39 | "run": "nginx"
40 | }
41 | },
42 | "spec": {
43 | "containers": [
44 | {
45 | "image": "nginx",
46 | "imagePullPolicy": "Always",
47 | "name": "nginx",
48 | "resources": {},
49 | "terminationMessagePath": "/dev/termination-log",
50 | "terminationMessagePolicy": "File"
51 | }
52 | ],
53 | "dnsPolicy": "ClusterFirst",
54 | "restartPolicy": "Always",
55 | "schedulerName": "default-scheduler",
56 | "securityContext": {},
57 | "terminationGracePeriodSeconds": 30
58 | }
59 | }
60 | },
61 | "status": {
62 | "conditions": [
63 | {
64 | "lastTransitionTime": "2019-07-16T23:18:14Z",
65 | "lastUpdateTime": "2019-07-16T23:18:14Z",
66 | "message": "Deployment does not have minimum availability.",
67 | "reason": "MinimumReplicasUnavailable",
68 | "status": "False",
69 | "type": "Available"
70 | }
71 | ],
72 | "observedGeneration": 1,
73 | "replicas": "xxx",
74 | "unavailableReplicas": 4,
75 | "updatedReplicas": 4
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/internal/types/testdata/deploy/zero-of-four.json:
--------------------------------------------------------------------------------
1 | {
2 | "apiVersion": "extensions/v1beta1",
3 | "kind": "Deployment",
4 | "metadata": {
5 | "annotations": {
6 | "deployment.kubernetes.io/revision": "1",
7 | "test/status": "0 of 4 updated replicas are available"
8 | },
9 | "creationTimestamp": "2019-07-16T23:18:14Z",
10 | "generation": 1,
11 | "labels": {
12 | "run": "nginx"
13 | },
14 | "name": "nginx",
15 | "namespace": "default",
16 | "resourceVersion": "146445064",
17 | "selfLink": "/apis/extensions/v1beta1/namespaces/default/deployments/nginx",
18 | "uid": "fc9c5c4f-a81f-11e9-9cb0-02b049f8a858"
19 | },
20 | "spec": {
21 | "progressDeadlineSeconds": 2147483647,
22 | "replicas": 4,
23 | "revisionHistoryLimit": 10,
24 | "selector": {
25 | "matchLabels": {
26 | "run": "nginx"
27 | }
28 | },
29 | "strategy": {
30 | "rollingUpdate": {
31 | "maxSurge": "25%",
32 | "maxUnavailable": "25%"
33 | },
34 | "type": "RollingUpdate"
35 | },
36 | "template": {
37 | "metadata": {
38 | "labels": {
39 | "run": "nginx"
40 | }
41 | },
42 | "spec": {
43 | "containers": [
44 | {
45 | "image": "nginx",
46 | "imagePullPolicy": "Always",
47 | "name": "nginx",
48 | "resources": {},
49 | "terminationMessagePath": "/dev/termination-log",
50 | "terminationMessagePolicy": "File"
51 | }
52 | ],
53 | "dnsPolicy": "ClusterFirst",
54 | "restartPolicy": "Always",
55 | "schedulerName": "default-scheduler",
56 | "securityContext": {},
57 | "terminationGracePeriodSeconds": 30
58 | }
59 | }
60 | },
61 | "status": {
62 | "conditions": [
63 | {
64 | "lastTransitionTime": "2019-07-16T23:18:14Z",
65 | "lastUpdateTime": "2019-07-16T23:18:14Z",
66 | "message": "Deployment does not have minimum availability.",
67 | "reason": "MinimumReplicasUnavailable",
68 | "status": "False",
69 | "type": "Available"
70 | }
71 | ],
72 | "observedGeneration": 1,
73 | "replicas": 4,
74 | "unavailableReplicas": 4,
75 | "updatedReplicas": 4
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/vm/internal/natives/yaml.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 Splunk Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package natives
16 |
17 | import (
18 | "io"
19 |
20 | v3yaml "go.yaml.in/yaml/v3"
21 | "k8s.io/apimachinery/pkg/util/yaml"
22 | )
23 |
24 | // ParseYAMLDocuments parses the contents of the reader into an array of
25 | // objects, one for each non-nil document in the input.
26 | func ParseYAMLDocuments(reader io.Reader) ([]interface{}, error) {
27 | ret := make([]interface{}, 0)
28 | d := yaml.NewYAMLToJSONDecoder(reader)
29 | for {
30 | var doc interface{}
31 | if err := d.Decode(&doc); err != nil {
32 | if err == io.EOF {
33 | break
34 | }
35 | return nil, err
36 | }
37 | if doc != nil {
38 | ret = append(ret, doc)
39 | }
40 | }
41 | return ret, nil
42 | }
43 |
44 | // RenderYAMLDocuments renders the supplied data as a series of YAML documents if the input is an array
45 | // or a single document when it is not. Nils are excluded from output.
46 | // If the caller wants an array to be rendered as a single document,
47 | // they need to wrap it in an array first. Note that this function is not a drop-in replacement for
48 | // data that requires ghodss/yaml to be rendered correctly.
49 | func RenderYAMLDocuments(data interface{}, writer io.Writer) (retErr error) {
50 | out, ok := data.([]interface{})
51 | if !ok {
52 | out = []interface{}{data}
53 | }
54 | enc := v3yaml.NewEncoder(writer)
55 | enc.SetIndent(2)
56 | defer func() {
57 | err := enc.Close()
58 | if err == nil && retErr == nil {
59 | retErr = err
60 | }
61 | }()
62 | for _, doc := range out {
63 | if doc == nil {
64 | continue
65 | }
66 | if err := enc.Encode(doc); err != nil {
67 | return err
68 | }
69 | }
70 | return nil
71 | }
72 |
--------------------------------------------------------------------------------
/internal/types/testdata/deploy/wait-observe.json:
--------------------------------------------------------------------------------
1 | {
2 | "apiVersion": "extensions/v1beta1",
3 | "kind": "Deployment",
4 | "metadata": {
5 | "annotations": {
6 | "deployment.kubernetes.io/revision": "1",
7 | "test/status": "waiting for spec update to be observed"
8 | },
9 | "creationTimestamp": "2019-07-16T23:18:14Z",
10 | "generation": 2,
11 | "labels": {
12 | "run": "nginx"
13 | },
14 | "name": "nginx",
15 | "namespace": "default",
16 | "resourceVersion": "146445064",
17 | "selfLink": "/apis/extensions/v1beta1/namespaces/default/deployments/nginx",
18 | "uid": "fc9c5c4f-a81f-11e9-9cb0-02b049f8a858"
19 | },
20 | "spec": {
21 | "progressDeadlineSeconds": 2147483647,
22 | "replicas": 4,
23 | "revisionHistoryLimit": 10,
24 | "selector": {
25 | "matchLabels": {
26 | "run": "nginx"
27 | }
28 | },
29 | "strategy": {
30 | "rollingUpdate": {
31 | "maxSurge": "25%",
32 | "maxUnavailable": "25%"
33 | },
34 | "type": "RollingUpdate"
35 | },
36 | "template": {
37 | "metadata": {
38 | "labels": {
39 | "run": "nginx"
40 | }
41 | },
42 | "spec": {
43 | "containers": [
44 | {
45 | "image": "nginx",
46 | "imagePullPolicy": "Always",
47 | "name": "nginx",
48 | "resources": {},
49 | "terminationMessagePath": "/dev/termination-log",
50 | "terminationMessagePolicy": "File"
51 | }
52 | ],
53 | "dnsPolicy": "ClusterFirst",
54 | "restartPolicy": "Always",
55 | "schedulerName": "default-scheduler",
56 | "securityContext": {},
57 | "terminationGracePeriodSeconds": 30
58 | }
59 | }
60 | },
61 | "status": {
62 | "conditions": [
63 | {
64 | "lastTransitionTime": "2019-07-16T23:18:14Z",
65 | "lastUpdateTime": "2019-07-16T23:18:14Z",
66 | "message": "Deployment does not have minimum availability.",
67 | "reason": "MinimumReplicasUnavailable",
68 | "status": "False",
69 | "type": "Available"
70 | }
71 | ],
72 | "observedGeneration": 1,
73 | "replicas": 4,
74 | "unavailableReplicas": 4,
75 | "updatedReplicas": 4
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/internal/types/testdata/deploy/missing-rev.json:
--------------------------------------------------------------------------------
1 | {
2 | "apiVersion": "extensions/v1beta1",
3 | "kind": "Deployment",
4 | "metadata": {
5 | "annotations": {
6 | "test/error": "desired revision (2) is different from the running revision (0)",
7 | "test-input/revision": "2"
8 | },
9 | "creationTimestamp": "2019-07-16T23:18:14Z",
10 | "generation": 1,
11 | "labels": {
12 | "run": "nginx"
13 | },
14 | "name": "nginx",
15 | "namespace": "default",
16 | "resourceVersion": "146445064",
17 | "selfLink": "/apis/extensions/v1beta1/namespaces/default/deployments/nginx",
18 | "uid": "fc9c5c4f-a81f-11e9-9cb0-02b049f8a858"
19 | },
20 | "spec": {
21 | "progressDeadlineSeconds": 2147483647,
22 | "replicas": 4,
23 | "revisionHistoryLimit": 10,
24 | "selector": {
25 | "matchLabels": {
26 | "run": "nginx"
27 | }
28 | },
29 | "strategy": {
30 | "rollingUpdate": {
31 | "maxSurge": "25%",
32 | "maxUnavailable": "25%"
33 | },
34 | "type": "RollingUpdate"
35 | },
36 | "template": {
37 | "metadata": {
38 | "labels": {
39 | "run": "nginx"
40 | }
41 | },
42 | "spec": {
43 | "containers": [
44 | {
45 | "image": "nginx",
46 | "imagePullPolicy": "Always",
47 | "name": "nginx",
48 | "resources": {},
49 | "terminationMessagePath": "/dev/termination-log",
50 | "terminationMessagePolicy": "File"
51 | }
52 | ],
53 | "dnsPolicy": "ClusterFirst",
54 | "restartPolicy": "Always",
55 | "schedulerName": "default-scheduler",
56 | "securityContext": {},
57 | "terminationGracePeriodSeconds": 30
58 | }
59 | }
60 | },
61 | "status": {
62 | "conditions": [
63 | {
64 | "lastTransitionTime": "2019-07-16T23:18:14Z",
65 | "lastUpdateTime": "2019-07-16T23:18:14Z",
66 | "message": "Deployment does not have minimum availability.",
67 | "reason": "MinimumReplicasUnavailable",
68 | "status": "False",
69 | "type": "Available"
70 | }
71 | ],
72 | "observedGeneration": 1,
73 | "replicas": 4,
74 | "unavailableReplicas": 4,
75 | "updatedReplicas": 4
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/internal/types/testdata/deploy/bad-rev.json:
--------------------------------------------------------------------------------
1 | {
2 | "apiVersion": "extensions/v1beta1",
3 | "kind": "Deployment",
4 | "metadata": {
5 | "annotations": {
6 | "deployment.kubernetes.io/revision": "xxx",
7 | "test/error": "/get revision: strconv.ParseInt/",
8 | "test-input/revision": "2"
9 | },
10 | "creationTimestamp": "2019-07-16T23:18:14Z",
11 | "generation": 1,
12 | "labels": {
13 | "run": "nginx"
14 | },
15 | "name": "nginx",
16 | "namespace": "default",
17 | "resourceVersion": "146445064",
18 | "selfLink": "/apis/extensions/v1beta1/namespaces/default/deployments/nginx",
19 | "uid": "fc9c5c4f-a81f-11e9-9cb0-02b049f8a858"
20 | },
21 | "spec": {
22 | "progressDeadlineSeconds": 2147483647,
23 | "replicas": 4,
24 | "revisionHistoryLimit": 10,
25 | "selector": {
26 | "matchLabels": {
27 | "run": "nginx"
28 | }
29 | },
30 | "strategy": {
31 | "rollingUpdate": {
32 | "maxSurge": "25%",
33 | "maxUnavailable": "25%"
34 | },
35 | "type": "RollingUpdate"
36 | },
37 | "template": {
38 | "metadata": {
39 | "labels": {
40 | "run": "nginx"
41 | }
42 | },
43 | "spec": {
44 | "containers": [
45 | {
46 | "image": "nginx",
47 | "imagePullPolicy": "Always",
48 | "name": "nginx",
49 | "resources": {},
50 | "terminationMessagePath": "/dev/termination-log",
51 | "terminationMessagePolicy": "File"
52 | }
53 | ],
54 | "dnsPolicy": "ClusterFirst",
55 | "restartPolicy": "Always",
56 | "schedulerName": "default-scheduler",
57 | "securityContext": {},
58 | "terminationGracePeriodSeconds": 30
59 | }
60 | }
61 | },
62 | "status": {
63 | "conditions": [
64 | {
65 | "lastTransitionTime": "2019-07-16T23:18:14Z",
66 | "lastUpdateTime": "2019-07-16T23:18:14Z",
67 | "message": "Deployment does not have minimum availability.",
68 | "reason": "MinimumReplicasUnavailable",
69 | "status": "False",
70 | "type": "Available"
71 | }
72 | ],
73 | "observedGeneration": 1,
74 | "replicas": 4,
75 | "unavailableReplicas": 4,
76 | "updatedReplicas": 4
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/internal/types/testdata/deploy/pend-term.json:
--------------------------------------------------------------------------------
1 | {
2 | "apiVersion": "extensions/v1beta1",
3 | "kind": "Deployment",
4 | "metadata": {
5 | "annotations": {
6 | "deployment.kubernetes.io/revision": "1",
7 | "test/status": "1 old replicas are pending termination"
8 | },
9 | "creationTimestamp": "2019-07-16T23:18:14Z",
10 | "generation": 1,
11 | "labels": {
12 | "run": "nginx"
13 | },
14 | "name": "nginx",
15 | "namespace": "default",
16 | "resourceVersion": "146445064",
17 | "selfLink": "/apis/extensions/v1beta1/namespaces/default/deployments/nginx",
18 | "uid": "fc9c5c4f-a81f-11e9-9cb0-02b049f8a858"
19 | },
20 | "spec": {
21 | "progressDeadlineSeconds": 2147483647,
22 | "replicas": 4,
23 | "revisionHistoryLimit": 10,
24 | "selector": {
25 | "matchLabels": {
26 | "run": "nginx"
27 | }
28 | },
29 | "strategy": {
30 | "rollingUpdate": {
31 | "maxSurge": "25%",
32 | "maxUnavailable": "25%"
33 | },
34 | "type": "RollingUpdate"
35 | },
36 | "template": {
37 | "metadata": {
38 | "labels": {
39 | "run": "nginx"
40 | }
41 | },
42 | "spec": {
43 | "containers": [
44 | {
45 | "image": "nginx",
46 | "imagePullPolicy": "Always",
47 | "name": "nginx",
48 | "resources": {},
49 | "terminationMessagePath": "/dev/termination-log",
50 | "terminationMessagePolicy": "File"
51 | }
52 | ],
53 | "dnsPolicy": "ClusterFirst",
54 | "restartPolicy": "Always",
55 | "schedulerName": "default-scheduler",
56 | "securityContext": {},
57 | "terminationGracePeriodSeconds": 30
58 | }
59 | }
60 | },
61 | "status": {
62 | "conditions": [
63 | {
64 | "lastTransitionTime": "2019-07-16T23:18:14Z",
65 | "lastUpdateTime": "2019-07-16T23:18:14Z",
66 | "message": "Deployment does not have minimum availability.",
67 | "reason": "MinimumReplicasUnavailable",
68 | "status": "False",
69 | "type": "Available"
70 | }
71 | ],
72 | "availableReplicas": 4,
73 | "observedGeneration": 1,
74 | "replicas": 5,
75 | "unavailableReplicas": 0,
76 | "updatedReplicas": 4
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/internal/types/testdata/deploy/fewer-updated.json:
--------------------------------------------------------------------------------
1 | {
2 | "apiVersion": "extensions/v1beta1",
3 | "kind": "Deployment",
4 | "metadata": {
5 | "annotations": {
6 | "deployment.kubernetes.io/revision": "1",
7 | "test/status": "3 out of 4 new replicas have been updated"
8 | },
9 | "creationTimestamp": "2019-07-16T23:18:14Z",
10 | "generation": 1,
11 | "labels": {
12 | "run": "nginx"
13 | },
14 | "name": "nginx",
15 | "namespace": "default",
16 | "resourceVersion": "146445064",
17 | "selfLink": "/apis/extensions/v1beta1/namespaces/default/deployments/nginx",
18 | "uid": "fc9c5c4f-a81f-11e9-9cb0-02b049f8a858"
19 | },
20 | "spec": {
21 | "progressDeadlineSeconds": 2147483647,
22 | "replicas": 4,
23 | "revisionHistoryLimit": 10,
24 | "selector": {
25 | "matchLabels": {
26 | "run": "nginx"
27 | }
28 | },
29 | "strategy": {
30 | "rollingUpdate": {
31 | "maxSurge": "25%",
32 | "maxUnavailable": "25%"
33 | },
34 | "type": "RollingUpdate"
35 | },
36 | "template": {
37 | "metadata": {
38 | "labels": {
39 | "run": "nginx"
40 | }
41 | },
42 | "spec": {
43 | "containers": [
44 | {
45 | "image": "nginx",
46 | "imagePullPolicy": "Always",
47 | "name": "nginx",
48 | "resources": {},
49 | "terminationMessagePath": "/dev/termination-log",
50 | "terminationMessagePolicy": "File"
51 | }
52 | ],
53 | "dnsPolicy": "ClusterFirst",
54 | "restartPolicy": "Always",
55 | "schedulerName": "default-scheduler",
56 | "securityContext": {},
57 | "terminationGracePeriodSeconds": 30
58 | }
59 | }
60 | },
61 | "status": {
62 | "availableReplicas": 0,
63 | "conditions": [
64 | {
65 | "lastTransitionTime": "2019-07-16T23:18:14Z",
66 | "lastUpdateTime": "2019-07-16T23:18:14Z",
67 | "message": "Deployment does not have minimum availability.",
68 | "reason": "MinimumReplicasUnavailable",
69 | "status": "False",
70 | "type": "Available"
71 | }
72 | ],
73 | "observedGeneration": 1,
74 | "replicas": 4,
75 | "unavailableReplicas": 4,
76 | "updatedReplicas": 3
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/internal/types/testdata/deploy/not-fully-available.json:
--------------------------------------------------------------------------------
1 | {
2 | "apiVersion": "extensions/v1beta1",
3 | "kind": "Deployment",
4 | "metadata": {
5 | "annotations": {
6 | "deployment.kubernetes.io/revision": "1",
7 | "test/status": "2 of 4 updated replicas are available"
8 | },
9 | "creationTimestamp": "2019-07-16T23:18:14Z",
10 | "generation": 1,
11 | "labels": {
12 | "run": "nginx"
13 | },
14 | "name": "nginx",
15 | "namespace": "default",
16 | "resourceVersion": "146445064",
17 | "selfLink": "/apis/extensions/v1beta1/namespaces/default/deployments/nginx",
18 | "uid": "fc9c5c4f-a81f-11e9-9cb0-02b049f8a858"
19 | },
20 | "spec": {
21 | "progressDeadlineSeconds": 2147483647,
22 | "replicas": 4,
23 | "revisionHistoryLimit": 10,
24 | "selector": {
25 | "matchLabels": {
26 | "run": "nginx"
27 | }
28 | },
29 | "strategy": {
30 | "rollingUpdate": {
31 | "maxSurge": "25%",
32 | "maxUnavailable": "25%"
33 | },
34 | "type": "RollingUpdate"
35 | },
36 | "template": {
37 | "metadata": {
38 | "labels": {
39 | "run": "nginx"
40 | }
41 | },
42 | "spec": {
43 | "containers": [
44 | {
45 | "image": "nginx",
46 | "imagePullPolicy": "Always",
47 | "name": "nginx",
48 | "resources": {},
49 | "terminationMessagePath": "/dev/termination-log",
50 | "terminationMessagePolicy": "File"
51 | }
52 | ],
53 | "dnsPolicy": "ClusterFirst",
54 | "restartPolicy": "Always",
55 | "schedulerName": "default-scheduler",
56 | "securityContext": {},
57 | "terminationGracePeriodSeconds": 30
58 | }
59 | }
60 | },
61 | "status": {
62 | "availableReplicas": 2,
63 | "conditions": [
64 | {
65 | "lastTransitionTime": "2019-07-16T23:18:14Z",
66 | "lastUpdateTime": "2019-07-16T23:18:14Z",
67 | "message": "Deployment does not have minimum availability.",
68 | "reason": "MinimumReplicasUnavailable",
69 | "status": "False",
70 | "type": "Available"
71 | }
72 | ],
73 | "observedGeneration": 1,
74 | "replicas": 4,
75 | "unavailableReplicas": 2,
76 | "updatedReplicas": 4
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/internal/types/testdata/deploy/diff-rev.json:
--------------------------------------------------------------------------------
1 | {
2 | "apiVersion": "extensions/v1beta1",
3 | "kind": "Deployment",
4 | "metadata": {
5 | "annotations": {
6 | "deployment.kubernetes.io/revision": "1",
7 | "test/error": "desired revision (2) is different from the running revision (1)",
8 | "test-input/revision": "2"
9 | },
10 | "creationTimestamp": "2019-07-16T23:18:14Z",
11 | "generation": 1,
12 | "labels": {
13 | "run": "nginx"
14 | },
15 | "name": "nginx",
16 | "namespace": "default",
17 | "resourceVersion": "146445064",
18 | "selfLink": "/apis/extensions/v1beta1/namespaces/default/deployments/nginx",
19 | "uid": "fc9c5c4f-a81f-11e9-9cb0-02b049f8a858"
20 | },
21 | "spec": {
22 | "progressDeadlineSeconds": 2147483647,
23 | "replicas": 4,
24 | "revisionHistoryLimit": 10,
25 | "selector": {
26 | "matchLabels": {
27 | "run": "nginx"
28 | }
29 | },
30 | "strategy": {
31 | "rollingUpdate": {
32 | "maxSurge": "25%",
33 | "maxUnavailable": "25%"
34 | },
35 | "type": "RollingUpdate"
36 | },
37 | "template": {
38 | "metadata": {
39 | "labels": {
40 | "run": "nginx"
41 | }
42 | },
43 | "spec": {
44 | "containers": [
45 | {
46 | "image": "nginx",
47 | "imagePullPolicy": "Always",
48 | "name": "nginx",
49 | "resources": {},
50 | "terminationMessagePath": "/dev/termination-log",
51 | "terminationMessagePolicy": "File"
52 | }
53 | ],
54 | "dnsPolicy": "ClusterFirst",
55 | "restartPolicy": "Always",
56 | "schedulerName": "default-scheduler",
57 | "securityContext": {},
58 | "terminationGracePeriodSeconds": 30
59 | }
60 | }
61 | },
62 | "status": {
63 | "conditions": [
64 | {
65 | "lastTransitionTime": "2019-07-16T23:18:14Z",
66 | "lastUpdateTime": "2019-07-16T23:18:14Z",
67 | "message": "Deployment does not have minimum availability.",
68 | "reason": "MinimumReplicasUnavailable",
69 | "status": "False",
70 | "type": "Available"
71 | }
72 | ],
73 | "observedGeneration": 1,
74 | "replicas": 4,
75 | "unavailableReplicas": 4,
76 | "updatedReplicas": 4
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/internal/types/testdata/deploy/three-of-four.json:
--------------------------------------------------------------------------------
1 | {
2 | "apiVersion": "extensions/v1beta1",
3 | "kind": "Deployment",
4 | "metadata": {
5 | "annotations": {
6 | "deployment.kubernetes.io/revision": "1",
7 | "test/status": "3 of 4 updated replicas are available"
8 | },
9 | "creationTimestamp": "2019-07-16T23:18:14Z",
10 | "generation": 1,
11 | "labels": {
12 | "run": "nginx"
13 | },
14 | "name": "nginx",
15 | "namespace": "default",
16 | "resourceVersion": "146445126",
17 | "selfLink": "/apis/extensions/v1beta1/namespaces/default/deployments/nginx",
18 | "uid": "fc9c5c4f-a81f-11e9-9cb0-02b049f8a858"
19 | },
20 | "spec": {
21 | "progressDeadlineSeconds": 2147483647,
22 | "replicas": 4,
23 | "revisionHistoryLimit": 10,
24 | "selector": {
25 | "matchLabels": {
26 | "run": "nginx"
27 | }
28 | },
29 | "strategy": {
30 | "rollingUpdate": {
31 | "maxSurge": "25%",
32 | "maxUnavailable": "25%"
33 | },
34 | "type": "RollingUpdate"
35 | },
36 | "template": {
37 | "metadata": {
38 | "creationTimestamp": null,
39 | "labels": {
40 | "run": "nginx"
41 | }
42 | },
43 | "spec": {
44 | "containers": [
45 | {
46 | "image": "nginx",
47 | "imagePullPolicy": "Always",
48 | "name": "nginx",
49 | "resources": {},
50 | "terminationMessagePath": "/dev/termination-log",
51 | "terminationMessagePolicy": "File"
52 | }
53 | ],
54 | "dnsPolicy": "ClusterFirst",
55 | "restartPolicy": "Always",
56 | "schedulerName": "default-scheduler",
57 | "securityContext": {},
58 | "terminationGracePeriodSeconds": 30
59 | }
60 | }
61 | },
62 | "status": {
63 | "availableReplicas": 3,
64 | "conditions": [
65 | {
66 | "lastTransitionTime": "2019-07-16T23:18:20Z",
67 | "lastUpdateTime": "2019-07-16T23:18:20Z",
68 | "message": "Deployment has minimum availability.",
69 | "reason": "MinimumReplicasAvailable",
70 | "status": "True",
71 | "type": "Available"
72 | }
73 | ],
74 | "observedGeneration": 1,
75 | "readyReplicas": 3,
76 | "replicas": 4,
77 | "unavailableReplicas": 1,
78 | "updatedReplicas": 4
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/site/content/userguide/usage/tips-and-tricks.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Tips and tricks
3 | weight: 40
4 | ---
5 |
6 | ## Runtime
7 |
8 | * qbec is written to have good performance even when dealing with hundreds of objects. That said,
9 | this is wholly dependent on how long a basic command like `qbec show` takes to execute. Most of the
10 | time taken by `qbec show` is in component evaluation, which in turn is dependent on the performance
11 | of jsonnet libraries that your components use. A good rule of thumb is that you will have an
12 | enjoyable experience with qbec if `qbec show` executes in less than a second or two and a poorer
13 | experience otherwise.
14 |
15 | * Organizing runtime parameters in the recommended manner will let you use the `param` subcommands
16 | effectively. In addition, restricting parameter values to simple scalar values, short arrays
17 | of scalar values or small, shallow objects will provide better listing and diffs.
18 |
19 | ## Development
20 |
21 | * If you typically work with just one qbec app, set the `QBEC_ROOT` environment variable to the app
22 | directory so that qbec works from any working directory.
23 |
24 | * Use the component and kind filters to restrict the scope of objects that are acted upon so that you
25 | see just the information you care about at any time.
26 |
27 | * Set up your development environment (e.g. the VSCode IDE) with a jsonnet executable that can
28 | parse YAML (qbec ships with the `jsonnet-qbec` command that provides all the native extensions it
29 | installs). Configure the `qbec.io/env` extension variable to a valid environment. With this in place,
30 | the IDE will be able to show you errors early during development, without even having to run any
31 | of the qbec commands.
32 |
33 | * Use the `--clean` option of the `show` command so you can see object contents without the additional qbec metadata.
34 |
35 | * Declare a post-processor to add common metadata to all objects.
36 |
37 | ## Continuous Integration
38 |
39 | * Set the `QBEC_YES` environment variable to `true` so that all qbec prompts are disabled.
40 |
41 | * Use the `--wait` option of the `apply` command so that qbec waits for deployments to fully roll out. Your subsequent
42 | functional tests can then rely on the rollout to be complete before they start executing. This ensures that your
43 | pods under test are ready and are of the desired version.
44 |
45 |
46 |
--------------------------------------------------------------------------------
/vm/internal/ds/factory/datasource.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 Splunk Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | // Package factory provides a mechanism to create data sources from URLs with custom schemes.
16 | package factory
17 |
18 | import (
19 | "fmt"
20 | "net/url"
21 |
22 | "github.com/pkg/errors"
23 | "github.com/splunk/qbec/vm/internal/ds"
24 | "github.com/splunk/qbec/vm/internal/ds/exec"
25 | "github.com/splunk/qbec/vm/internal/ds/helm3"
26 | )
27 |
28 | // Create creates a new data source from the supplied URL.
29 | // Such a URL has a scheme that is the type of supported data source,
30 | // a hostname that is the name that it should be referred to in user code,
31 | // and a query param called configVar which supplies the data source config.
32 | func Create(u string) (ds.DataSourceWithLifecycle, error) {
33 | parsed, err := url.Parse(u)
34 | if err != nil {
35 | return nil, errors.Wrapf(err, "parse URL '%s'", u)
36 | }
37 | scheme := parsed.Scheme
38 | switch scheme {
39 | case exec.Scheme:
40 | case helm3.Scheme:
41 | default:
42 | return nil, fmt.Errorf("data source URL '%s', unsupported scheme '%s'", u, scheme)
43 | }
44 | name := parsed.Host
45 | if name == "" {
46 | if parsed.Opaque != "" {
47 | return nil, fmt.Errorf("data source '%s' does not have a name (did you forget the '//' after the ':'?)", u)
48 | }
49 | return nil, fmt.Errorf("data source '%s' does not have a name", u)
50 | }
51 | varName := parsed.Query().Get("configVar")
52 | if varName == "" {
53 | return nil, fmt.Errorf("data source '%s' must have a configVar param", u)
54 | }
55 | switch scheme {
56 | case exec.Scheme:
57 | return makeLazy(exec.New(name, varName)), nil
58 | case helm3.Scheme:
59 | return makeLazy(helm3.New(name, varName)), nil
60 | default:
61 | return nil, fmt.Errorf("internal error: unable to create a data source for %s", u)
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/site/content/userguide/usage/basic.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Folders, files, parameters
3 | weight: 1
4 | ---
5 |
6 | At the very least, you need to have the following files and folders for a qbec app:
7 |
8 | * `qbec.yaml` - this needs to be at the root of the source directory and defines your application
9 | in terms of:
10 | * supported environments
11 | * components that should be excluded by default for all environments
12 | * specific components excluded and included in specific environments.
13 | * See the [reference document](../../../reference/qbec-yaml/) for more details.
14 | * a folder for components. By default, this is a folder called `components/` under the source root.
15 | You can change what this folder is called in `qbec.yaml`
16 | * a runtime configuration file that can return runtime parameters based on the value of the
17 | `qbec.io/env` jsonnet variable. By convention, this is a file called `params.libsonnet` under
18 | the source root. You can change this name in `qbec.yaml`.
19 |
20 | ## More on runtime configuration
21 |
22 | The runtime configuration file is only strictly used by qbec for the `param` sub-commands. The remaining
23 | commands do not make assumptions about how runtime parameters are returned. All they do is set the
24 | `qbec.io/env` external variable to the environment name and expect your code to correctly configure
25 | your components based on its value.
26 |
27 | That said, if you follow expected conventions you can use qbec to its fullest.
28 |
29 | ### The basic parameters object
30 |
31 | The parameters object is expected to be of the following form:
32 |
33 | ```json
34 | {
35 | "components": {
36 | "component1": {
37 | "param1": "value1",
38 | "param2": "value2"
39 | },
40 | "component2": {
41 | "param1": "value1"
42 | }
43 | }
44 | }
45 | ```
46 |
47 | That is, it has a top-level `components` key and the configuration values for each component under
48 | a key that is the component name.
49 |
50 | The top-level parameters file (typically, `params.libsonnet`) returns an instance of the above object
51 | that is different for every environment. Note that it needs to be able to handle the baseline
52 | environment (_) as well.
53 |
54 | The `qbec init` command shows you one way to organize your files such that all the above conditions are
55 | met and every environment produces a parameters object that is a specialization of the baseline
56 | configuration.
--------------------------------------------------------------------------------
/internal/types/testdata/deploy/success.json:
--------------------------------------------------------------------------------
1 | {
2 | "apiVersion": "extensions/v1beta1",
3 | "kind": "Deployment",
4 | "metadata": {
5 | "annotations": {
6 | "deployment.kubernetes.io/revision": "1",
7 | "test/status": "successfully rolled out",
8 | "test/done": "true"
9 | },
10 | "creationTimestamp": "2019-07-16T23:18:14Z",
11 | "generation": 1,
12 | "labels": {
13 | "run": "nginx"
14 | },
15 | "name": "nginx",
16 | "namespace": "default",
17 | "resourceVersion": "146445169",
18 | "selfLink": "/apis/extensions/v1beta1/namespaces/default/deployments/nginx",
19 | "uid": "fc9c5c4f-a81f-11e9-9cb0-02b049f8a858"
20 | },
21 | "spec": {
22 | "progressDeadlineSeconds": 2147483647,
23 | "replicas": 4,
24 | "revisionHistoryLimit": 10,
25 | "selector": {
26 | "matchLabels": {
27 | "run": "nginx"
28 | }
29 | },
30 | "strategy": {
31 | "rollingUpdate": {
32 | "maxSurge": "25%",
33 | "maxUnavailable": "25%"
34 | },
35 | "type": "RollingUpdate"
36 | },
37 | "template": {
38 | "metadata": {
39 | "annotations": {
40 | "sidecar.istio.io/inject": "false"
41 | },
42 | "creationTimestamp": null,
43 | "labels": {
44 | "run": "nginx"
45 | }
46 | },
47 | "spec": {
48 | "containers": [
49 | {
50 | "image": "nginx",
51 | "imagePullPolicy": "Always",
52 | "name": "nginx",
53 | "resources": {},
54 | "terminationMessagePath": "/dev/termination-log",
55 | "terminationMessagePolicy": "File"
56 | }
57 | ],
58 | "dnsPolicy": "ClusterFirst",
59 | "restartPolicy": "Always",
60 | "schedulerName": "default-scheduler",
61 | "securityContext": {},
62 | "terminationGracePeriodSeconds": 30
63 | }
64 | }
65 | },
66 | "status": {
67 | "availableReplicas": 4,
68 | "conditions": [
69 | {
70 | "lastTransitionTime": "2019-07-16T23:18:20Z",
71 | "lastUpdateTime": "2019-07-16T23:18:20Z",
72 | "message": "Deployment has minimum availability.",
73 | "reason": "MinimumReplicasAvailable",
74 | "status": "True",
75 | "type": "Available"
76 | }
77 | ],
78 | "observedGeneration": 1,
79 | "readyReplicas": 4,
80 | "replicas": 4,
81 | "updatedReplicas": 4
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/internal/cmd/lifecycle.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 Splunk Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package cmd
16 |
17 | import (
18 | "io"
19 | "os"
20 | "os/signal"
21 | "sync"
22 | "syscall"
23 | "time"
24 |
25 | "github.com/splunk/qbec/internal/sio"
26 | )
27 |
28 | type closers struct {
29 | l sync.Mutex
30 | closers []io.Closer
31 | }
32 |
33 | func (c *closers) add(closer io.Closer) {
34 | c.l.Lock()
35 | defer c.l.Unlock()
36 | c.closers = append(c.closers, closer)
37 | }
38 |
39 | func (c *closers) close() error {
40 | c.l.Lock()
41 | defer c.l.Unlock()
42 | var lastError error
43 | for _, c := range c.closers {
44 | err := c.Close()
45 | if err != nil {
46 | lastError = err
47 | }
48 | }
49 | return lastError // XXX: return a multi-error later
50 | }
51 |
52 | var cleanup = &closers{}
53 |
54 | // RegisterCleanupTask registers an io.Closer for eventual cleanup.
55 | func RegisterCleanupTask(closer io.Closer) {
56 | cleanup.add(closer)
57 | }
58 |
59 | // Close closes all the closers for the process and returns an error
60 | // if any of the closers return an error.
61 | func Close() error {
62 | return cleanup.close()
63 | }
64 |
65 | // RegisterSignalHandlers registers signal handlers for resource cleanup.
66 | func RegisterSignalHandlers() {
67 | ch := make(chan os.Signal, 5)
68 | signal.Notify(ch, os.Interrupt, syscall.SIGTERM)
69 | go func() {
70 | <-ch
71 | var err error
72 | done := make(chan struct{})
73 | go func() {
74 | defer close(done)
75 | err = Close()
76 | }()
77 | gracePeriod := 3 * time.Second
78 | sio.Println()
79 | for {
80 | select {
81 | case <-time.After(gracePeriod):
82 | sio.Errorln("cleanup took too long, exit")
83 | os.Exit(1)
84 | case <-done:
85 | if err != nil {
86 | sio.Errorln(err)
87 | } else {
88 | sio.Errorln("interrupted")
89 | }
90 | os.Exit(1)
91 | }
92 | }
93 | }()
94 | }
95 |
--------------------------------------------------------------------------------
/internal/remote/pool.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 Splunk Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package remote
16 |
17 | import (
18 | "sync"
19 |
20 | "k8s.io/apimachinery/pkg/runtime/schema"
21 | "k8s.io/client-go/dynamic"
22 | restclient "k8s.io/client-go/rest"
23 | )
24 |
25 | // clientPoolImpl implements resourceClient and caches clients for the resource group versions
26 | // is asked to retrieve. This type is thread safe.
27 | type clientPoolImpl struct {
28 | lock sync.RWMutex
29 | config *restclient.Config
30 | clients map[schema.GroupVersion]dynamic.Interface
31 | apiPathResolverFunc dynamic.APIPathResolverFunc
32 | }
33 |
34 | // newResourceClient instantiates a new dynamic client pool with the given config.
35 | func newResourceClient(cfg *restclient.Config) resourceClient {
36 | confCopy := *cfg
37 | return &clientPoolImpl{
38 | config: &confCopy,
39 | clients: map[schema.GroupVersion]dynamic.Interface{},
40 | apiPathResolverFunc: dynamic.LegacyAPIPathResolverFunc,
41 | }
42 | }
43 |
44 | // ClientForGroupVersion returns a client for the specified groupVersion, creates one if none exists. Kind
45 | // in the GroupVersionKind may be empty.
46 | func (c *clientPoolImpl) clientForGroupVersionKind(kind schema.GroupVersionKind) (dynamic.Interface, error) {
47 | c.lock.Lock()
48 | defer c.lock.Unlock()
49 |
50 | gv := kind.GroupVersion()
51 | // do we have a client already configured?
52 | if existingClient, found := c.clients[gv]; found {
53 | return existingClient, nil
54 | }
55 |
56 | // avoid changing the original config
57 | confCopy := *c.config
58 | conf := &confCopy
59 | conf.APIPath = c.apiPathResolverFunc(kind)
60 | conf.GroupVersion = &gv
61 |
62 | dynamicClient, err := dynamic.NewForConfig(conf)
63 | if err != nil {
64 | return nil, err
65 | }
66 | c.clients[gv] = dynamicClient
67 | return dynamicClient, nil
68 | }
69 |
--------------------------------------------------------------------------------
/examples/test-app/components/cluster-objects.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: rbac.authorization.k8s.io/v1beta1
2 | kind: ClusterRole
3 | metadata:
4 | name: allow-root-psp-policy
5 | rules:
6 | - apiGroups:
7 | - extensions
8 | resourceNames:
9 | - 200-allow-root
10 | resources:
11 | - podsecuritypolicies
12 | verbs:
13 | - use
14 | ---
15 | apiVersion: rbac.authorization.k8s.io/v1beta1
16 | kind: ClusterRoleBinding
17 | metadata:
18 | name: allow-root-psp-policy
19 | roleRef:
20 | apiGroup: rbac.authorization.k8s.io
21 | kind: ClusterRole
22 | name: allow-root-psp-policy
23 | subjects:
24 | - apiGroup: rbac.authorization.k8s.io
25 | kind: Group
26 | name: system:authenticated
27 | ---
28 | apiVersion: rbac.authorization.k8s.io/v1beta1
29 | kind: ClusterRoleBinding
30 | metadata:
31 | name: default-psp-policy
32 | roleRef:
33 | apiGroup: rbac.authorization.k8s.io
34 | kind: ClusterRole
35 | name: default-psp-policy
36 | subjects:
37 | - apiGroup: rbac.authorization.k8s.io
38 | kind: Group
39 | name: system:authenticated
40 | ---
41 | apiVersion: v1
42 | kind: Namespace
43 | metadata:
44 | labels:
45 | name: foo-system
46 | system: "true"
47 | annotations:
48 | name: foo-system
49 | name: foo-system
50 | ---
51 | apiVersion: v1
52 | kind: Namespace
53 | metadata:
54 | labels:
55 | name: bar-system
56 | system: "true"
57 | name: bar-system
58 | ---
59 | apiVersion: policy/v1beta1
60 | kind: PodSecurityPolicy
61 | metadata:
62 | name: 100-default
63 | spec:
64 | allowPrivilegeEscalation: false
65 | fsGroup:
66 | rule: RunAsAny
67 | hostPorts:
68 | - max: 65535
69 | min: 0
70 | runAsUser:
71 | rule: MustRunAsNonRoot
72 | seLinux:
73 | rule: RunAsAny
74 | supplementalGroups:
75 | rule: RunAsAny
76 | volumes:
77 | - configMap
78 | - downwardAPI
79 | - emptyDir
80 | - persistentVolumeClaim
81 | - secret
82 | - projected
83 | ---
84 | apiVersion: policy/v1beta1
85 | kind: PodSecurityPolicy
86 | metadata:
87 | name: 200-allow-root
88 | spec:
89 | allowPrivilegeEscalation: true
90 | fsGroup:
91 | rule: RunAsAny
92 | hostPorts:
93 | - max: 65535
94 | min: 0
95 | runAsUser:
96 | rule: RunAsAny
97 | seLinux:
98 | rule: RunAsAny
99 | supplementalGroups:
100 | rule: RunAsAny
101 | volumes:
102 | - configMap
103 | - downwardAPI
104 | - emptyDir
105 | - persistentVolumeClaim
106 | - secret
107 | - projected
108 |
--------------------------------------------------------------------------------