├── 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 | 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 | ![qbec](site/static/images/qbec-logo-black.svg) 2 | 3 | [![Github build status](https://github.com/splunk/qbec/workflows/build/badge.svg)](https://github.com/splunk/qbec/actions) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/splunk/qbec)](https://goreportcard.com/report/github.com/splunk/qbec) 5 | [![codecov](https://codecov.io/gh/splunk/qbec/branch/main/graph/badge.svg)](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 | # ![qbec](/images/qbec-logo-black.svg) 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 | --------------------------------------------------------------------------------