diff --git a/.github/workflows/gotest.yaml b/.github/workflows/gotest.yaml new file mode 100644 index 0000000..c3fe7cb --- /dev/null +++ b/.github/workflows/gotest.yaml @@ -0,0 +1,59 @@ +name: Devfile Go integration tests + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + schedule: + # every day at 9am EST + - cron: 0 1 * * * +jobs: + + build: + name: Run Tests + strategy: + matrix: + os: [ ubuntu-latest, macos-10.15 ] + runs-on: ${{ matrix.os }} + continue-on-error: true + timeout-minutes: 20 + + steps: + + - name: Setup Go environment + uses: actions/setup-go@v2.1.3 + with: + go-version: 1.15 + id: go + + - name: Check out code into the Go module directory + uses: actions/checkout@v2 + + - name: Check go mod status + run: | + make gomod_tidy + if [[ ! -z $(git status -s) ]] + then + echo "Go mod state is not clean" + git diff "$GITHUB_SHA" + exit 1 + fi + + - name: Check format + run: | + make gofmt + if [[ ! -z $(git status -s) ]] + then + echo "not well formatted sources are found : $(git status -s)" + exit 1 + fi + + - name: Run Go Tests + run: make test + +# - name: Upload Test Coverage results +# uses: actions/upload-artifact@v2 +# with: +# name: lib-test-coverage-html +# path: tests/v2/lib-test-coverage.html \ No newline at end of file diff --git a/.gitignore b/.gitignore index 2ffd1d0..8401ddf 100644 --- a/.gitignore +++ b/.gitignore @@ -141,3 +141,5 @@ dmypy.json # Pyre type checker .pyre/ +# Test temp directory +tests/v2/integrationTest/tmp/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..666c21a --- /dev/null +++ b/Makefile @@ -0,0 +1,29 @@ +FILES := main + +default: bin + +.PHONY: all +all: gomod_tidy gofmt test + +.PHONY: gomod_tidy +gomod_tidy: + go mod tidy + +.PHONY: gofmt +gofmt: + go fmt -x ./... + +.PHONY: bin +bin: + go build *.go + +.PHONY: test +test: + go test -v ./tests/v2/integrationTest +# go test -coverprofile tests/v2/lib-test-coverage.out -v ./... +# go tool cover -html=tests/v2/lib-test-coverage.out -o tests/v2/lib-test-coverage.html + +.PHONY: clean +clean: + @rm -rf $(FILES) + diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..0d6c216 --- /dev/null +++ b/go.mod @@ -0,0 +1,36 @@ +module github.com/devfile/library + +go 1.15 + +require ( + github.com/devfile/api/v2 v2.0.0-20220309195345-48ebbf1e51cf + github.com/fatih/color v1.7.0 + github.com/fsnotify/fsnotify v1.4.9 + github.com/gobwas/glob v0.2.3 + github.com/golang/mock v1.5.0 + github.com/google/go-cmp v0.5.5 + github.com/google/go-github v17.0.0+incompatible // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 + github.com/hashicorp/go-multierror v1.1.1 + github.com/hashicorp/go-version v1.3.0 + github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 + github.com/mattn/go-colorable v0.1.2 // indirect + github.com/mattn/go-isatty v0.0.12 // indirect + github.com/onsi/ginkgo v1.16.4 + github.com/onsi/gomega v1.14.0 + github.com/openshift/api v0.0.0-20200930075302-db52bc4ef99f + github.com/openshift/odo v1.2.6 + github.com/pkg/errors v0.9.1 + github.com/spf13/afero v1.2.2 + github.com/stretchr/testify v1.7.0 + github.com/tidwall/gjson v1.14.1 + github.com/xeipuuv/gojsonschema v1.2.0 + k8s.io/api v0.21.3 + k8s.io/apimachinery v0.21.3 + k8s.io/client-go v0.21.3 + k8s.io/klog v1.0.0 + k8s.io/utils v0.0.0-20210722164352-7f3ee0f31471 + sigs.k8s.io/controller-runtime v0.9.5 + sigs.k8s.io/yaml v1.2.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..00f325e --- /dev/null +++ b/go.sum @@ -0,0 +1,816 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest/autorest v0.11.12/go.mod h1:eipySxLmqSyC5s5k1CLupqet0PSENBEDP93LQ9a8QYw= +github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A= +github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= +github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/devfile/api/v2 v2.0.0-20220309195345-48ebbf1e51cf h1:FkwAOQtepscB5B0j++9S/eoicXj707MaP5HPIScz0sA= +github.com/devfile/api/v2 v2.0.0-20220309195345-48ebbf1e51cf/go.mod h1:kLX/nW93gigOHXK3NLeJL2fSS/sgEe+OHu8bo3aoOi4= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= +github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch v4.11.0+incompatible h1:glyUF9yIYtMHzn8xaKw5rMhdWcwsYV8dZHIq5567/xs= +github.com/evanphx/json-patch v4.11.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= +github.com/go-logr/logr v0.4.0 h1:K7/B1jt6fIBQVd4Owv2MqGQClcgf0R266+7C/QjRcLc= +github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= +github.com/go-logr/zapr v0.4.0 h1:uc1uML3hRYL9/ZZPdgHS/n8Nzo+eaYL/Efxkkamf7OM= +github.com/go-logr/zapr v0.4.0/go.mod h1:tabnROwaDl0UNxkVeFRbY8bwB37GwRv0P8lg6aAiEnk= +github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= +github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= +github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= +github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= +github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= +github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= +github.com/go-openapi/spec v0.19.5/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHKHb3gWUSk= +github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= +github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.5.0 h1:jlYHihg//f7RRwuPfptm04yp4s7O6Kw8EZiVYIGcH0g= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= +github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= +github.com/googleapis/gnostic v0.5.5 h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw= +github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.3.0 h1:McDWVJIU/y+u1BRV06dPaLfLCaT7fUTJLp5r04x7iNw= +github.com/hashicorp/go-version v1.3.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= +github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4= +github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= +github.com/lucasjones/reggen v0.0.0-20200904144131-37ba4fa293bb h1:w1g9wNDIE/pHSTmAaUhv4TZQuPBS6GV3mMz5hkgziIU= +github.com/lucasjones/reggen v0.0.0-20200904144131-37ba4fa293bb/go.mod h1:5ELEyG+X8f+meRWHuqUOewBOhvHkl7M76pdGEansxW4= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= +github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/reflectwalk v1.0.1 h1:FVzMWA5RllMAKIdUSC8mdWo3XtwoecrH79BY70sEEpE= +github.com/mitchellh/reflectwalk v1.0.1/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= +github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.14.0 h1:ep6kpPVwmr/nTbklSx2nrLNSIO62DoYAhnPNIMhK8gI= +github.com/onsi/gomega v1.14.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0= +github.com/openshift/api v0.0.0-20200930075302-db52bc4ef99f h1:/msM59v15x4DaAZeJnQwkVsCGTEa1mx+nSSMehZVAHs= +github.com/openshift/api v0.0.0-20200930075302-db52bc4ef99f/go.mod h1:Si/I9UGeRR3qzg01YWPmtlr0GeGk2fnuggXJRmjAZ6U= +github.com/openshift/build-machinery-go v0.0.0-20200819073603-48aa266c95f7/go.mod h1:b1BuldmJlbA/xYtdZvKi+7j5YGB44qJUJDZ9zwiNCfE= +github.com/openshift/odo v1.2.6 h1:FZcRwki2pNlUL6Ix7H/NHsv3kI/pwiyvxMrvm1c/8WY= +github.com/openshift/odo v1.2.6/go.mod h1:anQKj1V6PX2I8rxf02DHr+vmtcBPPoWr3kS6BzQQ48M= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ= +github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0 h1:iMAkS2TDoNWnKM+Kopnx/8tnEStIfpYA0ur0xQzzhMQ= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/santhosh-tekuri/jsonschema v1.2.4/go.mod h1:TEAUOeZSmIxTTuHatJzrvARHiuO9LYd+cIxzgEHCQI4= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/tidwall/gjson v1.14.1 h1:iymTbGkQBhveq21bEvAQ81I0LEBork8BFe1CUZXdyuo= +github.com/tidwall/gjson v1.14.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= +go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0= +go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.18.1 h1:CSUJ2mjFszzEWt4CdKISEuChVIXGBn3lAPwkRGyVrc4= +go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449 h1:xUIPaMhvROX9dhPvRCenIJtU78+lbEenGbgqB5hfHCQ= +golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210224082022-3d97a244fca7/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE= +golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs= +golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200616133436-c1934b75d054/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gomodules.xyz/jsonpatch/v2 v2.2.0 h1:4pT439QV83L+G9FkcCriY6EkpcK6r6bK+A5FBUMI7qY= +gomodules.xyz/jsonpatch/v2 v2.2.0/go.mod h1:WXp+iVDkoLQqPudfQ9GBlwB2eZ5DKOnjQZCYdOS8GPY= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= +gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +k8s.io/api v0.19.0/go.mod h1:I1K45XlvTrDjmj5LoM5LuP/KYrhWbjUKT/SoPG0qTjw= +k8s.io/api v0.21.3 h1:cblWILbLO8ar+Fj6xdDGr603HRsf8Wu9E9rngJeprZQ= +k8s.io/api v0.21.3/go.mod h1:hUgeYHUbBp23Ue4qdX9tR8/ANi/g3ehylAqDn9NWVOg= +k8s.io/apiextensions-apiserver v0.21.3 h1:+B6biyUWpqt41kz5x6peIsljlsuwvNAp/oFax/j2/aY= +k8s.io/apiextensions-apiserver v0.21.3/go.mod h1:kl6dap3Gd45+21Jnh6utCx8Z2xxLm8LGDkprcd+KbsE= +k8s.io/apimachinery v0.19.0/go.mod h1:DnPGDnARWFvYa3pMHgSxtbZb7gpzzAZ1pTfaUNDVlmA= +k8s.io/apimachinery v0.21.3 h1:3Ju4nvjCngxxMYby0BimUk+pQHPOQp3eCGChk5kfVII= +k8s.io/apimachinery v0.21.3/go.mod h1:H/IM+5vH9kZRNJ4l3x/fXP/5bOPJaVP/guptnZPeCFI= +k8s.io/apiserver v0.21.3/go.mod h1:eDPWlZG6/cCCMj/JBcEpDoK+I+6i3r9GsChYBHSbAzU= +k8s.io/client-go v0.21.3 h1:J9nxZTOmvkInRDCzcSNQmPJbDYN/PjlxXT9Mos3HcLg= +k8s.io/client-go v0.21.3/go.mod h1:+VPhCgTsaFmGILxR/7E1N0S+ryO010QBeNCv5JwRGYU= +k8s.io/code-generator v0.19.0/go.mod h1:moqLn7w0t9cMs4+5CQyxnfA/HV8MF6aAVENF+WZZhgk= +k8s.io/code-generator v0.21.3/go.mod h1:K3y0Bv9Cz2cOW2vXUrNZlFbflhuPvuadW6JdnN6gGKo= +k8s.io/component-base v0.21.3 h1:4WuuXY3Npa+iFfi2aDRiOz+anhNvRfye0859ZgfC5Og= +k8s.io/component-base v0.21.3/go.mod h1:kkuhtfEHeZM6LkX0saqSK8PbdO7A0HigUngmhhrwfGQ= +k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/gengo v0.0.0-20200428234225-8167cfdcfc14/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/gengo v0.0.0-20201214224949-b6c5ce23f027/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= +k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= +k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= +k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= +k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= +k8s.io/klog/v2 v2.8.0 h1:Q3gmuM9hKEjefWFFYF0Mat+YyFJvsUyYuwyNNJ5C9Ts= +k8s.io/klog/v2 v2.8.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= +k8s.io/kube-openapi v0.0.0-20200805222855-6aeccd4b50c6/go.mod h1:UuqjUnNftUyPE5H64/qeyjQoUZhGpeFDVdxjTeEVN2o= +k8s.io/kube-openapi v0.0.0-20210305001622-591a79e4bda7 h1:vEx13qjvaZ4yfObSSXW7BrMc/KQBBT/Jyee8XtLf4x0= +k8s.io/kube-openapi v0.0.0-20210305001622-591a79e4bda7/go.mod h1:wXW5VT87nVfh/iLV8FpR2uDvrFyomxbtb1KivDbvPTE= +k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +k8s.io/utils v0.0.0-20210722164352-7f3ee0f31471 h1:DnzUXII7sVg1FJ/4JX6YDRJfLNAC7idRatPwe07suiI= +k8s.io/utils v0.0.0-20210722164352-7f3ee0f31471/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.19/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= +sigs.k8s.io/controller-runtime v0.9.5 h1:WThcFE6cqctTn2jCZprLICO6BaKZfhsT37uAapTNfxc= +sigs.k8s.io/controller-runtime v0.9.5/go.mod h1:q6PpkM5vqQubEKUKOM6qr06oXGzOBcCby1DA9FbyZeA= +sigs.k8s.io/structured-merge-diff/v4 v4.0.1/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= +sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= +sigs.k8s.io/structured-merge-diff/v4 v4.1.2 h1:Hr/htKFmJEbtMgS/UD0N+gtgctAqz81t3nu+sPzynno= +sigs.k8s.io/structured-merge-diff/v4 v4.1.2/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= +sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/local/odo/tests/integration/devfile/cmd_devfile_create_test.go b/local/odo/tests/integration/devfile/cmd_devfile_create_test.go index 2529513..e2aba17 100644 --- a/local/odo/tests/integration/devfile/cmd_devfile_create_test.go +++ b/local/odo/tests/integration/devfile/cmd_devfile_create_test.go @@ -50,8 +50,8 @@ var _ = Describe("odo devfile create command tests", func() { }) Measure("should successfully create the devfile component with valid component name", func(b Benchmarker) { - runtime := b.Time("========== Command: odo create java-openliberty " + - cmpName + " ==========", func() { + runtime := b.Time("========== Command: odo create java-openliberty "+ + cmpName+" ==========", func() { helper.Cmd("odo", "create", "java-openliberty", cmpName).ShouldPass() }) b.RecordValueWithPrecision("========== Execution time in ms ==========", float64(runtime.Milliseconds()), "ms", 2) diff --git a/pkg/devfile/generator/generators.go b/pkg/devfile/generator/generators.go new file mode 100644 index 0000000..7ceafc1 --- /dev/null +++ b/pkg/devfile/generator/generators.go @@ -0,0 +1,448 @@ +package generator + +import ( + "fmt" + v1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + "github.com/devfile/library/pkg/devfile/parser" + "github.com/devfile/library/pkg/devfile/parser/data/v2/common" + "github.com/devfile/library/pkg/util" + buildv1 "github.com/openshift/api/build/v1" + imagev1 "github.com/openshift/api/image/v1" + routev1 "github.com/openshift/api/route/v1" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + extensionsv1 "k8s.io/api/extensions/v1beta1" + networkingv1 "k8s.io/api/networking/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + // DevfileSourceVolumeMount is the default directory to mount the volume in the container + DevfileSourceVolumeMount = "/projects" + + // EnvProjectsRoot is the env defined for project mount in a component container when component's mountSources=true + EnvProjectsRoot = "PROJECTS_ROOT" + + // EnvProjectsSrc is the env defined for path to the project source in a component container + EnvProjectsSrc = "PROJECT_SOURCE" + + deploymentKind = "Deployment" + deploymentAPIVersion = "apps/v1" + + containerNameMaxLen = 55 +) + +// GetTypeMeta gets a type meta of the specified kind and version +func GetTypeMeta(kind string, APIVersion string) metav1.TypeMeta { + return metav1.TypeMeta{ + Kind: kind, + APIVersion: APIVersion, + } +} + +// GetObjectMeta gets an object meta with the parameters +func GetObjectMeta(name, namespace string, labels, annotations map[string]string) metav1.ObjectMeta { + + objectMeta := metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Labels: labels, + Annotations: annotations, + } + + return objectMeta +} + +// GetContainers iterates through all container components, filters out init containers and returns corresponding containers +func GetContainers(devfileObj parser.DevfileObj, options common.DevfileOptions) ([]corev1.Container, error) { + allContainers, err := getAllContainers(devfileObj, options) + if err != nil { + return nil, err + } + + // filter out containers for preStart and postStop events + preStartEvents := devfileObj.Data.GetEvents().PreStart + postStopEvents := devfileObj.Data.GetEvents().PostStop + if len(preStartEvents) > 0 || len(postStopEvents) > 0 { + var eventCommands []string + commands, err := devfileObj.Data.GetCommands(common.DevfileOptions{}) + if err != nil { + return nil, err + } + + commandsMap := common.GetCommandsMap(commands) + + for _, event := range preStartEvents { + eventSubCommands := common.GetCommandsFromEvent(commandsMap, event) + eventCommands = append(eventCommands, eventSubCommands...) + } + + for _, event := range postStopEvents { + eventSubCommands := common.GetCommandsFromEvent(commandsMap, event) + eventCommands = append(eventCommands, eventSubCommands...) + } + + for _, commandName := range eventCommands { + command, _ := commandsMap[commandName] + component := common.GetApplyComponent(command) + + // Get the container info for the given component + for i, container := range allContainers { + if container.Name == component { + allContainers = append(allContainers[:i], allContainers[i+1:]...) + } + } + } + } + + return allContainers, nil + +} + +// GetInitContainers gets the init container for every preStart devfile event +func GetInitContainers(devfileObj parser.DevfileObj) ([]corev1.Container, error) { + containers, err := getAllContainers(devfileObj, common.DevfileOptions{}) + if err != nil { + return nil, err + } + preStartEvents := devfileObj.Data.GetEvents().PreStart + var initContainers []corev1.Container + if len(preStartEvents) > 0 { + var eventCommands []string + commands, err := devfileObj.Data.GetCommands(common.DevfileOptions{}) + if err != nil { + return nil, err + } + + commandsMap := common.GetCommandsMap(commands) + + for _, event := range preStartEvents { + eventSubCommands := common.GetCommandsFromEvent(commandsMap, event) + eventCommands = append(eventCommands, eventSubCommands...) + } + + for i, commandName := range eventCommands { + command, _ := commandsMap[commandName] + component := common.GetApplyComponent(command) + + // Get the container info for the given component + for _, container := range containers { + if container.Name == component { + // Override the init container name since there cannot be two containers with the same + // name in a pod. This applies to pod containers and pod init containers. The convention + // for init container name here is, containername-eventname- + // If there are two events referencing the same devfile component, then we will have + // tools-event1-1 & tools-event2-3, for example. And if in the edge case, the same command is + // executed twice by preStart events, then we will have tools-event1-1 & tools-event1-2 + initContainerName := fmt.Sprintf("%s-%s", container.Name, commandName) + initContainerName = util.TruncateString(initContainerName, containerNameMaxLen) + initContainerName = fmt.Sprintf("%s-%d", initContainerName, i+1) + container.Name = initContainerName + + initContainers = append(initContainers, container) + } + } + } + } + + return initContainers, nil +} + +// DeploymentParams is a struct that contains the required data to create a deployment object +type DeploymentParams struct { + TypeMeta metav1.TypeMeta + ObjectMeta metav1.ObjectMeta + InitContainers []corev1.Container + Containers []corev1.Container + Volumes []corev1.Volume + PodSelectorLabels map[string]string + Replicas *int32 +} + +// GetDeployment gets a deployment object +func GetDeployment(devfileObj parser.DevfileObj, deployParams DeploymentParams) (*appsv1.Deployment, error) { + + podTemplateSpecParams := podTemplateSpecParams{ + ObjectMeta: deployParams.ObjectMeta, + InitContainers: deployParams.InitContainers, + Containers: deployParams.Containers, + Volumes: deployParams.Volumes, + } + + deploySpecParams := deploymentSpecParams{ + PodTemplateSpec: *getPodTemplateSpec(podTemplateSpecParams), + PodSelectorLabels: deployParams.PodSelectorLabels, + Replicas: deployParams.Replicas, + } + + containerAnnotations, err := getContainerAnnotations(devfileObj, common.DevfileOptions{}) + if err != nil { + return nil, err + } + deployParams.ObjectMeta.Annotations = mergeMaps(deployParams.ObjectMeta.Annotations, containerAnnotations.Deployment) + + deployment := &appsv1.Deployment{ + TypeMeta: deployParams.TypeMeta, + ObjectMeta: deployParams.ObjectMeta, + Spec: *getDeploymentSpec(deploySpecParams), + } + + return deployment, nil +} + +// PVCParams is a struct to create PVC +type PVCParams struct { + TypeMeta metav1.TypeMeta + ObjectMeta metav1.ObjectMeta + Quantity resource.Quantity +} + +// GetPVC returns a PVC +func GetPVC(pvcParams PVCParams) *corev1.PersistentVolumeClaim { + pvcSpec := getPVCSpec(pvcParams.Quantity) + + pvc := &corev1.PersistentVolumeClaim{ + TypeMeta: pvcParams.TypeMeta, + ObjectMeta: pvcParams.ObjectMeta, + Spec: *pvcSpec, + } + + return pvc +} + +// ServiceParams is a struct that contains the required data to create a service object +type ServiceParams struct { + TypeMeta metav1.TypeMeta + ObjectMeta metav1.ObjectMeta + SelectorLabels map[string]string +} + +// GetService gets the service +func GetService(devfileObj parser.DevfileObj, serviceParams ServiceParams, options common.DevfileOptions) (*corev1.Service, error) { + + serviceSpec, err := getServiceSpec(devfileObj, serviceParams.SelectorLabels, options) + if err != nil { + return nil, err + } + containerAnnotations, err := getContainerAnnotations(devfileObj, options) + if err != nil { + return nil, err + } + serviceParams.ObjectMeta.Annotations = mergeMaps(serviceParams.ObjectMeta.Annotations, containerAnnotations.Service) + service := &corev1.Service{ + TypeMeta: serviceParams.TypeMeta, + ObjectMeta: serviceParams.ObjectMeta, + Spec: *serviceSpec, + } + + return service, nil +} + +// IngressParams is a struct that contains the required data to create an ingress object +type IngressParams struct { + TypeMeta metav1.TypeMeta + ObjectMeta metav1.ObjectMeta + IngressSpecParams IngressSpecParams +} + +// GetIngress gets an ingress +func GetIngress(endpoint v1.Endpoint, ingressParams IngressParams) *extensionsv1.Ingress { + ingressSpec := getIngressSpec(ingressParams.IngressSpecParams) + ingressParams.ObjectMeta.Annotations = mergeMaps(ingressParams.ObjectMeta.Annotations, endpoint.Annotations) + + ingress := &extensionsv1.Ingress{ + TypeMeta: ingressParams.TypeMeta, + ObjectMeta: ingressParams.ObjectMeta, + Spec: *ingressSpec, + } + + return ingress +} + +// GetNetworkingV1Ingress gets a networking v1 ingress +func GetNetworkingV1Ingress(endpoint v1.Endpoint, ingressParams IngressParams) *networkingv1.Ingress { + ingressSpec := getNetworkingV1IngressSpec(ingressParams.IngressSpecParams) + ingressParams.ObjectMeta.Annotations = mergeMaps(ingressParams.ObjectMeta.Annotations, endpoint.Annotations) + + ingress := &networkingv1.Ingress{ + TypeMeta: ingressParams.TypeMeta, + ObjectMeta: ingressParams.ObjectMeta, + Spec: *ingressSpec, + } + + return ingress +} + +// RouteParams is a struct that contains the required data to create a route object +type RouteParams struct { + TypeMeta metav1.TypeMeta + ObjectMeta metav1.ObjectMeta + RouteSpecParams RouteSpecParams +} + +// GetRoute gets a route +func GetRoute(endpoint v1.Endpoint, routeParams RouteParams) *routev1.Route { + + routeSpec := getRouteSpec(routeParams.RouteSpecParams) + routeParams.ObjectMeta.Annotations = mergeMaps(routeParams.ObjectMeta.Annotations, endpoint.Annotations) + + route := &routev1.Route{ + TypeMeta: routeParams.TypeMeta, + ObjectMeta: routeParams.ObjectMeta, + Spec: *routeSpec, + } + + return route +} + +// GetOwnerReference generates an ownerReference from the deployment which can then be set as +// owner for various Kubernetes objects and ensure that when the owner object is deleted from the +// cluster, all other objects are automatically removed by Kubernetes garbage collector +func GetOwnerReference(deployment *appsv1.Deployment) metav1.OwnerReference { + + ownerReference := metav1.OwnerReference{ + APIVersion: deploymentAPIVersion, + Kind: deploymentKind, + Name: deployment.Name, + UID: deployment.UID, + } + + return ownerReference +} + +// BuildConfigParams is a struct that contains the required data to create a build config object +type BuildConfigParams struct { + TypeMeta metav1.TypeMeta + ObjectMeta metav1.ObjectMeta + BuildConfigSpecParams BuildConfigSpecParams +} + +// GetBuildConfig gets a build config +func GetBuildConfig(buildConfigParams BuildConfigParams) *buildv1.BuildConfig { + + buildConfigSpec := getBuildConfigSpec(buildConfigParams.BuildConfigSpecParams) + + buildConfig := &buildv1.BuildConfig{ + TypeMeta: buildConfigParams.TypeMeta, + ObjectMeta: buildConfigParams.ObjectMeta, + Spec: *buildConfigSpec, + } + + return buildConfig +} + +// GetSourceBuildStrategy gets the source build strategy +func GetSourceBuildStrategy(imageName, imageNamespace string) buildv1.BuildStrategy { + return buildv1.BuildStrategy{ + SourceStrategy: &buildv1.SourceBuildStrategy{ + From: corev1.ObjectReference{ + Kind: "ImageStreamTag", + Name: imageName, + Namespace: imageNamespace, + }, + }, + } +} + +// GetDockerBuildStrategy gets the docker build strategy +func GetDockerBuildStrategy(dockerfilePath string, env []corev1.EnvVar) buildv1.BuildStrategy { + return buildv1.BuildStrategy{ + Type: buildv1.DockerBuildStrategyType, + DockerStrategy: &buildv1.DockerBuildStrategy{ + DockerfilePath: dockerfilePath, + Env: env, + }, + } +} + +// ImageStreamParams is a struct that contains the required data to create an image stream object +type ImageStreamParams struct { + TypeMeta metav1.TypeMeta + ObjectMeta metav1.ObjectMeta +} + +// GetImageStream is a function to return the image stream +func GetImageStream(imageStreamParams ImageStreamParams) imagev1.ImageStream { + imageStream := imagev1.ImageStream{ + TypeMeta: imageStreamParams.TypeMeta, + ObjectMeta: imageStreamParams.ObjectMeta, + } + return imageStream +} + +// VolumeInfo is a struct to hold the pvc name and the volume name to create a volume. +type VolumeInfo struct { + PVCName string + VolumeName string +} + +// VolumeParams is a struct that contains the required data to create Kubernetes Volumes and mount Volumes in Containers +type VolumeParams struct { + // Containers is a list of containers that needs to be updated for the volume mounts + Containers []corev1.Container + + // VolumeNameToVolumeInfo is a map of the devfile volume name to the volume info containing the pvc name and the volume name. + VolumeNameToVolumeInfo map[string]VolumeInfo +} + +// GetVolumesAndVolumeMounts gets the PVC volumes and updates the containers with the volume mounts. +func GetVolumesAndVolumeMounts(devfileObj parser.DevfileObj, volumeParams VolumeParams, options common.DevfileOptions) ([]corev1.Volume, error) { + + options.ComponentOptions = common.ComponentOptions{ + ComponentType: v1.ContainerComponentType, + } + containerComponents, err := devfileObj.Data.GetComponents(options) + if err != nil { + return nil, err + } + + options.ComponentOptions = common.ComponentOptions{ + ComponentType: v1.VolumeComponentType, + } + volumeComponent, err := devfileObj.Data.GetComponents(options) + if err != nil { + return nil, err + } + + var pvcVols []corev1.Volume + for volName, volInfo := range volumeParams.VolumeNameToVolumeInfo { + emptyDirVolume := false + for _, volumeComp := range volumeComponent { + if volumeComp.Name == volName && *volumeComp.Volume.Ephemeral { + emptyDirVolume = true + break + } + } + + // if `ephemeral=true`, a volume with emptyDir should be created + if emptyDirVolume { + pvcVols = append(pvcVols, getEmptyDirVol(volInfo.VolumeName)) + } else { + pvcVols = append(pvcVols, getPVC(volInfo.VolumeName, volInfo.PVCName)) + } + + // containerNameToMountPaths is a map of the Devfile container name to their Devfile Volume Mount Paths for a given Volume Name + containerNameToMountPaths := make(map[string][]string) + for _, containerComp := range containerComponents { + for _, volumeMount := range containerComp.Container.VolumeMounts { + if volName == volumeMount.Name { + containerNameToMountPaths[containerComp.Name] = append(containerNameToMountPaths[containerComp.Name], GetVolumeMountPath(volumeMount)) + } + } + } + + addVolumeMountToContainers(volumeParams.Containers, volInfo.VolumeName, containerNameToMountPaths) + } + return pvcVols, nil +} + +// GetVolumeMountPath gets the volume mount's path. +func GetVolumeMountPath(volumeMount v1.VolumeMount) string { + // if there is no volume mount path, default to volume mount name as per devfile schema + if volumeMount.Path == "" { + volumeMount.Path = "/" + volumeMount.Name + } + + return volumeMount.Path +} diff --git a/pkg/devfile/generator/generators_test.go b/pkg/devfile/generator/generators_test.go new file mode 100644 index 0000000..49b1b31 --- /dev/null +++ b/pkg/devfile/generator/generators_test.go @@ -0,0 +1,1149 @@ +package generator + +import ( + "fmt" + "github.com/stretchr/testify/assert" + appsv1 "k8s.io/api/apps/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/utils/pointer" + "reflect" + "strings" + "testing" + + v1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + "github.com/devfile/api/v2/pkg/attributes" + "github.com/devfile/library/pkg/devfile/parser" + "github.com/devfile/library/pkg/devfile/parser/data" + "github.com/devfile/library/pkg/devfile/parser/data/v2/common" + "github.com/devfile/library/pkg/testingutil" + "github.com/devfile/library/pkg/util" + "github.com/golang/mock/gomock" + + corev1 "k8s.io/api/core/v1" +) + +var fakeResources corev1.ResourceRequirements + +func init() { + fakeResources, _ = testingutil.FakeResourceRequirements("0.5m", "300Mi") +} + +func TestGetContainers(t *testing.T) { + + containerNames := []string{"testcontainer1", "testcontainer2", "testcontainer3"} + containerImages := []string{"image1", "image2", "image3"} + trueMountSources := true + falseMountSources := false + + projects := []v1.Project{ + { + ClonePath: "test-project/", + Name: "project0", + ProjectSource: v1.ProjectSource{ + Git: &v1.GitProjectSource{ + GitLikeProjectSource: v1.GitLikeProjectSource{ + Remotes: map[string]string{ + "origin": "repo", + }, + }, + }, + }, + }, + } + + applyCommands := []v1.Command{ + { + Id: "apply1", + CommandUnion: v1.CommandUnion{ + Apply: &v1.ApplyCommand{ + Component: containerNames[1], + }, + }, + }, + { + Id: "apply2", + CommandUnion: v1.CommandUnion{ + Apply: &v1.ApplyCommand{ + Component: containerNames[2], + }, + }, + }, + } + + errMatches := "an expected error" + + type EventCommands struct { + preStart []string + postStop []string + } + + tests := []struct { + name string + eventCommands EventCommands + containerComponents []v1.Component + filteredComponents []v1.Component + filterOptions common.DevfileOptions + wantContainerName string + wantContainerImage string + wantContainerEnv []corev1.EnvVar + wantContainerVolMount []corev1.VolumeMount + wantErr *string + }{ + { + name: "Container with default project root", + containerComponents: []v1.Component{ + { + Name: containerNames[0], + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: containerImages[0], + MountSources: &trueMountSources, + }, + }, + }, + }, + }, + wantContainerName: containerNames[0], + wantContainerImage: containerImages[0], + wantContainerEnv: []corev1.EnvVar{ + + { + Name: "PROJECTS_ROOT", + Value: "/projects", + }, + { + Name: "PROJECT_SOURCE", + Value: "/projects/test-project", + }, + }, + wantContainerVolMount: []corev1.VolumeMount{ + { + Name: "devfile-projects", + MountPath: "/projects", + }, + }, + }, + { + name: "Container with source mapping", + containerComponents: []v1.Component{ + { + Name: containerNames[0], + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: containerImages[0], + MountSources: &trueMountSources, + SourceMapping: "/myroot", + }, + }, + }, + }, + }, + wantContainerName: containerNames[0], + wantContainerImage: containerImages[0], + wantContainerEnv: []corev1.EnvVar{ + + { + Name: "PROJECTS_ROOT", + Value: "/myroot", + }, + { + Name: "PROJECT_SOURCE", + Value: "/myroot/test-project", + }, + }, + wantContainerVolMount: []corev1.VolumeMount{ + { + Name: "devfile-projects", + MountPath: "/myroot", + }, + }, + }, + { + name: "Container with no mount source", + containerComponents: []v1.Component{ + { + Name: containerNames[0], + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: containerImages[0], + MountSources: &falseMountSources, + }, + }, + }, + }, + }, + wantContainerName: containerNames[0], + wantContainerImage: containerImages[0], + }, + { + name: "Filter containers", + containerComponents: []v1.Component{ + { + Name: containerNames[0], + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: containerImages[0], + MountSources: &falseMountSources, + }, + }, + }, + }, + { + Name: containerNames[1], + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + "thirdString": "thirdStringValue", + }), + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: containerImages[0], + MountSources: &falseMountSources, + }, + }, + }, + }, + }, + wantContainerName: containerNames[1], + wantContainerImage: containerImages[0], + filteredComponents: []v1.Component{ + { + Name: containerNames[1], + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + "thirdString": "thirdStringValue", + }), + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: containerImages[0], + MountSources: &falseMountSources, + }, + }, + }, + }, + }, + filterOptions: common.DevfileOptions{ + Filter: map[string]interface{}{ + "firstString": "firstStringValue", + }, + }, + }, + { + name: "should not return containers for preStart and postStop events", + eventCommands: EventCommands{ + preStart: []string{"apply1"}, + postStop: []string{"apply2"}, + }, + containerComponents: []v1.Component{ + { + Name: containerNames[0], + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: containerImages[0], + MountSources: &falseMountSources, + }, + }, + }, + }, + { + Name: containerNames[1], + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: containerImages[1], + MountSources: &falseMountSources, + }, + }, + }, + }, + { + Name: containerNames[2], + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: containerImages[2], + MountSources: &falseMountSources, + }, + }, + }, + }, + }, + wantContainerName: containerNames[0], + wantContainerImage: containerImages[0], + }, + { + name: "Simulating error case, check if error matches", + wantErr: &errMatches, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockDevfileData := data.NewMockDevfileData(ctrl) + + tt.filterOptions.ComponentOptions = common.ComponentOptions{ + ComponentType: v1.ContainerComponentType, + } + mockGetComponents := mockDevfileData.EXPECT().GetComponents(tt.filterOptions) + + // set up the mock data + if len(tt.filterOptions.Filter) == 0 { + mockGetComponents.Return(tt.containerComponents, nil).AnyTimes() + } else { + mockGetComponents.Return(tt.filteredComponents, nil).AnyTimes() + } + if tt.wantErr != nil { + mockGetComponents.Return(nil, fmt.Errorf(*tt.wantErr)) + } + mockDevfileData.EXPECT().GetProjects(common.DevfileOptions{}).Return(projects, nil).AnyTimes() + + // to set up the prestartevent and apply command for init container + mockGetCommands := mockDevfileData.EXPECT().GetCommands(common.DevfileOptions{}) + mockGetCommands.Return(applyCommands, nil).AnyTimes() + events := v1.Events{ + DevWorkspaceEvents: v1.DevWorkspaceEvents{ + PreStart: tt.eventCommands.preStart, + PostStop: tt.eventCommands.postStop, + }, + } + mockDevfileData.EXPECT().GetEvents().Return(events).AnyTimes() + + devObj := parser.DevfileObj{ + Data: mockDevfileData, + } + + containers, err := GetContainers(devObj, tt.filterOptions) + // Unexpected error + if (err != nil) != (tt.wantErr != nil) { + t.Errorf("TestGetContainers() error: %v, wantErr %v", err, tt.wantErr) + } else if err == nil { + for _, container := range containers { + if container.Name != tt.wantContainerName { + t.Errorf("TestGetContainers() error: Name mismatch - got: %s, wanted: %s", container.Name, tt.wantContainerName) + } + if container.Image != tt.wantContainerImage { + t.Errorf("TestGetContainers() error: Image mismatch - got: %s, wanted: %s", container.Image, tt.wantContainerImage) + } + if len(container.Env) > 0 && !reflect.DeepEqual(container.Env, tt.wantContainerEnv) { + t.Errorf("TestGetContainers() error: Env mismatch - got: %+v, wanted: %+v", container.Env, tt.wantContainerEnv) + } + if len(container.VolumeMounts) > 0 && !reflect.DeepEqual(container.VolumeMounts, tt.wantContainerVolMount) { + t.Errorf("TestGetContainers() error: Vol Mount mismatch - got: %+v, wanted: %+v", container.VolumeMounts, tt.wantContainerVolMount) + } + } + } else { + assert.Regexp(t, *tt.wantErr, err.Error(), "TestGetContainers(): Error message does not match") + } + }) + } + +} + +func TestGetVolumesAndVolumeMounts(t *testing.T) { + + type testVolumeMountInfo struct { + mountPath string + volumeName string + } + + errMatches := "an expected error" + trueEphemeral := true + + tests := []struct { + name string + containerComponents []v1.Component + volumeComponents []v1.Component + volumeNameToVolInfo map[string]VolumeInfo + wantContainerToVol map[string][]testVolumeMountInfo + ephemeralVol bool + wantErr *string + }{ + { + name: "One volume mounted", + containerComponents: []v1.Component{testingutil.GetFakeContainerComponent("comp1"), testingutil.GetFakeContainerComponent("comp2")}, + volumeNameToVolInfo: map[string]VolumeInfo{ + "myvolume1": { + PVCName: "volume1-pvc", + VolumeName: "volume1-pvc-vol", + }, + }, + wantContainerToVol: map[string][]testVolumeMountInfo{ + "comp1": { + { + mountPath: "/my/volume/mount/path1", + volumeName: "volume1-pvc-vol", + }, + }, + "comp2": { + { + mountPath: "/my/volume/mount/path1", + volumeName: "volume1-pvc-vol", + }, + }, + }, + }, + { + name: "One volume mounted at diff locations", + containerComponents: []v1.Component{ + { + Name: "container1", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + VolumeMounts: []v1.VolumeMount{ + { + Name: "volume1", + Path: "/path1", + }, + { + Name: "volume1", + Path: "/path2", + }, + }, + }, + }, + }, + }, + }, + volumeNameToVolInfo: map[string]VolumeInfo{ + "volume1": { + PVCName: "volume1-pvc", + VolumeName: "volume1-pvc-vol", + }, + }, + wantContainerToVol: map[string][]testVolumeMountInfo{ + "container1": { + { + mountPath: "/path1", + volumeName: "volume1-pvc-vol", + }, + { + mountPath: "/path2", + volumeName: "volume1-pvc-vol", + }, + }, + }, + }, + { + name: "One volume mounted at diff container components", + containerComponents: []v1.Component{ + { + Name: "container1", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + VolumeMounts: []v1.VolumeMount{ + { + Name: "volume1", + Path: "/path1", + }, + }, + }, + }, + }, + }, + { + Name: "container2", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + VolumeMounts: []v1.VolumeMount{ + { + Name: "volume1", + Path: "/path2", + }, + }, + }, + }, + }, + }, + }, + volumeNameToVolInfo: map[string]VolumeInfo{ + "volume1": { + PVCName: "volume1-pvc", + VolumeName: "volume1-pvc-vol", + }, + }, + wantContainerToVol: map[string][]testVolumeMountInfo{ + "container1": { + { + mountPath: "/path1", + volumeName: "volume1-pvc-vol", + }, + }, + "container2": { + { + mountPath: "/path2", + volumeName: "volume1-pvc-vol", + }, + }, + }, + }, + { + name: "Ephemeral volume", + containerComponents: []v1.Component{ + { + Name: "container1", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + VolumeMounts: []v1.VolumeMount{ + { + Name: "volume1", + Path: "/path1", + }, + }, + }, + }, + }, + }, + }, + volumeComponents: []v1.Component{ + { + Name: "volume1", + ComponentUnion: v1.ComponentUnion{ + Volume: &v1.VolumeComponent{ + Volume: v1.Volume{ + Ephemeral: &trueEphemeral, + }, + }, + }, + }, + }, + volumeNameToVolInfo: map[string]VolumeInfo{ + "volume1": { + PVCName: "volume1-pvc", + VolumeName: "volume1-pvc-vol", + }, + }, + ephemeralVol: true, + wantContainerToVol: map[string][]testVolumeMountInfo{ + "container1": { + { + mountPath: "/path1", + volumeName: "volume1-pvc-vol", + }, + }, + }, + }, + { + name: "Simulating error case, check if error matches", + wantErr: &errMatches, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockDevfileData := data.NewMockDevfileData(ctrl) + + mockGetContainerComponents := mockDevfileData.EXPECT().GetComponents(common.DevfileOptions{ + ComponentOptions: common.ComponentOptions{ + ComponentType: v1.ContainerComponentType, + }, + }) + + mockGetVolumeComponents := mockDevfileData.EXPECT().GetComponents(common.DevfileOptions{ + ComponentOptions: common.ComponentOptions{ + ComponentType: v1.VolumeComponentType, + }, + }) + + // set up the mock data + mockGetContainerComponents.Return(tt.containerComponents, nil).AnyTimes() + mockGetVolumeComponents.Return(tt.volumeComponents, nil).AnyTimes() + mockDevfileData.EXPECT().GetProjects(common.DevfileOptions{}).Return(nil, nil).AnyTimes() + + devObj := parser.DevfileObj{ + Data: mockDevfileData, + } + + containers, err := getAllContainers(devObj, common.DevfileOptions{}) + if err != nil { + t.Errorf("TestGetVolumesAndVolumeMounts error - %v", err) + return + } + + if tt.wantErr != nil { + // simulate error condition + mockGetContainerComponents.Return(nil, fmt.Errorf(*tt.wantErr)) + + } + + volumeParams := VolumeParams{ + Containers: containers, + VolumeNameToVolumeInfo: tt.volumeNameToVolInfo, + } + + pvcVols, err := GetVolumesAndVolumeMounts(devObj, volumeParams, common.DevfileOptions{}) + if (err != nil) != (tt.wantErr != nil) { + t.Errorf("TestGetVolumesAndVolumeMounts() error: %v, wantErr %v", err, tt.wantErr) + } else if err == nil { + // check if the pvc volumes returned are correct + for _, volInfo := range tt.volumeNameToVolInfo { + matched := false + for _, pvcVol := range pvcVols { + emptyDirVolCondition := tt.ephemeralVol && reflect.DeepEqual(pvcVol.EmptyDir, &corev1.EmptyDirVolumeSource{}) + pvcCondition := pvcVol.PersistentVolumeClaim != nil && volInfo.PVCName == pvcVol.PersistentVolumeClaim.ClaimName + if volInfo.VolumeName == pvcVol.Name && (emptyDirVolCondition || pvcCondition) { + matched = true + } + } + + if !matched { + t.Errorf("TestGetVolumesAndVolumeMounts() error: could not find volume details %s in the actual result", volInfo.VolumeName) + } + } + + // check the volume mounts of the containers + for _, container := range containers { + if volMounts, ok := tt.wantContainerToVol[container.Name]; !ok { + t.Errorf("TestGetVolumesAndVolumeMounts() error: did not find the expected container %s", container.Name) + return + } else { + for _, expectedVolMount := range volMounts { + matched := false + for _, actualVolMount := range container.VolumeMounts { + if expectedVolMount.volumeName == actualVolMount.Name && expectedVolMount.mountPath == actualVolMount.MountPath { + matched = true + } + } + + if !matched { + t.Errorf("TestGetVolumesAndVolumeMounts() error: could not find volume mount details for path %s in the actual result for container %s", expectedVolMount.mountPath, container.Name) + } + } + } + } + } else { + assert.Regexp(t, *tt.wantErr, err.Error(), "TestGetVolumesAndVolumeMounts(): Error message does not match") + } + }) + } +} + +func TestGetVolumeMountPath(t *testing.T) { + + tests := []struct { + name string + volumeMount v1.VolumeMount + wantPath string + }{ + { + name: "Mount Path is present", + volumeMount: v1.VolumeMount{ + Name: "name1", + Path: "/path1", + }, + wantPath: "/path1", + }, + { + name: "Mount Path is absent", + volumeMount: v1.VolumeMount{ + Name: "name1", + }, + wantPath: "/name1", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + path := GetVolumeMountPath(tt.volumeMount) + + if path != tt.wantPath { + t.Errorf("TestGetVolumeMountPath() error: mount path mismatch, expected: %v got: %v", tt.wantPath, path) + } + }) + } + +} + +func TestGetInitContainers(t *testing.T) { + shellExecutable := "/bin/sh" + containers := []v1.Component{ + { + Name: "container1", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "container1", + Command: []string{shellExecutable, "-c", "cd execworkdir1 && execcommand1"}, + }, + }, + }, + }, + { + Name: "container2", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "container2", + Command: []string{shellExecutable, "-c", "cd execworkdir3 && execcommand3"}, + }, + }, + }, + }, + } + + applyCommands := []v1.Command{ + { + Id: "apply1", + CommandUnion: v1.CommandUnion{ + Apply: &v1.ApplyCommand{ + Component: "container1", + }, + }, + }, + { + Id: "apply2", + CommandUnion: v1.CommandUnion{ + Apply: &v1.ApplyCommand{ + Component: "container1", + }, + }, + }, + { + Id: "apply3", + CommandUnion: v1.CommandUnion{ + Apply: &v1.ApplyCommand{ + Component: "container2", + }, + }, + }, + } + + compCommands := []v1.Command{ + { + Id: "comp1", + CommandUnion: v1.CommandUnion{ + Composite: &v1.CompositeCommand{ + Commands: []string{ + "apply1", + "apply3", + }, + }, + }, + }, + } + + longContainerName := "thisisaverylongcontainerandkuberneteshasalimitforanamesize-exec2" + trimmedLongContainerName := util.TruncateString(longContainerName, containerNameMaxLen) + + errMatches := "an expected error" + + tests := []struct { + name string + eventCommands []string + wantInitContainer map[string]corev1.Container + longName bool + wantErr *string + }{ + { + name: "Composite and Exec events", + eventCommands: []string{ + "apply1", + "apply3", + "apply2", + }, + wantInitContainer: map[string]corev1.Container{ + "container1-apply1": { + Command: []string{shellExecutable, "-c", "cd execworkdir1 && execcommand1"}, + }, + "container1-apply2": { + Command: []string{shellExecutable, "-c", "cd execworkdir1 && execcommand1"}, + }, + "container2-apply3": { + Command: []string{shellExecutable, "-c", "cd execworkdir3 && execcommand3"}, + }, + }, + }, + { + name: "Simulate error case, check if error matches", + eventCommands: []string{ + "apply1", + "apply3", + "apply2", + }, + wantErr: &errMatches, + }, + { + name: "Long Container Name", + eventCommands: []string{ + "apply2", + }, + wantInitContainer: map[string]corev1.Container{ + trimmedLongContainerName: { + Command: []string{shellExecutable, "-c", "cd execworkdir1 && execcommand1"}, + }, + }, + longName: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + preStartEvents := v1.Events{ + DevWorkspaceEvents: v1.DevWorkspaceEvents{ + PreStart: tt.eventCommands, + }, + } + + if tt.longName { + containers[0].Name = longContainerName + applyCommands[1].Apply.Component = longContainerName + } + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockDevfileData := data.NewMockDevfileData(ctrl) + + mockGetCommands := mockDevfileData.EXPECT().GetCommands(common.DevfileOptions{}) + + // set up the mock data + mockDevfileData.EXPECT().GetComponents(common.DevfileOptions{ + ComponentOptions: common.ComponentOptions{ + ComponentType: v1.ContainerComponentType, + }, + }).Return(containers, nil).AnyTimes() + mockDevfileData.EXPECT().GetProjects(common.DevfileOptions{}).Return(nil, nil).AnyTimes() + mockDevfileData.EXPECT().GetEvents().Return(preStartEvents).AnyTimes() + mockGetCommands.Return(append(applyCommands, compCommands...), nil).AnyTimes() + + if tt.wantErr != nil { + mockGetCommands.Return(nil, fmt.Errorf(*tt.wantErr)).AnyTimes() + } + + devObj := parser.DevfileObj{ + Data: mockDevfileData, + } + + initContainers, err := GetInitContainers(devObj) + if (err != nil) != (tt.wantErr != nil) { + t.Errorf("TestGetInitContainers() error: %v, wantErr %v", err, tt.wantErr) + } else if err != nil { + assert.Regexp(t, *tt.wantErr, err.Error(), "TestGetInitContainers: Error message does not match") + return + } + + if len(tt.wantInitContainer) != len(initContainers) { + t.Errorf("TestGetInitContainers() error: init container length mismatch, wanted %v got %v", len(tt.wantInitContainer), len(initContainers)) + } + + for _, initContainer := range initContainers { + nameMatched := false + commandMatched := false + for containerName, container := range tt.wantInitContainer { + if strings.Contains(initContainer.Name, containerName) { + nameMatched = true + } + + if reflect.DeepEqual(initContainer.Command, container.Command) { + commandMatched = true + } + } + + if !nameMatched { + t.Errorf("TestGetInitContainers() error: init container name mismatch, container name not present in %v", initContainer.Name) + } + + if !commandMatched { + t.Errorf("TestGetInitContainers() error: init container command mismatch, command not found in %v", initContainer.Command) + } + } + }) + } + +} + +func TestGetService(t *testing.T) { + trueBool := true + + serviceParams := ServiceParams{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "preserved-key": "preserved-value", + }, + }, + } + + tests := []struct { + name string + containerComponents []v1.Component + expected corev1.Service + }{ + { + // Currently dedicatedPod can only filter out annotations + // ToDo: dedicatedPod support: https://github.com/devfile/api/issues/670 + name: "has dedicated pod", + containerComponents: []v1.Component{ + testingutil.GenerateDummyContainerComponent("container1", nil, []v1.Endpoint{ + { + Name: "http-8080", + TargetPort: 8080, + }, + }, nil, v1.Annotation{ + Service: map[string]string{ + "key1": "value1", + }, + }, nil), + testingutil.GenerateDummyContainerComponent("container2", nil, nil, nil, v1.Annotation{ + Service: map[string]string{ + "key2": "value2", + }, + }, &trueBool), + }, + expected: corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "preserved-key": "preserved-value", + "key1": "value1", + }, + }, + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + { + Name: "http-8080", + Port: 8080, + TargetPort: intstr.FromInt(8080), + }, + }, + }, + }, + }, + { + name: "no dedicated pod", + containerComponents: []v1.Component{ + testingutil.GenerateDummyContainerComponent("container1", nil, []v1.Endpoint{ + { + Name: "http-8080", + TargetPort: 8080, + }, + }, nil, v1.Annotation{ + Service: map[string]string{ + "key1": "value1", + }, + }, nil), + testingutil.GenerateDummyContainerComponent("container2", nil, nil, nil, v1.Annotation{ + Service: map[string]string{ + "key2": "value2", + }, + }, nil), + }, + expected: corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "preserved-key": "preserved-value", + "key1": "value1", + "key2": "value2", + }, + }, + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + { + Name: "http-8080", + Port: 8080, + TargetPort: intstr.FromInt(8080), + }, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockDevfileData := data.NewMockDevfileData(ctrl) + + options := common.DevfileOptions{ + ComponentOptions: common.ComponentOptions{ + ComponentType: v1.ContainerComponentType, + }, + } + // set up the mock data + mockGetComponents := mockDevfileData.EXPECT().GetComponents(options) + mockGetComponents.Return(tt.containerComponents, nil).AnyTimes() + mockDevfileData.EXPECT().GetProjects(common.DevfileOptions{}).Return(nil, nil).AnyTimes() + mockDevfileData.EXPECT().GetEvents().Return(v1.Events{}).AnyTimes() + + devObj := parser.DevfileObj{ + Data: mockDevfileData, + } + svc, err := GetService(devObj, serviceParams, common.DevfileOptions{}) + // Checks for unexpected error cases + if err != nil { + t.Errorf("TestGetService(): unexpected error %v", err) + } + assert.Equal(t, tt.expected, *svc, "TestGetService(): The two values should be the same.") + + }) + } +} + +func TestGetDeployment(t *testing.T) { + trueBool := true + containers := []corev1.Container{ + { + Name: "container1", + }, + { + Name: "container2", + }, + } + + objectMeta := metav1.ObjectMeta{ + Annotations: map[string]string{ + "preserved-key": "preserved-value", + "key1": "value1", + "key2": "value2", + }, + } + + objectMetaDedicatedPod := metav1.ObjectMeta{ + Annotations: map[string]string{ + "preserved-key": "preserved-value", + "key1": "value1", + }, + } + + tests := []struct { + name string + containerComponents []v1.Component + deploymentParams DeploymentParams + expected appsv1.Deployment + }{ + { + // Currently dedicatedPod can only filter out annotations + // ToDo: dedicatedPod support: https://github.com/devfile/api/issues/670 + name: "has dedicated pod", + containerComponents: []v1.Component{ + testingutil.GenerateDummyContainerComponent("container1", nil, []v1.Endpoint{ + { + Name: "http-8080", + TargetPort: 8080, + }, + }, nil, v1.Annotation{ + Deployment: map[string]string{ + "key1": "value1", + }, + }, nil), + testingutil.GenerateDummyContainerComponent("container2", nil, nil, nil, v1.Annotation{ + Deployment: map[string]string{ + "key2": "value2", + }, + }, &trueBool), + }, + deploymentParams: DeploymentParams{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "preserved-key": "preserved-value", + }, + }, + Containers: containers, + Replicas: pointer.Int32Ptr(1), + }, + expected: appsv1.Deployment{ + ObjectMeta: objectMetaDedicatedPod, + Spec: appsv1.DeploymentSpec{ + Strategy: appsv1.DeploymentStrategy{ + Type: appsv1.RecreateDeploymentStrategyType, + }, + Selector: &metav1.LabelSelector{ + MatchLabels: nil, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: objectMetaDedicatedPod, + Spec: corev1.PodSpec{ + Containers: containers, + }, + }, + Replicas: pointer.Int32Ptr(1), + }, + }, + }, + { + name: "no dedicated pod", + containerComponents: []v1.Component{ + testingutil.GenerateDummyContainerComponent("container1", nil, []v1.Endpoint{ + { + Name: "http-8080", + TargetPort: 8080, + }, + }, nil, v1.Annotation{ + Deployment: map[string]string{ + "key1": "value1", + }, + }, nil), + testingutil.GenerateDummyContainerComponent("container2", nil, nil, nil, v1.Annotation{ + Deployment: map[string]string{ + "key2": "value2", + }, + }, nil), + }, + deploymentParams: DeploymentParams{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "preserved-key": "preserved-value", + }, + }, + Containers: containers, + }, + expected: appsv1.Deployment{ + ObjectMeta: objectMeta, + Spec: appsv1.DeploymentSpec{ + Strategy: appsv1.DeploymentStrategy{ + Type: appsv1.RecreateDeploymentStrategyType, + }, + Selector: &metav1.LabelSelector{ + MatchLabels: nil, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: objectMeta, + Spec: corev1.PodSpec{ + Containers: containers, + }, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockDevfileData := data.NewMockDevfileData(ctrl) + + options := common.DevfileOptions{ + ComponentOptions: common.ComponentOptions{ + ComponentType: v1.ContainerComponentType, + }, + } + // set up the mock data + mockGetComponents := mockDevfileData.EXPECT().GetComponents(options) + mockGetComponents.Return(tt.containerComponents, nil).AnyTimes() + + devObj := parser.DevfileObj{ + Data: mockDevfileData, + } + deploy, err := GetDeployment(devObj, tt.deploymentParams) + // Checks for unexpected error cases + if err != nil { + t.Errorf("TestGetDeployment(): unexpected error %v", err) + } + assert.Equal(t, tt.expected, *deploy, "TestGetDeployment(): The two values should be the same.") + + }) + } +} diff --git a/pkg/devfile/generator/utils.go b/pkg/devfile/generator/utils.go new file mode 100644 index 0000000..35af324 --- /dev/null +++ b/pkg/devfile/generator/utils.go @@ -0,0 +1,646 @@ +package generator + +import ( + "fmt" + "github.com/hashicorp/go-multierror" + "path/filepath" + "reflect" + "strings" + + v1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + "github.com/devfile/library/pkg/devfile/parser" + "github.com/devfile/library/pkg/devfile/parser/data/v2/common" + buildv1 "github.com/openshift/api/build/v1" + routev1 "github.com/openshift/api/route/v1" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + extensionsv1 "k8s.io/api/extensions/v1beta1" + networkingv1 "k8s.io/api/networking/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" +) + +// convertEnvs converts environment variables from the devfile structure to kubernetes structure +func convertEnvs(vars []v1.EnvVar) []corev1.EnvVar { + kVars := []corev1.EnvVar{} + for _, env := range vars { + kVars = append(kVars, corev1.EnvVar{ + Name: env.Name, + Value: env.Value, + }) + } + return kVars +} + +// convertPorts converts endpoint variables from the devfile structure to kubernetes ContainerPort +func convertPorts(endpoints []v1.Endpoint) []corev1.ContainerPort { + containerPorts := []corev1.ContainerPort{} + portMap := make(map[string]bool) + for _, endpoint := range endpoints { + var portProtocol corev1.Protocol + portNumber := int32(endpoint.TargetPort) + + if endpoint.Protocol == v1.UDPEndpointProtocol { + portProtocol = corev1.ProtocolUDP + } else { + portProtocol = corev1.ProtocolTCP + } + name := endpoint.Name + if len(name) > 15 { + // to be compatible with endpoint longer than 15 chars + name = fmt.Sprintf("port-%v", portNumber) + } + + if _, exist := portMap[name]; !exist { + portMap[name] = true + containerPorts = append(containerPorts, corev1.ContainerPort{ + Name: name, + ContainerPort: portNumber, + Protocol: portProtocol, + }) + } + } + return containerPorts +} + +// getResourceReqs creates a kubernetes ResourceRequirements object based on resource requirements set in the devfile +func getResourceReqs(comp v1.Component) (corev1.ResourceRequirements, error) { + reqs := corev1.ResourceRequirements{} + limits := make(corev1.ResourceList) + requests := make(corev1.ResourceList) + var returnedErr error + if comp.Container != nil { + if comp.Container.MemoryLimit != "" { + memoryLimit, err := resource.ParseQuantity(comp.Container.MemoryLimit) + if err != nil { + errMsg := fmt.Errorf("error parsing memoryLimit requirement for component %s: %v", comp.Name, err.Error()) + returnedErr = multierror.Append(returnedErr, errMsg) + } else { + limits[corev1.ResourceMemory] = memoryLimit + } + } + if comp.Container.CpuLimit != "" { + cpuLimit, err := resource.ParseQuantity(comp.Container.CpuLimit) + if err != nil { + errMsg := fmt.Errorf("error parsing cpuLimit requirement for component %s: %v", comp.Name, err.Error()) + returnedErr = multierror.Append(returnedErr, errMsg) + } else { + limits[corev1.ResourceCPU] = cpuLimit + } + } + if comp.Container.MemoryRequest != "" { + memoryRequest, err := resource.ParseQuantity(comp.Container.MemoryRequest) + if err != nil { + errMsg := fmt.Errorf("error parsing memoryRequest requirement for component %s: %v", comp.Name, err.Error()) + returnedErr = multierror.Append(returnedErr, errMsg) + } else { + requests[corev1.ResourceMemory] = memoryRequest + } + } + if comp.Container.CpuRequest != "" { + cpuRequest, err := resource.ParseQuantity(comp.Container.CpuRequest) + if err != nil { + errMsg := fmt.Errorf("error parsing cpuRequest requirement for component %s: %v", comp.Name, err.Error()) + returnedErr = multierror.Append(returnedErr, errMsg) + } else { + requests[corev1.ResourceCPU] = cpuRequest + } + } + if !reflect.DeepEqual(limits, corev1.ResourceList{}) { + reqs.Limits = limits + } + if !reflect.DeepEqual(requests, corev1.ResourceList{}) { + reqs.Requests = requests + } + } + return reqs, returnedErr +} + +// addSyncRootFolder adds the sync root folder to the container env +func addSyncRootFolder(container *corev1.Container, sourceMapping string) string { + var syncRootFolder string + if sourceMapping != "" { + syncRootFolder = sourceMapping + } else { + syncRootFolder = DevfileSourceVolumeMount + } + + // Note: PROJECTS_ROOT & PROJECT_SOURCE are validated at the devfile parser level + // Add PROJECTS_ROOT to the container + container.Env = append(container.Env, + corev1.EnvVar{ + Name: EnvProjectsRoot, + Value: syncRootFolder, + }) + + return syncRootFolder +} + +// addSyncFolder adds the sync folder path to the container env +// sourceVolumePath: mount path of the empty dir volume to sync source code +// projects: list of projects from devfile +func addSyncFolder(container *corev1.Container, sourceVolumePath string, projects []v1.Project) error { + var syncFolder string + + // if there are no projects in the devfile, source would be synced to $PROJECTS_ROOT + if len(projects) == 0 { + syncFolder = sourceVolumePath + } else { + // if there is one or more projects in the devfile, get the first project and check its clonepath + project := projects[0] + // If clonepath does not exist source would be synced to $PROJECTS_ROOT/projectName + syncFolder = filepath.ToSlash(filepath.Join(sourceVolumePath, project.Name)) + + if project.ClonePath != "" { + if strings.HasPrefix(project.ClonePath, "/") { + return fmt.Errorf("the clonePath %s in the devfile project %s must be a relative path", project.ClonePath, project.Name) + } + if strings.Contains(project.ClonePath, "..") { + return fmt.Errorf("the clonePath %s in the devfile project %s cannot escape the value defined by $PROJECTS_ROOT. Please avoid using \"..\" in clonePath", project.ClonePath, project.Name) + } + // If clonepath exist source would be synced to $PROJECTS_ROOT/clonePath + syncFolder = filepath.ToSlash(filepath.Join(sourceVolumePath, project.ClonePath)) + } + } + + container.Env = append(container.Env, + corev1.EnvVar{ + Name: EnvProjectsSrc, + Value: syncFolder, + }) + + return nil +} + +// containerParams is a struct that contains the required data to create a container object +type containerParams struct { + Name string + Image string + IsPrivileged bool + Command []string + Args []string + EnvVars []corev1.EnvVar + ResourceReqs corev1.ResourceRequirements + Ports []corev1.ContainerPort +} + +// getContainer gets a container struct that can be used when creating a pod +func getContainer(containerParams containerParams) *corev1.Container { + container := &corev1.Container{ + Name: containerParams.Name, + Image: containerParams.Image, + ImagePullPolicy: corev1.PullAlways, + Resources: containerParams.ResourceReqs, + Env: containerParams.EnvVars, + Ports: containerParams.Ports, + Command: containerParams.Command, + Args: containerParams.Args, + } + + if containerParams.IsPrivileged { + container.SecurityContext = &corev1.SecurityContext{ + Privileged: &containerParams.IsPrivileged, + } + } + + return container +} + +// podTemplateSpecParams is a struct that contains the required data to create a pod template spec object +type podTemplateSpecParams struct { + ObjectMeta metav1.ObjectMeta + InitContainers []corev1.Container + Containers []corev1.Container + Volumes []corev1.Volume +} + +// getPodTemplateSpec gets a pod template spec that can be used to create a deployment spec +func getPodTemplateSpec(podTemplateSpecParams podTemplateSpecParams) *corev1.PodTemplateSpec { + podTemplateSpec := &corev1.PodTemplateSpec{ + ObjectMeta: podTemplateSpecParams.ObjectMeta, + Spec: corev1.PodSpec{ + InitContainers: podTemplateSpecParams.InitContainers, + Containers: podTemplateSpecParams.Containers, + Volumes: podTemplateSpecParams.Volumes, + }, + } + + return podTemplateSpec +} + +// deploymentSpecParams is a struct that contains the required data to create a deployment spec object +type deploymentSpecParams struct { + PodTemplateSpec corev1.PodTemplateSpec + PodSelectorLabels map[string]string + Replicas *int32 +} + +// getDeploymentSpec gets a deployment spec +func getDeploymentSpec(deploySpecParams deploymentSpecParams) *appsv1.DeploymentSpec { + deploymentSpec := &appsv1.DeploymentSpec{ + Strategy: appsv1.DeploymentStrategy{ + Type: appsv1.RecreateDeploymentStrategyType, + }, + Selector: &metav1.LabelSelector{ + MatchLabels: deploySpecParams.PodSelectorLabels, + }, + Template: deploySpecParams.PodTemplateSpec, + Replicas: deploySpecParams.Replicas, + } + + return deploymentSpec +} + +// getServiceSpec iterates through the devfile components and returns a ServiceSpec +func getServiceSpec(devfileObj parser.DevfileObj, selectorLabels map[string]string, options common.DevfileOptions) (*corev1.ServiceSpec, error) { + + var containerPorts []corev1.ContainerPort + portExposureMap, err := getPortExposure(devfileObj, options) + if err != nil { + return nil, err + } + containers, err := GetContainers(devfileObj, options) + if err != nil { + return nil, err + } + for _, c := range containers { + for _, port := range c.Ports { + portExist := false + for _, entry := range containerPorts { + if entry.ContainerPort == port.ContainerPort { + portExist = true + break + } + } + // if Exposure == none, should not create a service for that port + if !portExist && portExposureMap[int(port.ContainerPort)] != v1.NoneEndpointExposure { + containerPorts = append(containerPorts, port) + } + } + } + + var svcPorts []corev1.ServicePort + for _, containerPort := range containerPorts { + svcPort := corev1.ServicePort{ + + Name: containerPort.Name, + Port: containerPort.ContainerPort, + TargetPort: intstr.FromInt(int(containerPort.ContainerPort)), + } + svcPorts = append(svcPorts, svcPort) + } + svcSpec := &corev1.ServiceSpec{ + Ports: svcPorts, + Selector: selectorLabels, + } + + return svcSpec, nil +} + +// getPortExposure iterates through all endpoints and returns the highest exposure level of all TargetPort. +// exposure level: public > internal > none +func getPortExposure(devfileObj parser.DevfileObj, options common.DevfileOptions) (map[int]v1.EndpointExposure, error) { + portExposureMap := make(map[int]v1.EndpointExposure) + options.ComponentOptions = common.ComponentOptions{ + ComponentType: v1.ContainerComponentType, + } + containerComponents, err := devfileObj.Data.GetComponents(options) + if err != nil { + return portExposureMap, err + } + for _, comp := range containerComponents { + for _, endpoint := range comp.Container.Endpoints { + // if exposure=public, no need to check for existence + if endpoint.Exposure == v1.PublicEndpointExposure || endpoint.Exposure == "" { + portExposureMap[endpoint.TargetPort] = v1.PublicEndpointExposure + } else if exposure, exist := portExposureMap[endpoint.TargetPort]; exist { + // if a container has multiple identical ports with different exposure levels, save the highest level in the map + if endpoint.Exposure == v1.InternalEndpointExposure && exposure == v1.NoneEndpointExposure { + portExposureMap[endpoint.TargetPort] = v1.InternalEndpointExposure + } + } else { + portExposureMap[endpoint.TargetPort] = endpoint.Exposure + } + } + + } + return portExposureMap, nil +} + +// IngressSpecParams struct for function GenerateIngressSpec +// serviceName is the name of the service for the target reference +// ingressDomain is the ingress domain to use for the ingress +// portNumber is the target port of the ingress +// Path is the path of the ingress +// TLSSecretName is the target TLS Secret name of the ingress +type IngressSpecParams struct { + ServiceName string + IngressDomain string + PortNumber intstr.IntOrString + TLSSecretName string + Path string +} + +// getIngressSpec gets an ingress spec +func getIngressSpec(ingressSpecParams IngressSpecParams) *extensionsv1.IngressSpec { + path := "/" + if ingressSpecParams.Path != "" { + path = ingressSpecParams.Path + } + ingressSpec := &extensionsv1.IngressSpec{ + Rules: []extensionsv1.IngressRule{ + { + Host: ingressSpecParams.IngressDomain, + IngressRuleValue: extensionsv1.IngressRuleValue{ + HTTP: &extensionsv1.HTTPIngressRuleValue{ + Paths: []extensionsv1.HTTPIngressPath{ + { + Path: path, + Backend: extensionsv1.IngressBackend{ + ServiceName: ingressSpecParams.ServiceName, + ServicePort: ingressSpecParams.PortNumber, + }, + }, + }, + }, + }, + }, + }, + } + secretNameLength := len(ingressSpecParams.TLSSecretName) + if secretNameLength != 0 { + ingressSpec.TLS = []extensionsv1.IngressTLS{ + { + Hosts: []string{ + ingressSpecParams.IngressDomain, + }, + SecretName: ingressSpecParams.TLSSecretName, + }, + } + } + + return ingressSpec +} + +// getNetworkingV1IngressSpec gets a networking v1 ingress spec +func getNetworkingV1IngressSpec(ingressSpecParams IngressSpecParams) *networkingv1.IngressSpec { + path := "/" + pathTypeImplementationSpecific := networkingv1.PathTypeImplementationSpecific + if ingressSpecParams.Path != "" { + path = ingressSpecParams.Path + } + ingressSpec := &networkingv1.IngressSpec{ + Rules: []networkingv1.IngressRule{ + { + Host: ingressSpecParams.IngressDomain, + IngressRuleValue: networkingv1.IngressRuleValue{ + HTTP: &networkingv1.HTTPIngressRuleValue{ + Paths: []networkingv1.HTTPIngressPath{ + { + Path: path, + Backend: networkingv1.IngressBackend{ + Service: &networkingv1.IngressServiceBackend{ + Name: ingressSpecParams.ServiceName, + Port: networkingv1.ServiceBackendPort{ + Number: ingressSpecParams.PortNumber.IntVal, + }, + }, + }, + //Field is required to be set based on attempt to create the ingress + PathType: &pathTypeImplementationSpecific, + }, + }, + }, + }, + }, + }, + } + secretNameLength := len(ingressSpecParams.TLSSecretName) + if secretNameLength != 0 { + ingressSpec.TLS = []networkingv1.IngressTLS{ + { + Hosts: []string{ + ingressSpecParams.IngressDomain, + }, + SecretName: ingressSpecParams.TLSSecretName, + }, + } + } + + return ingressSpec +} + +// RouteSpecParams struct for function GenerateRouteSpec +// serviceName is the name of the service for the target reference +// portNumber is the target port of the ingress +// Path is the path of the route +type RouteSpecParams struct { + ServiceName string + PortNumber intstr.IntOrString + Path string + Secure bool +} + +// GetRouteSpec gets a route spec +func getRouteSpec(routeParams RouteSpecParams) *routev1.RouteSpec { + routePath := "/" + if routeParams.Path != "" { + routePath = routeParams.Path + } + routeSpec := &routev1.RouteSpec{ + To: routev1.RouteTargetReference{ + Kind: "Service", + Name: routeParams.ServiceName, + }, + Port: &routev1.RoutePort{ + TargetPort: routeParams.PortNumber, + }, + Path: routePath, + } + + if routeParams.Secure { + routeSpec.TLS = &routev1.TLSConfig{ + Termination: routev1.TLSTerminationEdge, + InsecureEdgeTerminationPolicy: routev1.InsecureEdgeTerminationPolicyRedirect, + } + } + + return routeSpec +} + +// getPVCSpec gets a RWO pvc spec +func getPVCSpec(quantity resource.Quantity) *corev1.PersistentVolumeClaimSpec { + + pvcSpec := &corev1.PersistentVolumeClaimSpec{ + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceStorage: quantity, + }, + }, + AccessModes: []corev1.PersistentVolumeAccessMode{ + corev1.ReadWriteOnce, + }, + } + + return pvcSpec +} + +// BuildConfigSpecParams is a struct to create build config spec +type BuildConfigSpecParams struct { + ImageStreamTagName string + GitURL string + GitRef string + ContextDir string + BuildStrategy buildv1.BuildStrategy +} + +// getBuildConfigSpec gets the build config spec and outputs the build to the image stream +func getBuildConfigSpec(buildConfigSpecParams BuildConfigSpecParams) *buildv1.BuildConfigSpec { + + return &buildv1.BuildConfigSpec{ + CommonSpec: buildv1.CommonSpec{ + Output: buildv1.BuildOutput{ + To: &corev1.ObjectReference{ + Kind: "ImageStreamTag", + Name: buildConfigSpecParams.ImageStreamTagName + ":latest", + }, + }, + Source: buildv1.BuildSource{ + Git: &buildv1.GitBuildSource{ + URI: buildConfigSpecParams.GitURL, + Ref: buildConfigSpecParams.GitRef, + }, + ContextDir: buildConfigSpecParams.ContextDir, + Type: buildv1.BuildSourceGit, + }, + Strategy: buildConfigSpecParams.BuildStrategy, + }, + } +} + +// getPVC gets a pvc type volume with the given volume name and pvc name. +func getPVC(volumeName, pvcName string) corev1.Volume { + + return corev1.Volume{ + Name: volumeName, + VolumeSource: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: pvcName, + }, + }, + } +} + +// getEmptyDirVol gets a volume with emptyDir +func getEmptyDirVol(volumeName string) corev1.Volume { + return corev1.Volume{ + Name: volumeName, + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + } +} + +// addVolumeMountToContainers adds the Volume Mounts in containerNameToMountPaths to the containers for a given volumeName. +// containerNameToMountPaths is a map of a container name to an array of its Mount Paths. +func addVolumeMountToContainers(containers []corev1.Container, volumeName string, containerNameToMountPaths map[string][]string) { + + for containerName, mountPaths := range containerNameToMountPaths { + for i := range containers { + if containers[i].Name == containerName { + for _, mountPath := range mountPaths { + containers[i].VolumeMounts = append(containers[i].VolumeMounts, corev1.VolumeMount{ + Name: volumeName, + MountPath: mountPath, + }, + ) + } + } + } + } +} + +// getAllContainers iterates through the devfile components and returns all container components +func getAllContainers(devfileObj parser.DevfileObj, options common.DevfileOptions) ([]corev1.Container, error) { + var containers []corev1.Container + + options.ComponentOptions = common.ComponentOptions{ + ComponentType: v1.ContainerComponentType, + } + containerComponents, err := devfileObj.Data.GetComponents(options) + if err != nil { + return nil, err + } + for _, comp := range containerComponents { + envVars := convertEnvs(comp.Container.Env) + resourceReqs, err := getResourceReqs(comp) + if err != nil { + return containers, err + } + ports := convertPorts(comp.Container.Endpoints) + containerParams := containerParams{ + Name: comp.Name, + Image: comp.Container.Image, + IsPrivileged: false, + Command: comp.Container.Command, + Args: comp.Container.Args, + EnvVars: envVars, + ResourceReqs: resourceReqs, + Ports: ports, + } + container := getContainer(containerParams) + + // If `mountSources: true` was set PROJECTS_ROOT & PROJECT_SOURCE env + if comp.Container.MountSources == nil || *comp.Container.MountSources { + syncRootFolder := addSyncRootFolder(container, comp.Container.SourceMapping) + + projects, err := devfileObj.Data.GetProjects(common.DevfileOptions{}) + if err != nil { + return nil, err + } + err = addSyncFolder(container, syncRootFolder, projects) + if err != nil { + return nil, err + } + } + containers = append(containers, *container) + } + return containers, nil +} + +// getContainerAnnotations iterates through container components and returns all annotations +func getContainerAnnotations(devfileObj parser.DevfileObj, options common.DevfileOptions) (v1.Annotation, error) { + options.ComponentOptions = common.ComponentOptions{ + ComponentType: v1.ContainerComponentType, + } + containerComponents, err := devfileObj.Data.GetComponents(options) + if err != nil { + return v1.Annotation{}, err + } + var annotations v1.Annotation + annotations.Service = make(map[string]string) + annotations.Deployment = make(map[string]string) + for _, comp := range containerComponents { + // ToDo: dedicatedPod support: https://github.com/devfile/api/issues/670 + if comp.Container.DedicatedPod != nil && *comp.Container.DedicatedPod { + continue + } + if comp.Container.Annotation != nil { + mergeMaps(annotations.Service, comp.Container.Annotation.Service) + mergeMaps(annotations.Deployment, comp.Container.Annotation.Deployment) + } + } + + return annotations, nil +} + +func mergeMaps(dest map[string]string, src map[string]string) map[string]string { + if dest == nil { + dest = make(map[string]string) + } + for k, v := range src { + dest[k] = v + } + return dest +} diff --git a/pkg/devfile/generator/utils_test.go b/pkg/devfile/generator/utils_test.go new file mode 100644 index 0000000..575ecc4 --- /dev/null +++ b/pkg/devfile/generator/utils_test.go @@ -0,0 +1,1707 @@ +package generator + +import ( + "github.com/hashicorp/go-multierror" + "github.com/stretchr/testify/assert" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/devfile/api/v2/pkg/attributes" + "github.com/devfile/library/pkg/devfile/parser" + "github.com/devfile/library/pkg/devfile/parser/data" + "github.com/devfile/library/pkg/devfile/parser/data/v2/common" + "github.com/devfile/library/pkg/testingutil" + "github.com/golang/mock/gomock" + buildv1 "github.com/openshift/api/build/v1" + + v1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + "k8s.io/apimachinery/pkg/util/intstr" +) + +var isTrue bool = true + +func TestConvertEnvs(t *testing.T) { + envVarsNames := []string{"test", "sample-var", "myvar"} + envVarsValues := []string{"value1", "value2", "value3"} + tests := []struct { + name string + envVars []v1.EnvVar + want []corev1.EnvVar + }{ + { + name: "One env var", + envVars: []v1.EnvVar{ + { + Name: envVarsNames[0], + Value: envVarsValues[0], + }, + }, + want: []corev1.EnvVar{ + { + Name: envVarsNames[0], + Value: envVarsValues[0], + }, + }, + }, + { + name: "Multiple env vars", + envVars: []v1.EnvVar{ + { + Name: envVarsNames[0], + Value: envVarsValues[0], + }, + { + Name: envVarsNames[1], + Value: envVarsValues[1], + }, + { + Name: envVarsNames[2], + Value: envVarsValues[2], + }, + }, + want: []corev1.EnvVar{ + { + Name: envVarsNames[0], + Value: envVarsValues[0], + }, + { + Name: envVarsNames[1], + Value: envVarsValues[1], + }, + { + Name: envVarsNames[2], + Value: envVarsValues[2], + }, + }, + }, + { + name: "No env vars", + envVars: []v1.EnvVar{}, + want: []corev1.EnvVar{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + envVars := convertEnvs(tt.envVars) + if !reflect.DeepEqual(tt.want, envVars) { + t.Errorf("TestConvertEnvs() error: expected %v, wanted %v", envVars, tt.want) + } + }) + } +} + +func TestConvertPorts(t *testing.T) { + endpointsNames := []string{"endpoint1", "endpoint2", "a-very-long-port-name-before-endpoint-length-limit-8080"} + endpointsPorts := []int{8080, 9090} + tests := []struct { + name string + endpoints []v1.Endpoint + want []corev1.ContainerPort + }{ + { + name: "One Endpoint", + endpoints: []v1.Endpoint{ + { + Name: endpointsNames[0], + TargetPort: endpointsPorts[0], + }, + }, + want: []corev1.ContainerPort{ + { + Name: endpointsNames[0], + ContainerPort: int32(endpointsPorts[0]), + Protocol: "TCP", + }, + }, + }, + { + name: "One Endpoint with >15 chars length", + endpoints: []v1.Endpoint{ + { + Name: endpointsNames[2], + TargetPort: endpointsPorts[0], + }, + }, + want: []corev1.ContainerPort{ + { + Name: "port-8080", + ContainerPort: int32(endpointsPorts[0]), + Protocol: "TCP", + }, + }, + }, + { + name: "Multiple endpoints", + endpoints: []v1.Endpoint{ + { + Name: endpointsNames[0], + TargetPort: endpointsPorts[0], + }, + { + Name: endpointsNames[1], + TargetPort: endpointsPorts[1], + }, + }, + want: []corev1.ContainerPort{ + { + Name: endpointsNames[0], + ContainerPort: int32(endpointsPorts[0]), + Protocol: "TCP", + }, + { + Name: endpointsNames[1], + ContainerPort: int32(endpointsPorts[1]), + Protocol: "TCP", + }, + }, + }, + { + name: "No endpoints", + endpoints: []v1.Endpoint{}, + want: []corev1.ContainerPort{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ports := convertPorts(tt.endpoints) + if !reflect.DeepEqual(tt.want, ports) { + t.Errorf("TestConvertPorts() error: expected %v, wanted %v", ports, tt.want) + } + }) + } +} + +func TestGetResourceReqs(t *testing.T) { + memoryLimit := "1024Mi" + memoryRequest := "1Gi" + cpuRequest := "1m" + cpuLimit := "1m" + + memoryLimitQuantity, err := resource.ParseQuantity(memoryLimit) + memoryRequestQuantity, err := resource.ParseQuantity(memoryRequest) + cpuRequestQuantity, err := resource.ParseQuantity(cpuRequest) + cpuLimitQuantity, err := resource.ParseQuantity(cpuLimit) + if err != nil { + t.Errorf("TestGetResourceReqs() unexpected error: %v", err) + } + tests := []struct { + name string + component v1.Component + want corev1.ResourceRequirements + wantErr []string + }{ + { + name: "generate resource limit", + component: v1.Component{ + Name: "testcomponent", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + MemoryLimit: memoryLimit, + MemoryRequest: memoryRequest, + CpuRequest: cpuRequest, + CpuLimit: cpuLimit, + }, + }, + }, + }, + want: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceMemory: memoryLimitQuantity, + corev1.ResourceCPU: cpuLimitQuantity, + }, + Requests: corev1.ResourceList{ + corev1.ResourceMemory: memoryRequestQuantity, + corev1.ResourceCPU: cpuRequestQuantity, + }, + }, + }, + { + name: "Empty Component", + component: v1.Component{}, + want: corev1.ResourceRequirements{}, + }, + { + name: "Valid container, but empty memoryLimit", + component: v1.Component{ + Name: "testcomponent", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "testimage", + }, + }, + }, + }, + want: corev1.ResourceRequirements{}, + }, + { + name: "test error case", + component: v1.Component{ + Name: "testcomponent", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + MemoryLimit: "invalid", + MemoryRequest: "invalid", + CpuRequest: "invalid", + CpuLimit: "invalid", + }, + }, + }, + }, + wantErr: []string{ + "error parsing memoryLimit requirement.*", + "error parsing cpuLimit requirement.*", + "error parsing memoryRequest requirement.*", + "error parsing cpuRequest requirement.*", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req, err := getResourceReqs(tt.component) + if merr, ok := err.(*multierror.Error); ok && tt.wantErr != nil { + assert.Equal(t, len(tt.wantErr), len(merr.Errors), "Error list length should match") + for i := 0; i < len(merr.Errors); i++ { + assert.Regexp(t, tt.wantErr[i], merr.Errors[i].Error(), "Error message should match") + } + } else if !reflect.DeepEqual(tt.want, req) { + assert.Equal(t, tt.want, req, "TestGetResourceReqs(): The two values should be the same.") + } + }) + } +} + +func TestAddSyncRootFolder(t *testing.T) { + + tests := []struct { + name string + sourceMapping string + wantSyncRootFolder string + }{ + { + name: "Valid Source Mapping", + sourceMapping: "/mypath", + wantSyncRootFolder: "/mypath", + }, + { + name: "No Source Mapping", + sourceMapping: "", + wantSyncRootFolder: DevfileSourceVolumeMount, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + container := testingutil.CreateFakeContainer("container1") + + syncRootFolder := addSyncRootFolder(&container, tt.sourceMapping) + + if syncRootFolder != tt.wantSyncRootFolder { + t.Errorf("TestAddSyncRootFolder() sync root folder error: expected %v got %v", tt.wantSyncRootFolder, syncRootFolder) + } + + for _, env := range container.Env { + if env.Name == EnvProjectsRoot && env.Value != tt.wantSyncRootFolder { + t.Errorf("TestAddSyncRootFolder() PROJECT_ROOT error: expected %s, actual %s", tt.wantSyncRootFolder, env.Value) + } + } + }) + } +} + +func TestAddSyncFolder(t *testing.T) { + projectNames := []string{"some-name", "another-name"} + projectRepos := []string{"https://github.com/some/repo.git", "https://github.com/another/repo.git"} + projectClonePath := "src/github.com/golang/example/" + invalidClonePaths := []string{"/var", "../var", "pkg/../../var"} + sourceVolumePath := "/projects/app" + + absoluteClonePathErr := "the clonePath .* in the devfile project .* must be a relative path" + escapeClonePathErr := "the clonePath .* in the devfile project .* cannot escape the value defined by [$]PROJECTS_ROOT. Please avoid using \"..\" in clonePath" + + tests := []struct { + name string + projects []v1.Project + want string + wantErr *string + }{ + { + name: "No projects", + projects: []v1.Project{}, + want: sourceVolumePath, + }, + { + name: "One project", + projects: []v1.Project{ + { + Name: projectNames[0], + ProjectSource: v1.ProjectSource{ + Git: &v1.GitProjectSource{ + GitLikeProjectSource: v1.GitLikeProjectSource{ + Remotes: map[string]string{"origin": projectRepos[0]}, + }, + }, + }, + }, + }, + want: filepath.ToSlash(filepath.Join(sourceVolumePath, projectNames[0])), + }, + { + name: "Multiple projects", + projects: []v1.Project{ + { + Name: projectNames[0], + ProjectSource: v1.ProjectSource{ + Git: &v1.GitProjectSource{ + GitLikeProjectSource: v1.GitLikeProjectSource{ + Remotes: map[string]string{"origin": projectRepos[0]}, + }, + }, + }, + }, + { + Name: projectNames[1], + ProjectSource: v1.ProjectSource{ + Zip: &v1.ZipProjectSource{ + Location: projectRepos[1], + }, + }, + }, + }, + want: filepath.ToSlash(filepath.Join(sourceVolumePath, projectNames[0])), + }, + { + name: "Clone path set", + projects: []v1.Project{ + { + ClonePath: projectClonePath, + Name: projectNames[0], + ProjectSource: v1.ProjectSource{ + Zip: &v1.ZipProjectSource{ + Location: projectRepos[0], + }, + }, + }, + }, + want: filepath.ToSlash(filepath.Join(sourceVolumePath, projectClonePath)), + }, + { + name: "Invalid clone path, set with absolute path", + projects: []v1.Project{ + { + ClonePath: invalidClonePaths[0], + Name: projectNames[0], + ProjectSource: v1.ProjectSource{ + Git: &v1.GitProjectSource{ + GitLikeProjectSource: v1.GitLikeProjectSource{ + Remotes: map[string]string{"origin": projectRepos[0]}, + }, + }, + }, + }, + }, + want: "", + wantErr: &absoluteClonePathErr, + }, + { + name: "Invalid clone path, starts with ..", + projects: []v1.Project{ + { + ClonePath: invalidClonePaths[1], + Name: projectNames[0], + ProjectSource: v1.ProjectSource{ + Git: &v1.GitProjectSource{ + GitLikeProjectSource: v1.GitLikeProjectSource{ + Remotes: map[string]string{"origin": projectRepos[0]}, + }, + }, + }, + }, + }, + want: "", + wantErr: &escapeClonePathErr, + }, + { + name: "Invalid clone path, contains ..", + projects: []v1.Project{ + { + ClonePath: invalidClonePaths[2], + Name: projectNames[0], + ProjectSource: v1.ProjectSource{ + Zip: &v1.ZipProjectSource{ + Location: projectRepos[0], + }, + }, + }, + }, + want: "", + wantErr: &escapeClonePathErr, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + container := testingutil.CreateFakeContainer("container1") + + err := addSyncFolder(&container, sourceVolumePath, tt.projects) + + if (err != nil) != (tt.wantErr != nil) { + t.Errorf("TestAddSyncFolder() error: unexpected error %v, want %v", err, tt.wantErr) + } else if err == nil { + for _, env := range container.Env { + if env.Name == EnvProjectsSrc && env.Value != tt.want { + t.Errorf("TestAddSyncFolder() error: expected %s, actual %s", tt.want, env.Value) + } + } + } else { + assert.Regexp(t, *tt.wantErr, err.Error(), "TestAddSyncFolder(): Error message should match") + } + }) + } +} + +func TestGetContainer(t *testing.T) { + + tests := []struct { + name string + containerName string + image string + isPrivileged bool + command []string + args []string + envVars []corev1.EnvVar + resourceReqs corev1.ResourceRequirements + ports []corev1.ContainerPort + }{ + { + name: "Empty container params", + containerName: "", + image: "", + isPrivileged: false, + command: []string{}, + args: []string{}, + envVars: []corev1.EnvVar{}, + resourceReqs: corev1.ResourceRequirements{}, + ports: []corev1.ContainerPort{}, + }, + { + name: "Valid container params", + containerName: "container1", + image: "quay.io/eclipse/che-java8-maven:nightly", + isPrivileged: true, + command: []string{"tail"}, + args: []string{"-f", "/dev/null"}, + envVars: []corev1.EnvVar{ + { + Name: "test", + Value: "123", + }, + }, + resourceReqs: fakeResources, + ports: []corev1.ContainerPort{ + { + Name: "port-9090", + ContainerPort: 9090, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + containerParams := containerParams{ + Name: tt.containerName, + Image: tt.image, + IsPrivileged: tt.isPrivileged, + Command: tt.command, + Args: tt.args, + EnvVars: tt.envVars, + ResourceReqs: tt.resourceReqs, + Ports: tt.ports, + } + container := getContainer(containerParams) + + if container.Name != tt.containerName { + t.Errorf("TestGetContainer() error: expected containerName %s, actual %s", tt.containerName, container.Name) + } + + if container.Image != tt.image { + t.Errorf("TestGetContainer() error: expected image %s, actual %s", tt.image, container.Image) + } + + if tt.isPrivileged { + if *container.SecurityContext.Privileged != tt.isPrivileged { + t.Errorf("TestGetContainer() error: expected isPrivileged %t, actual %t", tt.isPrivileged, *container.SecurityContext.Privileged) + } + } else if tt.isPrivileged == false && container.SecurityContext != nil { + t.Errorf("expected security context to be nil but it was defined") + } + + if len(container.Command) != len(tt.command) { + t.Errorf("TestGetContainer() error: expected command length %d, actual %d", len(tt.command), len(container.Command)) + } else { + for i := range container.Command { + if container.Command[i] != tt.command[i] { + t.Errorf("TestGetContainer() error: expected command %s, actual %s", tt.command[i], container.Command[i]) + } + } + } + + if len(container.Args) != len(tt.args) { + t.Errorf("TestGetContainer() error: expected container args length %d, actual %d", len(tt.args), len(container.Args)) + } else { + for i := range container.Args { + if container.Args[i] != tt.args[i] { + t.Errorf("TestGetContainer() error: expected container args %s, actual %s", tt.args[i], container.Args[i]) + } + } + } + + if len(container.Env) != len(tt.envVars) { + t.Errorf("TestGetContainer() error: expected container env length %d, actual %d", len(tt.envVars), len(container.Env)) + } else { + for i := range container.Env { + if container.Env[i].Name != tt.envVars[i].Name { + t.Errorf("TestGetContainer() error: expected env name %s, actual %s", tt.envVars[i].Name, container.Env[i].Name) + } + if container.Env[i].Value != tt.envVars[i].Value { + t.Errorf("TestGetContainer() error: expected env value %s, actual %s", tt.envVars[i].Value, container.Env[i].Value) + } + } + } + + if len(container.Ports) != len(tt.ports) { + t.Errorf("TestGetContainer() error: expected container port length %d, actual %d", len(tt.ports), len(container.Ports)) + } else { + for i := range container.Ports { + if container.Ports[i].Name != tt.ports[i].Name { + t.Errorf("TestGetContainer() error: expected port name %s, actual %s", tt.ports[i].Name, container.Ports[i].Name) + } + if container.Ports[i].ContainerPort != tt.ports[i].ContainerPort { + t.Errorf("TestGetContainer() error: expected port number %v, actual %v", tt.ports[i].ContainerPort, container.Ports[i].ContainerPort) + } + } + } + + }) + } +} + +func TestGetPodTemplateSpec(t *testing.T) { + + container := []corev1.Container{ + { + Name: "container1", + Image: "image1", + ImagePullPolicy: corev1.PullAlways, + + Command: []string{"tail"}, + Args: []string{"-f", "/dev/null"}, + Env: []corev1.EnvVar{}, + }, + } + + volume := []corev1.Volume{ + { + Name: "vol1", + }, + } + + tests := []struct { + podName string + namespace string + serviceAccount string + labels map[string]string + }{ + { + podName: "podSpecTest", + namespace: "default", + serviceAccount: "default", + labels: map[string]string{ + "app": "app", + "component": "frontend", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.podName, func(t *testing.T) { + + objectMeta := GetObjectMeta(tt.podName, tt.namespace, tt.labels, nil) + podTemplateSpecParams := podTemplateSpecParams{ + ObjectMeta: objectMeta, + Containers: container, + Volumes: volume, + InitContainers: container, + } + podTemplateSpec := getPodTemplateSpec(podTemplateSpecParams) + + if podTemplateSpec.Name != tt.podName { + t.Errorf("TestGetPodTemplateSpec() error: expected podName %s, actual %s", tt.podName, podTemplateSpec.Name) + } + if podTemplateSpec.Namespace != tt.namespace { + t.Errorf("TestGetPodTemplateSpec() error: expected namespace %s, actual %s", tt.namespace, podTemplateSpec.Namespace) + } + if !hasVolumeWithName("vol1", podTemplateSpec.Spec.Volumes) { + t.Errorf("TestGetPodTemplateSpec() error: volume with name: %s not found", "vol1") + } + if !reflect.DeepEqual(podTemplateSpec.Labels, tt.labels) { + t.Errorf("TestGetPodTemplateSpec() error: expected labels %+v, actual %+v", tt.labels, podTemplateSpec.Labels) + } + if !reflect.DeepEqual(podTemplateSpec.Spec.Containers, container) { + t.Errorf("TestGetPodTemplateSpec() error: expected container %+v, actual %+v", container, podTemplateSpec.Spec.Containers) + } + if !reflect.DeepEqual(podTemplateSpec.Spec.InitContainers, container) { + t.Errorf("TestGetPodTemplateSpec() error: expected InitContainers %+v, actual %+v", container, podTemplateSpec.Spec.InitContainers) + } + }) + } +} + +func TestGetServiceSpec(t *testing.T) { + + endpointNames := []string{"port-8080-url", "port-9090-url", "a-very-long-port-name-before-endpoint-length-limit-8080"} + + tests := []struct { + name string + containerComponents []v1.Component + filteredComponents []v1.Component + labels map[string]string + filterOptions common.DevfileOptions + wantPorts []corev1.ServicePort + }{ + { + name: "multiple endpoints have different ports", + containerComponents: []v1.Component{ + { + Name: "testcontainer1", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Endpoints: []v1.Endpoint{ + { + Name: endpointNames[0], + TargetPort: 8080, + }, + { + Name: endpointNames[1], + TargetPort: 9090, + }, + }, + }, + }, + }, + }, + labels: map[string]string{ + "component": "testcomponent", + }, + wantPorts: []corev1.ServicePort{ + { + Name: endpointNames[0], + Port: 8080, + TargetPort: intstr.FromInt(8080), + }, + { + Name: endpointNames[1], + Port: 9090, + TargetPort: intstr.FromInt(9090), + }, + }, + }, + { + name: "long port name before endpoint length limit to <=15", + containerComponents: []v1.Component{ + { + Name: "testcontainer1", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Endpoints: []v1.Endpoint{ + { + Name: endpointNames[2], + TargetPort: 8080, + }, + }, + }, + }, + }, + }, + labels: map[string]string{ + "component": "testcomponent", + }, + wantPorts: []corev1.ServicePort{ + { + Name: "port-8080", + Port: 8080, + TargetPort: intstr.FromInt(8080), + }, + }, + }, + { + name: "filter components", + containerComponents: []v1.Component{ + { + Name: "testcontainer1", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Endpoints: []v1.Endpoint{ + { + Name: endpointNames[0], + TargetPort: 8080, + }, + }, + }, + }, + }, + { + Name: "testcontainer2", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + "thirdString": "thirdStringValue", + }), + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Endpoints: []v1.Endpoint{ + { + Name: endpointNames[1], + TargetPort: 9090, + }, + }, + }, + }, + }, + }, + labels: map[string]string{ + "component": "testcomponent", + }, + wantPorts: []corev1.ServicePort{ + { + Name: endpointNames[1], + Port: 9090, + TargetPort: intstr.FromInt(9090), + }, + }, + filteredComponents: []v1.Component{ + { + Name: "testcontainer2", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + "thirdString": "thirdStringValue", + }), + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Endpoints: []v1.Endpoint{ + { + Name: endpointNames[1], + TargetPort: 9090, + }, + }, + }, + }, + }, + }, + filterOptions: common.DevfileOptions{ + Filter: map[string]interface{}{ + "firstString": "firstStringValue", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockDevfileData := data.NewMockDevfileData(ctrl) + + tt.filterOptions.ComponentOptions = common.ComponentOptions{ + ComponentType: v1.ContainerComponentType, + } + mockGetComponents := mockDevfileData.EXPECT().GetComponents(tt.filterOptions) + + // set up the mock data + if len(tt.filterOptions.Filter) == 0 { + mockGetComponents.Return(tt.containerComponents, nil).AnyTimes() + } else { + mockGetComponents.Return(tt.filteredComponents, nil).AnyTimes() + } + mockDevfileData.EXPECT().GetProjects(common.DevfileOptions{}).Return(nil, nil).AnyTimes() + mockDevfileData.EXPECT().GetEvents().Return(v1.Events{}).AnyTimes() + devObj := parser.DevfileObj{ + Data: mockDevfileData, + } + + serviceSpec, err := getServiceSpec(devObj, tt.labels, tt.filterOptions) + + // Unexpected error + if err != nil { + t.Errorf("TestGetServiceSpec() unexpected error: %v", err) + } else { + if !reflect.DeepEqual(serviceSpec.Selector, tt.labels) { + t.Errorf("TestGetServiceSpec() error: expected service selector is %v, actual %v", tt.labels, serviceSpec.Selector) + } + if len(serviceSpec.Ports) != len(tt.wantPorts) { + t.Errorf("TestGetServiceSpec() error: expected service ports length is %v, actual %v", len(tt.wantPorts), len(serviceSpec.Ports)) + } else { + for i := range serviceSpec.Ports { + if serviceSpec.Ports[i].Name != tt.wantPorts[i].Name { + t.Errorf("TestGetServiceSpec() error: expected name %s, actual name %s", tt.wantPorts[i].Name, serviceSpec.Ports[i].Name) + } + if serviceSpec.Ports[i].Port != tt.wantPorts[i].Port { + t.Errorf("TestGetServiceSpec() error: expected port number is %v, actual %v", tt.wantPorts[i].Port, serviceSpec.Ports[i].Port) + } + } + } + } + }) + } +} + +func TestGetPortExposure(t *testing.T) { + urlName := "testurl" + urlName2 := "testurl2" + tests := []struct { + name string + containerComponents []v1.Component + filteredComponents []v1.Component + filterOptions common.DevfileOptions + wantMap map[int]v1.EndpointExposure + }{ + { + name: "devfile has single container with single endpoint", + wantMap: map[int]v1.EndpointExposure{ + 8080: v1.PublicEndpointExposure, + }, + containerComponents: []v1.Component{ + { + Name: "testcontainer1", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "image", + }, + Endpoints: []v1.Endpoint{ + { + Name: urlName, + TargetPort: 8080, + Exposure: v1.PublicEndpointExposure, + }, + }, + }, + }, + }, + }, + }, + { + name: "devfile no endpoints", + wantMap: map[int]v1.EndpointExposure{}, + containerComponents: []v1.Component{ + { + Name: "testcontainer1", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "image", + }, + }, + }, + }, + }, + }, + { + name: "devfile has multiple endpoints with same port, 1 public and 1 internal, should assign public", + wantMap: map[int]v1.EndpointExposure{ + 8080: v1.PublicEndpointExposure, + }, + containerComponents: []v1.Component{ + { + Name: "testcontainer1", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "image", + }, + Endpoints: []v1.Endpoint{ + { + Name: urlName, + TargetPort: 8080, + Exposure: v1.PublicEndpointExposure, + }, + { + Name: urlName, + TargetPort: 8080, + Exposure: v1.InternalEndpointExposure, + }, + }, + }, + }, + }, + }, + }, + { + name: "devfile has multiple endpoints with same port, 1 public and 1 none, should assign public", + wantMap: map[int]v1.EndpointExposure{ + 8080: v1.PublicEndpointExposure, + }, + containerComponents: []v1.Component{ + { + Name: "testcontainer1", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "image", + }, + Endpoints: []v1.Endpoint{ + { + Name: urlName, + TargetPort: 8080, + Exposure: v1.PublicEndpointExposure, + }, + { + Name: urlName, + TargetPort: 8080, + Exposure: v1.NoneEndpointExposure, + }, + }, + }, + }, + }, + }, + }, + { + name: "devfile has multiple endpoints with same port, 1 internal and 1 none, should assign internal", + wantMap: map[int]v1.EndpointExposure{ + 8080: v1.InternalEndpointExposure, + }, + containerComponents: []v1.Component{ + { + Name: "testcontainer1", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "image", + }, + Endpoints: []v1.Endpoint{ + { + Name: urlName, + TargetPort: 8080, + Exposure: v1.InternalEndpointExposure, + }, + { + Name: urlName, + TargetPort: 8080, + Exposure: v1.NoneEndpointExposure, + }, + }, + }, + }, + }, + }, + }, + { + name: "devfile has multiple endpoints with different port", + wantMap: map[int]v1.EndpointExposure{ + 8080: v1.PublicEndpointExposure, + 9090: v1.InternalEndpointExposure, + 3000: v1.NoneEndpointExposure, + }, + containerComponents: []v1.Component{ + { + Name: "testcontainer1", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "image", + }, + Endpoints: []v1.Endpoint{ + { + Name: urlName, + TargetPort: 8080, + }, + { + Name: urlName, + TargetPort: 3000, + Exposure: v1.NoneEndpointExposure, + }, + }, + }, + }, + }, + { + Name: "testcontainer2", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "image", + }, + Endpoints: []v1.Endpoint{ + { + Name: urlName2, + TargetPort: 9090, + Secure: &isTrue, + Path: "/testpath", + Exposure: v1.InternalEndpointExposure, + Protocol: v1.HTTPSEndpointProtocol, + }, + }, + }, + }, + }, + }, + }, + { + name: "Filter components", + wantMap: map[int]v1.EndpointExposure{ + 8080: v1.PublicEndpointExposure, + 3000: v1.NoneEndpointExposure, + }, + containerComponents: []v1.Component{ + { + Name: "testcontainer1", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + "thirdString": "thirdStringValue", + }), + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "image", + }, + Endpoints: []v1.Endpoint{ + { + Name: urlName, + TargetPort: 8080, + }, + { + Name: urlName, + TargetPort: 3000, + Exposure: v1.NoneEndpointExposure, + }, + }, + }, + }, + }, + { + Name: "testcontainer2", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "image", + }, + Endpoints: []v1.Endpoint{ + { + Name: urlName2, + TargetPort: 9090, + Secure: &isTrue, + Path: "/testpath", + Exposure: v1.InternalEndpointExposure, + Protocol: v1.HTTPSEndpointProtocol, + }, + }, + }, + }, + }, + }, + filteredComponents: []v1.Component{ + { + Name: "testcontainer1", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + "thirdString": "thirdStringValue", + }), + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "image", + }, + Endpoints: []v1.Endpoint{ + { + Name: urlName, + TargetPort: 8080, + }, + { + Name: urlName, + TargetPort: 3000, + Exposure: v1.NoneEndpointExposure, + }, + }, + }, + }, + }, + }, + filterOptions: common.DevfileOptions{ + Filter: map[string]interface{}{ + "firstString": "firstStringValue", + }, + }, + }, + { + name: "Wrong filter components", + wantMap: map[int]v1.EndpointExposure{}, + containerComponents: []v1.Component{ + { + Name: "testcontainer1", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + "thirdString": "thirdStringValue", + }), + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "image", + }, + Endpoints: []v1.Endpoint{ + { + Name: urlName, + TargetPort: 8080, + }, + { + Name: urlName, + TargetPort: 3000, + Exposure: v1.NoneEndpointExposure, + }, + }, + }, + }, + }, + }, + filteredComponents: nil, + filterOptions: common.DevfileOptions{ + Filter: map[string]interface{}{ + "firstStringWrong": "firstStringValue", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockDevfileData := data.NewMockDevfileData(ctrl) + + tt.filterOptions.ComponentOptions = common.ComponentOptions{ + ComponentType: v1.ContainerComponentType, + } + mockGetComponents := mockDevfileData.EXPECT().GetComponents(tt.filterOptions) + + // set up the mock data + if len(tt.filterOptions.Filter) == 0 { + mockGetComponents.Return(tt.containerComponents, nil).AnyTimes() + } else { + mockGetComponents.Return(tt.filteredComponents, nil).AnyTimes() + } + devObj := parser.DevfileObj{ + Data: mockDevfileData, + } + + mapCreated, err := getPortExposure(devObj, tt.filterOptions) + // Checks for unexpected error cases + if err != nil { + t.Errorf("TestGetPortExposure() unexpected error: %v", err) + } else if !reflect.DeepEqual(mapCreated, tt.wantMap) { + t.Errorf("TestGetPortExposure() error: expected: %v, got %v", tt.wantMap, mapCreated) + } + + }) + } + +} + +func TestGetIngressSpec(t *testing.T) { + + tests := []struct { + name string + parameter IngressSpecParams + }{ + { + name: "1", + parameter: IngressSpecParams{ + ServiceName: "service1", + IngressDomain: "test.1.2.3.4.nip.io", + PortNumber: intstr.IntOrString{ + IntVal: 8080, + }, + TLSSecretName: "testTLSSecret", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + ingressSpec := getIngressSpec(tt.parameter) + + if ingressSpec.Rules[0].Host != tt.parameter.IngressDomain { + t.Errorf("TestGetIngressSpec() error: expected ingressDomain %s, actual %s", tt.parameter.IngressDomain, ingressSpec.Rules[0].Host) + } + + if ingressSpec.Rules[0].HTTP.Paths[0].Backend.ServicePort != tt.parameter.PortNumber { + t.Errorf("TestGetIngressSpec() error: expected portNumber %v, actual %v", tt.parameter.PortNumber, ingressSpec.Rules[0].HTTP.Paths[0].Backend.ServicePort) + } + + if ingressSpec.Rules[0].HTTP.Paths[0].Backend.ServiceName != tt.parameter.ServiceName { + t.Errorf("TestGetIngressSpec() error: expected serviceName %s, actual %s", tt.parameter.ServiceName, ingressSpec.Rules[0].HTTP.Paths[0].Backend.ServiceName) + } + + if ingressSpec.TLS[0].SecretName != tt.parameter.TLSSecretName { + t.Errorf("TestGetIngressSpec() error: expected TLSSecretName %s, actual %s", tt.parameter.TLSSecretName, ingressSpec.TLS[0].SecretName) + } + + }) + } +} + +func TestGetNetworkingV1IngressSpec(t *testing.T) { + + tests := []struct { + name string + parameter IngressSpecParams + }{ + { + name: "1", + parameter: IngressSpecParams{ + ServiceName: "service1", + IngressDomain: "test.1.2.3.4.nip.io", + PortNumber: intstr.IntOrString{ + IntVal: 8080, + }, + TLSSecretName: "testTLSSecret", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + ingressSpec := getNetworkingV1IngressSpec(tt.parameter) + + if ingressSpec.Rules[0].Host != tt.parameter.IngressDomain { + t.Errorf("TestGetNetworkingV1IngressSpec() error: expected IngressDomain %s, actual %s", tt.parameter.IngressDomain, ingressSpec.Rules[0].Host) + } + + if ingressSpec.Rules[0].HTTP.Paths[0].Backend.Service.Port.Number != tt.parameter.PortNumber.IntVal { + t.Errorf("TestGetNetworkingV1IngressSpec() error: expected PortNumber %v, actual %v", tt.parameter.PortNumber, ingressSpec.Rules[0].HTTP.Paths[0].Backend.Service.Port.Number) + } + + if ingressSpec.Rules[0].HTTP.Paths[0].Backend.Service.Name != tt.parameter.ServiceName { + t.Errorf("TestGetNetworkingV1IngressSpec() error: expected ServiceName %s, actual %s", tt.parameter.ServiceName, ingressSpec.Rules[0].HTTP.Paths[0].Backend.Service.Name) + } + + if ingressSpec.TLS[0].SecretName != tt.parameter.TLSSecretName { + t.Errorf("TestGetNetworkingV1IngressSpec() error: expected TLSSecretName %s, actual %s", tt.parameter.TLSSecretName, ingressSpec.TLS[0].SecretName) + } + + }) + } +} + +func TestGetRouteSpec(t *testing.T) { + + tests := []struct { + name string + parameter RouteSpecParams + }{ + { + name: "insecure route", + parameter: RouteSpecParams{ + ServiceName: "service1", + PortNumber: intstr.IntOrString{ + IntVal: 8080, + }, + Secure: false, + Path: "/test", + }, + }, + { + name: "secure route", + parameter: RouteSpecParams{ + ServiceName: "service1", + PortNumber: intstr.IntOrString{ + IntVal: 8080, + }, + Secure: true, + Path: "/test", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + routeSpec := getRouteSpec(tt.parameter) + + if routeSpec.Port.TargetPort != tt.parameter.PortNumber { + t.Errorf("TestGetRouteSpec() error: expected PortNumber %v, actual %v", tt.parameter.PortNumber, routeSpec.Port.TargetPort) + } + + if routeSpec.To.Name != tt.parameter.ServiceName { + t.Errorf("TestGetRouteSpec() error: expected ServiceName %s, actual %s", tt.parameter.ServiceName, routeSpec.To.Name) + } + + if routeSpec.Path != tt.parameter.Path { + t.Errorf("TestGetRouteSpec() error: expected Path %s, actual %s", tt.parameter.Path, routeSpec.Path) + } + + if (routeSpec.TLS != nil) != tt.parameter.Secure { + t.Errorf("TestGetRouteSpec() error: the route TLS does not match secure level %v", tt.parameter.Secure) + } + + }) + } +} + +func TestGetPVCSpec(t *testing.T) { + + tests := []struct { + name string + size string + wantErr bool + }{ + { + name: "Valid resource size", + size: "1Gi", + wantErr: false, + }, + { + name: "Resource size missing", + size: "", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + quantity, err := resource.ParseQuantity(tt.size) + // Checks for unexpected error cases + if !tt.wantErr == (err != nil) { + t.Errorf("TestGetPVCSpec() error: resource.ParseQuantity unexpected error %v, wantErr %v", err, tt.wantErr) + } else if err == nil { + pvcSpec := getPVCSpec(quantity) + if pvcSpec.AccessModes[0] != corev1.ReadWriteOnce { + t.Errorf("TestGetPVCSpec() error: AccessMode Error: expected %s, actual %s", corev1.ReadWriteMany, pvcSpec.AccessModes[0]) + } + + pvcSpecQuantity := pvcSpec.Resources.Requests["storage"] + if pvcSpecQuantity.String() != quantity.String() { + t.Errorf("TestGetPVCSpec() error: pvcSpec.Resources.Requests Error: expected %v, actual %v", pvcSpecQuantity.String(), quantity.String()) + } + } + }) + } +} + +func hasVolumeWithName(name string, volMounts []corev1.Volume) bool { + for _, vm := range volMounts { + if vm.Name == name { + return true + } + } + return false +} + +func TestGetBuildConfigSpec(t *testing.T) { + + image := "image" + namespace := "namespace" + + tests := []struct { + name string + GitURL string + GitRef string + ContextDir string + buildStrategy buildv1.BuildStrategy + }{ + { + name: "Get a Source Strategy Build Config", + GitURL: "url", + GitRef: "ref", + buildStrategy: GetSourceBuildStrategy(image, namespace), + }, + { + name: "Get a Docker Strategy Build Config", + GitURL: "url", + GitRef: "ref", + ContextDir: "./", + buildStrategy: GetDockerBuildStrategy("dockerfilePath", []corev1.EnvVar{}), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + commonObjectMeta := GetObjectMeta(image, namespace, nil, nil) + params := BuildConfigSpecParams{ + ImageStreamTagName: commonObjectMeta.Name, + BuildStrategy: tt.buildStrategy, + GitURL: tt.GitURL, + GitRef: tt.GitRef, + ContextDir: tt.ContextDir, + } + buildConfigSpec := getBuildConfigSpec(params) + + if !strings.Contains(buildConfigSpec.CommonSpec.Output.To.Name, image) { + t.Error("TestGetBuildConfigSpec() error: build config output name does not match") + } + + if buildConfigSpec.Source.Git.Ref != tt.GitRef || buildConfigSpec.Source.Git.URI != tt.GitURL { + t.Error("TestGetBuildConfigSpec() error: build config git source does not match") + } + + if buildConfigSpec.CommonSpec.Source.ContextDir != tt.ContextDir { + t.Error("TestGetBuildConfigSpec() error: context dir does not match") + } + }) + } + +} + +func TestGetPVC(t *testing.T) { + + tests := []struct { + name string + pvc string + volumeName string + }{ + { + name: "Get PVC vol for given pvc name and volume name", + pvc: "mypvc", + volumeName: "myvolume", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + volume := getPVC(tt.volumeName, tt.pvc) + + if volume.Name != tt.volumeName { + t.Errorf("TestGetPVC() error: volume name does not match; expected %s got %s", tt.volumeName, volume.Name) + } + + if volume.PersistentVolumeClaim.ClaimName != tt.pvc { + t.Errorf("TestGetPVC() error: pvc name does not match; expected %s got %s", tt.pvc, volume.PersistentVolumeClaim.ClaimName) + } + }) + } +} + +func TestAddVolumeMountToContainers(t *testing.T) { + + tests := []struct { + name string + volumeName string + containerMountPathsMap map[string][]string + container corev1.Container + }{ + { + name: "Successfully mount volume to container", + volumeName: "myvolume", + containerMountPathsMap: map[string][]string{ + "container1": {"/tmp/path1", "/tmp/path2"}, + }, + container: corev1.Container{ + Name: "container1", + Image: "image1", + ImagePullPolicy: corev1.PullAlways, + + Command: []string{"tail"}, + Args: []string{"-f", "/dev/null"}, + Env: []corev1.EnvVar{}, + }, + }, + { + name: "No Container present to mount volume", + volumeName: "myvolume", + containerMountPathsMap: map[string][]string{ + "container1": {"/tmp/path1", "/tmp/path2"}, + }, + container: corev1.Container{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + containers := []corev1.Container{tt.container} + addVolumeMountToContainers(containers, tt.volumeName, tt.containerMountPathsMap) + + mountPathCount := 0 + for _, container := range containers { + if container.Name == tt.container.Name { + for _, volumeMount := range container.VolumeMounts { + if volumeMount.Name == tt.volumeName { + for _, mountPath := range tt.containerMountPathsMap[tt.container.Name] { + if volumeMount.MountPath == mountPath { + mountPathCount++ + } + } + } + } + } + } + + if mountPathCount != len(tt.containerMountPathsMap[tt.container.Name]) { + t.Errorf("TestAddVolumeMountToContainers() error: Volume Mounts for %s have not been properly mounted to the container", tt.volumeName) + } + }) + } +} + +func TestGetContainerAnnotations(t *testing.T) { + trueBool := true + + tests := []struct { + name string + containerComponents []v1.Component + expected v1.Annotation + }{ + { + name: "no dedicated pod", + containerComponents: []v1.Component{ + testingutil.GenerateDummyContainerComponent("container1", nil, nil, nil, v1.Annotation{ + Service: map[string]string{ + "key1": "value1", + }, + Deployment: map[string]string{ + "key1": "value1", + }, + }, nil), + testingutil.GenerateDummyContainerComponent("container2", nil, nil, nil, v1.Annotation{ + Service: map[string]string{ + "key2": "value2", + }, + Deployment: map[string]string{ + "key2": "value2", + }, + }, nil), + }, + expected: v1.Annotation{ + Service: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + Deployment: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + }, + }, + { + name: "has dedicated pod", + containerComponents: []v1.Component{ + testingutil.GenerateDummyContainerComponent("container1", nil, nil, nil, v1.Annotation{ + Service: map[string]string{ + "key1": "value1", + }, + Deployment: map[string]string{ + "key1": "value1", + }, + }, nil), + testingutil.GenerateDummyContainerComponent("container2", nil, nil, nil, v1.Annotation{ + Service: map[string]string{ + "key2": "value2", + }, + Deployment: map[string]string{ + "key2": "value2", + }, + }, &trueBool), + }, + expected: v1.Annotation{ + Service: map[string]string{ + "key1": "value1", + }, + Deployment: map[string]string{ + "key1": "value1", + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockDevfileData := data.NewMockDevfileData(ctrl) + + options := common.DevfileOptions{ + ComponentOptions: common.ComponentOptions{ + ComponentType: v1.ContainerComponentType, + }, + } + // set up the mock data + mockGetComponents := mockDevfileData.EXPECT().GetComponents(options) + mockGetComponents.Return(tt.containerComponents, nil).AnyTimes() + + devObj := parser.DevfileObj{ + Data: mockDevfileData, + } + annotations, err := getContainerAnnotations(devObj, common.DevfileOptions{}) + // Checks for unexpected error cases + if err != nil { + t.Errorf("TestGetContainerAnnotations(): unexpected error %v", err) + } + assert.Equal(t, tt.expected, annotations, "TestGetContainerAnnotations(): The two values should be the same.") + + }) + } +} + +func TestMergeMaps(t *testing.T) { + + tests := []struct { + name string + dest map[string]string + src map[string]string + expected map[string]string + }{ + { + name: "dest is nil", + dest: nil, + src: map[string]string{ + "key3": "value3", + }, + expected: map[string]string{ + "key3": "value3", + }, + }, + { + name: "src is nil", + dest: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + src: nil, + expected: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + }, + { + name: "no nil maps", + dest: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + src: map[string]string{ + "key3": "value3", + }, + expected: map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := mergeMaps(tt.dest, tt.src) + assert.Equal(t, tt.expected, result, "TestmergeMaps(): The two values should be the same.") + + }) + } +} diff --git a/pkg/devfile/parse.go b/pkg/devfile/parse.go new file mode 100644 index 0000000..6eba767 --- /dev/null +++ b/pkg/devfile/parse.go @@ -0,0 +1,94 @@ +package devfile + +import ( + "github.com/devfile/api/v2/pkg/validation/variables" + "github.com/devfile/library/pkg/devfile/parser" + "github.com/devfile/library/pkg/devfile/validate" +) + +// ParseFromURLAndValidate func parses the devfile data from the url +// and validates the devfile integrity with the schema +// and validates the devfile data. +// Creates devfile context and runtime objects. +// Deprecated, use ParseDevfileAndValidate() instead +func ParseFromURLAndValidate(url string) (d parser.DevfileObj, err error) { + + // read and parse devfile from the given URL + d, err = parser.ParseFromURL(url) + if err != nil { + return d, err + } + + // generic validation on devfile content + err = validate.ValidateDevfileData(d.Data) + if err != nil { + return d, err + } + + return d, err +} + +// ParseFromDataAndValidate func parses the devfile data +// and validates the devfile integrity with the schema +// and validates the devfile data. +// Creates devfile context and runtime objects. +// Deprecated, use ParseDevfileAndValidate() instead +func ParseFromDataAndValidate(data []byte) (d parser.DevfileObj, err error) { + // read and parse devfile from the given bytes + d, err = parser.ParseFromData(data) + if err != nil { + return d, err + } + // generic validation on devfile content + err = validate.ValidateDevfileData(d.Data) + if err != nil { + return d, err + } + + return d, err +} + +// ParseAndValidate func parses the devfile data +// and validates the devfile integrity with the schema +// and validates the devfile data. +// Creates devfile context and runtime objects. +// Deprecated, use ParseDevfileAndValidate() instead +func ParseAndValidate(path string) (d parser.DevfileObj, err error) { + + // read and parse devfile from given path + d, err = parser.Parse(path) + if err != nil { + return d, err + } + + // generic validation on devfile content + err = validate.ValidateDevfileData(d.Data) + if err != nil { + return d, err + } + + return d, err +} + +// ParseDevfileAndValidate func parses the devfile data, validates the devfile integrity with the schema +// replaces the top-level variable keys if present and validates the devfile data. +// It returns devfile context and runtime objects, variable substitution warning if any and an error. +func ParseDevfileAndValidate(args parser.ParserArgs) (d parser.DevfileObj, varWarning variables.VariableWarning, err error) { + d, err = parser.ParseDevfile(args) + if err != nil { + return d, varWarning, err + } + + if d.Data.GetSchemaVersion() != "2.0.0" { + // replace the top level variable keys with their values in the devfile + varWarning = variables.ValidateAndReplaceGlobalVariable(d.Data.GetDevfileWorkspaceSpec()) + } + + // generic validation on devfile content + err = validate.ValidateDevfileData(d.Data) + if err != nil { + return d, varWarning, err + } + + return d, varWarning, err +} diff --git a/pkg/devfile/parser/configurables.go b/pkg/devfile/parser/configurables.go new file mode 100644 index 0000000..e05b464 --- /dev/null +++ b/pkg/devfile/parser/configurables.go @@ -0,0 +1,119 @@ +package parser + +import ( + v1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + "github.com/devfile/library/pkg/devfile/parser/data/v2/common" +) + +const ( + Name = "Name" + Ports = "Ports" + Memory = "Memory" + PortsDescription = "Ports to be opened in all component containers" + MemoryDescription = "The Maximum memory all the component containers can consume" + NameDescription = "The name of the component" +) + +// SetMetadataName set metadata name in a devfile +func (d DevfileObj) SetMetadataName(name string) error { + metadata := d.Data.GetMetadata() + metadata.Name = name + d.Data.SetMetadata(metadata) + return d.WriteYamlDevfile() +} + +// AddEnvVars accepts a map of container name mapped to an array of the env vars to be set; +// it adds the envirnoment variables to a given container name, and writes to the devfile +// Example of containerEnvMap : {"runtime": {{Name: "Foo", Value: "Bar"}}} +func (d DevfileObj) AddEnvVars(containerEnvMap map[string][]v1.EnvVar) error { + err := d.Data.AddEnvVars(containerEnvMap) + if err != nil { + return err + } + return d.WriteYamlDevfile() +} + +// RemoveEnvVars accepts a map of container name mapped to an array of environment variables to be removed; +// it removes the env vars from the specified container name and writes it to the devfile +func (d DevfileObj) RemoveEnvVars(containerEnvMap map[string][]string) (err error) { + err = d.Data.RemoveEnvVars(containerEnvMap) + if err != nil { + return err + } + return d.WriteYamlDevfile() +} + +// SetPorts accepts a map of container name mapped to an array of port numbers to be set; +// it converts ports to endpoints, sets the endpoint to a given container name, and writes to the devfile +// Example of containerPortsMap: {"runtime": {"8080", "9000"}, "wildfly": {"12956"}} +func (d DevfileObj) SetPorts(containerPortsMap map[string][]string) error { + err := d.Data.SetPorts(containerPortsMap) + if err != nil { + return err + } + return d.WriteYamlDevfile() +} + +// RemovePorts accepts a map of container name mapped to an array of port numbers to be removed; +// it removes the container endpoints with the specified port numbers of the specified container, and writes to the devfile +// Example of containerPortsMap: {"runtime": {"8080", "9000"}, "wildfly": {"12956"}} +func (d DevfileObj) RemovePorts(containerPortsMap map[string][]string) error { + err := d.Data.RemovePorts(containerPortsMap) + if err != nil { + return err + } + return d.WriteYamlDevfile() +} + +// HasPorts checks if a devfile contains container endpoints +func (d DevfileObj) HasPorts() bool { + components, err := d.Data.GetComponents(common.DevfileOptions{}) + if err != nil { + return false + } + for _, component := range components { + if component.Container != nil { + if len(component.Container.Endpoints) > 0 { + return true + } + } + } + return false +} + +// SetMemory sets memoryLimit in devfile container +func (d DevfileObj) SetMemory(memory string) error { + components, err := d.Data.GetComponents(common.DevfileOptions{}) + if err != nil { + return err + } + for _, component := range components { + if component.Container != nil { + component.Container.MemoryLimit = memory + d.Data.UpdateComponent(component) + } + } + return d.WriteYamlDevfile() +} + +// GetMemory gets memoryLimit from devfile container +func (d DevfileObj) GetMemory() string { + components, err := d.Data.GetComponents(common.DevfileOptions{}) + if err != nil { + return "" + } + for _, component := range components { + if component.Container != nil { + if component.Container.MemoryLimit != "" { + return component.Container.MemoryLimit + } + } + + } + return "" +} + +// GetMetadataName gets metadata name from a devfile +func (d DevfileObj) GetMetadataName() string { + return d.Data.GetMetadata().Name +} diff --git a/pkg/devfile/parser/context/apiVersion.go b/pkg/devfile/parser/context/apiVersion.go new file mode 100644 index 0000000..e07d706 --- /dev/null +++ b/pkg/devfile/parser/context/apiVersion.go @@ -0,0 +1,57 @@ +package parser + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/devfile/library/pkg/devfile/parser/data" + "github.com/pkg/errors" + "k8s.io/klog" +) + +// SetDevfileAPIVersion returns the devfile APIVersion +func (d *DevfileCtx) SetDevfileAPIVersion() error { + + // Unmarshal JSON into map + var r map[string]interface{} + err := json.Unmarshal(d.rawContent, &r) + if err != nil { + return errors.Wrapf(err, "failed to decode devfile json") + } + + // Get "schemaVersion" value from map for devfile V2 + schemaVersion, okSchema := r["schemaVersion"] + var devfilePath string + if d.GetAbsPath() != "" { + devfilePath = d.GetAbsPath() + } else if d.GetURL() != "" { + devfilePath = d.GetURL() + } + + if okSchema { + // SchemaVersion cannot be empty + if schemaVersion.(string) == "" { + return fmt.Errorf("schemaVersion in devfile: %s cannot be empty", devfilePath) + } + } else { + return fmt.Errorf("schemaVersion not present in devfile: %s", devfilePath) + } + + // Successful + // split by `-` and get the first substring as schema version, schemaVersion without `-` won't get affected + // e.g. 2.2.0-latest => 2.2.0, 2.2.0 => 2.2.0 + d.apiVersion = strings.Split(schemaVersion.(string), "-")[0] + klog.V(4).Infof("devfile schemaVersion: '%s'", d.apiVersion) + return nil +} + +// GetApiVersion returns apiVersion stored in devfile context +func (d *DevfileCtx) GetApiVersion() string { + return d.apiVersion +} + +// IsApiVersionSupported return true if the apiVersion in DevfileCtx is supported +func (d *DevfileCtx) IsApiVersionSupported() bool { + return data.IsApiVersionSupported(d.apiVersion) +} diff --git a/pkg/devfile/parser/context/apiVersion_test.go b/pkg/devfile/parser/context/apiVersion_test.go new file mode 100644 index 0000000..f294ee2 --- /dev/null +++ b/pkg/devfile/parser/context/apiVersion_test.go @@ -0,0 +1,93 @@ +package parser + +import ( + "fmt" + "reflect" + "testing" +) + +func TestSetDevfileAPIVersion(t *testing.T) { + + const ( + schemaVersion = "2.2.0" + validJson = `{"schemaVersion": "2.2.0"}` + concreteSchema = `{"schemaVersion": "2.2.0-latest"}` + emptyJson = "{}" + emptySchemaVersionJson = `{"schemaVersion": ""}` + devfilePath = "/testpath/devfile.yaml" + devfileURL = "http://server/devfile.yaml" + ) + + // test table + tests := []struct { + name string + devfileCtx DevfileCtx + want string + wantErr error + }{ + { + name: "valid schemaVersion", + devfileCtx: DevfileCtx{rawContent: []byte(validJson), absPath: devfilePath}, + want: schemaVersion, + wantErr: nil, + }, + { + name: "concrete schemaVersion", + devfileCtx: DevfileCtx{rawContent: []byte(concreteSchema), absPath: devfilePath}, + want: schemaVersion, + wantErr: nil, + }, + { + name: "schemaVersion not present", + devfileCtx: DevfileCtx{rawContent: []byte(emptyJson), absPath: devfilePath}, + want: "", + wantErr: fmt.Errorf("schemaVersion not present in devfile: %s", devfilePath), + }, + { + name: "schemaVersion empty", + devfileCtx: DevfileCtx{rawContent: []byte(emptySchemaVersionJson), url: devfileURL}, + want: "", + wantErr: fmt.Errorf("schemaVersion in devfile: %s cannot be empty", devfileURL), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + // new devfile context object + d := tt.devfileCtx + + // SetDevfileAPIVersion + gotErr := d.SetDevfileAPIVersion() + got := d.apiVersion + + if !reflect.DeepEqual(gotErr, tt.wantErr) { + t.Errorf("TestSetDevfileAPIVersion() unexpected error: '%v', wantErr: '%v'", gotErr, tt.wantErr) + } + + if got != tt.want { + t.Errorf("TestSetDevfileAPIVersion() want: '%v', got: '%v'", tt.want, got) + } + }) + } +} + +func TestGetApiVersion(t *testing.T) { + + const ( + apiVersion = "2.0.0" + ) + + t.Run("get apiVersion", func(t *testing.T) { + + var ( + d = DevfileCtx{apiVersion: apiVersion} + want = apiVersion + got = d.GetApiVersion() + ) + + if got != want { + t.Errorf("TestGetApiVersion() want: '%v', got: '%v'", want, got) + } + }) +} diff --git a/pkg/devfile/parser/context/content.go b/pkg/devfile/parser/context/content.go new file mode 100644 index 0000000..a4a4ddf --- /dev/null +++ b/pkg/devfile/parser/context/content.go @@ -0,0 +1,88 @@ +package parser + +import ( + "bytes" + "unicode" + + "github.com/devfile/library/pkg/util" + "github.com/pkg/errors" + "k8s.io/klog" + "sigs.k8s.io/yaml" +) + +// Every JSON document starts with "{" +var jsonPrefix = []byte("{") + +// YAMLToJSON converts a single YAML document into a JSON document +// or returns an error. If the document appears to be JSON the +// YAML decoding path is not used. +func YAMLToJSON(data []byte) ([]byte, error) { + + // Is already JSON + if hasJSONPrefix(data) { + return data, nil + } + + // Is YAML, convert to JSON + data, err := yaml.YAMLToJSON(data) + if err != nil { + return data, errors.Wrapf(err, "failed to convert devfile yaml to json") + } + + // Successful + klog.V(4).Infof("converted devfile YAML to JSON") + return data, nil +} + +// hasJSONPrefix returns true if the provided buffer appears to start with +// a JSON open brace. +func hasJSONPrefix(buf []byte) bool { + return hasPrefix(buf, jsonPrefix) +} + +// hasPrefix returns true if the first non-whitespace bytes in buf is prefix. +func hasPrefix(buf []byte, prefix []byte) bool { + trim := bytes.TrimLeftFunc(buf, unicode.IsSpace) + return bytes.HasPrefix(trim, prefix) +} + +// SetDevfileContent reads devfile and if devfile is in YAML format converts it to JSON +func (d *DevfileCtx) SetDevfileContent() error { + + var err error + var data []byte + if d.url != "" { + data, err = util.DownloadFileInMemory(d.url) + if err != nil { + return errors.Wrap(err, "error getting devfile info from url") + } + } else if d.absPath != "" { + // Read devfile + fs := d.GetFs() + data, err = fs.ReadFile(d.absPath) + if err != nil { + return errors.Wrapf(err, "failed to read devfile from path '%s'", d.absPath) + } + } + + // set devfile content + return d.SetDevfileContentFromBytes(data) +} + +// SetDevfileContentFromBytes sets devfile content from byte input +func (d *DevfileCtx) SetDevfileContentFromBytes(data []byte) error { + // If YAML file convert it to JSON + var err error + d.rawContent, err = YAMLToJSON(data) + if err != nil { + return err + } + + // Successful + return nil +} + +// GetDevfileContent returns the devfile content +func (d *DevfileCtx) GetDevfileContent() []byte { + return d.rawContent +} diff --git a/pkg/devfile/parser/context/content_test.go b/pkg/devfile/parser/context/content_test.go new file mode 100644 index 0000000..004ce49 --- /dev/null +++ b/pkg/devfile/parser/context/content_test.go @@ -0,0 +1,177 @@ +package parser + +import ( + "os" + "testing" + + "github.com/devfile/library/pkg/testingutil/filesystem" +) + +const ( + TempJSONDevfilePrefix = "odo-devfile.*.json" + InvalidDevfileContent = ":: invalid :: content" +) + +func TestSetDevfileContent(t *testing.T) { + + const ( + InvalidDevfilePath = "/invalid/path" + ) + + // createTempDevfile helper creates temp devfile + createTempDevfile := func(t *testing.T, content []byte, fakeFs filesystem.Filesystem) (f filesystem.File) { + + t.Helper() + + // Create tempfile + f, err := fakeFs.TempFile(os.TempDir(), TempJSONDevfilePrefix) + if err != nil { + t.Errorf("failed to create temp devfile, %v", err) + return f + } + + // Write content to devfile + if _, err := f.Write(content); err != nil { + t.Errorf("failed to write to temp devfile") + return f + } + + // Successful + return f + } + + t.Run("valid file", func(t *testing.T) { + + var ( + fakeFs = filesystem.NewFakeFs() + tempDevfile = createTempDevfile(t, validJsonRawContent200(), fakeFs) + d = DevfileCtx{ + absPath: tempDevfile.Name(), + fs: fakeFs, + } + ) + defer os.Remove(tempDevfile.Name()) + + err := d.SetDevfileContent() + + if err != nil { + t.Errorf("unexpected error '%v'", err) + } + + if err := tempDevfile.Close(); err != nil { + t.Errorf("failed to close temp devfile") + } + }) + + t.Run("invalid content", func(t *testing.T) { + + var ( + fakeFs = filesystem.NewFakeFs() + tempDevfile = createTempDevfile(t, []byte(InvalidDevfileContent), fakeFs) + d = DevfileCtx{ + absPath: tempDevfile.Name(), + fs: fakeFs, + } + ) + defer os.Remove(tempDevfile.Name()) + + err := d.SetDevfileContent() + + if err == nil { + t.Errorf("expected error, didn't get one ") + } + + if err := tempDevfile.Close(); err != nil { + t.Errorf("failed to close temp devfile") + } + }) + + t.Run("invalid filepath", func(t *testing.T) { + + var ( + fakeFs = filesystem.NewFakeFs() + d = DevfileCtx{ + absPath: InvalidDevfilePath, + fs: fakeFs, + } + ) + + err := d.SetDevfileContent() + + if err == nil { + t.Errorf("expected an error, didn't get one") + } + }) +} + +func TestSetDevfileContentFromBytes(t *testing.T) { + + // createTempDevfile helper creates temp devfile + createTempDevfile := func(t *testing.T, content []byte, fakeFs filesystem.Filesystem) (f filesystem.File) { + + t.Helper() + + // Create tempfile + f, err := fakeFs.TempFile(os.TempDir(), TempJSONDevfilePrefix) + if err != nil { + t.Errorf("failed to create temp devfile, %v", err) + return f + } + + // Write content to devfile + if _, err := f.Write(content); err != nil { + t.Errorf("failed to write to temp devfile") + return f + } + + // Successful + return f + } + + t.Run("valid data passed", func(t *testing.T) { + + var ( + fakeFs = filesystem.NewFakeFs() + tempDevfile = createTempDevfile(t, validJsonRawContent200(), fakeFs) + d = DevfileCtx{ + absPath: tempDevfile.Name(), + fs: fakeFs, + } + ) + + defer os.Remove(tempDevfile.Name()) + + err := d.SetDevfileContentFromBytes(validJsonRawContent200()) + + if err != nil { + t.Errorf("unexpected error '%v'", err) + } + + if err := tempDevfile.Close(); err != nil { + t.Errorf("failed to close temp devfile") + } + }) + + t.Run("invalid data passed", func(t *testing.T) { + + var ( + fakeFs = filesystem.NewFakeFs() + tempDevfile = createTempDevfile(t, []byte(validJsonRawContent200()), fakeFs) + d = DevfileCtx{ + absPath: tempDevfile.Name(), + fs: fakeFs, + } + ) + defer os.Remove(tempDevfile.Name()) + + err := d.SetDevfileContentFromBytes([]byte(InvalidDevfileContent)) + + if err == nil { + t.Errorf("expected error, didn't get one ") + } + + if err := tempDevfile.Close(); err != nil { + t.Errorf("failed to close temp devfile") + } + }) +} diff --git a/pkg/devfile/parser/context/context.go b/pkg/devfile/parser/context/context.go new file mode 100644 index 0000000..64e3b5d --- /dev/null +++ b/pkg/devfile/parser/context/context.go @@ -0,0 +1,145 @@ +package parser + +import ( + "fmt" + "net/url" + "os" + "path/filepath" + "strings" + + "github.com/devfile/library/pkg/testingutil/filesystem" + "github.com/devfile/library/pkg/util" + "k8s.io/klog" +) + +// DevfileCtx stores context info regarding devfile +type DevfileCtx struct { + + // devfile ApiVersion + apiVersion string + + // absolute path of devfile + absPath string + + // relative path of devfile + relPath string + + // raw content of the devfile + rawContent []byte + + // devfile json schema + jsonSchema string + + //url path of the devfile + url string + + // filesystem for devfile + fs filesystem.Filesystem +} + +// NewDevfileCtx returns a new DevfileCtx type object +func NewDevfileCtx(path string) DevfileCtx { + return DevfileCtx{ + relPath: path, + fs: filesystem.DefaultFs{}, + } +} + +// NewURLDevfileCtx returns a new DevfileCtx type object +func NewURLDevfileCtx(url string) DevfileCtx { + return DevfileCtx{ + url: url, + } +} + +// NewByteContentDevfileCtx set devfile content from byte data and returns a new DevfileCtx type object and error +func NewByteContentDevfileCtx(data []byte) (d DevfileCtx, err error) { + err = d.SetDevfileContentFromBytes(data) + if err != nil { + return DevfileCtx{}, err + } + return d, nil +} + +// populateDevfile checks the API version is supported and returns the JSON schema for the given devfile API Version +func (d *DevfileCtx) populateDevfile() (err error) { + + // Get devfile APIVersion + if err := d.SetDevfileAPIVersion(); err != nil { + return err + } + + // Read and save devfile JSON schema for provided apiVersion + return d.SetDevfileJSONSchema() +} + +// Populate fills the DevfileCtx struct with relevant context info +func (d *DevfileCtx) Populate() (err error) { + if !strings.HasSuffix(d.relPath, ".yaml") { + if _, err := os.Stat(filepath.Join(d.relPath, "devfile.yaml")); os.IsNotExist(err) { + if _, err := os.Stat(filepath.Join(d.relPath, ".devfile.yaml")); os.IsNotExist(err) { + return fmt.Errorf("the provided path is not a valid yaml filepath, and devfile.yaml or .devfile.yaml not found in the provided path : %s", d.relPath) + } else { + d.relPath = filepath.Join(d.relPath, ".devfile.yaml") + } + } else { + d.relPath = filepath.Join(d.relPath, "devfile.yaml") + } + } + if err := d.SetAbsPath(); err != nil { + return err + } + klog.V(4).Infof("absolute devfile path: '%s'", d.absPath) + // Read and save devfile content + if err := d.SetDevfileContent(); err != nil { + return err + } + return d.populateDevfile() +} + +// PopulateFromURL fills the DevfileCtx struct with relevant context info +func (d *DevfileCtx) PopulateFromURL() (err error) { + _, err = url.ParseRequestURI(d.url) + if err != nil { + return err + } + // Read and save devfile content + if err := d.SetDevfileContent(); err != nil { + return err + } + return d.populateDevfile() +} + +// PopulateFromRaw fills the DevfileCtx struct with relevant context info +func (d *DevfileCtx) PopulateFromRaw() (err error) { + return d.populateDevfile() +} + +// Validate func validates devfile JSON schema for the given apiVersion +func (d *DevfileCtx) Validate() error { + + // Validate devfile + return d.ValidateDevfileSchema() +} + +// GetAbsPath func returns current devfile absolute path +func (d *DevfileCtx) GetAbsPath() string { + return d.absPath +} + +// GetURL func returns current devfile absolute URL address +func (d *DevfileCtx) GetURL() string { + return d.url +} + +// SetAbsPath sets absolute file path for devfile +func (d *DevfileCtx) SetAbsPath() (err error) { + // Set devfile absolute path + if d.absPath, err = util.GetAbsPath(d.relPath); err != nil { + return err + } + klog.V(2).Infof("absolute devfile path: '%s'", d.absPath) + + return nil + +} diff --git a/pkg/devfile/parser/context/context_test.go b/pkg/devfile/parser/context/context_test.go new file mode 100644 index 0000000..02eb88f --- /dev/null +++ b/pkg/devfile/parser/context/context_test.go @@ -0,0 +1,73 @@ +package parser + +import ( + "github.com/stretchr/testify/assert" + "net/http" + "net/http/httptest" + "testing" +) + +func TestPopulateFromBytes(t *testing.T) { + failedToConvertYamlErr := "failed to convert devfile yaml to json: yaml: mapping values are not allowed in this context" + + tests := []struct { + name string + dataFunc func() []byte + expectError *string + }{ + { + name: "valid data passed", + dataFunc: validJsonRawContent200, + }, + { + name: "invalid data passed", + dataFunc: invalidJsonRawContent200, + expectError: &failedToConvertYamlErr, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, err := w.Write(tt.dataFunc()) + if err != nil { + t.Error(err) + } + })) + var ( + d = DevfileCtx{ + url: testServer.URL, + } + ) + defer testServer.Close() + err := d.PopulateFromURL() + if (tt.expectError != nil) != (err != nil) { + t.Errorf("TestPopulateFromBytes(): unexpected error: %v, wantErr: %v", err, tt.expectError) + } else if tt.expectError != nil { + assert.Regexp(t, *tt.expectError, err.Error(), "TestPopulateFromBytes(): Error message should match") + } + }) + } +} + +func TestPopulateFromInvalidURL(t *testing.T) { + expectError := ".*invalid URI for request" + t.Run("Populate from invalid URL", func(t *testing.T) { + var ( + d = DevfileCtx{ + url: "blah", + } + ) + + err := d.PopulateFromURL() + + if err == nil { + t.Errorf("TestPopulateFromInvalidURL(): expected an error, didn't get one") + } else { + assert.Regexp(t, expectError, err.Error(), "TestPopulateFromInvalidURL(): Error message should match") + } + }) +} + +func invalidJsonRawContent200() []byte { + return []byte(InvalidDevfileContent) +} diff --git a/pkg/devfile/parser/context/fakecontext.go b/pkg/devfile/parser/context/fakecontext.go new file mode 100644 index 0000000..2db3c1b --- /dev/null +++ b/pkg/devfile/parser/context/fakecontext.go @@ -0,0 +1,10 @@ +package parser + +import "github.com/devfile/library/pkg/testingutil/filesystem" + +func FakeContext(fs filesystem.Filesystem, absPath string) DevfileCtx { + return DevfileCtx{ + fs: fs, + absPath: absPath, + } +} diff --git a/pkg/devfile/parser/context/fs.go b/pkg/devfile/parser/context/fs.go new file mode 100644 index 0000000..d399248 --- /dev/null +++ b/pkg/devfile/parser/context/fs.go @@ -0,0 +1,8 @@ +package parser + +import "github.com/devfile/library/pkg/testingutil/filesystem" + +// GetFs returns the filesystem object +func (d *DevfileCtx) GetFs() filesystem.Filesystem { + return d.fs +} diff --git a/pkg/devfile/parser/context/schema.go b/pkg/devfile/parser/context/schema.go new file mode 100644 index 0000000..13326e9 --- /dev/null +++ b/pkg/devfile/parser/context/schema.go @@ -0,0 +1,48 @@ +package parser + +import ( + "fmt" + + "github.com/devfile/library/pkg/devfile/parser/data" + "github.com/pkg/errors" + "github.com/xeipuuv/gojsonschema" + "k8s.io/klog" +) + +// SetDevfileJSONSchema returns the JSON schema for the given devfile apiVersion +func (d *DevfileCtx) SetDevfileJSONSchema() error { + + // Check if json schema is present for the given apiVersion + jsonSchema, err := data.GetDevfileJSONSchema(d.apiVersion) + if err != nil { + return err + } + d.jsonSchema = jsonSchema + return nil +} + +// ValidateDevfileSchema validate JSON schema of the provided devfile +func (d *DevfileCtx) ValidateDevfileSchema() error { + var ( + schemaLoader = gojsonschema.NewStringLoader(d.jsonSchema) + documentLoader = gojsonschema.NewStringLoader(string(d.rawContent)) + ) + + // Validate devfile with JSON schema + result, err := gojsonschema.Validate(schemaLoader, documentLoader) + if err != nil { + return errors.Wrapf(err, "failed to validate devfile schema") + } + + if !result.Valid() { + errMsg := "invalid devfile schema. errors :\n" + for _, desc := range result.Errors() { + errMsg = errMsg + fmt.Sprintf("- %s\n", desc) + } + return fmt.Errorf(errMsg) + } + + // Sucessful + klog.V(4).Info("validated devfile schema") + return nil +} diff --git a/pkg/devfile/parser/context/schema_test.go b/pkg/devfile/parser/context/schema_test.go new file mode 100644 index 0000000..3d0710f --- /dev/null +++ b/pkg/devfile/parser/context/schema_test.go @@ -0,0 +1,159 @@ +package parser + +import ( + "github.com/stretchr/testify/assert" + "testing" + + v200 "github.com/devfile/library/pkg/devfile/parser/data/v2/2.0.0" +) + +const ( + validJson200 = `{ + "schemaVersion": "2.0.0", + "metadata": { + "name": "nodejs-stack" + }, + "projects": [ + { + "name": "project", + "git": { + "remotes": { + "origin": "https://github.com/che-samples/web-nodejs-sample.git" + } + } + } + ], + "components": [ + { + "name": "che-theia-plugin", + "plugin": { + "id": "eclipse/che-theia/7.1.0" + } + }, + { + "name": "che-exec-plugin", + "plugin": { + "id": "eclipse/che-machine-exec-plugin/7.1.0" + } + }, + { + "name": "typescript-plugin", + "plugin": { + "id": "che-incubator/typescript/1.30.2", + "components": [ + { + "name": "somecontainer", + "container": { + "memoryLimit": "512Mi" + } + } + ] + } + }, + { + "name": "nodejs", + "container": { + "image": "quay.io/eclipse/che-nodejs10-ubi:nightly", + "memoryLimit": "512Mi", + "endpoints": [ + { + "name": "nodejs", + "protocol": "http", + "targetPort": 3000 + } + ], + "mountSources": true + } + } + ], + "commands": [ + { + "id": "download-dependencies", + "exec": { + "component": "nodejs", + "commandLine": "npm install", + "workingDir": "${PROJECTS_ROOT}/project/app", + "group": { + "kind": "build" + } + } + }, + { + "id": "run-the-app", + "exec": { + "component": "nodejs", + "commandLine": "nodemon app.js", + "workingDir": "${PROJECTS_ROOT}/project/app", + "group": { + "kind": "run", + "isDefault": true + } + } + }, + { + "id": "run-the-app-debugging-enabled", + "exec": { + "component": "nodejs", + "commandLine": "nodemon --inspect app.js", + "workingDir": "${PROJECTS_ROOT}/project/app", + "group": { + "kind": "run" + } + } + }, + { + "id": "stop-the-app", + "exec": { + "component": "nodejs", + "commandLine": "node_server_pids=$(pgrep -fx '.*nodemon (--inspect )?app.js' | tr \"\\\\n\" \" \") && echo \"Stopping node server with PIDs: ${node_server_pids}\" && kill -15 ${node_server_pids} &>/dev/null && echo 'Done.'" + } + }, + { + "id": "attach-remote-debugger", + "vscodeLaunch": { + "inlined": "{\n \"version\": \"0.2.0\",\n \"configurations\": [\n {\n \"type\": \"node\",\n \"request\": \"attach\",\n \"name\": \"Attach to Remote\",\n \"address\": \"localhost\",\n \"port\": 9229,\n \"localRoot\": \"${workspaceFolder}\",\n \"remoteRoot\": \"${workspaceFolder}\"\n }\n ]\n}\n" + } + } + ] + }` +) + +func TestValidateDevfileSchema(t *testing.T) { + + t.Run("valid 2.0.0 json schema", func(t *testing.T) { + + var ( + d = DevfileCtx{ + jsonSchema: v200.JsonSchema200, + rawContent: validJsonRawContent200(), + } + ) + + err := d.ValidateDevfileSchema() + if err != nil { + t.Errorf("TestValidateDevfileSchema() unexpected error: '%v'", err) + } + }) + + expectedErr := "invalid devfile schema. errors :\n*.*schemaVersion is required" + t.Run("invalid 2.0.0 json schema", func(t *testing.T) { + + var ( + d = DevfileCtx{ + jsonSchema: v200.JsonSchema200, + rawContent: []byte("{}"), + } + ) + + err := d.ValidateDevfileSchema() + if err == nil { + t.Errorf("TestValidateDevfileSchema() expected error, didn't get one") + } else { + assert.Regexp(t, expectedErr, err.Error(), "TestValidateDevfileSchema(): Error message should match") + } + }) +} + +func validJsonRawContent200() []byte { + return []byte(validJson200) +} diff --git a/pkg/devfile/parser/data/helper.go b/pkg/devfile/parser/data/helper.go new file mode 100644 index 0000000..190acbc --- /dev/null +++ b/pkg/devfile/parser/data/helper.go @@ -0,0 +1,50 @@ +package data + +import ( + "fmt" + "reflect" + "strings" + + "k8s.io/klog" +) + +// String converts supportedApiVersion type to string type +func (s supportedApiVersion) String() string { + return string(s) +} + +// NewDevfileData returns relevant devfile struct for the provided API version +func NewDevfileData(version string) (obj DevfileData, err error) { + + // Fetch devfile struct type from map + devfileType, ok := apiVersionToDevfileStruct[supportedApiVersion(version)] + if !ok { + errMsg := fmt.Sprintf("devfile type not present for apiVersion '%s'", version) + return obj, fmt.Errorf(errMsg) + } + + return reflect.New(devfileType).Interface().(DevfileData), nil +} + +// GetDevfileJSONSchema returns the devfile JSON schema of the supported apiVersion +func GetDevfileJSONSchema(version string) (string, error) { + + // Fetch json schema from the devfileApiVersionToJSONSchema map + schema, ok := devfileApiVersionToJSONSchema[supportedApiVersion(version)] + if !ok { + var supportedVersions []string + for version := range devfileApiVersionToJSONSchema { + supportedVersions = append(supportedVersions, string(version)) + } + return "", fmt.Errorf("unable to find schema for version %q. The parser supports devfile schema for version %s", version, strings.Join(supportedVersions, ", ")) + } + klog.V(4).Infof("devfile apiVersion '%s' is supported", version) + + // Successful + return schema, nil +} + +// IsApiVersionSupported returns true if the API version is supported +func IsApiVersionSupported(version string) bool { + return apiVersionToDevfileStruct[supportedApiVersion(version)] != nil +} diff --git a/pkg/devfile/parser/data/helper_test.go b/pkg/devfile/parser/data/helper_test.go new file mode 100644 index 0000000..e35de0b --- /dev/null +++ b/pkg/devfile/parser/data/helper_test.go @@ -0,0 +1,107 @@ +package data + +import ( + "reflect" + "strings" + "testing" + + v2 "github.com/devfile/library/pkg/devfile/parser/data/v2" + v200 "github.com/devfile/library/pkg/devfile/parser/data/v2/2.0.0" +) + +func TestNewDevfileData(t *testing.T) { + + t.Run("valid devfile apiVersion", func(t *testing.T) { + + var ( + version = APISchemaVersion200 + want = reflect.TypeOf(&v2.DevfileV2{}) + obj, err = NewDevfileData(string(version)) + got = reflect.TypeOf(obj) + ) + + // got and want should be equal + if !reflect.DeepEqual(got, want) { + t.Errorf("got: '%v', want: '%s'", got, want) + } + + // no error should be received + if err != nil { + t.Errorf("did not expect an error '%v'", err) + } + }) + + t.Run("invalid devfile apiVersion", func(t *testing.T) { + + var ( + version = "invalidVersion" + _, err = NewDevfileData(string(version)) + ) + + // no error should be received + if err == nil { + t.Errorf("did not expect an error '%v'", err) + } + }) +} + +func TestGetDevfileJSONSchema(t *testing.T) { + + t.Run("valid devfile apiVersion", func(t *testing.T) { + + var ( + version = APISchemaVersion200 + want = v200.JsonSchema200 + got, err = GetDevfileJSONSchema(string(version)) + ) + + if err != nil { + t.Errorf("did not expect an error '%v'", err) + } + + if strings.Compare(got, want) != 0 { + t.Errorf("incorrect json schema") + } + }) + + t.Run("invalid devfile apiVersion", func(t *testing.T) { + + var ( + version = "invalidVersion" + _, err = GetDevfileJSONSchema(string(version)) + ) + + if err == nil { + t.Errorf("expected an error, didn't get one") + } + }) +} + +func TestIsApiVersionSupported(t *testing.T) { + + t.Run("valid devfile apiVersion", func(t *testing.T) { + + var ( + version = APISchemaVersion200 + want = true + got = IsApiVersionSupported(string(version)) + ) + + if got != want { + t.Errorf("want: '%t', got: '%t'", want, got) + } + }) + + t.Run("invalid devfile apiVersion", func(t *testing.T) { + + var ( + version = "invalidVersion" + want = false + got = IsApiVersionSupported(string(version)) + ) + + if got != want { + t.Errorf("expected an error, didn't get one") + } + }) +} diff --git a/pkg/devfile/parser/data/interface.go b/pkg/devfile/parser/data/interface.go new file mode 100644 index 0000000..743077e --- /dev/null +++ b/pkg/devfile/parser/data/interface.go @@ -0,0 +1,91 @@ +package data + +import ( + v1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + "github.com/devfile/api/v2/pkg/attributes" + devfilepkg "github.com/devfile/api/v2/pkg/devfile" + "github.com/devfile/library/pkg/devfile/parser/data/v2/common" +) + +// Generate mock interfaces for DevfileData by executing the following cmd in pkg/devfile/parser/data +// mockgen -package=data -source=interface.go DevfileData > /tmp/mock_interface.go ; cp /tmp/mock_interface.go ./mock_interface.go + +// DevfileData is an interface that defines functions for Devfile data operations +type DevfileData interface { + + // header related methods + + GetSchemaVersion() string + SetSchemaVersion(version string) + GetMetadata() devfilepkg.DevfileMetadata + SetMetadata(metadata devfilepkg.DevfileMetadata) + + // top-level attributes related method + + GetAttributes() (attributes.Attributes, error) + AddAttributes(key string, value interface{}) error + UpdateAttributes(key string, value interface{}) error + + // parent related methods + + GetParent() *v1.Parent + SetParent(parent *v1.Parent) + + // event related methods + + GetEvents() v1.Events + AddEvents(events v1.Events) error + UpdateEvents(postStart, postStop, preStart, preStop []string) + + // component related methods + + GetComponents(common.DevfileOptions) ([]v1.Component, error) + AddComponents(components []v1.Component) error + UpdateComponent(component v1.Component) error + DeleteComponent(name string) error + + // project related methods + + GetProjects(common.DevfileOptions) ([]v1.Project, error) + AddProjects(projects []v1.Project) error + UpdateProject(project v1.Project) error + DeleteProject(name string) error + + // starter projects related commands + + GetStarterProjects(common.DevfileOptions) ([]v1.StarterProject, error) + AddStarterProjects(projects []v1.StarterProject) error + UpdateStarterProject(project v1.StarterProject) error + DeleteStarterProject(name string) error + + // command related methods + + GetCommands(common.DevfileOptions) ([]v1.Command, error) + AddCommands(commands []v1.Command) error + UpdateCommand(command v1.Command) error + DeleteCommand(id string) error + + // volume mount related methods + + AddVolumeMounts(containerName string, volumeMounts []v1.VolumeMount) error + DeleteVolumeMount(name string) error + GetVolumeMountPaths(mountName, containerName string) ([]string, error) + + // workspace related methods + + GetDevfileWorkspaceSpecContent() *v1.DevWorkspaceTemplateSpecContent + SetDevfileWorkspaceSpecContent(content v1.DevWorkspaceTemplateSpecContent) + GetDevfileWorkspaceSpec() *v1.DevWorkspaceTemplateSpec + SetDevfileWorkspaceSpec(spec v1.DevWorkspaceTemplateSpec) + + // utils + + GetDevfileContainerComponents(common.DevfileOptions) ([]v1.Component, error) + GetDevfileVolumeComponents(common.DevfileOptions) ([]v1.Component, error) + + // containers + RemoveEnvVars(containerEnvMap map[string][]string) error + SetPorts(containerPortsMap map[string][]string) error + AddEnvVars(containerEnvMap map[string][]v1.EnvVar) error + RemovePorts(containerPortsMap map[string][]string) error +} diff --git a/pkg/devfile/parser/data/mock_interface.go b/pkg/devfile/parser/data/mock_interface.go new file mode 100644 index 0000000..fb21d6a --- /dev/null +++ b/pkg/devfile/parser/data/mock_interface.go @@ -0,0 +1,608 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: interface.go + +// Package data is a generated GoMock package. +package data + +import ( + reflect "reflect" + + v1alpha2 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + attributes "github.com/devfile/api/v2/pkg/attributes" + devfile "github.com/devfile/api/v2/pkg/devfile" + common "github.com/devfile/library/pkg/devfile/parser/data/v2/common" + gomock "github.com/golang/mock/gomock" +) + +// MockDevfileData is a mock of DevfileData interface. +type MockDevfileData struct { + ctrl *gomock.Controller + recorder *MockDevfileDataMockRecorder +} + +// MockDevfileDataMockRecorder is the mock recorder for MockDevfileData. +type MockDevfileDataMockRecorder struct { + mock *MockDevfileData +} + +// NewMockDevfileData creates a new mock instance. +func NewMockDevfileData(ctrl *gomock.Controller) *MockDevfileData { + mock := &MockDevfileData{ctrl: ctrl} + mock.recorder = &MockDevfileDataMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockDevfileData) EXPECT() *MockDevfileDataMockRecorder { + return m.recorder +} + +// AddAttributes mocks base method. +func (m *MockDevfileData) AddAttributes(key string, value interface{}) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddAttributes", key, value) + ret0, _ := ret[0].(error) + return ret0 +} + +// AddAttributes indicates an expected call of AddAttributes. +func (mr *MockDevfileDataMockRecorder) AddAttributes(key, value interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddAttributes", reflect.TypeOf((*MockDevfileData)(nil).AddAttributes), key, value) +} + +// AddCommands mocks base method. +func (m *MockDevfileData) AddCommands(commands []v1alpha2.Command) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddCommands", commands) + ret0, _ := ret[0].(error) + return ret0 +} + +// AddCommands indicates an expected call of AddCommands. +func (mr *MockDevfileDataMockRecorder) AddCommands(commands interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddCommands", reflect.TypeOf((*MockDevfileData)(nil).AddCommands), commands) +} + +// AddComponents mocks base method. +func (m *MockDevfileData) AddComponents(components []v1alpha2.Component) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddComponents", components) + ret0, _ := ret[0].(error) + return ret0 +} + +// AddComponents indicates an expected call of AddComponents. +func (mr *MockDevfileDataMockRecorder) AddComponents(components interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddComponents", reflect.TypeOf((*MockDevfileData)(nil).AddComponents), components) +} + +// AddEvents mocks base method. +func (m *MockDevfileData) AddEvents(events v1alpha2.Events) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddEvents", events) + ret0, _ := ret[0].(error) + return ret0 +} + +// AddEvents indicates an expected call of AddEvents. +func (mr *MockDevfileDataMockRecorder) AddEvents(events interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddEvents", reflect.TypeOf((*MockDevfileData)(nil).AddEvents), events) +} + +// AddProjects mocks base method. +func (m *MockDevfileData) AddProjects(projects []v1alpha2.Project) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddProjects", projects) + ret0, _ := ret[0].(error) + return ret0 +} + +// AddProjects indicates an expected call of AddProjects. +func (mr *MockDevfileDataMockRecorder) AddProjects(projects interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddProjects", reflect.TypeOf((*MockDevfileData)(nil).AddProjects), projects) +} + +// AddStarterProjects mocks base method. +func (m *MockDevfileData) AddStarterProjects(projects []v1alpha2.StarterProject) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddStarterProjects", projects) + ret0, _ := ret[0].(error) + return ret0 +} + +// AddStarterProjects indicates an expected call of AddStarterProjects. +func (mr *MockDevfileDataMockRecorder) AddStarterProjects(projects interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddStarterProjects", reflect.TypeOf((*MockDevfileData)(nil).AddStarterProjects), projects) +} + +// AddVolumeMounts mocks base method. +func (m *MockDevfileData) AddVolumeMounts(containerName string, volumeMounts []v1alpha2.VolumeMount) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddVolumeMounts", containerName, volumeMounts) + ret0, _ := ret[0].(error) + return ret0 +} + +// AddVolumeMounts indicates an expected call of AddVolumeMounts. +func (mr *MockDevfileDataMockRecorder) AddVolumeMounts(containerName, volumeMounts interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddVolumeMounts", reflect.TypeOf((*MockDevfileData)(nil).AddVolumeMounts), containerName, volumeMounts) +} + +// DeleteCommand mocks base method. +func (m *MockDevfileData) DeleteCommand(id string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteCommand", id) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteCommand indicates an expected call of DeleteCommand. +func (mr *MockDevfileDataMockRecorder) DeleteCommand(id interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteCommand", reflect.TypeOf((*MockDevfileData)(nil).DeleteCommand), id) +} + +// DeleteComponent mocks base method. +func (m *MockDevfileData) DeleteComponent(name string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteComponent", name) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteComponent indicates an expected call of DeleteComponent. +func (mr *MockDevfileDataMockRecorder) DeleteComponent(name interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteComponent", reflect.TypeOf((*MockDevfileData)(nil).DeleteComponent), name) +} + +// DeleteProject mocks base method. +func (m *MockDevfileData) DeleteProject(name string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteProject", name) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteProject indicates an expected call of DeleteProject. +func (mr *MockDevfileDataMockRecorder) DeleteProject(name interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteProject", reflect.TypeOf((*MockDevfileData)(nil).DeleteProject), name) +} + +// DeleteStarterProject mocks base method. +func (m *MockDevfileData) DeleteStarterProject(name string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteStarterProject", name) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteStarterProject indicates an expected call of DeleteStarterProject. +func (mr *MockDevfileDataMockRecorder) DeleteStarterProject(name interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteStarterProject", reflect.TypeOf((*MockDevfileData)(nil).DeleteStarterProject), name) +} + +// DeleteVolumeMount mocks base method. +func (m *MockDevfileData) DeleteVolumeMount(name string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteVolumeMount", name) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteVolumeMount indicates an expected call of DeleteVolumeMount. +func (mr *MockDevfileDataMockRecorder) DeleteVolumeMount(name interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteVolumeMount", reflect.TypeOf((*MockDevfileData)(nil).DeleteVolumeMount), name) +} + +// GetAttributes mocks base method. +func (m *MockDevfileData) GetAttributes() (attributes.Attributes, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAttributes") + ret0, _ := ret[0].(attributes.Attributes) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAttributes indicates an expected call of GetAttributes. +func (mr *MockDevfileDataMockRecorder) GetAttributes() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAttributes", reflect.TypeOf((*MockDevfileData)(nil).GetAttributes)) +} + +// GetCommands mocks base method. +func (m *MockDevfileData) GetCommands(arg0 common.DevfileOptions) ([]v1alpha2.Command, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCommands", arg0) + ret0, _ := ret[0].([]v1alpha2.Command) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCommands indicates an expected call of GetCommands. +func (mr *MockDevfileDataMockRecorder) GetCommands(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCommands", reflect.TypeOf((*MockDevfileData)(nil).GetCommands), arg0) +} + +// GetComponents mocks base method. +func (m *MockDevfileData) GetComponents(arg0 common.DevfileOptions) ([]v1alpha2.Component, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetComponents", arg0) + ret0, _ := ret[0].([]v1alpha2.Component) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetComponents indicates an expected call of GetComponents. +func (mr *MockDevfileDataMockRecorder) GetComponents(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetComponents", reflect.TypeOf((*MockDevfileData)(nil).GetComponents), arg0) +} + +// GetDevfileContainerComponents mocks base method. +func (m *MockDevfileData) GetDevfileContainerComponents(arg0 common.DevfileOptions) ([]v1alpha2.Component, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetDevfileContainerComponents", arg0) + ret0, _ := ret[0].([]v1alpha2.Component) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetDevfileContainerComponents indicates an expected call of GetDevfileContainerComponents. +func (mr *MockDevfileDataMockRecorder) GetDevfileContainerComponents(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDevfileContainerComponents", reflect.TypeOf((*MockDevfileData)(nil).GetDevfileContainerComponents), arg0) +} + +// GetDevfileVolumeComponents mocks base method. +func (m *MockDevfileData) GetDevfileVolumeComponents(arg0 common.DevfileOptions) ([]v1alpha2.Component, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetDevfileVolumeComponents", arg0) + ret0, _ := ret[0].([]v1alpha2.Component) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetDevfileVolumeComponents indicates an expected call of GetDevfileVolumeComponents. +func (mr *MockDevfileDataMockRecorder) GetDevfileVolumeComponents(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDevfileVolumeComponents", reflect.TypeOf((*MockDevfileData)(nil).GetDevfileVolumeComponents), arg0) +} + +// GetDevfileWorkspaceSpec mocks base method. +func (m *MockDevfileData) GetDevfileWorkspaceSpec() *v1alpha2.DevWorkspaceTemplateSpec { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetDevfileWorkspaceSpec") + ret0, _ := ret[0].(*v1alpha2.DevWorkspaceTemplateSpec) + return ret0 +} + +// GetDevfileWorkspaceSpec indicates an expected call of GetDevfileWorkspaceSpec. +func (mr *MockDevfileDataMockRecorder) GetDevfileWorkspaceSpec() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDevfileWorkspaceSpec", reflect.TypeOf((*MockDevfileData)(nil).GetDevfileWorkspaceSpec)) +} + +// GetDevfileWorkspaceSpecContent mocks base method. +func (m *MockDevfileData) GetDevfileWorkspaceSpecContent() *v1alpha2.DevWorkspaceTemplateSpecContent { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetDevfileWorkspaceSpecContent") + ret0, _ := ret[0].(*v1alpha2.DevWorkspaceTemplateSpecContent) + return ret0 +} + +// GetDevfileWorkspaceSpecContent indicates an expected call of GetDevfileWorkspaceSpecContent. +func (mr *MockDevfileDataMockRecorder) GetDevfileWorkspaceSpecContent() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDevfileWorkspaceSpecContent", reflect.TypeOf((*MockDevfileData)(nil).GetDevfileWorkspaceSpecContent)) +} + +// GetEvents mocks base method. +func (m *MockDevfileData) GetEvents() v1alpha2.Events { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetEvents") + ret0, _ := ret[0].(v1alpha2.Events) + return ret0 +} + +// GetEvents indicates an expected call of GetEvents. +func (mr *MockDevfileDataMockRecorder) GetEvents() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEvents", reflect.TypeOf((*MockDevfileData)(nil).GetEvents)) +} + +// GetMetadata mocks base method. +func (m *MockDevfileData) GetMetadata() devfile.DevfileMetadata { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetMetadata") + ret0, _ := ret[0].(devfile.DevfileMetadata) + return ret0 +} + +// GetMetadata indicates an expected call of GetMetadata. +func (mr *MockDevfileDataMockRecorder) GetMetadata() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMetadata", reflect.TypeOf((*MockDevfileData)(nil).GetMetadata)) +} + +// GetParent mocks base method. +func (m *MockDevfileData) GetParent() *v1alpha2.Parent { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetParent") + ret0, _ := ret[0].(*v1alpha2.Parent) + return ret0 +} + +// GetParent indicates an expected call of GetParent. +func (mr *MockDevfileDataMockRecorder) GetParent() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetParent", reflect.TypeOf((*MockDevfileData)(nil).GetParent)) +} + +// GetProjects mocks base method. +func (m *MockDevfileData) GetProjects(arg0 common.DevfileOptions) ([]v1alpha2.Project, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetProjects", arg0) + ret0, _ := ret[0].([]v1alpha2.Project) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetProjects indicates an expected call of GetProjects. +func (mr *MockDevfileDataMockRecorder) GetProjects(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProjects", reflect.TypeOf((*MockDevfileData)(nil).GetProjects), arg0) +} + +// GetSchemaVersion mocks base method. +func (m *MockDevfileData) GetSchemaVersion() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSchemaVersion") + ret0, _ := ret[0].(string) + return ret0 +} + +// GetSchemaVersion indicates an expected call of GetSchemaVersion. +func (mr *MockDevfileDataMockRecorder) GetSchemaVersion() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSchemaVersion", reflect.TypeOf((*MockDevfileData)(nil).GetSchemaVersion)) +} + +// GetStarterProjects mocks base method. +func (m *MockDevfileData) GetStarterProjects(arg0 common.DevfileOptions) ([]v1alpha2.StarterProject, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetStarterProjects", arg0) + ret0, _ := ret[0].([]v1alpha2.StarterProject) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetStarterProjects indicates an expected call of GetStarterProjects. +func (mr *MockDevfileDataMockRecorder) GetStarterProjects(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetStarterProjects", reflect.TypeOf((*MockDevfileData)(nil).GetStarterProjects), arg0) +} + +// GetVolumeMountPaths mocks base method. +func (m *MockDevfileData) GetVolumeMountPaths(mountName, containerName string) ([]string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetVolumeMountPaths", mountName, containerName) + ret0, _ := ret[0].([]string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetVolumeMountPaths indicates an expected call of GetVolumeMountPaths. +func (mr *MockDevfileDataMockRecorder) GetVolumeMountPaths(mountName, containerName interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetVolumeMountPaths", reflect.TypeOf((*MockDevfileData)(nil).GetVolumeMountPaths), mountName, containerName) +} + +// SetDevfileWorkspaceSpec mocks base method. +func (m *MockDevfileData) SetDevfileWorkspaceSpec(spec v1alpha2.DevWorkspaceTemplateSpec) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetDevfileWorkspaceSpec", spec) +} + +// SetDevfileWorkspaceSpec indicates an expected call of SetDevfileWorkspaceSpec. +func (mr *MockDevfileDataMockRecorder) SetDevfileWorkspaceSpec(spec interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDevfileWorkspaceSpec", reflect.TypeOf((*MockDevfileData)(nil).SetDevfileWorkspaceSpec), spec) +} + +// SetDevfileWorkspaceSpecContent mocks base method. +func (m *MockDevfileData) SetDevfileWorkspaceSpecContent(content v1alpha2.DevWorkspaceTemplateSpecContent) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetDevfileWorkspaceSpecContent", content) +} + +// SetDevfileWorkspaceSpecContent indicates an expected call of SetDevfileWorkspaceSpecContent. +func (mr *MockDevfileDataMockRecorder) SetDevfileWorkspaceSpecContent(content interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDevfileWorkspaceSpecContent", reflect.TypeOf((*MockDevfileData)(nil).SetDevfileWorkspaceSpecContent), content) +} + +// SetMetadata mocks base method. +func (m *MockDevfileData) SetMetadata(metadata devfile.DevfileMetadata) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetMetadata", metadata) +} + +// SetMetadata indicates an expected call of SetMetadata. +func (mr *MockDevfileDataMockRecorder) SetMetadata(metadata interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetMetadata", reflect.TypeOf((*MockDevfileData)(nil).SetMetadata), metadata) +} + +// SetParent mocks base method. +func (m *MockDevfileData) SetParent(parent *v1alpha2.Parent) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetParent", parent) +} + +// SetParent indicates an expected call of SetParent. +func (mr *MockDevfileDataMockRecorder) SetParent(parent interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetParent", reflect.TypeOf((*MockDevfileData)(nil).SetParent), parent) +} + +// SetSchemaVersion mocks base method. +func (m *MockDevfileData) SetSchemaVersion(version string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetSchemaVersion", version) +} + +// SetSchemaVersion indicates an expected call of SetSchemaVersion. +func (mr *MockDevfileDataMockRecorder) SetSchemaVersion(version interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetSchemaVersion", reflect.TypeOf((*MockDevfileData)(nil).SetSchemaVersion), version) +} + +// UpdateAttributes mocks base method. +func (m *MockDevfileData) UpdateAttributes(key string, value interface{}) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateAttributes", key, value) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateAttributes indicates an expected call of UpdateAttributes. +func (mr *MockDevfileDataMockRecorder) UpdateAttributes(key, value interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAttributes", reflect.TypeOf((*MockDevfileData)(nil).UpdateAttributes), key, value) +} + +// UpdateCommand mocks base method. +func (m *MockDevfileData) UpdateCommand(command v1alpha2.Command) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateCommand", command) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateCommand indicates an expected call of UpdateCommand. +func (mr *MockDevfileDataMockRecorder) UpdateCommand(command interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateCommand", reflect.TypeOf((*MockDevfileData)(nil).UpdateCommand), command) +} + +// UpdateComponent mocks base method. +func (m *MockDevfileData) UpdateComponent(component v1alpha2.Component) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateComponent", component) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateComponent indicates an expected call of UpdateComponent. +func (mr *MockDevfileDataMockRecorder) UpdateComponent(component interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateComponent", reflect.TypeOf((*MockDevfileData)(nil).UpdateComponent), component) +} + +// UpdateEvents mocks base method. +func (m *MockDevfileData) UpdateEvents(postStart, postStop, preStart, preStop []string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "UpdateEvents", postStart, postStop, preStart, preStop) +} + +// UpdateEvents indicates an expected call of UpdateEvents. +func (mr *MockDevfileDataMockRecorder) UpdateEvents(postStart, postStop, preStart, preStop interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateEvents", reflect.TypeOf((*MockDevfileData)(nil).UpdateEvents), postStart, postStop, preStart, preStop) +} + +// UpdateProject mocks base method. +func (m *MockDevfileData) UpdateProject(project v1alpha2.Project) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateProject", project) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateProject indicates an expected call of UpdateProject. +func (mr *MockDevfileDataMockRecorder) UpdateProject(project interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateProject", reflect.TypeOf((*MockDevfileData)(nil).UpdateProject), project) +} + +// UpdateStarterProject mocks base method. +func (m *MockDevfileData) UpdateStarterProject(project v1alpha2.StarterProject) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateStarterProject", project) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateStarterProject indicates an expected call of UpdateStarterProject. +func (mr *MockDevfileDataMockRecorder) UpdateStarterProject(project interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateStarterProject", reflect.TypeOf((*MockDevfileData)(nil).UpdateStarterProject), project) +} + +// RemoveEnvVars mocks base method +func (m *MockDevfileData) RemoveEnvVars(containerEnvMap map[string][]string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RemoveEnvVars", containerEnvMap) + ret0, _ := ret[0].(error) + return ret0 +} + +// RemoveEnvVars indicates an expected call of RemoveEnvVars +func (mr *MockDevfileDataMockRecorder) RemoveEnvVars(containerEnvMap interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveEnvVars", reflect.TypeOf((*MockDevfileData)(nil).RemoveEnvVars), containerEnvMap) +} + +// SetPorts mocks base method +func (m *MockDevfileData) SetPorts(containerPortsMap map[string][]string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetPorts", containerPortsMap) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetPorts indicates an expected call of SetPorts +func (mr *MockDevfileDataMockRecorder) SetPorts(containerPortsMap interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetPorts", reflect.TypeOf((*MockDevfileData)(nil).SetPorts), containerPortsMap) +} + +// AddEnvVars mocks base method +func (m *MockDevfileData) AddEnvVars(containerEnvMap map[string][]v1alpha2.EnvVar) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddEnvVars", containerEnvMap) + ret0, _ := ret[0].(error) + return ret0 +} + +// AddEnvVars indicates an expected call of AddEnvVars +func (mr *MockDevfileDataMockRecorder) AddEnvVars(containerEnvMap interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddEnvVars", reflect.TypeOf((*MockDevfileData)(nil).AddEnvVars), containerEnvMap) +} + +// RemovePorts mocks base method +func (m *MockDevfileData) RemovePorts(containerPortsMap map[string][]string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RemovePorts", containerPortsMap) + ret0, _ := ret[0].(error) + return ret0 +} + +// RemovePorts indicates an expected call of RemovePorts +func (mr *MockDevfileDataMockRecorder) RemovePorts(containerPortsMap interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemovePorts", reflect.TypeOf((*MockDevfileData)(nil).RemovePorts), containerPortsMap) +} diff --git a/pkg/devfile/parser/data/v2/2.0.0/devfileJsonSchema200.go b/pkg/devfile/parser/data/v2/2.0.0/devfileJsonSchema200.go new file mode 100644 index 0000000..b4c2cd5 --- /dev/null +++ b/pkg/devfile/parser/data/v2/2.0.0/devfileJsonSchema200.go @@ -0,0 +1,3378 @@ +package version200 + +// https://raw.githubusercontent.com/devfile/api/2.0.x/schemas/latest/devfile.json +const JsonSchema200 = `{ + "description": "Devfile describes the structure of a cloud-native workspace and development environment.", + "type": "object", + "title": "Devfile schema - Version 2.0.0", + "required": [ + "schemaVersion" + ], + "properties": { + "commands": { + "description": "Predefined, ready-to-use, workspace-related commands", + "type": "array", + "items": { + "type": "object", + "required": [ + "id" + ], + "oneOf": [ + { + "required": [ + "exec" + ] + }, + { + "required": [ + "apply" + ] + }, + { + "required": [ + "vscodeTask" + ] + }, + { + "required": [ + "vscodeLaunch" + ] + }, + { + "required": [ + "composite" + ] + } + ], + "properties": { + "apply": { + "description": "Command that consists in applying a given component definition, typically bound to a workspace event.\n\nFor example, when an 'apply' command is bound to a 'preStart' event, and references a 'container' component, it will start the container as a K8S initContainer in the workspace POD, unless the component has its 'dedicatedPod' field set to 'true'.\n\nWhen no 'apply' command exist for a given component, it is assumed the component will be applied at workspace start by default.", + "type": "object", + "required": [ + "component" + ], + "properties": { + "component": { + "description": "Describes component that will be applied", + "type": "string" + }, + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "required": [ + "kind" + ], + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": [ + "build", + "run", + "test", + "debug" + ] + } + }, + "additionalProperties": false + }, + "label": { + "description": "Optional label that provides a label for this command to be used in Editor UI menus for example", + "type": "string" + } + }, + "additionalProperties": false + }, + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "composite": { + "description": "Composite command that allows executing several sub-commands either sequentially or concurrently", + "type": "object", + "properties": { + "commands": { + "description": "The commands that comprise this composite command", + "type": "array", + "items": { + "type": "string" + } + }, + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "required": [ + "kind" + ], + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": [ + "build", + "run", + "test", + "debug" + ] + } + }, + "additionalProperties": false + }, + "label": { + "description": "Optional label that provides a label for this command to be used in Editor UI menus for example", + "type": "string" + }, + "parallel": { + "description": "Indicates if the sub-commands should be executed concurrently", + "type": "boolean" + } + }, + "additionalProperties": false + }, + "exec": { + "description": "CLI Command executed in an existing component container", + "type": "object", + "required": [ + "commandLine", + "component" + ], + "properties": { + "commandLine": { + "description": "The actual command-line string\n\nSpecial variables that can be used:\n\n - '$PROJECTS_ROOT': A path where projects sources are mounted as defined by container component's sourceMapping.\n\n - '$PROJECT_SOURCE': A path to a project source ($PROJECTS_ROOT/\u003cproject-name\u003e). If there are multiple projects, this will point to the directory of the first one.", + "type": "string" + }, + "component": { + "description": "Describes component to which given action relates", + "type": "string" + }, + "env": { + "description": "Optional list of environment variables that have to be set before running the command", + "type": "array", + "items": { + "type": "object", + "required": [ + "name", + "value" + ], + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "required": [ + "kind" + ], + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": [ + "build", + "run", + "test", + "debug" + ] + } + }, + "additionalProperties": false + }, + "hotReloadCapable": { + "description": "Whether the command is capable to reload itself when source code changes. If set to 'true' the command won't be restarted and it is expected to handle file changes on its own.\n\nDefault value is 'false'", + "type": "boolean" + }, + "label": { + "description": "Optional label that provides a label for this command to be used in Editor UI menus for example", + "type": "string" + }, + "workingDir": { + "description": "Working directory where the command should be executed\n\nSpecial variables that can be used:\n\n - '$PROJECTS_ROOT': A path where projects sources are mounted as defined by container component's sourceMapping.\n\n - '$PROJECT_SOURCE': A path to a project source ($PROJECTS_ROOT/\u003cproject-name\u003e). If there are multiple projects, this will point to the directory of the first one.", + "type": "string" + } + }, + "additionalProperties": false + }, + "id": { + "description": "Mandatory identifier that allows referencing this command in composite commands, from a parent, or in events.", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "vscodeLaunch": { + "description": "Command providing the definition of a VsCode launch action", + "type": "object", + "oneOf": [ + { + "required": [ + "uri" + ] + }, + { + "required": [ + "inlined" + ] + } + ], + "properties": { + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "required": [ + "kind" + ], + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": [ + "build", + "run", + "test", + "debug" + ] + } + }, + "additionalProperties": false + }, + "inlined": { + "description": "Inlined content of the VsCode configuration", + "type": "string" + }, + "uri": { + "description": "Location as an absolute of relative URI the VsCode configuration will be fetched from", + "type": "string" + } + }, + "additionalProperties": false + }, + "vscodeTask": { + "description": "Command providing the definition of a VsCode Task", + "type": "object", + "oneOf": [ + { + "required": [ + "uri" + ] + }, + { + "required": [ + "inlined" + ] + } + ], + "properties": { + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "required": [ + "kind" + ], + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": [ + "build", + "run", + "test", + "debug" + ] + } + }, + "additionalProperties": false + }, + "inlined": { + "description": "Inlined content of the VsCode configuration", + "type": "string" + }, + "uri": { + "description": "Location as an absolute of relative URI the VsCode configuration will be fetched from", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "components": { + "description": "List of the workspace components, such as editor and plugins, user-provided containers, or other types of components", + "type": "array", + "items": { + "type": "object", + "required": [ + "name" + ], + "oneOf": [ + { + "required": [ + "container" + ] + }, + { + "required": [ + "kubernetes" + ] + }, + { + "required": [ + "openshift" + ] + }, + { + "required": [ + "volume" + ] + }, + { + "required": [ + "plugin" + ] + } + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "container": { + "description": "Allows adding and configuring workspace-related containers", + "type": "object", + "required": [ + "image" + ], + "properties": { + "args": { + "description": "The arguments to supply to the command running the dockerimage component. The arguments are supplied either to the default command provided in the image or to the overridden command.\n\nDefaults to an empty array, meaning use whatever is defined in the image.", + "type": "array", + "items": { + "type": "string" + } + }, + "command": { + "description": "The command to run in the dockerimage component instead of the default one provided in the image.\n\nDefaults to an empty array, meaning use whatever is defined in the image.", + "type": "array", + "items": { + "type": "string" + } + }, + "dedicatedPod": { + "description": "Specify if a container should run in its own separated pod, instead of running as part of the main development environment pod.\n\nDefault value is 'false'", + "type": "boolean" + }, + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": [ + "name", + "targetPort" + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\",", + "type": "object", + "additionalProperties": true + }, + "exposure": { + "description": "Describes how the endpoint should be exposed on the network.\n- 'public' means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- 'internal' means that the endpoint will be exposed internally outside of the main workspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- 'none' means that the endpoint will not be exposed and will only be accessible inside the main workspace POD, on a local address.\n\nDefault value is 'public'", + "type": "string", + "default": "public", + "enum": [ + "public", + "internal", + "none" + ] + }, + "name": { + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "Path of the endpoint URL", + "type": "string" + }, + "protocol": { + "description": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- 'http': Endpoint will have 'http' traffic, typically on a TCP connection. It will be automaticaly promoted to 'https' when the 'secure' field is set to 'true'.\n- 'https': Endpoint will have 'https' traffic, typically on a TCP connection.\n- 'ws': Endpoint will have 'ws' traffic, typically on a TCP connection. It will be automaticaly promoted to 'wss' when the 'secure' field is set to 'true'.\n- 'wss': Endpoint will have 'wss' traffic, typically on a TCP connection.\n- 'tcp': Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- 'udp': Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is 'http'", + "type": "string", + "default": "http", + "enum": [ + "http", + "https", + "ws", + "wss", + "tcp", + "udp" + ] + }, + "secure": { + "description": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of 'https' or 'wss'.", + "type": "boolean" + }, + "targetPort": { + "type": "integer" + } + }, + "additionalProperties": false + } + }, + "env": { + "description": "Environment variables used in this container.\n\nThe following variables are reserved and cannot be overridden via env:\n\n - '$PROJECTS_ROOT'\n\n - '$PROJECT_SOURCE'", + "type": "array", + "items": { + "type": "object", + "required": [ + "name", + "value" + ], + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "image": { + "type": "string" + }, + "memoryLimit": { + "type": "string" + }, + "mountSources": { + "description": "Toggles whether or not the project source code should be mounted in the component.\n\nDefaults to true for all component types except plugins and components that set 'dedicatedPod' to true.", + "type": "boolean" + }, + "sourceMapping": { + "description": "Optional specification of the path in the container where project sources should be transferred/mounted when 'mountSources' is 'true'. When omitted, the default value of /projects is used.", + "type": "string", + "default": "/projects" + }, + "volumeMounts": { + "description": "List of volumes mounts that should be mounted is this container.", + "type": "array", + "items": { + "description": "Volume that should be mounted to a component container", + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "description": "The volume mount name is the name of an existing 'Volume' component. If several containers mount the same volume name then they will reuse the same volume and will be able to access to the same files.", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "The path in the component container where the volume should be mounted. If not path is mentioned, default path is the is '/\u003cname\u003e'.", + "type": "string" + } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false + }, + "kubernetes": { + "description": "Allows importing into the workspace the Kubernetes resources defined in a given manifest. For example this allows reusing the Kubernetes definitions used to deploy some runtime components in production.", + "type": "object", + "oneOf": [ + { + "required": [ + "uri" + ] + }, + { + "required": [ + "inlined" + ] + } + ], + "properties": { + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": [ + "name", + "targetPort" + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\",", + "type": "object", + "additionalProperties": true + }, + "exposure": { + "description": "Describes how the endpoint should be exposed on the network.\n- 'public' means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- 'internal' means that the endpoint will be exposed internally outside of the main workspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- 'none' means that the endpoint will not be exposed and will only be accessible inside the main workspace POD, on a local address.\n\nDefault value is 'public'", + "type": "string", + "default": "public", + "enum": [ + "public", + "internal", + "none" + ] + }, + "name": { + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "Path of the endpoint URL", + "type": "string" + }, + "protocol": { + "description": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- 'http': Endpoint will have 'http' traffic, typically on a TCP connection. It will be automaticaly promoted to 'https' when the 'secure' field is set to 'true'.\n- 'https': Endpoint will have 'https' traffic, typically on a TCP connection.\n- 'ws': Endpoint will have 'ws' traffic, typically on a TCP connection. It will be automaticaly promoted to 'wss' when the 'secure' field is set to 'true'.\n- 'wss': Endpoint will have 'wss' traffic, typically on a TCP connection.\n- 'tcp': Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- 'udp': Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is 'http'", + "type": "string", + "default": "http", + "enum": [ + "http", + "https", + "ws", + "wss", + "tcp", + "udp" + ] + }, + "secure": { + "description": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of 'https' or 'wss'.", + "type": "boolean" + }, + "targetPort": { + "type": "integer" + } + }, + "additionalProperties": false + } + }, + "inlined": { + "description": "Inlined manifest", + "type": "string" + }, + "uri": { + "description": "Location in a file fetched from a uri.", + "type": "string" + } + }, + "additionalProperties": false + }, + "name": { + "description": "Mandatory name that allows referencing the component from other elements (such as commands) or from an external devfile that may reference this component through a parent or a plugin.", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "openshift": { + "description": "Allows importing into the workspace the OpenShift resources defined in a given manifest. For example this allows reusing the OpenShift definitions used to deploy some runtime components in production.", + "type": "object", + "oneOf": [ + { + "required": [ + "uri" + ] + }, + { + "required": [ + "inlined" + ] + } + ], + "properties": { + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": [ + "name", + "targetPort" + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\",", + "type": "object", + "additionalProperties": true + }, + "exposure": { + "description": "Describes how the endpoint should be exposed on the network.\n- 'public' means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- 'internal' means that the endpoint will be exposed internally outside of the main workspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- 'none' means that the endpoint will not be exposed and will only be accessible inside the main workspace POD, on a local address.\n\nDefault value is 'public'", + "type": "string", + "default": "public", + "enum": [ + "public", + "internal", + "none" + ] + }, + "name": { + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "Path of the endpoint URL", + "type": "string" + }, + "protocol": { + "description": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- 'http': Endpoint will have 'http' traffic, typically on a TCP connection. It will be automaticaly promoted to 'https' when the 'secure' field is set to 'true'.\n- 'https': Endpoint will have 'https' traffic, typically on a TCP connection.\n- 'ws': Endpoint will have 'ws' traffic, typically on a TCP connection. It will be automaticaly promoted to 'wss' when the 'secure' field is set to 'true'.\n- 'wss': Endpoint will have 'wss' traffic, typically on a TCP connection.\n- 'tcp': Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- 'udp': Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is 'http'", + "type": "string", + "default": "http", + "enum": [ + "http", + "https", + "ws", + "wss", + "tcp", + "udp" + ] + }, + "secure": { + "description": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of 'https' or 'wss'.", + "type": "boolean" + }, + "targetPort": { + "type": "integer" + } + }, + "additionalProperties": false + } + }, + "inlined": { + "description": "Inlined manifest", + "type": "string" + }, + "uri": { + "description": "Location in a file fetched from a uri.", + "type": "string" + } + }, + "additionalProperties": false + }, + "plugin": { + "description": "Allows importing a plugin.\n\nPlugins are mainly imported devfiles that contribute components, commands and events as a consistent single unit. They are defined in either YAML files following the devfile syntax, or as 'DevWorkspaceTemplate' Kubernetes Custom Resources", + "type": "object", + "oneOf": [ + { + "required": [ + "uri" + ] + }, + { + "required": [ + "id" + ] + }, + { + "required": [ + "kubernetes" + ] + } + ], + "properties": { + "commands": { + "description": "Overrides of commands encapsulated in a parent devfile or a plugin. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "array", + "items": { + "type": "object", + "required": [ + "id" + ], + "oneOf": [ + { + "required": [ + "exec" + ] + }, + { + "required": [ + "apply" + ] + }, + { + "required": [ + "vscodeTask" + ] + }, + { + "required": [ + "vscodeLaunch" + ] + }, + { + "required": [ + "composite" + ] + } + ], + "properties": { + "apply": { + "description": "Command that consists in applying a given component definition, typically bound to a workspace event.\n\nFor example, when an 'apply' command is bound to a 'preStart' event, and references a 'container' component, it will start the container as a K8S initContainer in the workspace POD, unless the component has its 'dedicatedPod' field set to 'true'.\n\nWhen no 'apply' command exist for a given component, it is assumed the component will be applied at workspace start by default.", + "type": "object", + "properties": { + "component": { + "description": "Describes component that will be applied", + "type": "string" + }, + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": [ + "build", + "run", + "test", + "debug" + ] + } + }, + "additionalProperties": false + }, + "label": { + "description": "Optional label that provides a label for this command to be used in Editor UI menus for example", + "type": "string" + } + }, + "additionalProperties": false + }, + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "composite": { + "description": "Composite command that allows executing several sub-commands either sequentially or concurrently", + "type": "object", + "properties": { + "commands": { + "description": "The commands that comprise this composite command", + "type": "array", + "items": { + "type": "string" + } + }, + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": [ + "build", + "run", + "test", + "debug" + ] + } + }, + "additionalProperties": false + }, + "label": { + "description": "Optional label that provides a label for this command to be used in Editor UI menus for example", + "type": "string" + }, + "parallel": { + "description": "Indicates if the sub-commands should be executed concurrently", + "type": "boolean" + } + }, + "additionalProperties": false + }, + "exec": { + "description": "CLI Command executed in an existing component container", + "type": "object", + "properties": { + "commandLine": { + "description": "The actual command-line string\n\nSpecial variables that can be used:\n\n - '$PROJECTS_ROOT': A path where projects sources are mounted as defined by container component's sourceMapping.\n\n - '$PROJECT_SOURCE': A path to a project source ($PROJECTS_ROOT/\u003cproject-name\u003e). If there are multiple projects, this will point to the directory of the first one.", + "type": "string" + }, + "component": { + "description": "Describes component to which given action relates", + "type": "string" + }, + "env": { + "description": "Optional list of environment variables that have to be set before running the command", + "type": "array", + "items": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": [ + "build", + "run", + "test", + "debug" + ] + } + }, + "additionalProperties": false + }, + "hotReloadCapable": { + "description": "Whether the command is capable to reload itself when source code changes. If set to 'true' the command won't be restarted and it is expected to handle file changes on its own.\n\nDefault value is 'false'", + "type": "boolean" + }, + "label": { + "description": "Optional label that provides a label for this command to be used in Editor UI menus for example", + "type": "string" + }, + "workingDir": { + "description": "Working directory where the command should be executed\n\nSpecial variables that can be used:\n\n - '$PROJECTS_ROOT': A path where projects sources are mounted as defined by container component's sourceMapping.\n\n - '$PROJECT_SOURCE': A path to a project source ($PROJECTS_ROOT/\u003cproject-name\u003e). If there are multiple projects, this will point to the directory of the first one.", + "type": "string" + } + }, + "additionalProperties": false + }, + "id": { + "description": "Mandatory identifier that allows referencing this command in composite commands, from a parent, or in events.", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "vscodeLaunch": { + "description": "Command providing the definition of a VsCode launch action", + "type": "object", + "oneOf": [ + { + "required": [ + "uri" + ] + }, + { + "required": [ + "inlined" + ] + } + ], + "properties": { + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": [ + "build", + "run", + "test", + "debug" + ] + } + }, + "additionalProperties": false + }, + "inlined": { + "description": "Inlined content of the VsCode configuration", + "type": "string" + }, + "uri": { + "description": "Location as an absolute of relative URI the VsCode configuration will be fetched from", + "type": "string" + } + }, + "additionalProperties": false + }, + "vscodeTask": { + "description": "Command providing the definition of a VsCode Task", + "type": "object", + "oneOf": [ + { + "required": [ + "uri" + ] + }, + { + "required": [ + "inlined" + ] + } + ], + "properties": { + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": [ + "build", + "run", + "test", + "debug" + ] + } + }, + "additionalProperties": false + }, + "inlined": { + "description": "Inlined content of the VsCode configuration", + "type": "string" + }, + "uri": { + "description": "Location as an absolute of relative URI the VsCode configuration will be fetched from", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "components": { + "description": "Overrides of components encapsulated in a parent devfile or a plugin. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "array", + "items": { + "type": "object", + "required": [ + "name" + ], + "oneOf": [ + { + "required": [ + "container" + ] + }, + { + "required": [ + "kubernetes" + ] + }, + { + "required": [ + "openshift" + ] + }, + { + "required": [ + "volume" + ] + } + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "container": { + "description": "Allows adding and configuring workspace-related containers", + "type": "object", + "properties": { + "args": { + "description": "The arguments to supply to the command running the dockerimage component. The arguments are supplied either to the default command provided in the image or to the overridden command.\n\nDefaults to an empty array, meaning use whatever is defined in the image.", + "type": "array", + "items": { + "type": "string" + } + }, + "command": { + "description": "The command to run in the dockerimage component instead of the default one provided in the image.\n\nDefaults to an empty array, meaning use whatever is defined in the image.", + "type": "array", + "items": { + "type": "string" + } + }, + "dedicatedPod": { + "description": "Specify if a container should run in its own separated pod, instead of running as part of the main development environment pod.\n\nDefault value is 'false'", + "type": "boolean" + }, + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\",", + "type": "object", + "additionalProperties": true + }, + "exposure": { + "description": "Describes how the endpoint should be exposed on the network.\n- 'public' means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- 'internal' means that the endpoint will be exposed internally outside of the main workspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- 'none' means that the endpoint will not be exposed and will only be accessible inside the main workspace POD, on a local address.\n\nDefault value is 'public'", + "type": "string", + "enum": [ + "public", + "internal", + "none" + ] + }, + "name": { + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "Path of the endpoint URL", + "type": "string" + }, + "protocol": { + "description": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- 'http': Endpoint will have 'http' traffic, typically on a TCP connection. It will be automaticaly promoted to 'https' when the 'secure' field is set to 'true'.\n- 'https': Endpoint will have 'https' traffic, typically on a TCP connection.\n- 'ws': Endpoint will have 'ws' traffic, typically on a TCP connection. It will be automaticaly promoted to 'wss' when the 'secure' field is set to 'true'.\n- 'wss': Endpoint will have 'wss' traffic, typically on a TCP connection.\n- 'tcp': Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- 'udp': Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is 'http'", + "type": "string", + "enum": [ + "http", + "https", + "ws", + "wss", + "tcp", + "udp" + ] + }, + "secure": { + "description": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of 'https' or 'wss'.", + "type": "boolean" + }, + "targetPort": { + "type": "integer" + } + }, + "additionalProperties": false + } + }, + "env": { + "description": "Environment variables used in this container.\n\nThe following variables are reserved and cannot be overridden via env:\n\n - '$PROJECTS_ROOT'\n\n - '$PROJECT_SOURCE'", + "type": "array", + "items": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "image": { + "type": "string" + }, + "memoryLimit": { + "type": "string" + }, + "mountSources": { + "description": "Toggles whether or not the project source code should be mounted in the component.\n\nDefaults to true for all component types except plugins and components that set 'dedicatedPod' to true.", + "type": "boolean" + }, + "sourceMapping": { + "description": "Optional specification of the path in the container where project sources should be transferred/mounted when 'mountSources' is 'true'. When omitted, the default value of /projects is used.", + "type": "string" + }, + "volumeMounts": { + "description": "List of volumes mounts that should be mounted is this container.", + "type": "array", + "items": { + "description": "Volume that should be mounted to a component container", + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "description": "The volume mount name is the name of an existing 'Volume' component. If several containers mount the same volume name then they will reuse the same volume and will be able to access to the same files.", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "The path in the component container where the volume should be mounted. If not path is mentioned, default path is the is '/\u003cname\u003e'.", + "type": "string" + } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false + }, + "kubernetes": { + "description": "Allows importing into the workspace the Kubernetes resources defined in a given manifest. For example this allows reusing the Kubernetes definitions used to deploy some runtime components in production.", + "type": "object", + "oneOf": [ + { + "required": [ + "uri" + ] + }, + { + "required": [ + "inlined" + ] + } + ], + "properties": { + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\",", + "type": "object", + "additionalProperties": true + }, + "exposure": { + "description": "Describes how the endpoint should be exposed on the network.\n- 'public' means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- 'internal' means that the endpoint will be exposed internally outside of the main workspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- 'none' means that the endpoint will not be exposed and will only be accessible inside the main workspace POD, on a local address.\n\nDefault value is 'public'", + "type": "string", + "enum": [ + "public", + "internal", + "none" + ] + }, + "name": { + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "Path of the endpoint URL", + "type": "string" + }, + "protocol": { + "description": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- 'http': Endpoint will have 'http' traffic, typically on a TCP connection. It will be automaticaly promoted to 'https' when the 'secure' field is set to 'true'.\n- 'https': Endpoint will have 'https' traffic, typically on a TCP connection.\n- 'ws': Endpoint will have 'ws' traffic, typically on a TCP connection. It will be automaticaly promoted to 'wss' when the 'secure' field is set to 'true'.\n- 'wss': Endpoint will have 'wss' traffic, typically on a TCP connection.\n- 'tcp': Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- 'udp': Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is 'http'", + "type": "string", + "enum": [ + "http", + "https", + "ws", + "wss", + "tcp", + "udp" + ] + }, + "secure": { + "description": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of 'https' or 'wss'.", + "type": "boolean" + }, + "targetPort": { + "type": "integer" + } + }, + "additionalProperties": false + } + }, + "inlined": { + "description": "Inlined manifest", + "type": "string" + }, + "uri": { + "description": "Location in a file fetched from a uri.", + "type": "string" + } + }, + "additionalProperties": false + }, + "name": { + "description": "Mandatory name that allows referencing the component from other elements (such as commands) or from an external devfile that may reference this component through a parent or a plugin.", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "openshift": { + "description": "Allows importing into the workspace the OpenShift resources defined in a given manifest. For example this allows reusing the OpenShift definitions used to deploy some runtime components in production.", + "type": "object", + "oneOf": [ + { + "required": [ + "uri" + ] + }, + { + "required": [ + "inlined" + ] + } + ], + "properties": { + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\",", + "type": "object", + "additionalProperties": true + }, + "exposure": { + "description": "Describes how the endpoint should be exposed on the network.\n- 'public' means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- 'internal' means that the endpoint will be exposed internally outside of the main workspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- 'none' means that the endpoint will not be exposed and will only be accessible inside the main workspace POD, on a local address.\n\nDefault value is 'public'", + "type": "string", + "enum": [ + "public", + "internal", + "none" + ] + }, + "name": { + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "Path of the endpoint URL", + "type": "string" + }, + "protocol": { + "description": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- 'http': Endpoint will have 'http' traffic, typically on a TCP connection. It will be automaticaly promoted to 'https' when the 'secure' field is set to 'true'.\n- 'https': Endpoint will have 'https' traffic, typically on a TCP connection.\n- 'ws': Endpoint will have 'ws' traffic, typically on a TCP connection. It will be automaticaly promoted to 'wss' when the 'secure' field is set to 'true'.\n- 'wss': Endpoint will have 'wss' traffic, typically on a TCP connection.\n- 'tcp': Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- 'udp': Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is 'http'", + "type": "string", + "enum": [ + "http", + "https", + "ws", + "wss", + "tcp", + "udp" + ] + }, + "secure": { + "description": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of 'https' or 'wss'.", + "type": "boolean" + }, + "targetPort": { + "type": "integer" + } + }, + "additionalProperties": false + } + }, + "inlined": { + "description": "Inlined manifest", + "type": "string" + }, + "uri": { + "description": "Location in a file fetched from a uri.", + "type": "string" + } + }, + "additionalProperties": false + }, + "volume": { + "description": "Allows specifying the definition of a volume shared by several other components", + "type": "object", + "properties": { + "size": { + "description": "Size of the volume", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "id": { + "description": "Id in a registry that contains a Devfile yaml file", + "type": "string" + }, + "kubernetes": { + "description": "Reference to a Kubernetes CRD of type DevWorkspaceTemplate", + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + } + }, + "additionalProperties": false + }, + "registryUrl": { + "type": "string" + }, + "uri": { + "description": "Uri of a Devfile yaml file", + "type": "string" + } + }, + "additionalProperties": false + }, + "volume": { + "description": "Allows specifying the definition of a volume shared by several other components", + "type": "object", + "properties": { + "size": { + "description": "Size of the volume", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "events": { + "description": "Bindings of commands to events. Each command is referred-to by its name.", + "type": "object", + "properties": { + "postStart": { + "description": "IDs of commands that should be executed after the workspace is completely started. In the case of Che-Theia, these commands should be executed after all plugins and extensions have started, including project cloning. This means that those commands are not triggered until the user opens the IDE in his browser.", + "type": "array", + "items": { + "type": "string" + } + }, + "postStop": { + "description": "IDs of commands that should be executed after stopping the workspace.", + "type": "array", + "items": { + "type": "string" + } + }, + "preStart": { + "description": "IDs of commands that should be executed before the workspace start. Kubernetes-wise, these commands would typically be executed in init containers of the workspace POD.", + "type": "array", + "items": { + "type": "string" + } + }, + "preStop": { + "description": "IDs of commands that should be executed before stopping the workspace.", + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "metadata": { + "description": "Optional metadata", + "type": "object", + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "description": { + "description": "Optional devfile description", + "type": "string" + }, + "displayName": { + "description": "Optional devfile display name", + "type": "string" + }, + "globalMemoryLimit": { + "description": "Optional devfile global memory limit", + "type": "string" + }, + "icon": { + "description": "Optional devfile icon", + "type": "string" + }, + "name": { + "description": "Optional devfile name", + "type": "string" + }, + "tags": { + "description": "Optional devfile tags", + "type": "array", + "items": { + "type": "string" + } + }, + "version": { + "description": "Optional semver-compatible version", + "type": "string", + "pattern": "^([0-9]+)\\.([0-9]+)\\.([0-9]+)(\\-[0-9a-z-]+(\\.[0-9a-z-]+)*)?(\\+[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?$" + } + }, + "additionalProperties": true + }, + "parent": { + "description": "Parent workspace template", + "type": "object", + "oneOf": [ + { + "required": [ + "uri" + ] + }, + { + "required": [ + "id" + ] + }, + { + "required": [ + "kubernetes" + ] + } + ], + "properties": { + "commands": { + "description": "Overrides of commands encapsulated in a parent devfile or a plugin. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "array", + "items": { + "type": "object", + "required": [ + "id" + ], + "oneOf": [ + { + "required": [ + "exec" + ] + }, + { + "required": [ + "apply" + ] + }, + { + "required": [ + "vscodeTask" + ] + }, + { + "required": [ + "vscodeLaunch" + ] + }, + { + "required": [ + "composite" + ] + } + ], + "properties": { + "apply": { + "description": "Command that consists in applying a given component definition, typically bound to a workspace event.\n\nFor example, when an 'apply' command is bound to a 'preStart' event, and references a 'container' component, it will start the container as a K8S initContainer in the workspace POD, unless the component has its 'dedicatedPod' field set to 'true'.\n\nWhen no 'apply' command exist for a given component, it is assumed the component will be applied at workspace start by default.", + "type": "object", + "properties": { + "component": { + "description": "Describes component that will be applied", + "type": "string" + }, + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": [ + "build", + "run", + "test", + "debug" + ] + } + }, + "additionalProperties": false + }, + "label": { + "description": "Optional label that provides a label for this command to be used in Editor UI menus for example", + "type": "string" + } + }, + "additionalProperties": false + }, + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "composite": { + "description": "Composite command that allows executing several sub-commands either sequentially or concurrently", + "type": "object", + "properties": { + "commands": { + "description": "The commands that comprise this composite command", + "type": "array", + "items": { + "type": "string" + } + }, + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": [ + "build", + "run", + "test", + "debug" + ] + } + }, + "additionalProperties": false + }, + "label": { + "description": "Optional label that provides a label for this command to be used in Editor UI menus for example", + "type": "string" + }, + "parallel": { + "description": "Indicates if the sub-commands should be executed concurrently", + "type": "boolean" + } + }, + "additionalProperties": false + }, + "exec": { + "description": "CLI Command executed in an existing component container", + "type": "object", + "properties": { + "commandLine": { + "description": "The actual command-line string\n\nSpecial variables that can be used:\n\n - '$PROJECTS_ROOT': A path where projects sources are mounted as defined by container component's sourceMapping.\n\n - '$PROJECT_SOURCE': A path to a project source ($PROJECTS_ROOT/\u003cproject-name\u003e). If there are multiple projects, this will point to the directory of the first one.", + "type": "string" + }, + "component": { + "description": "Describes component to which given action relates", + "type": "string" + }, + "env": { + "description": "Optional list of environment variables that have to be set before running the command", + "type": "array", + "items": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": [ + "build", + "run", + "test", + "debug" + ] + } + }, + "additionalProperties": false + }, + "hotReloadCapable": { + "description": "Whether the command is capable to reload itself when source code changes. If set to 'true' the command won't be restarted and it is expected to handle file changes on its own.\n\nDefault value is 'false'", + "type": "boolean" + }, + "label": { + "description": "Optional label that provides a label for this command to be used in Editor UI menus for example", + "type": "string" + }, + "workingDir": { + "description": "Working directory where the command should be executed\n\nSpecial variables that can be used:\n\n - '$PROJECTS_ROOT': A path where projects sources are mounted as defined by container component's sourceMapping.\n\n - '$PROJECT_SOURCE': A path to a project source ($PROJECTS_ROOT/\u003cproject-name\u003e). If there are multiple projects, this will point to the directory of the first one.", + "type": "string" + } + }, + "additionalProperties": false + }, + "id": { + "description": "Mandatory identifier that allows referencing this command in composite commands, from a parent, or in events.", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "vscodeLaunch": { + "description": "Command providing the definition of a VsCode launch action", + "type": "object", + "oneOf": [ + { + "required": [ + "uri" + ] + }, + { + "required": [ + "inlined" + ] + } + ], + "properties": { + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": [ + "build", + "run", + "test", + "debug" + ] + } + }, + "additionalProperties": false + }, + "inlined": { + "description": "Inlined content of the VsCode configuration", + "type": "string" + }, + "uri": { + "description": "Location as an absolute of relative URI the VsCode configuration will be fetched from", + "type": "string" + } + }, + "additionalProperties": false + }, + "vscodeTask": { + "description": "Command providing the definition of a VsCode Task", + "type": "object", + "oneOf": [ + { + "required": [ + "uri" + ] + }, + { + "required": [ + "inlined" + ] + } + ], + "properties": { + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": [ + "build", + "run", + "test", + "debug" + ] + } + }, + "additionalProperties": false + }, + "inlined": { + "description": "Inlined content of the VsCode configuration", + "type": "string" + }, + "uri": { + "description": "Location as an absolute of relative URI the VsCode configuration will be fetched from", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "components": { + "description": "Overrides of components encapsulated in a parent devfile or a plugin. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "array", + "items": { + "type": "object", + "required": [ + "name" + ], + "oneOf": [ + { + "required": [ + "container" + ] + }, + { + "required": [ + "kubernetes" + ] + }, + { + "required": [ + "openshift" + ] + }, + { + "required": [ + "volume" + ] + }, + { + "required": [ + "plugin" + ] + } + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "container": { + "description": "Allows adding and configuring workspace-related containers", + "type": "object", + "properties": { + "args": { + "description": "The arguments to supply to the command running the dockerimage component. The arguments are supplied either to the default command provided in the image or to the overridden command.\n\nDefaults to an empty array, meaning use whatever is defined in the image.", + "type": "array", + "items": { + "type": "string" + } + }, + "command": { + "description": "The command to run in the dockerimage component instead of the default one provided in the image.\n\nDefaults to an empty array, meaning use whatever is defined in the image.", + "type": "array", + "items": { + "type": "string" + } + }, + "dedicatedPod": { + "description": "Specify if a container should run in its own separated pod, instead of running as part of the main development environment pod.\n\nDefault value is 'false'", + "type": "boolean" + }, + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\",", + "type": "object", + "additionalProperties": true + }, + "exposure": { + "description": "Describes how the endpoint should be exposed on the network.\n- 'public' means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- 'internal' means that the endpoint will be exposed internally outside of the main workspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- 'none' means that the endpoint will not be exposed and will only be accessible inside the main workspace POD, on a local address.\n\nDefault value is 'public'", + "type": "string", + "enum": [ + "public", + "internal", + "none" + ] + }, + "name": { + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "Path of the endpoint URL", + "type": "string" + }, + "protocol": { + "description": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- 'http': Endpoint will have 'http' traffic, typically on a TCP connection. It will be automaticaly promoted to 'https' when the 'secure' field is set to 'true'.\n- 'https': Endpoint will have 'https' traffic, typically on a TCP connection.\n- 'ws': Endpoint will have 'ws' traffic, typically on a TCP connection. It will be automaticaly promoted to 'wss' when the 'secure' field is set to 'true'.\n- 'wss': Endpoint will have 'wss' traffic, typically on a TCP connection.\n- 'tcp': Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- 'udp': Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is 'http'", + "type": "string", + "enum": [ + "http", + "https", + "ws", + "wss", + "tcp", + "udp" + ] + }, + "secure": { + "description": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of 'https' or 'wss'.", + "type": "boolean" + }, + "targetPort": { + "type": "integer" + } + }, + "additionalProperties": false + } + }, + "env": { + "description": "Environment variables used in this container.\n\nThe following variables are reserved and cannot be overridden via env:\n\n - '$PROJECTS_ROOT'\n\n - '$PROJECT_SOURCE'", + "type": "array", + "items": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "image": { + "type": "string" + }, + "memoryLimit": { + "type": "string" + }, + "mountSources": { + "description": "Toggles whether or not the project source code should be mounted in the component.\n\nDefaults to true for all component types except plugins and components that set 'dedicatedPod' to true.", + "type": "boolean" + }, + "sourceMapping": { + "description": "Optional specification of the path in the container where project sources should be transferred/mounted when 'mountSources' is 'true'. When omitted, the default value of /projects is used.", + "type": "string" + }, + "volumeMounts": { + "description": "List of volumes mounts that should be mounted is this container.", + "type": "array", + "items": { + "description": "Volume that should be mounted to a component container", + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "description": "The volume mount name is the name of an existing 'Volume' component. If several containers mount the same volume name then they will reuse the same volume and will be able to access to the same files.", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "The path in the component container where the volume should be mounted. If not path is mentioned, default path is the is '/\u003cname\u003e'.", + "type": "string" + } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false + }, + "kubernetes": { + "description": "Allows importing into the workspace the Kubernetes resources defined in a given manifest. For example this allows reusing the Kubernetes definitions used to deploy some runtime components in production.", + "type": "object", + "oneOf": [ + { + "required": [ + "uri" + ] + }, + { + "required": [ + "inlined" + ] + } + ], + "properties": { + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\",", + "type": "object", + "additionalProperties": true + }, + "exposure": { + "description": "Describes how the endpoint should be exposed on the network.\n- 'public' means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- 'internal' means that the endpoint will be exposed internally outside of the main workspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- 'none' means that the endpoint will not be exposed and will only be accessible inside the main workspace POD, on a local address.\n\nDefault value is 'public'", + "type": "string", + "enum": [ + "public", + "internal", + "none" + ] + }, + "name": { + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "Path of the endpoint URL", + "type": "string" + }, + "protocol": { + "description": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- 'http': Endpoint will have 'http' traffic, typically on a TCP connection. It will be automaticaly promoted to 'https' when the 'secure' field is set to 'true'.\n- 'https': Endpoint will have 'https' traffic, typically on a TCP connection.\n- 'ws': Endpoint will have 'ws' traffic, typically on a TCP connection. It will be automaticaly promoted to 'wss' when the 'secure' field is set to 'true'.\n- 'wss': Endpoint will have 'wss' traffic, typically on a TCP connection.\n- 'tcp': Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- 'udp': Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is 'http'", + "type": "string", + "enum": [ + "http", + "https", + "ws", + "wss", + "tcp", + "udp" + ] + }, + "secure": { + "description": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of 'https' or 'wss'.", + "type": "boolean" + }, + "targetPort": { + "type": "integer" + } + }, + "additionalProperties": false + } + }, + "inlined": { + "description": "Inlined manifest", + "type": "string" + }, + "uri": { + "description": "Location in a file fetched from a uri.", + "type": "string" + } + }, + "additionalProperties": false + }, + "name": { + "description": "Mandatory name that allows referencing the component from other elements (such as commands) or from an external devfile that may reference this component through a parent or a plugin.", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "openshift": { + "description": "Allows importing into the workspace the OpenShift resources defined in a given manifest. For example this allows reusing the OpenShift definitions used to deploy some runtime components in production.", + "type": "object", + "oneOf": [ + { + "required": [ + "uri" + ] + }, + { + "required": [ + "inlined" + ] + } + ], + "properties": { + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\",", + "type": "object", + "additionalProperties": true + }, + "exposure": { + "description": "Describes how the endpoint should be exposed on the network.\n- 'public' means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- 'internal' means that the endpoint will be exposed internally outside of the main workspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- 'none' means that the endpoint will not be exposed and will only be accessible inside the main workspace POD, on a local address.\n\nDefault value is 'public'", + "type": "string", + "enum": [ + "public", + "internal", + "none" + ] + }, + "name": { + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "Path of the endpoint URL", + "type": "string" + }, + "protocol": { + "description": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- 'http': Endpoint will have 'http' traffic, typically on a TCP connection. It will be automaticaly promoted to 'https' when the 'secure' field is set to 'true'.\n- 'https': Endpoint will have 'https' traffic, typically on a TCP connection.\n- 'ws': Endpoint will have 'ws' traffic, typically on a TCP connection. It will be automaticaly promoted to 'wss' when the 'secure' field is set to 'true'.\n- 'wss': Endpoint will have 'wss' traffic, typically on a TCP connection.\n- 'tcp': Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- 'udp': Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is 'http'", + "type": "string", + "enum": [ + "http", + "https", + "ws", + "wss", + "tcp", + "udp" + ] + }, + "secure": { + "description": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of 'https' or 'wss'.", + "type": "boolean" + }, + "targetPort": { + "type": "integer" + } + }, + "additionalProperties": false + } + }, + "inlined": { + "description": "Inlined manifest", + "type": "string" + }, + "uri": { + "description": "Location in a file fetched from a uri.", + "type": "string" + } + }, + "additionalProperties": false + }, + "plugin": { + "description": "Allows importing a plugin.\n\nPlugins are mainly imported devfiles that contribute components, commands and events as a consistent single unit. They are defined in either YAML files following the devfile syntax, or as 'DevWorkspaceTemplate' Kubernetes Custom Resources", + "type": "object", + "oneOf": [ + { + "required": [ + "uri" + ] + }, + { + "required": [ + "id" + ] + }, + { + "required": [ + "kubernetes" + ] + } + ], + "properties": { + "commands": { + "description": "Overrides of commands encapsulated in a parent devfile or a plugin. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "array", + "items": { + "type": "object", + "required": [ + "id" + ], + "oneOf": [ + { + "required": [ + "exec" + ] + }, + { + "required": [ + "apply" + ] + }, + { + "required": [ + "vscodeTask" + ] + }, + { + "required": [ + "vscodeLaunch" + ] + }, + { + "required": [ + "composite" + ] + } + ], + "properties": { + "apply": { + "description": "Command that consists in applying a given component definition, typically bound to a workspace event.\n\nFor example, when an 'apply' command is bound to a 'preStart' event, and references a 'container' component, it will start the container as a K8S initContainer in the workspace POD, unless the component has its 'dedicatedPod' field set to 'true'.\n\nWhen no 'apply' command exist for a given component, it is assumed the component will be applied at workspace start by default.", + "type": "object", + "properties": { + "component": { + "description": "Describes component that will be applied", + "type": "string" + }, + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": [ + "build", + "run", + "test", + "debug" + ] + } + }, + "additionalProperties": false + }, + "label": { + "description": "Optional label that provides a label for this command to be used in Editor UI menus for example", + "type": "string" + } + }, + "additionalProperties": false + }, + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "composite": { + "description": "Composite command that allows executing several sub-commands either sequentially or concurrently", + "type": "object", + "properties": { + "commands": { + "description": "The commands that comprise this composite command", + "type": "array", + "items": { + "type": "string" + } + }, + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": [ + "build", + "run", + "test", + "debug" + ] + } + }, + "additionalProperties": false + }, + "label": { + "description": "Optional label that provides a label for this command to be used in Editor UI menus for example", + "type": "string" + }, + "parallel": { + "description": "Indicates if the sub-commands should be executed concurrently", + "type": "boolean" + } + }, + "additionalProperties": false + }, + "exec": { + "description": "CLI Command executed in an existing component container", + "type": "object", + "properties": { + "commandLine": { + "description": "The actual command-line string\n\nSpecial variables that can be used:\n\n - '$PROJECTS_ROOT': A path where projects sources are mounted as defined by container component's sourceMapping.\n\n - '$PROJECT_SOURCE': A path to a project source ($PROJECTS_ROOT/\u003cproject-name\u003e). If there are multiple projects, this will point to the directory of the first one.", + "type": "string" + }, + "component": { + "description": "Describes component to which given action relates", + "type": "string" + }, + "env": { + "description": "Optional list of environment variables that have to be set before running the command", + "type": "array", + "items": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": [ + "build", + "run", + "test", + "debug" + ] + } + }, + "additionalProperties": false + }, + "hotReloadCapable": { + "description": "Whether the command is capable to reload itself when source code changes. If set to 'true' the command won't be restarted and it is expected to handle file changes on its own.\n\nDefault value is 'false'", + "type": "boolean" + }, + "label": { + "description": "Optional label that provides a label for this command to be used in Editor UI menus for example", + "type": "string" + }, + "workingDir": { + "description": "Working directory where the command should be executed\n\nSpecial variables that can be used:\n\n - '$PROJECTS_ROOT': A path where projects sources are mounted as defined by container component's sourceMapping.\n\n - '$PROJECT_SOURCE': A path to a project source ($PROJECTS_ROOT/\u003cproject-name\u003e). If there are multiple projects, this will point to the directory of the first one.", + "type": "string" + } + }, + "additionalProperties": false + }, + "id": { + "description": "Mandatory identifier that allows referencing this command in composite commands, from a parent, or in events.", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "vscodeLaunch": { + "description": "Command providing the definition of a VsCode launch action", + "type": "object", + "oneOf": [ + { + "required": [ + "uri" + ] + }, + { + "required": [ + "inlined" + ] + } + ], + "properties": { + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": [ + "build", + "run", + "test", + "debug" + ] + } + }, + "additionalProperties": false + }, + "inlined": { + "description": "Inlined content of the VsCode configuration", + "type": "string" + }, + "uri": { + "description": "Location as an absolute of relative URI the VsCode configuration will be fetched from", + "type": "string" + } + }, + "additionalProperties": false + }, + "vscodeTask": { + "description": "Command providing the definition of a VsCode Task", + "type": "object", + "oneOf": [ + { + "required": [ + "uri" + ] + }, + { + "required": [ + "inlined" + ] + } + ], + "properties": { + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": [ + "build", + "run", + "test", + "debug" + ] + } + }, + "additionalProperties": false + }, + "inlined": { + "description": "Inlined content of the VsCode configuration", + "type": "string" + }, + "uri": { + "description": "Location as an absolute of relative URI the VsCode configuration will be fetched from", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "components": { + "description": "Overrides of components encapsulated in a parent devfile or a plugin. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "array", + "items": { + "type": "object", + "required": [ + "name" + ], + "oneOf": [ + { + "required": [ + "container" + ] + }, + { + "required": [ + "kubernetes" + ] + }, + { + "required": [ + "openshift" + ] + }, + { + "required": [ + "volume" + ] + } + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "container": { + "description": "Allows adding and configuring workspace-related containers", + "type": "object", + "properties": { + "args": { + "description": "The arguments to supply to the command running the dockerimage component. The arguments are supplied either to the default command provided in the image or to the overridden command.\n\nDefaults to an empty array, meaning use whatever is defined in the image.", + "type": "array", + "items": { + "type": "string" + } + }, + "command": { + "description": "The command to run in the dockerimage component instead of the default one provided in the image.\n\nDefaults to an empty array, meaning use whatever is defined in the image.", + "type": "array", + "items": { + "type": "string" + } + }, + "dedicatedPod": { + "description": "Specify if a container should run in its own separated pod, instead of running as part of the main development environment pod.\n\nDefault value is 'false'", + "type": "boolean" + }, + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\",", + "type": "object", + "additionalProperties": true + }, + "exposure": { + "description": "Describes how the endpoint should be exposed on the network.\n- 'public' means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- 'internal' means that the endpoint will be exposed internally outside of the main workspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- 'none' means that the endpoint will not be exposed and will only be accessible inside the main workspace POD, on a local address.\n\nDefault value is 'public'", + "type": "string", + "enum": [ + "public", + "internal", + "none" + ] + }, + "name": { + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "Path of the endpoint URL", + "type": "string" + }, + "protocol": { + "description": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- 'http': Endpoint will have 'http' traffic, typically on a TCP connection. It will be automaticaly promoted to 'https' when the 'secure' field is set to 'true'.\n- 'https': Endpoint will have 'https' traffic, typically on a TCP connection.\n- 'ws': Endpoint will have 'ws' traffic, typically on a TCP connection. It will be automaticaly promoted to 'wss' when the 'secure' field is set to 'true'.\n- 'wss': Endpoint will have 'wss' traffic, typically on a TCP connection.\n- 'tcp': Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- 'udp': Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is 'http'", + "type": "string", + "enum": [ + "http", + "https", + "ws", + "wss", + "tcp", + "udp" + ] + }, + "secure": { + "description": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of 'https' or 'wss'.", + "type": "boolean" + }, + "targetPort": { + "type": "integer" + } + }, + "additionalProperties": false + } + }, + "env": { + "description": "Environment variables used in this container.\n\nThe following variables are reserved and cannot be overridden via env:\n\n - '$PROJECTS_ROOT'\n\n - '$PROJECT_SOURCE'", + "type": "array", + "items": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "image": { + "type": "string" + }, + "memoryLimit": { + "type": "string" + }, + "mountSources": { + "description": "Toggles whether or not the project source code should be mounted in the component.\n\nDefaults to true for all component types except plugins and components that set 'dedicatedPod' to true.", + "type": "boolean" + }, + "sourceMapping": { + "description": "Optional specification of the path in the container where project sources should be transferred/mounted when 'mountSources' is 'true'. When omitted, the default value of /projects is used.", + "type": "string" + }, + "volumeMounts": { + "description": "List of volumes mounts that should be mounted is this container.", + "type": "array", + "items": { + "description": "Volume that should be mounted to a component container", + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "description": "The volume mount name is the name of an existing 'Volume' component. If several containers mount the same volume name then they will reuse the same volume and will be able to access to the same files.", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "The path in the component container where the volume should be mounted. If not path is mentioned, default path is the is '/\u003cname\u003e'.", + "type": "string" + } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false + }, + "kubernetes": { + "description": "Allows importing into the workspace the Kubernetes resources defined in a given manifest. For example this allows reusing the Kubernetes definitions used to deploy some runtime components in production.", + "type": "object", + "oneOf": [ + { + "required": [ + "uri" + ] + }, + { + "required": [ + "inlined" + ] + } + ], + "properties": { + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\",", + "type": "object", + "additionalProperties": true + }, + "exposure": { + "description": "Describes how the endpoint should be exposed on the network.\n- 'public' means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- 'internal' means that the endpoint will be exposed internally outside of the main workspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- 'none' means that the endpoint will not be exposed and will only be accessible inside the main workspace POD, on a local address.\n\nDefault value is 'public'", + "type": "string", + "enum": [ + "public", + "internal", + "none" + ] + }, + "name": { + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "Path of the endpoint URL", + "type": "string" + }, + "protocol": { + "description": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- 'http': Endpoint will have 'http' traffic, typically on a TCP connection. It will be automaticaly promoted to 'https' when the 'secure' field is set to 'true'.\n- 'https': Endpoint will have 'https' traffic, typically on a TCP connection.\n- 'ws': Endpoint will have 'ws' traffic, typically on a TCP connection. It will be automaticaly promoted to 'wss' when the 'secure' field is set to 'true'.\n- 'wss': Endpoint will have 'wss' traffic, typically on a TCP connection.\n- 'tcp': Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- 'udp': Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is 'http'", + "type": "string", + "enum": [ + "http", + "https", + "ws", + "wss", + "tcp", + "udp" + ] + }, + "secure": { + "description": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of 'https' or 'wss'.", + "type": "boolean" + }, + "targetPort": { + "type": "integer" + } + }, + "additionalProperties": false + } + }, + "inlined": { + "description": "Inlined manifest", + "type": "string" + }, + "uri": { + "description": "Location in a file fetched from a uri.", + "type": "string" + } + }, + "additionalProperties": false + }, + "name": { + "description": "Mandatory name that allows referencing the component from other elements (such as commands) or from an external devfile that may reference this component through a parent or a plugin.", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "openshift": { + "description": "Allows importing into the workspace the OpenShift resources defined in a given manifest. For example this allows reusing the OpenShift definitions used to deploy some runtime components in production.", + "type": "object", + "oneOf": [ + { + "required": [ + "uri" + ] + }, + { + "required": [ + "inlined" + ] + } + ], + "properties": { + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\",", + "type": "object", + "additionalProperties": true + }, + "exposure": { + "description": "Describes how the endpoint should be exposed on the network.\n- 'public' means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- 'internal' means that the endpoint will be exposed internally outside of the main workspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- 'none' means that the endpoint will not be exposed and will only be accessible inside the main workspace POD, on a local address.\n\nDefault value is 'public'", + "type": "string", + "enum": [ + "public", + "internal", + "none" + ] + }, + "name": { + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "Path of the endpoint URL", + "type": "string" + }, + "protocol": { + "description": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- 'http': Endpoint will have 'http' traffic, typically on a TCP connection. It will be automaticaly promoted to 'https' when the 'secure' field is set to 'true'.\n- 'https': Endpoint will have 'https' traffic, typically on a TCP connection.\n- 'ws': Endpoint will have 'ws' traffic, typically on a TCP connection. It will be automaticaly promoted to 'wss' when the 'secure' field is set to 'true'.\n- 'wss': Endpoint will have 'wss' traffic, typically on a TCP connection.\n- 'tcp': Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- 'udp': Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is 'http'", + "type": "string", + "enum": [ + "http", + "https", + "ws", + "wss", + "tcp", + "udp" + ] + }, + "secure": { + "description": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of 'https' or 'wss'.", + "type": "boolean" + }, + "targetPort": { + "type": "integer" + } + }, + "additionalProperties": false + } + }, + "inlined": { + "description": "Inlined manifest", + "type": "string" + }, + "uri": { + "description": "Location in a file fetched from a uri.", + "type": "string" + } + }, + "additionalProperties": false + }, + "volume": { + "description": "Allows specifying the definition of a volume shared by several other components", + "type": "object", + "properties": { + "size": { + "description": "Size of the volume", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "id": { + "description": "Id in a registry that contains a Devfile yaml file", + "type": "string" + }, + "kubernetes": { + "description": "Reference to a Kubernetes CRD of type DevWorkspaceTemplate", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + } + }, + "additionalProperties": false + }, + "registryUrl": { + "type": "string" + }, + "uri": { + "description": "Uri of a Devfile yaml file", + "type": "string" + } + }, + "additionalProperties": false + }, + "volume": { + "description": "Allows specifying the definition of a volume shared by several other components", + "type": "object", + "properties": { + "size": { + "description": "Size of the volume", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "id": { + "description": "Id in a registry that contains a Devfile yaml file", + "type": "string" + }, + "kubernetes": { + "description": "Reference to a Kubernetes CRD of type DevWorkspaceTemplate", + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + } + }, + "additionalProperties": false + }, + "projects": { + "description": "Overrides of projects encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "array", + "items": { + "type": "object", + "required": [ + "name" + ], + "oneOf": [ + { + "required": [ + "git" + ] + }, + { + "required": [ + "github" + ] + }, + { + "required": [ + "zip" + ] + } + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "clonePath": { + "description": "Path relative to the root of the projects to which this project should be cloned into. This is a unix-style relative path (i.e. uses forward slashes). The path is invalid if it is absolute or tries to escape the project root through the usage of '..'. If not specified, defaults to the project name.", + "type": "string" + }, + "git": { + "description": "Project's Git source", + "type": "object", + "properties": { + "checkoutFrom": { + "description": "Defines from what the project should be checked out. Required if there are more than one remote configured", + "type": "object", + "properties": { + "remote": { + "description": "The remote name should be used as init. Required if there are more than one remote configured", + "type": "string" + }, + "revision": { + "description": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found.", + "type": "string" + } + }, + "additionalProperties": false + }, + "remotes": { + "description": "The remotes map which should be initialized in the git project. Must have at least one remote configured", + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "github": { + "description": "Project's GitHub source", + "type": "object", + "properties": { + "checkoutFrom": { + "description": "Defines from what the project should be checked out. Required if there are more than one remote configured", + "type": "object", + "properties": { + "remote": { + "description": "The remote name should be used as init. Required if there are more than one remote configured", + "type": "string" + }, + "revision": { + "description": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found.", + "type": "string" + } + }, + "additionalProperties": false + }, + "remotes": { + "description": "The remotes map which should be initialized in the git project. Must have at least one remote configured", + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "name": { + "description": "Project name", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "sparseCheckoutDirs": { + "description": "Populate the project sparsely with selected directories.", + "type": "array", + "items": { + "type": "string" + } + }, + "zip": { + "description": "Project's Zip source", + "type": "object", + "properties": { + "location": { + "description": "Zip project's source location address. Should be file path of the archive, e.g. file://$FILE_PATH", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "registryUrl": { + "type": "string" + }, + "starterProjects": { + "description": "Overrides of starterProjects encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "array", + "items": { + "type": "object", + "required": [ + "name" + ], + "oneOf": [ + { + "required": [ + "git" + ] + }, + { + "required": [ + "github" + ] + }, + { + "required": [ + "zip" + ] + } + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "description": { + "description": "Description of a starter project", + "type": "string" + }, + "git": { + "description": "Project's Git source", + "type": "object", + "properties": { + "checkoutFrom": { + "description": "Defines from what the project should be checked out. Required if there are more than one remote configured", + "type": "object", + "properties": { + "remote": { + "description": "The remote name should be used as init. Required if there are more than one remote configured", + "type": "string" + }, + "revision": { + "description": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found.", + "type": "string" + } + }, + "additionalProperties": false + }, + "remotes": { + "description": "The remotes map which should be initialized in the git project. Must have at least one remote configured", + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "github": { + "description": "Project's GitHub source", + "type": "object", + "properties": { + "checkoutFrom": { + "description": "Defines from what the project should be checked out. Required if there are more than one remote configured", + "type": "object", + "properties": { + "remote": { + "description": "The remote name should be used as init. Required if there are more than one remote configured", + "type": "string" + }, + "revision": { + "description": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found.", + "type": "string" + } + }, + "additionalProperties": false + }, + "remotes": { + "description": "The remotes map which should be initialized in the git project. Must have at least one remote configured", + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "name": { + "description": "Project name", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "subDir": { + "description": "Sub-directory from a starter project to be used as root for starter project.", + "type": "string" + }, + "zip": { + "description": "Project's Zip source", + "type": "object", + "properties": { + "location": { + "description": "Zip project's source location address. Should be file path of the archive, e.g. file://$FILE_PATH", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "uri": { + "description": "Uri of a Devfile yaml file", + "type": "string" + } + }, + "additionalProperties": false + }, + "projects": { + "description": "Projects worked on in the workspace, containing names and sources locations", + "type": "array", + "items": { + "type": "object", + "required": [ + "name" + ], + "oneOf": [ + { + "required": [ + "git" + ] + }, + { + "required": [ + "github" + ] + }, + { + "required": [ + "zip" + ] + } + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "clonePath": { + "description": "Path relative to the root of the projects to which this project should be cloned into. This is a unix-style relative path (i.e. uses forward slashes). The path is invalid if it is absolute or tries to escape the project root through the usage of '..'. If not specified, defaults to the project name.", + "type": "string" + }, + "git": { + "description": "Project's Git source", + "type": "object", + "required": [ + "remotes" + ], + "properties": { + "checkoutFrom": { + "description": "Defines from what the project should be checked out. Required if there are more than one remote configured", + "type": "object", + "properties": { + "remote": { + "description": "The remote name should be used as init. Required if there are more than one remote configured", + "type": "string" + }, + "revision": { + "description": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found.", + "type": "string" + } + }, + "additionalProperties": false + }, + "remotes": { + "description": "The remotes map which should be initialized in the git project. Must have at least one remote configured", + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "github": { + "description": "Project's GitHub source", + "type": "object", + "required": [ + "remotes" + ], + "properties": { + "checkoutFrom": { + "description": "Defines from what the project should be checked out. Required if there are more than one remote configured", + "type": "object", + "properties": { + "remote": { + "description": "The remote name should be used as init. Required if there are more than one remote configured", + "type": "string" + }, + "revision": { + "description": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found.", + "type": "string" + } + }, + "additionalProperties": false + }, + "remotes": { + "description": "The remotes map which should be initialized in the git project. Must have at least one remote configured", + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "name": { + "description": "Project name", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "sparseCheckoutDirs": { + "description": "Populate the project sparsely with selected directories.", + "type": "array", + "items": { + "type": "string" + } + }, + "zip": { + "description": "Project's Zip source", + "type": "object", + "properties": { + "location": { + "description": "Zip project's source location address. Should be file path of the archive, e.g. file://$FILE_PATH", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "schemaVersion": { + "description": "Devfile schema version", + "type": "string", + "pattern": "^([2-9])\\.([0-9]+)\\.([0-9]+)(\\-[0-9a-z-]+(\\.[0-9a-z-]+)*)?(\\+[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?$" + }, + "starterProjects": { + "description": "StarterProjects is a project that can be used as a starting point when bootstrapping new projects", + "type": "array", + "items": { + "type": "object", + "required": [ + "name" + ], + "oneOf": [ + { + "required": [ + "git" + ] + }, + { + "required": [ + "github" + ] + }, + { + "required": [ + "zip" + ] + } + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "description": { + "description": "Description of a starter project", + "type": "string" + }, + "git": { + "description": "Project's Git source", + "type": "object", + "required": [ + "remotes" + ], + "properties": { + "checkoutFrom": { + "description": "Defines from what the project should be checked out. Required if there are more than one remote configured", + "type": "object", + "properties": { + "remote": { + "description": "The remote name should be used as init. Required if there are more than one remote configured", + "type": "string" + }, + "revision": { + "description": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found.", + "type": "string" + } + }, + "additionalProperties": false + }, + "remotes": { + "description": "The remotes map which should be initialized in the git project. Must have at least one remote configured", + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "github": { + "description": "Project's GitHub source", + "type": "object", + "required": [ + "remotes" + ], + "properties": { + "checkoutFrom": { + "description": "Defines from what the project should be checked out. Required if there are more than one remote configured", + "type": "object", + "properties": { + "remote": { + "description": "The remote name should be used as init. Required if there are more than one remote configured", + "type": "string" + }, + "revision": { + "description": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found.", + "type": "string" + } + }, + "additionalProperties": false + }, + "remotes": { + "description": "The remotes map which should be initialized in the git project. Must have at least one remote configured", + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "name": { + "description": "Project name", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "subDir": { + "description": "Sub-directory from a starter project to be used as root for starter project.", + "type": "string" + }, + "zip": { + "description": "Project's Zip source", + "type": "object", + "properties": { + "location": { + "description": "Zip project's source location address. Should be file path of the archive, e.g. file://$FILE_PATH", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false +} +` diff --git a/pkg/devfile/parser/data/v2/2.1.0/devfileJsonSchema210.go b/pkg/devfile/parser/data/v2/2.1.0/devfileJsonSchema210.go new file mode 100644 index 0000000..7145fd0 --- /dev/null +++ b/pkg/devfile/parser/data/v2/2.1.0/devfileJsonSchema210.go @@ -0,0 +1,1662 @@ +package version210 + +// https://raw.githubusercontent.com/devfile/api/2.1.x/schemas/latest/devfile.json +const JsonSchema210 = `{ + "description": "Devfile describes the structure of a cloud-native devworkspace and development environment.", + "type": "object", + "title": "Devfile schema - Version 2.1.0", + "required": [ + "schemaVersion" + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "commands": { + "description": "Predefined, ready-to-use, devworkspace-related commands", + "type": "array", + "items": { + "type": "object", + "required": [ + "id" + ], + "oneOf": [ + { + "required": [ + "exec" + ] + }, + { + "required": [ + "apply" + ] + }, + { + "required": [ + "composite" + ] + } + ], + "properties": { + "apply": { + "description": "Command that consists in applying a given component definition, typically bound to a devworkspace event.\n\nFor example, when an 'apply' command is bound to a 'preStart' event, and references a 'container' component, it will start the container as a K8S initContainer in the devworkspace POD, unless the component has its 'dedicatedPod' field set to 'true'.\n\nWhen no 'apply' command exist for a given component, it is assumed the component will be applied at devworkspace start by default.", + "type": "object", + "required": [ + "component" + ], + "properties": { + "component": { + "description": "Describes component that will be applied", + "type": "string" + }, + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "required": [ + "kind" + ], + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": [ + "build", + "run", + "test", + "debug" + ] + } + }, + "additionalProperties": false + }, + "label": { + "description": "Optional label that provides a label for this command to be used in Editor UI menus for example", + "type": "string" + } + }, + "additionalProperties": false + }, + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "composite": { + "description": "Composite command that allows executing several sub-commands either sequentially or concurrently", + "type": "object", + "properties": { + "commands": { + "description": "The commands that comprise this composite command", + "type": "array", + "items": { + "type": "string" + } + }, + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "required": [ + "kind" + ], + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": [ + "build", + "run", + "test", + "debug" + ] + } + }, + "additionalProperties": false + }, + "label": { + "description": "Optional label that provides a label for this command to be used in Editor UI menus for example", + "type": "string" + }, + "parallel": { + "description": "Indicates if the sub-commands should be executed concurrently", + "type": "boolean" + } + }, + "additionalProperties": false + }, + "exec": { + "description": "CLI Command executed in an existing component container", + "type": "object", + "required": [ + "commandLine", + "component" + ], + "properties": { + "commandLine": { + "description": "The actual command-line string\n\nSpecial variables that can be used:\n\n - '$PROJECTS_ROOT': A path where projects sources are mounted as defined by container component's sourceMapping.\n\n - '$PROJECT_SOURCE': A path to a project source ($PROJECTS_ROOT/\u003cproject-name\u003e). If there are multiple projects, this will point to the directory of the first one.", + "type": "string" + }, + "component": { + "description": "Describes component to which given action relates", + "type": "string" + }, + "env": { + "description": "Optional list of environment variables that have to be set before running the command", + "type": "array", + "items": { + "type": "object", + "required": [ + "name", + "value" + ], + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "required": [ + "kind" + ], + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": [ + "build", + "run", + "test", + "debug" + ] + } + }, + "additionalProperties": false + }, + "hotReloadCapable": { + "description": "Whether the command is capable to reload itself when source code changes. If set to 'true' the command won't be restarted and it is expected to handle file changes on its own.\n\nDefault value is 'false'", + "type": "boolean" + }, + "label": { + "description": "Optional label that provides a label for this command to be used in Editor UI menus for example", + "type": "string" + }, + "workingDir": { + "description": "Working directory where the command should be executed\n\nSpecial variables that can be used:\n\n - '$PROJECTS_ROOT': A path where projects sources are mounted as defined by container component's sourceMapping.\n\n - '$PROJECT_SOURCE': A path to a project source ($PROJECTS_ROOT/\u003cproject-name\u003e). If there are multiple projects, this will point to the directory of the first one.", + "type": "string" + } + }, + "additionalProperties": false + }, + "id": { + "description": "Mandatory identifier that allows referencing this command in composite commands, from a parent, or in events.", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + } + }, + "additionalProperties": false + } + }, + "components": { + "description": "List of the devworkspace components, such as editor and plugins, user-provided containers, or other types of components", + "type": "array", + "items": { + "type": "object", + "required": [ + "name" + ], + "oneOf": [ + { + "required": [ + "container" + ] + }, + { + "required": [ + "kubernetes" + ] + }, + { + "required": [ + "openshift" + ] + }, + { + "required": [ + "volume" + ] + } + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "container": { + "description": "Allows adding and configuring devworkspace-related containers", + "type": "object", + "required": [ + "image" + ], + "properties": { + "args": { + "description": "The arguments to supply to the command running the dockerimage component. The arguments are supplied either to the default command provided in the image or to the overridden command.\n\nDefaults to an empty array, meaning use whatever is defined in the image.", + "type": "array", + "items": { + "type": "string" + } + }, + "command": { + "description": "The command to run in the dockerimage component instead of the default one provided in the image.\n\nDefaults to an empty array, meaning use whatever is defined in the image.", + "type": "array", + "items": { + "type": "string" + } + }, + "cpuLimit": { + "type": "string" + }, + "cpuRequest": { + "type": "string" + }, + "dedicatedPod": { + "description": "Specify if a container should run in its own separated pod, instead of running as part of the main development environment pod.\n\nDefault value is 'false'", + "type": "boolean" + }, + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": [ + "name", + "targetPort" + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\",", + "type": "object", + "additionalProperties": true + }, + "exposure": { + "description": "Describes how the endpoint should be exposed on the network.\n- 'public' means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- 'internal' means that the endpoint will be exposed internally outside of the main devworkspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- 'none' means that the endpoint will not be exposed and will only be accessible inside the main devworkspace POD, on a local address.\n\nDefault value is 'public'", + "type": "string", + "default": "public", + "enum": [ + "public", + "internal", + "none" + ] + }, + "name": { + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "Path of the endpoint URL", + "type": "string" + }, + "protocol": { + "description": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- 'http': Endpoint will have 'http' traffic, typically on a TCP connection. It will be automaticaly promoted to 'https' when the 'secure' field is set to 'true'.\n- 'https': Endpoint will have 'https' traffic, typically on a TCP connection.\n- 'ws': Endpoint will have 'ws' traffic, typically on a TCP connection. It will be automaticaly promoted to 'wss' when the 'secure' field is set to 'true'.\n- 'wss': Endpoint will have 'wss' traffic, typically on a TCP connection.\n- 'tcp': Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- 'udp': Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is 'http'", + "type": "string", + "default": "http", + "enum": [ + "http", + "https", + "ws", + "wss", + "tcp", + "udp" + ] + }, + "secure": { + "description": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of 'https' or 'wss'.", + "type": "boolean" + }, + "targetPort": { + "type": "integer" + } + }, + "additionalProperties": false + } + }, + "env": { + "description": "Environment variables used in this container.\n\nThe following variables are reserved and cannot be overridden via env:\n\n - '$PROJECTS_ROOT'\n\n - '$PROJECT_SOURCE'", + "type": "array", + "items": { + "type": "object", + "required": [ + "name", + "value" + ], + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "image": { + "type": "string" + }, + "memoryLimit": { + "type": "string" + }, + "memoryRequest": { + "type": "string" + }, + "mountSources": { + "description": "Toggles whether or not the project source code should be mounted in the component.\n\nDefaults to true for all component types except plugins and components that set 'dedicatedPod' to true.", + "type": "boolean" + }, + "sourceMapping": { + "description": "Optional specification of the path in the container where project sources should be transferred/mounted when 'mountSources' is 'true'. When omitted, the default value of /projects is used.", + "type": "string", + "default": "/projects" + }, + "volumeMounts": { + "description": "List of volumes mounts that should be mounted is this container.", + "type": "array", + "items": { + "description": "Volume that should be mounted to a component container", + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "description": "The volume mount name is the name of an existing 'Volume' component. If several containers mount the same volume name then they will reuse the same volume and will be able to access to the same files.", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "The path in the component container where the volume should be mounted. If not path is mentioned, default path is the is '/\u003cname\u003e'.", + "type": "string" + } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false + }, + "kubernetes": { + "description": "Allows importing into the devworkspace the Kubernetes resources defined in a given manifest. For example this allows reusing the Kubernetes definitions used to deploy some runtime components in production.", + "type": "object", + "oneOf": [ + { + "required": [ + "uri" + ] + }, + { + "required": [ + "inlined" + ] + } + ], + "properties": { + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": [ + "name", + "targetPort" + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\",", + "type": "object", + "additionalProperties": true + }, + "exposure": { + "description": "Describes how the endpoint should be exposed on the network.\n- 'public' means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- 'internal' means that the endpoint will be exposed internally outside of the main devworkspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- 'none' means that the endpoint will not be exposed and will only be accessible inside the main devworkspace POD, on a local address.\n\nDefault value is 'public'", + "type": "string", + "default": "public", + "enum": [ + "public", + "internal", + "none" + ] + }, + "name": { + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "Path of the endpoint URL", + "type": "string" + }, + "protocol": { + "description": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- 'http': Endpoint will have 'http' traffic, typically on a TCP connection. It will be automaticaly promoted to 'https' when the 'secure' field is set to 'true'.\n- 'https': Endpoint will have 'https' traffic, typically on a TCP connection.\n- 'ws': Endpoint will have 'ws' traffic, typically on a TCP connection. It will be automaticaly promoted to 'wss' when the 'secure' field is set to 'true'.\n- 'wss': Endpoint will have 'wss' traffic, typically on a TCP connection.\n- 'tcp': Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- 'udp': Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is 'http'", + "type": "string", + "default": "http", + "enum": [ + "http", + "https", + "ws", + "wss", + "tcp", + "udp" + ] + }, + "secure": { + "description": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of 'https' or 'wss'.", + "type": "boolean" + }, + "targetPort": { + "type": "integer" + } + }, + "additionalProperties": false + } + }, + "inlined": { + "description": "Inlined manifest", + "type": "string" + }, + "uri": { + "description": "Location in a file fetched from a uri.", + "type": "string" + } + }, + "additionalProperties": false + }, + "name": { + "description": "Mandatory name that allows referencing the component from other elements (such as commands) or from an external devfile that may reference this component through a parent or a plugin.", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "openshift": { + "description": "Allows importing into the devworkspace the OpenShift resources defined in a given manifest. For example this allows reusing the OpenShift definitions used to deploy some runtime components in production.", + "type": "object", + "oneOf": [ + { + "required": [ + "uri" + ] + }, + { + "required": [ + "inlined" + ] + } + ], + "properties": { + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": [ + "name", + "targetPort" + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\",", + "type": "object", + "additionalProperties": true + }, + "exposure": { + "description": "Describes how the endpoint should be exposed on the network.\n- 'public' means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- 'internal' means that the endpoint will be exposed internally outside of the main devworkspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- 'none' means that the endpoint will not be exposed and will only be accessible inside the main devworkspace POD, on a local address.\n\nDefault value is 'public'", + "type": "string", + "default": "public", + "enum": [ + "public", + "internal", + "none" + ] + }, + "name": { + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "Path of the endpoint URL", + "type": "string" + }, + "protocol": { + "description": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- 'http': Endpoint will have 'http' traffic, typically on a TCP connection. It will be automaticaly promoted to 'https' when the 'secure' field is set to 'true'.\n- 'https': Endpoint will have 'https' traffic, typically on a TCP connection.\n- 'ws': Endpoint will have 'ws' traffic, typically on a TCP connection. It will be automaticaly promoted to 'wss' when the 'secure' field is set to 'true'.\n- 'wss': Endpoint will have 'wss' traffic, typically on a TCP connection.\n- 'tcp': Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- 'udp': Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is 'http'", + "type": "string", + "default": "http", + "enum": [ + "http", + "https", + "ws", + "wss", + "tcp", + "udp" + ] + }, + "secure": { + "description": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of 'https' or 'wss'.", + "type": "boolean" + }, + "targetPort": { + "type": "integer" + } + }, + "additionalProperties": false + } + }, + "inlined": { + "description": "Inlined manifest", + "type": "string" + }, + "uri": { + "description": "Location in a file fetched from a uri.", + "type": "string" + } + }, + "additionalProperties": false + }, + "volume": { + "description": "Allows specifying the definition of a volume shared by several other components", + "type": "object", + "properties": { + "ephemeral": { + "description": "Ephemeral volumes are not stored persistently across restarts. Defaults to false", + "type": "boolean" + }, + "size": { + "description": "Size of the volume", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "events": { + "description": "Bindings of commands to events. Each command is referred-to by its name.", + "type": "object", + "properties": { + "postStart": { + "description": "IDs of commands that should be executed after the devworkspace is completely started. In the case of Che-Theia, these commands should be executed after all plugins and extensions have started, including project cloning. This means that those commands are not triggered until the user opens the IDE in his browser.", + "type": "array", + "items": { + "type": "string" + } + }, + "postStop": { + "description": "IDs of commands that should be executed after stopping the devworkspace.", + "type": "array", + "items": { + "type": "string" + } + }, + "preStart": { + "description": "IDs of commands that should be executed before the devworkspace start. Kubernetes-wise, these commands would typically be executed in init containers of the devworkspace POD.", + "type": "array", + "items": { + "type": "string" + } + }, + "preStop": { + "description": "IDs of commands that should be executed before stopping the devworkspace.", + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "metadata": { + "description": "Optional metadata", + "type": "object", + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes. Deprecated, use the top-level attributes field instead.", + "type": "object", + "additionalProperties": true + }, + "description": { + "description": "Optional devfile description", + "type": "string" + }, + "displayName": { + "description": "Optional devfile display name", + "type": "string" + }, + "globalMemoryLimit": { + "description": "Optional devfile global memory limit", + "type": "string" + }, + "icon": { + "description": "Optional devfile icon, can be a URI or a relative path in the project", + "type": "string" + }, + "language": { + "description": "Optional devfile language", + "type": "string" + }, + "name": { + "description": "Optional devfile name", + "type": "string" + }, + "projectType": { + "description": "Optional devfile project type", + "type": "string" + }, + "tags": { + "description": "Optional devfile tags", + "type": "array", + "items": { + "type": "string" + } + }, + "version": { + "description": "Optional semver-compatible version", + "type": "string", + "pattern": "^([0-9]+)\\.([0-9]+)\\.([0-9]+)(\\-[0-9a-z-]+(\\.[0-9a-z-]+)*)?(\\+[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?$" + }, + "website": { + "description": "Optional devfile website", + "type": "string" + } + }, + "additionalProperties": true + }, + "parent": { + "description": "Parent devworkspace template", + "type": "object", + "oneOf": [ + { + "required": [ + "uri" + ] + }, + { + "required": [ + "id" + ] + }, + { + "required": [ + "kubernetes" + ] + } + ], + "properties": { + "attributes": { + "description": "Overrides of attributes encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "object", + "additionalProperties": true + }, + "commands": { + "description": "Overrides of commands encapsulated in a parent devfile or a plugin. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "array", + "items": { + "type": "object", + "required": [ + "id" + ], + "oneOf": [ + { + "required": [ + "exec" + ] + }, + { + "required": [ + "apply" + ] + }, + { + "required": [ + "composite" + ] + } + ], + "properties": { + "apply": { + "description": "Command that consists in applying a given component definition, typically bound to a devworkspace event.\n\nFor example, when an 'apply' command is bound to a 'preStart' event, and references a 'container' component, it will start the container as a K8S initContainer in the devworkspace POD, unless the component has its 'dedicatedPod' field set to 'true'.\n\nWhen no 'apply' command exist for a given component, it is assumed the component will be applied at devworkspace start by default.", + "type": "object", + "properties": { + "component": { + "description": "Describes component that will be applied", + "type": "string" + }, + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": [ + "build", + "run", + "test", + "debug" + ] + } + }, + "additionalProperties": false + }, + "label": { + "description": "Optional label that provides a label for this command to be used in Editor UI menus for example", + "type": "string" + } + }, + "additionalProperties": false + }, + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "composite": { + "description": "Composite command that allows executing several sub-commands either sequentially or concurrently", + "type": "object", + "properties": { + "commands": { + "description": "The commands that comprise this composite command", + "type": "array", + "items": { + "type": "string" + } + }, + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": [ + "build", + "run", + "test", + "debug" + ] + } + }, + "additionalProperties": false + }, + "label": { + "description": "Optional label that provides a label for this command to be used in Editor UI menus for example", + "type": "string" + }, + "parallel": { + "description": "Indicates if the sub-commands should be executed concurrently", + "type": "boolean" + } + }, + "additionalProperties": false + }, + "exec": { + "description": "CLI Command executed in an existing component container", + "type": "object", + "properties": { + "commandLine": { + "description": "The actual command-line string\n\nSpecial variables that can be used:\n\n - '$PROJECTS_ROOT': A path where projects sources are mounted as defined by container component's sourceMapping.\n\n - '$PROJECT_SOURCE': A path to a project source ($PROJECTS_ROOT/\u003cproject-name\u003e). If there are multiple projects, this will point to the directory of the first one.", + "type": "string" + }, + "component": { + "description": "Describes component to which given action relates", + "type": "string" + }, + "env": { + "description": "Optional list of environment variables that have to be set before running the command", + "type": "array", + "items": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": [ + "build", + "run", + "test", + "debug" + ] + } + }, + "additionalProperties": false + }, + "hotReloadCapable": { + "description": "Whether the command is capable to reload itself when source code changes. If set to 'true' the command won't be restarted and it is expected to handle file changes on its own.\n\nDefault value is 'false'", + "type": "boolean" + }, + "label": { + "description": "Optional label that provides a label for this command to be used in Editor UI menus for example", + "type": "string" + }, + "workingDir": { + "description": "Working directory where the command should be executed\n\nSpecial variables that can be used:\n\n - '$PROJECTS_ROOT': A path where projects sources are mounted as defined by container component's sourceMapping.\n\n - '$PROJECT_SOURCE': A path to a project source ($PROJECTS_ROOT/\u003cproject-name\u003e). If there are multiple projects, this will point to the directory of the first one.", + "type": "string" + } + }, + "additionalProperties": false + }, + "id": { + "description": "Mandatory identifier that allows referencing this command in composite commands, from a parent, or in events.", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + } + }, + "additionalProperties": false + } + }, + "components": { + "description": "Overrides of components encapsulated in a parent devfile or a plugin. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "array", + "items": { + "type": "object", + "required": [ + "name" + ], + "oneOf": [ + { + "required": [ + "container" + ] + }, + { + "required": [ + "kubernetes" + ] + }, + { + "required": [ + "openshift" + ] + }, + { + "required": [ + "volume" + ] + } + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "container": { + "description": "Allows adding and configuring devworkspace-related containers", + "type": "object", + "properties": { + "args": { + "description": "The arguments to supply to the command running the dockerimage component. The arguments are supplied either to the default command provided in the image or to the overridden command.\n\nDefaults to an empty array, meaning use whatever is defined in the image.", + "type": "array", + "items": { + "type": "string" + } + }, + "command": { + "description": "The command to run in the dockerimage component instead of the default one provided in the image.\n\nDefaults to an empty array, meaning use whatever is defined in the image.", + "type": "array", + "items": { + "type": "string" + } + }, + "cpuLimit": { + "type": "string" + }, + "cpuRequest": { + "type": "string" + }, + "dedicatedPod": { + "description": "Specify if a container should run in its own separated pod, instead of running as part of the main development environment pod.\n\nDefault value is 'false'", + "type": "boolean" + }, + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\",", + "type": "object", + "additionalProperties": true + }, + "exposure": { + "description": "Describes how the endpoint should be exposed on the network.\n- 'public' means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- 'internal' means that the endpoint will be exposed internally outside of the main devworkspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- 'none' means that the endpoint will not be exposed and will only be accessible inside the main devworkspace POD, on a local address.\n\nDefault value is 'public'", + "type": "string", + "enum": [ + "public", + "internal", + "none" + ] + }, + "name": { + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "Path of the endpoint URL", + "type": "string" + }, + "protocol": { + "description": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- 'http': Endpoint will have 'http' traffic, typically on a TCP connection. It will be automaticaly promoted to 'https' when the 'secure' field is set to 'true'.\n- 'https': Endpoint will have 'https' traffic, typically on a TCP connection.\n- 'ws': Endpoint will have 'ws' traffic, typically on a TCP connection. It will be automaticaly promoted to 'wss' when the 'secure' field is set to 'true'.\n- 'wss': Endpoint will have 'wss' traffic, typically on a TCP connection.\n- 'tcp': Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- 'udp': Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is 'http'", + "type": "string", + "enum": [ + "http", + "https", + "ws", + "wss", + "tcp", + "udp" + ] + }, + "secure": { + "description": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of 'https' or 'wss'.", + "type": "boolean" + }, + "targetPort": { + "type": "integer" + } + }, + "additionalProperties": false + } + }, + "env": { + "description": "Environment variables used in this container.\n\nThe following variables are reserved and cannot be overridden via env:\n\n - '$PROJECTS_ROOT'\n\n - '$PROJECT_SOURCE'", + "type": "array", + "items": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "image": { + "type": "string" + }, + "memoryLimit": { + "type": "string" + }, + "memoryRequest": { + "type": "string" + }, + "mountSources": { + "description": "Toggles whether or not the project source code should be mounted in the component.\n\nDefaults to true for all component types except plugins and components that set 'dedicatedPod' to true.", + "type": "boolean" + }, + "sourceMapping": { + "description": "Optional specification of the path in the container where project sources should be transferred/mounted when 'mountSources' is 'true'. When omitted, the default value of /projects is used.", + "type": "string" + }, + "volumeMounts": { + "description": "List of volumes mounts that should be mounted is this container.", + "type": "array", + "items": { + "description": "Volume that should be mounted to a component container", + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "description": "The volume mount name is the name of an existing 'Volume' component. If several containers mount the same volume name then they will reuse the same volume and will be able to access to the same files.", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "The path in the component container where the volume should be mounted. If not path is mentioned, default path is the is '/\u003cname\u003e'.", + "type": "string" + } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false + }, + "kubernetes": { + "description": "Allows importing into the devworkspace the Kubernetes resources defined in a given manifest. For example this allows reusing the Kubernetes definitions used to deploy some runtime components in production.", + "type": "object", + "oneOf": [ + { + "required": [ + "uri" + ] + }, + { + "required": [ + "inlined" + ] + } + ], + "properties": { + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\",", + "type": "object", + "additionalProperties": true + }, + "exposure": { + "description": "Describes how the endpoint should be exposed on the network.\n- 'public' means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- 'internal' means that the endpoint will be exposed internally outside of the main devworkspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- 'none' means that the endpoint will not be exposed and will only be accessible inside the main devworkspace POD, on a local address.\n\nDefault value is 'public'", + "type": "string", + "enum": [ + "public", + "internal", + "none" + ] + }, + "name": { + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "Path of the endpoint URL", + "type": "string" + }, + "protocol": { + "description": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- 'http': Endpoint will have 'http' traffic, typically on a TCP connection. It will be automaticaly promoted to 'https' when the 'secure' field is set to 'true'.\n- 'https': Endpoint will have 'https' traffic, typically on a TCP connection.\n- 'ws': Endpoint will have 'ws' traffic, typically on a TCP connection. It will be automaticaly promoted to 'wss' when the 'secure' field is set to 'true'.\n- 'wss': Endpoint will have 'wss' traffic, typically on a TCP connection.\n- 'tcp': Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- 'udp': Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is 'http'", + "type": "string", + "enum": [ + "http", + "https", + "ws", + "wss", + "tcp", + "udp" + ] + }, + "secure": { + "description": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of 'https' or 'wss'.", + "type": "boolean" + }, + "targetPort": { + "type": "integer" + } + }, + "additionalProperties": false + } + }, + "inlined": { + "description": "Inlined manifest", + "type": "string" + }, + "uri": { + "description": "Location in a file fetched from a uri.", + "type": "string" + } + }, + "additionalProperties": false + }, + "name": { + "description": "Mandatory name that allows referencing the component from other elements (such as commands) or from an external devfile that may reference this component through a parent or a plugin.", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "openshift": { + "description": "Allows importing into the devworkspace the OpenShift resources defined in a given manifest. For example this allows reusing the OpenShift definitions used to deploy some runtime components in production.", + "type": "object", + "oneOf": [ + { + "required": [ + "uri" + ] + }, + { + "required": [ + "inlined" + ] + } + ], + "properties": { + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\",", + "type": "object", + "additionalProperties": true + }, + "exposure": { + "description": "Describes how the endpoint should be exposed on the network.\n- 'public' means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- 'internal' means that the endpoint will be exposed internally outside of the main devworkspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- 'none' means that the endpoint will not be exposed and will only be accessible inside the main devworkspace POD, on a local address.\n\nDefault value is 'public'", + "type": "string", + "enum": [ + "public", + "internal", + "none" + ] + }, + "name": { + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "Path of the endpoint URL", + "type": "string" + }, + "protocol": { + "description": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- 'http': Endpoint will have 'http' traffic, typically on a TCP connection. It will be automaticaly promoted to 'https' when the 'secure' field is set to 'true'.\n- 'https': Endpoint will have 'https' traffic, typically on a TCP connection.\n- 'ws': Endpoint will have 'ws' traffic, typically on a TCP connection. It will be automaticaly promoted to 'wss' when the 'secure' field is set to 'true'.\n- 'wss': Endpoint will have 'wss' traffic, typically on a TCP connection.\n- 'tcp': Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- 'udp': Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is 'http'", + "type": "string", + "enum": [ + "http", + "https", + "ws", + "wss", + "tcp", + "udp" + ] + }, + "secure": { + "description": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of 'https' or 'wss'.", + "type": "boolean" + }, + "targetPort": { + "type": "integer" + } + }, + "additionalProperties": false + } + }, + "inlined": { + "description": "Inlined manifest", + "type": "string" + }, + "uri": { + "description": "Location in a file fetched from a uri.", + "type": "string" + } + }, + "additionalProperties": false + }, + "volume": { + "description": "Allows specifying the definition of a volume shared by several other components", + "type": "object", + "properties": { + "ephemeral": { + "description": "Ephemeral volumes are not stored persistently across restarts. Defaults to false", + "type": "boolean" + }, + "size": { + "description": "Size of the volume", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "id": { + "description": "Id in a registry that contains a Devfile yaml file", + "type": "string" + }, + "kubernetes": { + "description": "Reference to a Kubernetes CRD of type DevWorkspaceTemplate", + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + } + }, + "additionalProperties": false + }, + "projects": { + "description": "Overrides of projects encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "array", + "items": { + "type": "object", + "required": [ + "name" + ], + "oneOf": [ + { + "required": [ + "git" + ] + }, + { + "required": [ + "zip" + ] + } + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "clonePath": { + "description": "Path relative to the root of the projects to which this project should be cloned into. This is a unix-style relative path (i.e. uses forward slashes). The path is invalid if it is absolute or tries to escape the project root through the usage of '..'. If not specified, defaults to the project name.", + "type": "string" + }, + "git": { + "description": "Project's Git source", + "type": "object", + "properties": { + "checkoutFrom": { + "description": "Defines from what the project should be checked out. Required if there are more than one remote configured", + "type": "object", + "properties": { + "remote": { + "description": "The remote name should be used as init. Required if there are more than one remote configured", + "type": "string" + }, + "revision": { + "description": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found.", + "type": "string" + } + }, + "additionalProperties": false + }, + "remotes": { + "description": "The remotes map which should be initialized in the git project. Must have at least one remote configured", + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "name": { + "description": "Project name", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "zip": { + "description": "Project's Zip source", + "type": "object", + "properties": { + "location": { + "description": "Zip project's source location address. Should be file path of the archive, e.g. file://$FILE_PATH", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "registryUrl": { + "description": "Registry URL to pull the parent devfile from when using id in the parent reference. To ensure the parent devfile gets resolved consistently in different environments, it is recommended to always specify the 'regsitryURL' when 'Id' is used.", + "type": "string" + }, + "starterProjects": { + "description": "Overrides of starterProjects encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "array", + "items": { + "type": "object", + "required": [ + "name" + ], + "oneOf": [ + { + "required": [ + "git" + ] + }, + { + "required": [ + "zip" + ] + } + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "description": { + "description": "Description of a starter project", + "type": "string" + }, + "git": { + "description": "Project's Git source", + "type": "object", + "properties": { + "checkoutFrom": { + "description": "Defines from what the project should be checked out. Required if there are more than one remote configured", + "type": "object", + "properties": { + "remote": { + "description": "The remote name should be used as init. Required if there are more than one remote configured", + "type": "string" + }, + "revision": { + "description": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found.", + "type": "string" + } + }, + "additionalProperties": false + }, + "remotes": { + "description": "The remotes map which should be initialized in the git project. Must have at least one remote configured", + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "name": { + "description": "Project name", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "subDir": { + "description": "Sub-directory from a starter project to be used as root for starter project.", + "type": "string" + }, + "zip": { + "description": "Project's Zip source", + "type": "object", + "properties": { + "location": { + "description": "Zip project's source location address. Should be file path of the archive, e.g. file://$FILE_PATH", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "uri": { + "description": "URI Reference of a parent devfile YAML file. It can be a full URL or a relative URI with the current devfile as the base URI.", + "type": "string" + }, + "variables": { + "description": "Overrides of variables encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "projects": { + "description": "Projects worked on in the devworkspace, containing names and sources locations", + "type": "array", + "items": { + "type": "object", + "required": [ + "name" + ], + "oneOf": [ + { + "required": [ + "git" + ] + }, + { + "required": [ + "zip" + ] + } + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "clonePath": { + "description": "Path relative to the root of the projects to which this project should be cloned into. This is a unix-style relative path (i.e. uses forward slashes). The path is invalid if it is absolute or tries to escape the project root through the usage of '..'. If not specified, defaults to the project name.", + "type": "string" + }, + "git": { + "description": "Project's Git source", + "type": "object", + "required": [ + "remotes" + ], + "properties": { + "checkoutFrom": { + "description": "Defines from what the project should be checked out. Required if there are more than one remote configured", + "type": "object", + "properties": { + "remote": { + "description": "The remote name should be used as init. Required if there are more than one remote configured", + "type": "string" + }, + "revision": { + "description": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found.", + "type": "string" + } + }, + "additionalProperties": false + }, + "remotes": { + "description": "The remotes map which should be initialized in the git project. Must have at least one remote configured", + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "name": { + "description": "Project name", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "zip": { + "description": "Project's Zip source", + "type": "object", + "properties": { + "location": { + "description": "Zip project's source location address. Should be file path of the archive, e.g. file://$FILE_PATH", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "schemaVersion": { + "description": "Devfile schema version", + "type": "string", + "pattern": "^([2-9])\\.([0-9]+)\\.([0-9]+)(\\-[0-9a-z-]+(\\.[0-9a-z-]+)*)?(\\+[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?$" + }, + "starterProjects": { + "description": "StarterProjects is a project that can be used as a starting point when bootstrapping new projects", + "type": "array", + "items": { + "type": "object", + "required": [ + "name" + ], + "oneOf": [ + { + "required": [ + "git" + ] + }, + { + "required": [ + "zip" + ] + } + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "description": { + "description": "Description of a starter project", + "type": "string" + }, + "git": { + "description": "Project's Git source", + "type": "object", + "required": [ + "remotes" + ], + "properties": { + "checkoutFrom": { + "description": "Defines from what the project should be checked out. Required if there are more than one remote configured", + "type": "object", + "properties": { + "remote": { + "description": "The remote name should be used as init. Required if there are more than one remote configured", + "type": "string" + }, + "revision": { + "description": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found.", + "type": "string" + } + }, + "additionalProperties": false + }, + "remotes": { + "description": "The remotes map which should be initialized in the git project. Must have at least one remote configured", + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "name": { + "description": "Project name", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "subDir": { + "description": "Sub-directory from a starter project to be used as root for starter project.", + "type": "string" + }, + "zip": { + "description": "Project's Zip source", + "type": "object", + "properties": { + "location": { + "description": "Zip project's source location address. Should be file path of the archive, e.g. file://$FILE_PATH", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "variables": { + "description": "Map of key-value variables used for string replacement in the devfile. Values can can be referenced via {{variable-key}} to replace the corresponding value in string fields in the devfile. Replacement cannot be used for\n\n - schemaVersion, metadata, parent source - element identifiers, e.g. command id, component name, endpoint name, project name - references to identifiers, e.g. in events, a command's component, container's volume mount name - string enums, e.g. command group kind, endpoint exposure", + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "additionalProperties": false +} +` diff --git a/pkg/devfile/parser/data/v2/2.2.0/devfileJsonSchema220.go b/pkg/devfile/parser/data/v2/2.2.0/devfileJsonSchema220.go new file mode 100644 index 0000000..d729f7c --- /dev/null +++ b/pkg/devfile/parser/data/v2/2.2.0/devfileJsonSchema220.go @@ -0,0 +1,2052 @@ +package version220 + +// https://raw.githubusercontent.com/devfile/api/main/schemas/latest/devfile.json +const JsonSchema220 = `{ + "description": "Devfile describes the structure of a cloud-native devworkspace and development environment.", + "type": "object", + "title": "Devfile schema - Version 2.2.0-alpha", + "required": [ + "schemaVersion" + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "commands": { + "description": "Predefined, ready-to-use, devworkspace-related commands", + "type": "array", + "items": { + "type": "object", + "required": [ + "id" + ], + "oneOf": [ + { + "required": [ + "exec" + ] + }, + { + "required": [ + "apply" + ] + }, + { + "required": [ + "composite" + ] + } + ], + "properties": { + "apply": { + "description": "Command that consists in applying a given component definition, typically bound to a devworkspace event.\n\nFor example, when an 'apply' command is bound to a 'preStart' event, and references a 'container' component, it will start the container as a K8S initContainer in the devworkspace POD, unless the component has its 'dedicatedPod' field set to 'true'.\n\nWhen no 'apply' command exist for a given component, it is assumed the component will be applied at devworkspace start by default, unless 'deployByDefault' for that component is set to false.", + "type": "object", + "required": [ + "component" + ], + "properties": { + "component": { + "description": "Describes component that will be applied", + "type": "string" + }, + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "required": [ + "kind" + ], + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": [ + "build", + "run", + "test", + "debug", + "deploy" + ] + } + }, + "additionalProperties": false + }, + "label": { + "description": "Optional label that provides a label for this command to be used in Editor UI menus for example", + "type": "string" + } + }, + "additionalProperties": false + }, + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "composite": { + "description": "Composite command that allows executing several sub-commands either sequentially or concurrently", + "type": "object", + "properties": { + "commands": { + "description": "The commands that comprise this composite command", + "type": "array", + "items": { + "type": "string" + } + }, + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "required": [ + "kind" + ], + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": [ + "build", + "run", + "test", + "debug", + "deploy" + ] + } + }, + "additionalProperties": false + }, + "label": { + "description": "Optional label that provides a label for this command to be used in Editor UI menus for example", + "type": "string" + }, + "parallel": { + "description": "Indicates if the sub-commands should be executed concurrently", + "type": "boolean" + } + }, + "additionalProperties": false + }, + "exec": { + "description": "CLI Command executed in an existing component container", + "type": "object", + "required": [ + "commandLine", + "component" + ], + "properties": { + "commandLine": { + "description": "The actual command-line string\n\nSpecial variables that can be used:\n\n - '$PROJECTS_ROOT': A path where projects sources are mounted as defined by container component's sourceMapping.\n\n - '$PROJECT_SOURCE': A path to a project source ($PROJECTS_ROOT/\u003cproject-name\u003e). If there are multiple projects, this will point to the directory of the first one.", + "type": "string" + }, + "component": { + "description": "Describes component to which given action relates", + "type": "string" + }, + "env": { + "description": "Optional list of environment variables that have to be set before running the command", + "type": "array", + "items": { + "type": "object", + "required": [ + "name", + "value" + ], + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "required": [ + "kind" + ], + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": [ + "build", + "run", + "test", + "debug", + "deploy" + ] + } + }, + "additionalProperties": false + }, + "hotReloadCapable": { + "description": "Whether the command is capable to reload itself when source code changes. If set to 'true' the command won't be restarted and it is expected to handle file changes on its own.\n\nDefault value is 'false'", + "type": "boolean" + }, + "label": { + "description": "Optional label that provides a label for this command to be used in Editor UI menus for example", + "type": "string" + }, + "workingDir": { + "description": "Working directory where the command should be executed\n\nSpecial variables that can be used:\n\n - '$PROJECTS_ROOT': A path where projects sources are mounted as defined by container component's sourceMapping.\n\n - '$PROJECT_SOURCE': A path to a project source ($PROJECTS_ROOT/\u003cproject-name\u003e). If there are multiple projects, this will point to the directory of the first one.", + "type": "string" + } + }, + "additionalProperties": false + }, + "id": { + "description": "Mandatory identifier that allows referencing this command in composite commands, from a parent, or in events.", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + } + }, + "additionalProperties": false + } + }, + "components": { + "description": "List of the devworkspace components, such as editor and plugins, user-provided containers, or other types of components", + "type": "array", + "items": { + "type": "object", + "required": [ + "name" + ], + "oneOf": [ + { + "required": [ + "container" + ] + }, + { + "required": [ + "kubernetes" + ] + }, + { + "required": [ + "openshift" + ] + }, + { + "required": [ + "volume" + ] + }, + { + "required": [ + "image" + ] + } + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "container": { + "description": "Allows adding and configuring devworkspace-related containers", + "type": "object", + "required": [ + "image" + ], + "properties": { + "annotation": { + "description": "Annotations that should be added to specific resources for this container", + "type": "object", + "properties": { + "deployment": { + "description": "Annotations to be added to deployment", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "service": { + "description": "Annotations to be added to service", + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "args": { + "description": "The arguments to supply to the command running the dockerimage component. The arguments are supplied either to the default command provided in the image or to the overridden command.\n\nDefaults to an empty array, meaning use whatever is defined in the image.", + "type": "array", + "items": { + "type": "string" + } + }, + "command": { + "description": "The command to run in the dockerimage component instead of the default one provided in the image.\n\nDefaults to an empty array, meaning use whatever is defined in the image.", + "type": "array", + "items": { + "type": "string" + } + }, + "cpuLimit": { + "type": "string" + }, + "cpuRequest": { + "type": "string" + }, + "dedicatedPod": { + "description": "Specify if a container should run in its own separated pod, instead of running as part of the main development environment pod.\n\nDefault value is 'false'", + "type": "boolean" + }, + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": [ + "name", + "targetPort" + ], + "properties": { + "annotation": { + "description": "Annotations to be added to Kubernetes Ingress or Openshift Route", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "attributes": { + "description": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\",", + "type": "object", + "additionalProperties": true + }, + "exposure": { + "description": "Describes how the endpoint should be exposed on the network.\n- 'public' means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- 'internal' means that the endpoint will be exposed internally outside of the main devworkspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- 'none' means that the endpoint will not be exposed and will only be accessible inside the main devworkspace POD, on a local address.\n\nDefault value is 'public'", + "type": "string", + "default": "public", + "enum": [ + "public", + "internal", + "none" + ] + }, + "name": { + "type": "string", + "maxLength": 15, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "Path of the endpoint URL", + "type": "string" + }, + "protocol": { + "description": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- 'http': Endpoint will have 'http' traffic, typically on a TCP connection. It will be automaticaly promoted to 'https' when the 'secure' field is set to 'true'.\n- 'https': Endpoint will have 'https' traffic, typically on a TCP connection.\n- 'ws': Endpoint will have 'ws' traffic, typically on a TCP connection. It will be automaticaly promoted to 'wss' when the 'secure' field is set to 'true'.\n- 'wss': Endpoint will have 'wss' traffic, typically on a TCP connection.\n- 'tcp': Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- 'udp': Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is 'http'", + "type": "string", + "default": "http", + "enum": [ + "http", + "https", + "ws", + "wss", + "tcp", + "udp" + ] + }, + "secure": { + "description": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of 'https' or 'wss'.", + "type": "boolean" + }, + "targetPort": { + "description": "The port number should be unique.", + "type": "integer" + } + }, + "additionalProperties": false + } + }, + "env": { + "description": "Environment variables used in this container.\n\nThe following variables are reserved and cannot be overridden via env:\n\n - '$PROJECTS_ROOT'\n\n - '$PROJECT_SOURCE'", + "type": "array", + "items": { + "type": "object", + "required": [ + "name", + "value" + ], + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "image": { + "type": "string" + }, + "memoryLimit": { + "type": "string" + }, + "memoryRequest": { + "type": "string" + }, + "mountSources": { + "description": "Toggles whether or not the project source code should be mounted in the component.\n\nDefaults to true for all component types except plugins and components that set 'dedicatedPod' to true.", + "type": "boolean" + }, + "sourceMapping": { + "description": "Optional specification of the path in the container where project sources should be transferred/mounted when 'mountSources' is 'true'. When omitted, the default value of /projects is used.", + "type": "string", + "default": "/projects" + }, + "volumeMounts": { + "description": "List of volumes mounts that should be mounted is this container.", + "type": "array", + "items": { + "description": "Volume that should be mounted to a component container", + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "description": "The volume mount name is the name of an existing 'Volume' component. If several containers mount the same volume name then they will reuse the same volume and will be able to access to the same files.", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "The path in the component container where the volume should be mounted. If not path is mentioned, default path is the is '/\u003cname\u003e'.", + "type": "string" + } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false + }, + "image": { + "description": "Allows specifying the definition of an image for outer loop builds", + "type": "object", + "required": [ + "imageName" + ], + "oneOf": [ + { + "required": [ + "dockerfile" + ] + } + ], + "properties": { + "autoBuild": { + "description": "Defines if the image should be built during startup.\n\nDefault value is 'false'", + "type": "boolean" + }, + "dockerfile": { + "description": "Allows specifying dockerfile type build", + "type": "object", + "oneOf": [ + { + "required": [ + "uri" + ] + }, + { + "required": [ + "devfileRegistry" + ] + }, + { + "required": [ + "git" + ] + } + ], + "properties": { + "args": { + "description": "The arguments to supply to the dockerfile build.", + "type": "array", + "items": { + "type": "string" + } + }, + "buildContext": { + "description": "Path of source directory to establish build context. Defaults to ${PROJECT_ROOT} in the container", + "type": "string" + }, + "devfileRegistry": { + "description": "Dockerfile's Devfile Registry source", + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "description": "Id in a devfile registry that contains a Dockerfile. The src in the OCI registry required for the Dockerfile build will be downloaded for building the image.", + "type": "string" + }, + "registryUrl": { + "description": "Devfile Registry URL to pull the Dockerfile from when using the Devfile Registry as Dockerfile src. To ensure the Dockerfile gets resolved consistently in different environments, it is recommended to always specify the 'devfileRegistryUrl' when 'Id' is used.", + "type": "string" + } + }, + "additionalProperties": false + }, + "git": { + "description": "Dockerfile's Git source", + "type": "object", + "required": [ + "remotes" + ], + "properties": { + "checkoutFrom": { + "description": "Defines from what the project should be checked out. Required if there are more than one remote configured", + "type": "object", + "properties": { + "remote": { + "description": "The remote name should be used as init. Required if there are more than one remote configured", + "type": "string" + }, + "revision": { + "description": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found.", + "type": "string" + } + }, + "additionalProperties": false + }, + "fileLocation": { + "description": "Location of the Dockerfile in the Git repository when using git as Dockerfile src. Defaults to Dockerfile.", + "type": "string" + }, + "remotes": { + "description": "The remotes map which should be initialized in the git project. Projects must have at least one remote configured while StarterProjects \u0026 Image Component's Git source can only have at most one remote configured.", + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "rootRequired": { + "description": "Specify if a privileged builder pod is required.\n\nDefault value is 'false'", + "type": "boolean" + }, + "uri": { + "description": "URI Reference of a Dockerfile. It can be a full URL or a relative URI from the current devfile as the base URI.", + "type": "string" + } + }, + "additionalProperties": false + }, + "imageName": { + "description": "Name of the image for the resulting outerloop build", + "type": "string" + } + }, + "additionalProperties": false + }, + "kubernetes": { + "description": "Allows importing into the devworkspace the Kubernetes resources defined in a given manifest. For example this allows reusing the Kubernetes definitions used to deploy some runtime components in production.", + "type": "object", + "oneOf": [ + { + "required": [ + "uri" + ] + }, + { + "required": [ + "inlined" + ] + } + ], + "properties": { + "deployByDefault": { + "description": "Defines if the component should be deployed during startup.\n\nDefault value is 'false'", + "type": "boolean" + }, + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": [ + "name", + "targetPort" + ], + "properties": { + "annotation": { + "description": "Annotations to be added to Kubernetes Ingress or Openshift Route", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "attributes": { + "description": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\",", + "type": "object", + "additionalProperties": true + }, + "exposure": { + "description": "Describes how the endpoint should be exposed on the network.\n- 'public' means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- 'internal' means that the endpoint will be exposed internally outside of the main devworkspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- 'none' means that the endpoint will not be exposed and will only be accessible inside the main devworkspace POD, on a local address.\n\nDefault value is 'public'", + "type": "string", + "default": "public", + "enum": [ + "public", + "internal", + "none" + ] + }, + "name": { + "type": "string", + "maxLength": 15, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "Path of the endpoint URL", + "type": "string" + }, + "protocol": { + "description": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- 'http': Endpoint will have 'http' traffic, typically on a TCP connection. It will be automaticaly promoted to 'https' when the 'secure' field is set to 'true'.\n- 'https': Endpoint will have 'https' traffic, typically on a TCP connection.\n- 'ws': Endpoint will have 'ws' traffic, typically on a TCP connection. It will be automaticaly promoted to 'wss' when the 'secure' field is set to 'true'.\n- 'wss': Endpoint will have 'wss' traffic, typically on a TCP connection.\n- 'tcp': Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- 'udp': Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is 'http'", + "type": "string", + "default": "http", + "enum": [ + "http", + "https", + "ws", + "wss", + "tcp", + "udp" + ] + }, + "secure": { + "description": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of 'https' or 'wss'.", + "type": "boolean" + }, + "targetPort": { + "description": "The port number should be unique.", + "type": "integer" + } + }, + "additionalProperties": false + } + }, + "inlined": { + "description": "Inlined manifest", + "type": "string" + }, + "uri": { + "description": "Location in a file fetched from a uri.", + "type": "string" + } + }, + "additionalProperties": false + }, + "name": { + "description": "Mandatory name that allows referencing the component from other elements (such as commands) or from an external devfile that may reference this component through a parent or a plugin.", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "openshift": { + "description": "Allows importing into the devworkspace the OpenShift resources defined in a given manifest. For example this allows reusing the OpenShift definitions used to deploy some runtime components in production.", + "type": "object", + "oneOf": [ + { + "required": [ + "uri" + ] + }, + { + "required": [ + "inlined" + ] + } + ], + "properties": { + "deployByDefault": { + "description": "Defines if the component should be deployed during startup.\n\nDefault value is 'false'", + "type": "boolean" + }, + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": [ + "name", + "targetPort" + ], + "properties": { + "annotation": { + "description": "Annotations to be added to Kubernetes Ingress or Openshift Route", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "attributes": { + "description": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\",", + "type": "object", + "additionalProperties": true + }, + "exposure": { + "description": "Describes how the endpoint should be exposed on the network.\n- 'public' means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- 'internal' means that the endpoint will be exposed internally outside of the main devworkspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- 'none' means that the endpoint will not be exposed and will only be accessible inside the main devworkspace POD, on a local address.\n\nDefault value is 'public'", + "type": "string", + "default": "public", + "enum": [ + "public", + "internal", + "none" + ] + }, + "name": { + "type": "string", + "maxLength": 15, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "Path of the endpoint URL", + "type": "string" + }, + "protocol": { + "description": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- 'http': Endpoint will have 'http' traffic, typically on a TCP connection. It will be automaticaly promoted to 'https' when the 'secure' field is set to 'true'.\n- 'https': Endpoint will have 'https' traffic, typically on a TCP connection.\n- 'ws': Endpoint will have 'ws' traffic, typically on a TCP connection. It will be automaticaly promoted to 'wss' when the 'secure' field is set to 'true'.\n- 'wss': Endpoint will have 'wss' traffic, typically on a TCP connection.\n- 'tcp': Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- 'udp': Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is 'http'", + "type": "string", + "default": "http", + "enum": [ + "http", + "https", + "ws", + "wss", + "tcp", + "udp" + ] + }, + "secure": { + "description": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of 'https' or 'wss'.", + "type": "boolean" + }, + "targetPort": { + "description": "The port number should be unique.", + "type": "integer" + } + }, + "additionalProperties": false + } + }, + "inlined": { + "description": "Inlined manifest", + "type": "string" + }, + "uri": { + "description": "Location in a file fetched from a uri.", + "type": "string" + } + }, + "additionalProperties": false + }, + "volume": { + "description": "Allows specifying the definition of a volume shared by several other components", + "type": "object", + "properties": { + "ephemeral": { + "description": "Ephemeral volumes are not stored persistently across restarts. Defaults to false", + "type": "boolean" + }, + "size": { + "description": "Size of the volume", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "events": { + "description": "Bindings of commands to events. Each command is referred-to by its name.", + "type": "object", + "properties": { + "postStart": { + "description": "IDs of commands that should be executed after the devworkspace is completely started. In the case of Che-Theia, these commands should be executed after all plugins and extensions have started, including project cloning. This means that those commands are not triggered until the user opens the IDE in his browser.", + "type": "array", + "items": { + "type": "string" + } + }, + "postStop": { + "description": "IDs of commands that should be executed after stopping the devworkspace.", + "type": "array", + "items": { + "type": "string" + } + }, + "preStart": { + "description": "IDs of commands that should be executed before the devworkspace start. Kubernetes-wise, these commands would typically be executed in init containers of the devworkspace POD.", + "type": "array", + "items": { + "type": "string" + } + }, + "preStop": { + "description": "IDs of commands that should be executed before stopping the devworkspace.", + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "metadata": { + "description": "Optional metadata", + "type": "object", + "properties": { + "architectures": { + "description": "Optional list of processor architectures that the devfile supports, empty list suggests that the devfile can be used on any architecture", + "type": "array", + "uniqueItems": true, + "items": { + "description": "Architecture describes the architecture type", + "type": "string", + "enum": [ + "amd64", + "arm64", + "ppc64le", + "s390x" + ] + } + }, + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes. Deprecated, use the top-level attributes field instead.", + "type": "object", + "additionalProperties": true + }, + "description": { + "description": "Optional devfile description", + "type": "string" + }, + "displayName": { + "description": "Optional devfile display name", + "type": "string" + }, + "globalMemoryLimit": { + "description": "Optional devfile global memory limit", + "type": "string" + }, + "icon": { + "description": "Optional devfile icon, can be a URI or a relative path in the project", + "type": "string" + }, + "language": { + "description": "Optional devfile language", + "type": "string" + }, + "name": { + "description": "Optional devfile name", + "type": "string" + }, + "projectType": { + "description": "Optional devfile project type", + "type": "string" + }, + "provider": { + "description": "Optional devfile provider information", + "type": "string" + }, + "supportUrl": { + "description": "Optional link to a page that provides support information", + "type": "string" + }, + "tags": { + "description": "Optional devfile tags", + "type": "array", + "items": { + "type": "string" + } + }, + "version": { + "description": "Optional semver-compatible version", + "type": "string", + "pattern": "^([0-9]+)\\.([0-9]+)\\.([0-9]+)(\\-[0-9a-z-]+(\\.[0-9a-z-]+)*)?(\\+[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?$" + }, + "website": { + "description": "Optional devfile website", + "type": "string" + } + }, + "additionalProperties": true + }, + "parent": { + "description": "Parent devworkspace template", + "type": "object", + "oneOf": [ + { + "required": [ + "uri" + ] + }, + { + "required": [ + "id" + ] + }, + { + "required": [ + "kubernetes" + ] + } + ], + "properties": { + "attributes": { + "description": "Overrides of attributes encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "object", + "additionalProperties": true + }, + "commands": { + "description": "Overrides of commands encapsulated in a parent devfile or a plugin. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "array", + "items": { + "type": "object", + "required": [ + "id" + ], + "oneOf": [ + { + "required": [ + "exec" + ] + }, + { + "required": [ + "apply" + ] + }, + { + "required": [ + "composite" + ] + } + ], + "properties": { + "apply": { + "description": "Command that consists in applying a given component definition, typically bound to a devworkspace event.\n\nFor example, when an 'apply' command is bound to a 'preStart' event, and references a 'container' component, it will start the container as a K8S initContainer in the devworkspace POD, unless the component has its 'dedicatedPod' field set to 'true'.\n\nWhen no 'apply' command exist for a given component, it is assumed the component will be applied at devworkspace start by default, unless 'deployByDefault' for that component is set to false.", + "type": "object", + "properties": { + "component": { + "description": "Describes component that will be applied", + "type": "string" + }, + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": [ + "build", + "run", + "test", + "debug", + "deploy" + ] + } + }, + "additionalProperties": false + }, + "label": { + "description": "Optional label that provides a label for this command to be used in Editor UI menus for example", + "type": "string" + } + }, + "additionalProperties": false + }, + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "composite": { + "description": "Composite command that allows executing several sub-commands either sequentially or concurrently", + "type": "object", + "properties": { + "commands": { + "description": "The commands that comprise this composite command", + "type": "array", + "items": { + "type": "string" + } + }, + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": [ + "build", + "run", + "test", + "debug", + "deploy" + ] + } + }, + "additionalProperties": false + }, + "label": { + "description": "Optional label that provides a label for this command to be used in Editor UI menus for example", + "type": "string" + }, + "parallel": { + "description": "Indicates if the sub-commands should be executed concurrently", + "type": "boolean" + } + }, + "additionalProperties": false + }, + "exec": { + "description": "CLI Command executed in an existing component container", + "type": "object", + "properties": { + "commandLine": { + "description": "The actual command-line string\n\nSpecial variables that can be used:\n\n - '$PROJECTS_ROOT': A path where projects sources are mounted as defined by container component's sourceMapping.\n\n - '$PROJECT_SOURCE': A path to a project source ($PROJECTS_ROOT/\u003cproject-name\u003e). If there are multiple projects, this will point to the directory of the first one.", + "type": "string" + }, + "component": { + "description": "Describes component to which given action relates", + "type": "string" + }, + "env": { + "description": "Optional list of environment variables that have to be set before running the command", + "type": "array", + "items": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": [ + "build", + "run", + "test", + "debug", + "deploy" + ] + } + }, + "additionalProperties": false + }, + "hotReloadCapable": { + "description": "Whether the command is capable to reload itself when source code changes. If set to 'true' the command won't be restarted and it is expected to handle file changes on its own.\n\nDefault value is 'false'", + "type": "boolean" + }, + "label": { + "description": "Optional label that provides a label for this command to be used in Editor UI menus for example", + "type": "string" + }, + "workingDir": { + "description": "Working directory where the command should be executed\n\nSpecial variables that can be used:\n\n - '$PROJECTS_ROOT': A path where projects sources are mounted as defined by container component's sourceMapping.\n\n - '$PROJECT_SOURCE': A path to a project source ($PROJECTS_ROOT/\u003cproject-name\u003e). If there are multiple projects, this will point to the directory of the first one.", + "type": "string" + } + }, + "additionalProperties": false + }, + "id": { + "description": "Mandatory identifier that allows referencing this command in composite commands, from a parent, or in events.", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + } + }, + "additionalProperties": false + } + }, + "components": { + "description": "Overrides of components encapsulated in a parent devfile or a plugin. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "array", + "items": { + "type": "object", + "required": [ + "name" + ], + "oneOf": [ + { + "required": [ + "container" + ] + }, + { + "required": [ + "kubernetes" + ] + }, + { + "required": [ + "openshift" + ] + }, + { + "required": [ + "volume" + ] + }, + { + "required": [ + "image" + ] + } + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "container": { + "description": "Allows adding and configuring devworkspace-related containers", + "type": "object", + "properties": { + "annotation": { + "description": "Annotations that should be added to specific resources for this container", + "type": "object", + "properties": { + "deployment": { + "description": "Annotations to be added to deployment", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "service": { + "description": "Annotations to be added to service", + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "args": { + "description": "The arguments to supply to the command running the dockerimage component. The arguments are supplied either to the default command provided in the image or to the overridden command.\n\nDefaults to an empty array, meaning use whatever is defined in the image.", + "type": "array", + "items": { + "type": "string" + } + }, + "command": { + "description": "The command to run in the dockerimage component instead of the default one provided in the image.\n\nDefaults to an empty array, meaning use whatever is defined in the image.", + "type": "array", + "items": { + "type": "string" + } + }, + "cpuLimit": { + "type": "string" + }, + "cpuRequest": { + "type": "string" + }, + "dedicatedPod": { + "description": "Specify if a container should run in its own separated pod, instead of running as part of the main development environment pod.\n\nDefault value is 'false'", + "type": "boolean" + }, + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "annotation": { + "description": "Annotations to be added to Kubernetes Ingress or Openshift Route", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "attributes": { + "description": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\",", + "type": "object", + "additionalProperties": true + }, + "exposure": { + "description": "Describes how the endpoint should be exposed on the network.\n- 'public' means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- 'internal' means that the endpoint will be exposed internally outside of the main devworkspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- 'none' means that the endpoint will not be exposed and will only be accessible inside the main devworkspace POD, on a local address.\n\nDefault value is 'public'", + "type": "string", + "enum": [ + "public", + "internal", + "none" + ] + }, + "name": { + "type": "string", + "maxLength": 15, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "Path of the endpoint URL", + "type": "string" + }, + "protocol": { + "description": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- 'http': Endpoint will have 'http' traffic, typically on a TCP connection. It will be automaticaly promoted to 'https' when the 'secure' field is set to 'true'.\n- 'https': Endpoint will have 'https' traffic, typically on a TCP connection.\n- 'ws': Endpoint will have 'ws' traffic, typically on a TCP connection. It will be automaticaly promoted to 'wss' when the 'secure' field is set to 'true'.\n- 'wss': Endpoint will have 'wss' traffic, typically on a TCP connection.\n- 'tcp': Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- 'udp': Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is 'http'", + "type": "string", + "enum": [ + "http", + "https", + "ws", + "wss", + "tcp", + "udp" + ] + }, + "secure": { + "description": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of 'https' or 'wss'.", + "type": "boolean" + }, + "targetPort": { + "description": "The port number should be unique.", + "type": "integer" + } + }, + "additionalProperties": false + } + }, + "env": { + "description": "Environment variables used in this container.\n\nThe following variables are reserved and cannot be overridden via env:\n\n - '$PROJECTS_ROOT'\n\n - '$PROJECT_SOURCE'", + "type": "array", + "items": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "image": { + "type": "string" + }, + "memoryLimit": { + "type": "string" + }, + "memoryRequest": { + "type": "string" + }, + "mountSources": { + "description": "Toggles whether or not the project source code should be mounted in the component.\n\nDefaults to true for all component types except plugins and components that set 'dedicatedPod' to true.", + "type": "boolean" + }, + "sourceMapping": { + "description": "Optional specification of the path in the container where project sources should be transferred/mounted when 'mountSources' is 'true'. When omitted, the default value of /projects is used.", + "type": "string" + }, + "volumeMounts": { + "description": "List of volumes mounts that should be mounted is this container.", + "type": "array", + "items": { + "description": "Volume that should be mounted to a component container", + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "description": "The volume mount name is the name of an existing 'Volume' component. If several containers mount the same volume name then they will reuse the same volume and will be able to access to the same files.", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "The path in the component container where the volume should be mounted. If not path is mentioned, default path is the is '/\u003cname\u003e'.", + "type": "string" + } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false + }, + "image": { + "description": "Allows specifying the definition of an image for outer loop builds", + "type": "object", + "oneOf": [ + { + "required": [ + "dockerfile" + ] + }, + { + "required": [ + "autoBuild" + ] + } + ], + "properties": { + "autoBuild": { + "description": "Defines if the image should be built during startup.\n\nDefault value is 'false'", + "type": "boolean" + }, + "dockerfile": { + "description": "Allows specifying dockerfile type build", + "type": "object", + "oneOf": [ + { + "required": [ + "uri" + ] + }, + { + "required": [ + "devfileRegistry" + ] + }, + { + "required": [ + "git" + ] + } + ], + "properties": { + "args": { + "description": "The arguments to supply to the dockerfile build.", + "type": "array", + "items": { + "type": "string" + } + }, + "buildContext": { + "description": "Path of source directory to establish build context. Defaults to ${PROJECT_ROOT} in the container", + "type": "string" + }, + "devfileRegistry": { + "description": "Dockerfile's Devfile Registry source", + "type": "object", + "properties": { + "id": { + "description": "Id in a devfile registry that contains a Dockerfile. The src in the OCI registry required for the Dockerfile build will be downloaded for building the image.", + "type": "string" + }, + "registryUrl": { + "description": "Devfile Registry URL to pull the Dockerfile from when using the Devfile Registry as Dockerfile src. To ensure the Dockerfile gets resolved consistently in different environments, it is recommended to always specify the 'devfileRegistryUrl' when 'Id' is used.", + "type": "string" + } + }, + "additionalProperties": false + }, + "git": { + "description": "Dockerfile's Git source", + "type": "object", + "properties": { + "checkoutFrom": { + "description": "Defines from what the project should be checked out. Required if there are more than one remote configured", + "type": "object", + "properties": { + "remote": { + "description": "The remote name should be used as init. Required if there are more than one remote configured", + "type": "string" + }, + "revision": { + "description": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found.", + "type": "string" + } + }, + "additionalProperties": false + }, + "fileLocation": { + "description": "Location of the Dockerfile in the Git repository when using git as Dockerfile src. Defaults to Dockerfile.", + "type": "string" + }, + "remotes": { + "description": "The remotes map which should be initialized in the git project. Projects must have at least one remote configured while StarterProjects \u0026 Image Component's Git source can only have at most one remote configured.", + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "rootRequired": { + "description": "Specify if a privileged builder pod is required.\n\nDefault value is 'false'", + "type": "boolean" + }, + "uri": { + "description": "URI Reference of a Dockerfile. It can be a full URL or a relative URI from the current devfile as the base URI.", + "type": "string" + } + }, + "additionalProperties": false + }, + "imageName": { + "description": "Name of the image for the resulting outerloop build", + "type": "string" + } + }, + "additionalProperties": false + }, + "kubernetes": { + "description": "Allows importing into the devworkspace the Kubernetes resources defined in a given manifest. For example this allows reusing the Kubernetes definitions used to deploy some runtime components in production.", + "type": "object", + "oneOf": [ + { + "required": [ + "uri" + ] + }, + { + "required": [ + "inlined" + ] + } + ], + "properties": { + "deployByDefault": { + "description": "Defines if the component should be deployed during startup.\n\nDefault value is 'false'", + "type": "boolean" + }, + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "annotation": { + "description": "Annotations to be added to Kubernetes Ingress or Openshift Route", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "attributes": { + "description": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\",", + "type": "object", + "additionalProperties": true + }, + "exposure": { + "description": "Describes how the endpoint should be exposed on the network.\n- 'public' means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- 'internal' means that the endpoint will be exposed internally outside of the main devworkspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- 'none' means that the endpoint will not be exposed and will only be accessible inside the main devworkspace POD, on a local address.\n\nDefault value is 'public'", + "type": "string", + "enum": [ + "public", + "internal", + "none" + ] + }, + "name": { + "type": "string", + "maxLength": 15, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "Path of the endpoint URL", + "type": "string" + }, + "protocol": { + "description": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- 'http': Endpoint will have 'http' traffic, typically on a TCP connection. It will be automaticaly promoted to 'https' when the 'secure' field is set to 'true'.\n- 'https': Endpoint will have 'https' traffic, typically on a TCP connection.\n- 'ws': Endpoint will have 'ws' traffic, typically on a TCP connection. It will be automaticaly promoted to 'wss' when the 'secure' field is set to 'true'.\n- 'wss': Endpoint will have 'wss' traffic, typically on a TCP connection.\n- 'tcp': Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- 'udp': Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is 'http'", + "type": "string", + "enum": [ + "http", + "https", + "ws", + "wss", + "tcp", + "udp" + ] + }, + "secure": { + "description": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of 'https' or 'wss'.", + "type": "boolean" + }, + "targetPort": { + "description": "The port number should be unique.", + "type": "integer" + } + }, + "additionalProperties": false + } + }, + "inlined": { + "description": "Inlined manifest", + "type": "string" + }, + "uri": { + "description": "Location in a file fetched from a uri.", + "type": "string" + } + }, + "additionalProperties": false + }, + "name": { + "description": "Mandatory name that allows referencing the component from other elements (such as commands) or from an external devfile that may reference this component through a parent or a plugin.", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "openshift": { + "description": "Allows importing into the devworkspace the OpenShift resources defined in a given manifest. For example this allows reusing the OpenShift definitions used to deploy some runtime components in production.", + "type": "object", + "oneOf": [ + { + "required": [ + "uri" + ] + }, + { + "required": [ + "inlined" + ] + } + ], + "properties": { + "deployByDefault": { + "description": "Defines if the component should be deployed during startup.\n\nDefault value is 'false'", + "type": "boolean" + }, + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "annotation": { + "description": "Annotations to be added to Kubernetes Ingress or Openshift Route", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "attributes": { + "description": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\",", + "type": "object", + "additionalProperties": true + }, + "exposure": { + "description": "Describes how the endpoint should be exposed on the network.\n- 'public' means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- 'internal' means that the endpoint will be exposed internally outside of the main devworkspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- 'none' means that the endpoint will not be exposed and will only be accessible inside the main devworkspace POD, on a local address.\n\nDefault value is 'public'", + "type": "string", + "enum": [ + "public", + "internal", + "none" + ] + }, + "name": { + "type": "string", + "maxLength": 15, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "Path of the endpoint URL", + "type": "string" + }, + "protocol": { + "description": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- 'http': Endpoint will have 'http' traffic, typically on a TCP connection. It will be automaticaly promoted to 'https' when the 'secure' field is set to 'true'.\n- 'https': Endpoint will have 'https' traffic, typically on a TCP connection.\n- 'ws': Endpoint will have 'ws' traffic, typically on a TCP connection. It will be automaticaly promoted to 'wss' when the 'secure' field is set to 'true'.\n- 'wss': Endpoint will have 'wss' traffic, typically on a TCP connection.\n- 'tcp': Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- 'udp': Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is 'http'", + "type": "string", + "enum": [ + "http", + "https", + "ws", + "wss", + "tcp", + "udp" + ] + }, + "secure": { + "description": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of 'https' or 'wss'.", + "type": "boolean" + }, + "targetPort": { + "description": "The port number should be unique.", + "type": "integer" + } + }, + "additionalProperties": false + } + }, + "inlined": { + "description": "Inlined manifest", + "type": "string" + }, + "uri": { + "description": "Location in a file fetched from a uri.", + "type": "string" + } + }, + "additionalProperties": false + }, + "volume": { + "description": "Allows specifying the definition of a volume shared by several other components", + "type": "object", + "properties": { + "ephemeral": { + "description": "Ephemeral volumes are not stored persistently across restarts. Defaults to false", + "type": "boolean" + }, + "size": { + "description": "Size of the volume", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "id": { + "description": "Id in a registry that contains a Devfile yaml file", + "type": "string" + }, + "kubernetes": { + "description": "Reference to a Kubernetes CRD of type DevWorkspaceTemplate", + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + } + }, + "additionalProperties": false + }, + "projects": { + "description": "Overrides of projects encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "array", + "items": { + "type": "object", + "required": [ + "name" + ], + "oneOf": [ + { + "required": [ + "git" + ] + }, + { + "required": [ + "zip" + ] + } + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "clonePath": { + "description": "Path relative to the root of the projects to which this project should be cloned into. This is a unix-style relative path (i.e. uses forward slashes). The path is invalid if it is absolute or tries to escape the project root through the usage of '..'. If not specified, defaults to the project name.", + "type": "string" + }, + "git": { + "description": "Project's Git source", + "type": "object", + "properties": { + "checkoutFrom": { + "description": "Defines from what the project should be checked out. Required if there are more than one remote configured", + "type": "object", + "properties": { + "remote": { + "description": "The remote name should be used as init. Required if there are more than one remote configured", + "type": "string" + }, + "revision": { + "description": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found.", + "type": "string" + } + }, + "additionalProperties": false + }, + "remotes": { + "description": "The remotes map which should be initialized in the git project. Projects must have at least one remote configured while StarterProjects \u0026 Image Component's Git source can only have at most one remote configured.", + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "name": { + "description": "Project name", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "zip": { + "description": "Project's Zip source", + "type": "object", + "properties": { + "location": { + "description": "Zip project's source location address. Should be file path of the archive, e.g. file://$FILE_PATH", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "registryUrl": { + "description": "Registry URL to pull the parent devfile from when using id in the parent reference. To ensure the parent devfile gets resolved consistently in different environments, it is recommended to always specify the 'registryUrl' when 'id' is used.", + "type": "string" + }, + "starterProjects": { + "description": "Overrides of starterProjects encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "array", + "items": { + "type": "object", + "required": [ + "name" + ], + "oneOf": [ + { + "required": [ + "git" + ] + }, + { + "required": [ + "zip" + ] + } + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "description": { + "description": "Description of a starter project", + "type": "string" + }, + "git": { + "description": "Project's Git source", + "type": "object", + "properties": { + "checkoutFrom": { + "description": "Defines from what the project should be checked out. Required if there are more than one remote configured", + "type": "object", + "properties": { + "remote": { + "description": "The remote name should be used as init. Required if there are more than one remote configured", + "type": "string" + }, + "revision": { + "description": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found.", + "type": "string" + } + }, + "additionalProperties": false + }, + "remotes": { + "description": "The remotes map which should be initialized in the git project. Projects must have at least one remote configured while StarterProjects \u0026 Image Component's Git source can only have at most one remote configured.", + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "name": { + "description": "Project name", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "subDir": { + "description": "Sub-directory from a starter project to be used as root for starter project.", + "type": "string" + }, + "zip": { + "description": "Project's Zip source", + "type": "object", + "properties": { + "location": { + "description": "Zip project's source location address. Should be file path of the archive, e.g. file://$FILE_PATH", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "uri": { + "description": "URI Reference of a parent devfile YAML file. It can be a full URL or a relative URI with the current devfile as the base URI.", + "type": "string" + }, + "variables": { + "description": "Overrides of variables encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "version": { + "description": "Specific stack/sample version to pull the parent devfile from, when using id in the parent reference. To specify 'version', 'id' must be defined and used as the import reference source. 'version' can be either a specific stack version, or 'latest'. If no 'version' specified, default version will be used.", + "type": "string", + "pattern": "^(latest)|(([1-9])\\.([0-9]+)\\.([0-9]+)(\\-[0-9a-z-]+(\\.[0-9a-z-]+)*)?(\\+[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?)$" + } + }, + "additionalProperties": false + }, + "projects": { + "description": "Projects worked on in the devworkspace, containing names and sources locations", + "type": "array", + "items": { + "type": "object", + "required": [ + "name" + ], + "oneOf": [ + { + "required": [ + "git" + ] + }, + { + "required": [ + "zip" + ] + } + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "clonePath": { + "description": "Path relative to the root of the projects to which this project should be cloned into. This is a unix-style relative path (i.e. uses forward slashes). The path is invalid if it is absolute or tries to escape the project root through the usage of '..'. If not specified, defaults to the project name.", + "type": "string" + }, + "git": { + "description": "Project's Git source", + "type": "object", + "required": [ + "remotes" + ], + "properties": { + "checkoutFrom": { + "description": "Defines from what the project should be checked out. Required if there are more than one remote configured", + "type": "object", + "properties": { + "remote": { + "description": "The remote name should be used as init. Required if there are more than one remote configured", + "type": "string" + }, + "revision": { + "description": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found.", + "type": "string" + } + }, + "additionalProperties": false + }, + "remotes": { + "description": "The remotes map which should be initialized in the git project. Projects must have at least one remote configured while StarterProjects \u0026 Image Component's Git source can only have at most one remote configured.", + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "name": { + "description": "Project name", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "zip": { + "description": "Project's Zip source", + "type": "object", + "properties": { + "location": { + "description": "Zip project's source location address. Should be file path of the archive, e.g. file://$FILE_PATH", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "schemaVersion": { + "description": "Devfile schema version", + "type": "string", + "pattern": "^([2-9])\\.([0-9]+)\\.([0-9]+)(\\-[0-9a-z-]+(\\.[0-9a-z-]+)*)?(\\+[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?$" + }, + "starterProjects": { + "description": "StarterProjects is a project that can be used as a starting point when bootstrapping new projects", + "type": "array", + "items": { + "type": "object", + "required": [ + "name" + ], + "oneOf": [ + { + "required": [ + "git" + ] + }, + { + "required": [ + "zip" + ] + } + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "description": { + "description": "Description of a starter project", + "type": "string" + }, + "git": { + "description": "Project's Git source", + "type": "object", + "required": [ + "remotes" + ], + "properties": { + "checkoutFrom": { + "description": "Defines from what the project should be checked out. Required if there are more than one remote configured", + "type": "object", + "properties": { + "remote": { + "description": "The remote name should be used as init. Required if there are more than one remote configured", + "type": "string" + }, + "revision": { + "description": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found.", + "type": "string" + } + }, + "additionalProperties": false + }, + "remotes": { + "description": "The remotes map which should be initialized in the git project. Projects must have at least one remote configured while StarterProjects \u0026 Image Component's Git source can only have at most one remote configured.", + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "name": { + "description": "Project name", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "subDir": { + "description": "Sub-directory from a starter project to be used as root for starter project.", + "type": "string" + }, + "zip": { + "description": "Project's Zip source", + "type": "object", + "properties": { + "location": { + "description": "Zip project's source location address. Should be file path of the archive, e.g. file://$FILE_PATH", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "variables": { + "description": "Map of key-value variables used for string replacement in the devfile. Values can be referenced via {{variable-key}} to replace the corresponding value in string fields in the devfile. Replacement cannot be used for\n\n - schemaVersion, metadata, parent source\n\n - element identifiers, e.g. command id, component name, endpoint name, project name\n\n - references to identifiers, e.g. in events, a command's component, container's volume mount name\n\n - string enums, e.g. command group kind, endpoint exposure", + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "additionalProperties": false +} +` diff --git a/pkg/devfile/parser/data/v2/attributes.go b/pkg/devfile/parser/data/v2/attributes.go new file mode 100644 index 0000000..d241a1b --- /dev/null +++ b/pkg/devfile/parser/data/v2/attributes.go @@ -0,0 +1,52 @@ +package v2 + +import ( + "fmt" + + "github.com/devfile/api/v2/pkg/attributes" +) + +// GetAttributes gets the devfile top level attributes +func (d *DevfileV2) GetAttributes() (attributes.Attributes, error) { + // This feature was introduced in 2.1.0; so any version 2.1.0 and up should use the 2.1.0 implementation + switch d.SchemaVersion { + case "2.0.0": + return attributes.Attributes{}, fmt.Errorf("top-level attributes is not supported in devfile schema version 2.0.0") + default: + return d.Attributes, nil + } +} + +// UpdateAttributes updates the devfile top level attribute for the specific key, err out if key is absent +func (d *DevfileV2) UpdateAttributes(key string, value interface{}) error { + var err error + + // This feature was introduced in 2.1.0; so any version 2.1.0 and up should use the 2.1.0 implementation + switch d.SchemaVersion { + case "2.0.0": + return fmt.Errorf("top-level attributes is not supported in devfile schema version 2.0.0") + default: + if d.Attributes.Exists(key) { + d.Attributes.Put(key, value, &err) + } else { + return fmt.Errorf("cannot update top-level attribute, key %s is not present", key) + } + } + + return err +} + +// AddAttributes adds to the devfile top level attributes, value will be overwritten if key is already present +func (d *DevfileV2) AddAttributes(key string, value interface{}) error { + var err error + + // This feature was introduced in 2.1.0; so any version 2.1.0 and up should use the 2.1.0 implementation + switch d.SchemaVersion { + case "2.0.0": + return fmt.Errorf("top-level attributes is not supported in devfile schema version 2.0.0") + default: + d.Attributes.Put(key, value, &err) + } + + return err +} diff --git a/pkg/devfile/parser/data/v2/attributes_test.go b/pkg/devfile/parser/data/v2/attributes_test.go new file mode 100644 index 0000000..e918fff --- /dev/null +++ b/pkg/devfile/parser/data/v2/attributes_test.go @@ -0,0 +1,256 @@ +package v2 + +import ( + "github.com/stretchr/testify/assert" + "reflect" + "testing" + + "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + "github.com/devfile/api/v2/pkg/attributes" + devfilepkg "github.com/devfile/api/v2/pkg/devfile" + "github.com/kylelemons/godebug/pretty" +) + +func TestGetAttributes(t *testing.T) { + schema200NoAttributeErr := "top-level attributes is not supported in devfile schema version 2.0.0" + + tests := []struct { + name string + devfilev2 *DevfileV2 + wantAttributes attributes.Attributes + wantErr *string + }{ + { + name: "Schema 2.0.0 does not have attributes", + devfilev2: &DevfileV2{ + v1alpha2.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: "2.0.0", + }, + }, + }, + wantErr: &schema200NoAttributeErr, + }, + { + name: "Schema 2.1.0 has attributes", + devfilev2: &DevfileV2{ + v1alpha2.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: "2.1.0", + }, + DevWorkspaceTemplateSpec: v1alpha2.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1alpha2.DevWorkspaceTemplateSpecContent{ + Attributes: attributes.Attributes{}.PutString("key1", "value1").PutString("key2", "value2"), + }, + }, + }, + }, + wantAttributes: attributes.Attributes{}.PutString("key1", "value1").PutString("key2", "value2"), + }, + { + name: "Schema 2.2.0 has attributes", + devfilev2: &DevfileV2{ + v1alpha2.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: "2.2.0", + }, + DevWorkspaceTemplateSpec: v1alpha2.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1alpha2.DevWorkspaceTemplateSpecContent{ + Attributes: attributes.Attributes{}.PutString("key1", "value1").PutString("key2", "value2"), + }, + }, + }, + }, + wantAttributes: attributes.Attributes{}.PutString("key1", "value1").PutString("key2", "value2"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + attributes, err := tt.devfilev2.GetAttributes() + if (err != nil) != (tt.wantErr != nil) { + t.Errorf("TestGetAttributes() error: %v, wantErr %v", err, tt.wantErr) + } else if err == nil { + if !reflect.DeepEqual(attributes, tt.wantAttributes) { + t.Errorf("TestGetAttributes() error: actual does not equal expected, difference at %+v", pretty.Compare(attributes, tt.wantAttributes)) + } + } else { + assert.Regexp(t, *tt.wantErr, err.Error(), "TestGetAttributes(): Error message should match") + } + }) + } +} + +func TestUpdateAttributes(t *testing.T) { + + nestedValue := map[string]interface{}{ + "key1.1": map[string]interface{}{ + "key1.1.1": "value1.1.1", + }, + } + + schema200NoAttributeErr := "top-level attributes is not supported in devfile schema version 2.0.0" + invalidKeyErr := "cannot update top-level attribute, key .* is not present" + + tests := []struct { + name string + devfilev2 *DevfileV2 + key string + value interface{} + wantAttributes attributes.Attributes + wantErr *string + }{ + { + name: "Schema 2.0.0 does not have attributes", + devfilev2: &DevfileV2{ + v1alpha2.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: "2.0.0", + }, + }, + }, + wantErr: &schema200NoAttributeErr, + }, + { + name: "Schema 2.1.0 has the top-level key attribute", + devfilev2: &DevfileV2{ + v1alpha2.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: "2.1.0", + }, + DevWorkspaceTemplateSpec: v1alpha2.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1alpha2.DevWorkspaceTemplateSpecContent{ + Attributes: attributes.Attributes{}.PutString("key1", "value1").PutString("key2", "value2"), + }, + }, + }, + }, + key: "key1", + value: nestedValue, + wantAttributes: attributes.Attributes{}.Put("key1", nestedValue, nil).PutString("key2", "value2"), + }, + { + name: "Schema 2.1.0 does not have the top-level key attribute", + devfilev2: &DevfileV2{ + v1alpha2.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: "2.1.0", + }, + DevWorkspaceTemplateSpec: v1alpha2.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1alpha2.DevWorkspaceTemplateSpecContent{ + Attributes: attributes.Attributes{}.PutString("key1", "value1").PutString("key2", "value2"), + }, + }, + }, + }, + key: "key_invalid", + value: nestedValue, + wantErr: &invalidKeyErr, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.devfilev2.UpdateAttributes(tt.key, tt.value) + if (err != nil) != (tt.wantErr != nil) { + t.Errorf("TestUpdateAttributes() error: %v, wantErr %v", err, tt.wantErr) + } else if err == nil { + attributes, err := tt.devfilev2.GetAttributes() + if err != nil { + t.Errorf("TestUpdateAttributes() error: %+v", err) + return + } + if !reflect.DeepEqual(attributes, tt.wantAttributes) { + t.Errorf("TestUpdateAttributes() mismatch error: expected %+v, actual %+v", tt.wantAttributes, attributes) + } + } else { + assert.Regexp(t, *tt.wantErr, err.Error(), "TestUpdateAttributes(): Error message should match") + } + }) + } +} + +func TestAddAttributes(t *testing.T) { + + nestedValue := map[string]interface{}{ + "key3.1": map[string]interface{}{ + "key3.1.1": "value3.1.1", + }, + } + + schema200NoAttributeErr := "top-level attributes is not supported in devfile schema version 2.0.0" + + tests := []struct { + name string + devfilev2 *DevfileV2 + key string + value interface{} + wantAttributes attributes.Attributes + wantErr *string + }{ + { + name: "Schema 2.0.0 does not have attributes", + devfilev2: &DevfileV2{ + v1alpha2.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: "2.0.0", + }, + }, + }, + wantErr: &schema200NoAttributeErr, + }, + { + name: "Schema 2.1.0 has attributes", + devfilev2: &DevfileV2{ + v1alpha2.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: "2.1.0", + }, + DevWorkspaceTemplateSpec: v1alpha2.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1alpha2.DevWorkspaceTemplateSpecContent{ + Attributes: attributes.Attributes{}.PutString("key1", "value1").PutString("key2", "value2"), + }, + }, + }, + }, + key: "key3", + value: nestedValue, + wantAttributes: attributes.Attributes{}.PutString("key1", "value1").Put("key3", nestedValue, nil).PutString("key2", "value2"), + }, + { + name: "If Schema 2.1.0 has an attribute already present, it should overwrite", + devfilev2: &DevfileV2{ + v1alpha2.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: "2.1.0", + }, + DevWorkspaceTemplateSpec: v1alpha2.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1alpha2.DevWorkspaceTemplateSpecContent{ + Attributes: attributes.Attributes{}.PutString("key1", "value1").PutString("key2", "value2"), + }, + }, + }, + }, + key: "key2", + value: "value2new", + wantAttributes: attributes.Attributes{}.PutString("key1", "value1").PutString("key2", "value2new"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.devfilev2.AddAttributes(tt.key, tt.value) + if (err != nil) != (tt.wantErr != nil) { + t.Errorf("TestAddAttributes() error: %v, wantErr %v", err, tt.wantErr) + } else if err == nil { + attributes, err := tt.devfilev2.GetAttributes() + if err != nil { + t.Errorf("TestAddAttributes() error: %+v", err) + return + } + if !reflect.DeepEqual(attributes, tt.wantAttributes) { + t.Errorf("TestAddAttributes() mismatch error: expected %+v, actual %+v", tt.wantAttributes, attributes) + } + } else { + assert.Regexp(t, *tt.wantErr, err.Error(), "TestAddAttributes(): Error message should match") + } + }) + } +} diff --git a/pkg/devfile/parser/data/v2/commands.go b/pkg/devfile/parser/data/v2/commands.go new file mode 100644 index 0000000..8b45991 --- /dev/null +++ b/pkg/devfile/parser/data/v2/commands.go @@ -0,0 +1,104 @@ +package v2 + +import ( + "fmt" + v1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + "github.com/devfile/library/pkg/devfile/parser/data/v2/common" + "reflect" + "strings" +) + +// GetCommands returns the slice of Command objects parsed from the Devfile +func (d *DevfileV2) GetCommands(options common.DevfileOptions) ([]v1.Command, error) { + + if reflect.DeepEqual(options, common.DevfileOptions{}) { + return d.Commands, nil + } + + var commands []v1.Command + for _, command := range d.Commands { + // Filter Command Attributes + filterIn, err := common.FilterDevfileObject(command.Attributes, options) + if err != nil { + return nil, err + } else if !filterIn { + continue + } + + // Filter Command Type - Exec, Composite, etc. + commandType, err := common.GetCommandType(command) + if err != nil { + return nil, err + } + if options.CommandOptions.CommandType != "" && commandType != options.CommandOptions.CommandType { + continue + } + + // Filter Command Group Kind - Run, Build, etc. + commandGroup := common.GetGroup(command) + // exclude conditions: + // 1. options group is present and command group is present but does not match + // 2. options group is present and command group is not present + if options.CommandOptions.CommandGroupKind != "" && ((commandGroup != nil && options.CommandOptions.CommandGroupKind != commandGroup.Kind) || commandGroup == nil) { + continue + } + + if options.FilterByName == "" || command.Id == options.FilterByName { + commands = append(commands, command) + } + } + + return commands, nil +} + +// AddCommands adds the slice of Command objects to the Devfile's commands +// a command is considered as invalid if it is already defined +// command list passed in will be all processed, and returns a total error of all invalid commands +func (d *DevfileV2) AddCommands(commands []v1.Command) error { + var errorsList []string + for _, command := range commands { + var err error + for _, devfileCommand := range d.Commands { + if command.Id == devfileCommand.Id { + err = &common.FieldAlreadyExistError{Name: command.Id, Field: "command"} + errorsList = append(errorsList, err.Error()) + break + } + } + if err == nil { + d.Commands = append(d.Commands, command) + } + } + if len(errorsList) > 0 { + return fmt.Errorf("errors while adding commands:\n%s", strings.Join(errorsList, "\n")) + } + return nil +} + +// UpdateCommand updates the command with the given id +// return an error if the command is not found +func (d *DevfileV2) UpdateCommand(command v1.Command) error { + for i := range d.Commands { + if d.Commands[i].Id == command.Id { + d.Commands[i] = command + return nil + } + } + return fmt.Errorf("update command failed: command %s not found", command.Id) +} + +// DeleteCommand removes the specified command +func (d *DevfileV2) DeleteCommand(id string) error { + + for i := range d.Commands { + if d.Commands[i].Id == id { + d.Commands = append(d.Commands[:i], d.Commands[i+1:]...) + return nil + } + } + + return &common.FieldNotFoundError{ + Field: "command", + Name: id, + } +} diff --git a/pkg/devfile/parser/data/v2/commands_test.go b/pkg/devfile/parser/data/v2/commands_test.go new file mode 100644 index 0000000..ebf70df --- /dev/null +++ b/pkg/devfile/parser/data/v2/commands_test.go @@ -0,0 +1,603 @@ +package v2 + +import ( + "fmt" + "github.com/kylelemons/godebug/pretty" + "reflect" + "testing" + + v1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + "github.com/devfile/api/v2/pkg/attributes" + "github.com/devfile/library/pkg/devfile/parser/data/v2/common" + "github.com/stretchr/testify/assert" +) + +func TestDevfile200_GetCommands(t *testing.T) { + + invalidCmdTypeErr := "unknown command type" + + tests := []struct { + name string + currentCommands []v1.Command + filterOptions common.DevfileOptions + wantCommands []string + wantErr *string + }{ + { + name: "Get all the commands", + currentCommands: []v1.Command{ + { + Id: "command1", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{}, + }, + }, + { + Id: "command2", + CommandUnion: v1.CommandUnion{ + Composite: &v1.CompositeCommand{}, + }, + }, + }, + wantCommands: []string{"command1", "command2"}, + }, + { + name: "Get the filtered commands", + currentCommands: []v1.Command{ + { + Id: "command1", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + "secondString": "secondStringValue", + }), + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{ + LabeledCommand: v1.LabeledCommand{ + BaseCommand: v1.BaseCommand{ + Group: &v1.CommandGroup{ + Kind: v1.BuildCommandGroupKind, + }, + }, + }, + }, + }, + }, + { + Id: "command2", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + "thirdString": "thirdStringValue", + }), + CommandUnion: v1.CommandUnion{ + Composite: &v1.CompositeCommand{}, + }, + }, + { + Id: "command3", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + "thirdString": "thirdStringValue", + }), + CommandUnion: v1.CommandUnion{ + Composite: &v1.CompositeCommand{ + LabeledCommand: v1.LabeledCommand{ + BaseCommand: v1.BaseCommand{ + Group: &v1.CommandGroup{ + Kind: v1.BuildCommandGroupKind, + }, + }, + }, + }, + }, + }, + { + Id: "command4", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "thirdString": "thirdStringValue", + }), + CommandUnion: v1.CommandUnion{ + Apply: &v1.ApplyCommand{ + LabeledCommand: v1.LabeledCommand{ + BaseCommand: v1.BaseCommand{ + Group: &v1.CommandGroup{ + Kind: v1.BuildCommandGroupKind, + }, + }, + }, + }, + }, + }, + { + Id: "command5", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + "thirdString": "thirdStringValue", + }), + CommandUnion: v1.CommandUnion{ + Composite: &v1.CompositeCommand{ + LabeledCommand: v1.LabeledCommand{ + BaseCommand: v1.BaseCommand{ + Group: &v1.CommandGroup{ + Kind: v1.RunCommandGroupKind, + }, + }, + }, + }, + }, + }, + }, + filterOptions: common.DevfileOptions{ + Filter: map[string]interface{}{ + "firstString": "firstStringValue", + }, + CommandOptions: common.CommandOptions{ + CommandGroupKind: v1.BuildCommandGroupKind, + CommandType: v1.CompositeCommandType, + }, + }, + wantCommands: []string{"command3"}, + }, + { + name: "Get command with the specified name", + currentCommands: []v1.Command{ + { + Id: "command1", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{}, + }, + }, + { + Id: "command2", + CommandUnion: v1.CommandUnion{ + Composite: &v1.CompositeCommand{}, + }, + }, + { + Id: "command3", + CommandUnion: v1.CommandUnion{ + Composite: &v1.CompositeCommand{}, + }, + }, + }, + filterOptions: common.DevfileOptions{ + FilterByName: "command3", + }, + wantCommands: []string{"command3"}, + }, + { + name: "command name not found", + currentCommands: []v1.Command{ + { + Id: "command1", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{}, + }, + }, + { + Id: "command2", + CommandUnion: v1.CommandUnion{ + Composite: &v1.CompositeCommand{}, + }, + }, + }, + filterOptions: common.DevfileOptions{ + FilterByName: "command3", + }, + wantCommands: []string{}, + }, + { + name: "Wrong filter for commands", + currentCommands: []v1.Command{ + { + Id: "command1", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + "secondString": "secondStringValue", + }), + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{}, + }, + }, + { + Id: "command2", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + "thirdString": "thirdStringValue", + }), + CommandUnion: v1.CommandUnion{ + Composite: &v1.CompositeCommand{}, + }, + }, + }, + filterOptions: common.DevfileOptions{ + Filter: map[string]interface{}{ + "firstStringIsWrong": "firstStringValue", + }, + }, + }, + { + name: "Invalid command type", + currentCommands: []v1.Command{ + { + Id: "command1", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + }), + CommandUnion: v1.CommandUnion{}, + }, + }, + filterOptions: common.DevfileOptions{ + Filter: map[string]interface{}{ + "firstString": "firstStringValue", + }, + }, + wantErr: &invalidCmdTypeErr, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := &DevfileV2{ + v1.Devfile{ + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Commands: tt.currentCommands, + }, + }, + }, + } + + commands, err := d.GetCommands(tt.filterOptions) + if (err != nil) != (tt.wantErr != nil) { + t.Errorf("TestDevfile200_GetCommands() unexpected error: %v, wantErr %v", err, tt.wantErr) + } else if err == nil { + // confirm the length of actual vs expected + if len(commands) != len(tt.wantCommands) { + t.Errorf("TestDevfile200_GetCommands() error: length of expected commands is not the same as the length of actual commands") + return + } + + // compare the command slices for content + for _, wantCommand := range tt.wantCommands { + matched := false + for _, command := range commands { + if wantCommand == command.Id { + matched = true + } + } + + if !matched { + t.Errorf("TestDevfile200_GetCommands() error: command %s not found in the devfile", wantCommand) + } + } + } else { + assert.Regexp(t, *tt.wantErr, err.Error(), "TestDevfile200_GetCommands(): Error message should match") + } + }) + } +} + +func TestDevfile200_AddCommands(t *testing.T) { + multipleDupError := fmt.Sprintf("%s\n%s", "command command1 already exists in devfile", "command command2 already exists in devfile") + + tests := []struct { + name string + currentCommands []v1.Command + newCommands []v1.Command + wantCommands []v1.Command + wantErr *string + }{ + { + name: "Command does not exist", + currentCommands: []v1.Command{ + { + Id: "command1", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{}, + }, + }, + }, + newCommands: []v1.Command{ + { + Id: "command2", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{}, + }, + }, + { + Id: "command3", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{}, + }, + }, + }, + wantCommands: []v1.Command{ + { + Id: "command1", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{}, + }, + }, + { + Id: "command2", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{}, + }, + }, + { + Id: "command3", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{}, + }, + }, + }, + wantErr: nil, + }, + { + name: "Multiple duplicate commands", + currentCommands: []v1.Command{ + { + Id: "command1", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{}, + }, + }, + { + Id: "command2", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{}, + }, + }, + }, + newCommands: []v1.Command{ + { + Id: "command1", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{}, + }, + }, + { + Id: "command2", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{}, + }, + }, + { + Id: "command3", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{}, + }, + }, + }, + wantCommands: []v1.Command{ + { + Id: "command1", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{}, + }, + }, + { + Id: "command2", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{}, + }, + }, + { + Id: "command3", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{}, + }, + }, + }, + wantErr: &multipleDupError, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := &DevfileV2{ + v1.Devfile{ + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Commands: tt.currentCommands, + }, + }, + }, + } + + err := d.AddCommands(tt.newCommands) + // Unexpected error + if (err != nil) != (tt.wantErr != nil) { + t.Errorf("TestDevfile200_AddCommands() unexpected error: %v, wantErr %v", err, tt.wantErr) + } else if tt.wantErr != nil { + assert.Regexp(t, *tt.wantErr, err.Error(), "TestDevfile200_AddCommands(): Error message should match") + } else { + if !reflect.DeepEqual(d.Commands, tt.wantCommands) { + t.Errorf("TestDevfile200_AddCommands() wanted: %v, got: %v, difference at %v", tt.wantCommands, d.Commands, pretty.Compare(tt.wantCommands, d.Commands)) + } + } + + }) + } +} + +func TestDevfile200_UpdateCommands(t *testing.T) { + invalidCmdErr := "update command failed: command .* not found" + + tests := []struct { + name string + currentCommands []v1.Command + newCommand v1.Command + wantErr *string + }{ + { + name: "successfully update the command", + currentCommands: []v1.Command{ + { + Id: "command1", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{ + Component: "component1", + }, + }, + }, + { + Id: "command2", + CommandUnion: v1.CommandUnion{ + Composite: &v1.CompositeCommand{}, + }, + }, + }, + newCommand: v1.Command{ + Id: "command1", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{ + Component: "component1new", + }, + }, + }, + }, + { + name: "fail to update the command if not exist", + currentCommands: []v1.Command{ + { + Id: "command1", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{ + Component: "component1", + }, + }, + }, + }, + newCommand: v1.Command{ + Id: "command2", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{ + Component: "component1new", + }, + }, + }, + wantErr: &invalidCmdErr, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := &DevfileV2{ + v1.Devfile{ + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Commands: tt.currentCommands, + }, + }, + }, + } + + err := d.UpdateCommand(tt.newCommand) + // Unexpected error + if (err != nil) != (tt.wantErr != nil) { + t.Errorf("TestDevfile200_UpdateCommands() unexpected error: %v, wantErr %v", err, tt.wantErr) + } else if err == nil { + commands, err := d.GetCommands(common.DevfileOptions{}) + if err != nil { + t.Errorf("TestDevfile200_UpdateCommands() unxpected error: %v", err) + return + } + + matched := false + for _, devfileCommand := range commands { + if tt.newCommand.Id == devfileCommand.Id { + matched = true + if !reflect.DeepEqual(devfileCommand, tt.newCommand) { + t.Errorf("TestDevfile200_UpdateCommands() error: command mismatch, wanted %+v, got %+v", tt.newCommand, devfileCommand) + } + } + } + + if !matched { + t.Errorf("TestDevfile200_UpdateCommands() error: command mismatch, did not find command with id %s", tt.newCommand.Id) + } + } else { + assert.Regexp(t, *tt.wantErr, err.Error(), "TestDevfile200_UpdateCommands(): Error message should match") + } + }) + } +} + +func TestDeleteCommands(t *testing.T) { + missingCmdErr := "command .* is not found in the devfile" + + d := &DevfileV2{ + v1.Devfile{ + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Commands: []v1.Command{ + { + Id: "command1", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{}, + }, + }, + { + Id: "command2", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{}, + }, + }, + { + Id: "command3", + CommandUnion: v1.CommandUnion{ + Composite: &v1.CompositeCommand{ + Commands: []string{"command1", "command2", "command1"}, + }, + }, + }, + }, + }, + }, + }, + } + + tests := []struct { + name string + commandToDelete string + wantCommands []v1.Command + wantErr *string + }{ + { + name: "Successfully delete command", + commandToDelete: "command1", + wantCommands: []v1.Command{ + { + Id: "command2", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{}, + }, + }, + { + Id: "command3", + CommandUnion: v1.CommandUnion{ + Composite: &v1.CompositeCommand{ + Commands: []string{"command1", "command2", "command1"}, + }, + }, + }, + }, + }, + { + name: "Missing Command", + commandToDelete: "command34", + wantErr: &missingCmdErr, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := d.DeleteCommand(tt.commandToDelete) + if (err != nil) != (tt.wantErr != nil) { + t.Errorf("TestDeleteCommands() unexpected error: %v, wantErr %v", err, tt.wantErr) + } else if err == nil { + assert.Equal(t, tt.wantCommands, d.Commands, "TestDeleteCommands(): The two values should be the same.") + } else { + assert.Regexp(t, *tt.wantErr, err.Error(), "TestDeleteCommands(): Error message should match") + } + }) + } + +} diff --git a/pkg/devfile/parser/data/v2/common/command_helper.go b/pkg/devfile/parser/data/v2/common/command_helper.go new file mode 100644 index 0000000..b3bf8a0 --- /dev/null +++ b/pkg/devfile/parser/data/v2/common/command_helper.go @@ -0,0 +1,105 @@ +package common + +import ( + "fmt" + + v1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" +) + +// GetGroup returns the group the command belongs to +func GetGroup(dc v1.Command) *v1.CommandGroup { + switch { + case dc.Composite != nil: + return dc.Composite.Group + case dc.Exec != nil: + return dc.Exec.Group + case dc.Apply != nil: + return dc.Apply.Group + case dc.Custom != nil: + return dc.Custom.Group + + default: + return nil + } +} + +// GetExecComponent returns the component of the exec command +func GetExecComponent(dc v1.Command) string { + if dc.Exec != nil { + return dc.Exec.Component + } + + return "" +} + +// GetExecCommandLine returns the command line of the exec command +func GetExecCommandLine(dc v1.Command) string { + if dc.Exec != nil { + return dc.Exec.CommandLine + } + + return "" +} + +// GetExecWorkingDir returns the working dir of the exec command +func GetExecWorkingDir(dc v1.Command) string { + if dc.Exec != nil { + return dc.Exec.WorkingDir + } + + return "" +} + +// GetApplyComponent returns the component of the apply command +func GetApplyComponent(dc v1.Command) string { + if dc.Apply != nil { + return dc.Apply.Component + } + + return "" +} + +// GetCommandType returns the command type of a given command +func GetCommandType(command v1.Command) (v1.CommandType, error) { + switch { + case command.Apply != nil: + return v1.ApplyCommandType, nil + case command.Composite != nil: + return v1.CompositeCommandType, nil + case command.Exec != nil: + return v1.ExecCommandType, nil + case command.Custom != nil: + return v1.CustomCommandType, nil + + default: + return "", fmt.Errorf("unknown command type") + } +} + +// GetCommandsMap returns a map of the command Id to the command +func GetCommandsMap(commands []v1.Command) map[string]v1.Command { + commandMap := make(map[string]v1.Command, len(commands)) + for _, command := range commands { + commandMap[command.Id] = command + } + return commandMap +} + +// GetCommandsFromEvent returns the list of commands from the event name. +// If the event is a composite command, it returns the sub-commands from the tree +func GetCommandsFromEvent(commandsMap map[string]v1.Command, eventName string) []string { + var commands []string + + if command, ok := commandsMap[eventName]; ok { + if command.Composite != nil { + for _, compositeSubCmd := range command.Composite.Commands { + subCommands := GetCommandsFromEvent(commandsMap, compositeSubCmd) + commands = append(commands, subCommands...) + } + } else { + commands = append(commands, command.Id) + } + } + + return commands +} diff --git a/pkg/devfile/parser/data/v2/common/command_helper_test.go b/pkg/devfile/parser/data/v2/common/command_helper_test.go new file mode 100644 index 0000000..4f54cf3 --- /dev/null +++ b/pkg/devfile/parser/data/v2/common/command_helper_test.go @@ -0,0 +1,412 @@ +package common + +import ( + "github.com/stretchr/testify/assert" + "reflect" + "testing" + + v1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" +) + +var isTrue bool = true + +func TestGetGroup(t *testing.T) { + + tests := []struct { + name string + command v1.Command + want *v1.CommandGroup + }{ + { + name: "Exec command group", + command: v1.Command{ + Id: "exec1", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{ + LabeledCommand: v1.LabeledCommand{ + BaseCommand: v1.BaseCommand{ + Group: &v1.CommandGroup{ + IsDefault: &isTrue, + Kind: v1.RunCommandGroupKind, + }, + }, + }, + }, + }, + }, + want: &v1.CommandGroup{ + IsDefault: &isTrue, + Kind: v1.RunCommandGroupKind, + }, + }, + { + name: "Composite command group", + command: v1.Command{ + Id: "composite1", + CommandUnion: v1.CommandUnion{ + Composite: &v1.CompositeCommand{ + LabeledCommand: v1.LabeledCommand{ + BaseCommand: v1.BaseCommand{ + Group: &v1.CommandGroup{ + IsDefault: &isTrue, + Kind: v1.BuildCommandGroupKind, + }, + }, + }, + }, + }, + }, + want: &v1.CommandGroup{ + IsDefault: &isTrue, + Kind: v1.BuildCommandGroupKind, + }, + }, + { + name: "Empty command", + command: v1.Command{}, + want: nil, + }, + { + name: "Apply command group", + command: v1.Command{ + Id: "apply1", + CommandUnion: v1.CommandUnion{ + Apply: &v1.ApplyCommand{ + LabeledCommand: v1.LabeledCommand{ + BaseCommand: v1.BaseCommand{ + Group: &v1.CommandGroup{ + IsDefault: &isTrue, + Kind: v1.TestCommandGroupKind, + }, + }, + }, + }, + }, + }, + want: &v1.CommandGroup{ + IsDefault: &isTrue, + Kind: v1.TestCommandGroupKind, + }, + }, + { + name: "Custom command group", + command: v1.Command{ + Id: "custom1", + CommandUnion: v1.CommandUnion{ + Custom: &v1.CustomCommand{ + LabeledCommand: v1.LabeledCommand{ + BaseCommand: v1.BaseCommand{ + Group: &v1.CommandGroup{ + IsDefault: &isTrue, + Kind: v1.BuildCommandGroupKind, + }, + }, + }, + }, + }, + }, + want: &v1.CommandGroup{ + IsDefault: &isTrue, + Kind: v1.BuildCommandGroupKind, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + commandGroup := GetGroup(tt.command) + if !reflect.DeepEqual(commandGroup, tt.want) { + t.Errorf("TestGetGroup() error: expected %v, actual %v", tt.want, commandGroup) + } + }) + } + +} + +func TestGetExecComponent(t *testing.T) { + + tests := []struct { + name string + command v1.Command + want string + }{ + { + name: "Exec component present", + command: v1.Command{ + Id: "exec1", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{ + Component: "component1", + }, + }, + }, + want: "component1", + }, + { + name: "Exec component absent", + command: v1.Command{ + Id: "exec1", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{}, + }, + }, + want: "", + }, + { + name: "Empty command", + command: v1.Command{}, + want: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + component := GetExecComponent(tt.command) + if component != tt.want { + t.Errorf("TestGetExecComponent() error: expected %v, actual %v", tt.want, component) + } + }) + } + +} + +func TestGetExecCommandLine(t *testing.T) { + + tests := []struct { + name string + command v1.Command + want string + }{ + { + name: "Exec command line present", + command: v1.Command{ + Id: "exec1", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{ + CommandLine: "commandline1", + }, + }, + }, + want: "commandline1", + }, + { + name: "Exec command line absent", + command: v1.Command{ + Id: "exec1", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{}, + }, + }, + want: "", + }, + { + name: "Empty command", + command: v1.Command{}, + want: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + commandLine := GetExecCommandLine(tt.command) + if commandLine != tt.want { + t.Errorf("TestGetExecCommandLine() error: expected %v, actual %v", tt.want, commandLine) + } + }) + } + +} + +func TestGetExecWorkingDir(t *testing.T) { + + tests := []struct { + name string + command v1.Command + want string + }{ + { + name: "Exec working dir present", + command: v1.Command{ + Id: "exec1", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{ + WorkingDir: "workingdir1", + }, + }, + }, + want: "workingdir1", + }, + { + name: "Exec working dir absent", + command: v1.Command{ + Id: "exec1", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{}, + }, + }, + want: "", + }, + { + name: "Empty command", + command: v1.Command{}, + want: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + workingDir := GetExecWorkingDir(tt.command) + if workingDir != tt.want { + t.Errorf("TestGetExecWorkingDir() error: expected %v, actual %v", tt.want, workingDir) + } + }) + } + +} + +func TestGetCommandType(t *testing.T) { + + cmdTypeErr := "unknown command type" + + tests := []struct { + name string + command v1.Command + wantErr *string + commandType v1.CommandType + }{ + { + name: "Exec command", + command: v1.Command{ + Id: "exec1", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{}, + }, + }, + commandType: v1.ExecCommandType, + }, + { + name: "Composite command", + command: v1.Command{ + Id: "comp1", + CommandUnion: v1.CommandUnion{ + Composite: &v1.CompositeCommand{}, + }, + }, + commandType: v1.CompositeCommandType, + }, + { + name: "Apply command", + command: v1.Command{ + Id: "apply1", + CommandUnion: v1.CommandUnion{ + Apply: &v1.ApplyCommand{}, + }, + }, + commandType: v1.ApplyCommandType, + }, + { + name: "Custom command", + command: v1.Command{ + Id: "custom", + CommandUnion: v1.CommandUnion{ + Custom: &v1.CustomCommand{}, + }, + }, + commandType: v1.CustomCommandType, + }, + { + name: "Unknown command", + command: v1.Command{ + Id: "unknown", + CommandUnion: v1.CommandUnion{}, + }, + wantErr: &cmdTypeErr, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := GetCommandType(tt.command) + // Unexpected error + if (err != nil) != (tt.wantErr != nil) { + t.Errorf("TestGetCommandType() unexpected error: %v, wantErr %v", err, tt.wantErr) + } else if err == nil && got != tt.commandType { + t.Errorf("TestGetCommandType() error: command type mismatch, expected: %v got: %v", tt.commandType, got) + } else if err != nil { + assert.Regexp(t, *tt.wantErr, err.Error(), "TestGetCommandType(): Error message should match") + } + }) + } + +} + +func TestGetCommandsFromEvent(t *testing.T) { + + execCommands := []v1.Command{ + { + Id: "exec1", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{}, + }, + }, + { + Id: "exec2", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{}, + }, + }, + { + Id: "exec3", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{}, + }, + }, + } + + compCommands := []v1.Command{ + { + Id: "comp1", + CommandUnion: v1.CommandUnion{ + Composite: &v1.CompositeCommand{ + Commands: []string{ + "exec1", + "exec3", + }, + }, + }, + }, + } + + commandsMap := map[string]v1.Command{ + compCommands[0].Id: compCommands[0], + execCommands[0].Id: execCommands[0], + execCommands[1].Id: execCommands[1], + execCommands[2].Id: execCommands[2], + } + + tests := []struct { + name string + eventName string + wantCommands []string + }{ + { + name: "composite event", + eventName: "comp1", + wantCommands: []string{ + "exec1", + "exec3", + }, + }, + { + name: "exec event", + eventName: "exec2", + wantCommands: []string{ + "exec2", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + commands := GetCommandsFromEvent(commandsMap, tt.eventName) + if !reflect.DeepEqual(tt.wantCommands, commands) { + t.Errorf("TestGetCommandsFromEvent() error: got %v expected %v", commands, tt.wantCommands) + } + }) + } + +} diff --git a/pkg/devfile/parser/data/v2/common/component_helper.go b/pkg/devfile/parser/data/v2/common/component_helper.go new file mode 100644 index 0000000..1a5ba33 --- /dev/null +++ b/pkg/devfile/parser/data/v2/common/component_helper.go @@ -0,0 +1,40 @@ +package common + +import ( + "fmt" + + v1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" +) + +// IsContainer checks if the component is a container +func IsContainer(component v1.Component) bool { + return component.Container != nil +} + +// IsVolume checks if the component is a volume +func IsVolume(component v1.Component) bool { + return component.Volume != nil +} + +// GetComponentType returns the component type of a given component +func GetComponentType(component v1.Component) (v1.ComponentType, error) { + switch { + case component.Container != nil: + return v1.ContainerComponentType, nil + case component.Volume != nil: + return v1.VolumeComponentType, nil + case component.Plugin != nil: + return v1.PluginComponentType, nil + case component.Kubernetes != nil: + return v1.KubernetesComponentType, nil + case component.Openshift != nil: + return v1.OpenshiftComponentType, nil + case component.Image != nil: + return v1.ImageComponentType, nil + case component.Custom != nil: + return v1.CustomComponentType, nil + + default: + return "", fmt.Errorf("unknown component type") + } +} diff --git a/pkg/devfile/parser/data/v2/common/component_helper_test.go b/pkg/devfile/parser/data/v2/common/component_helper_test.go new file mode 100644 index 0000000..a2e7a0b --- /dev/null +++ b/pkg/devfile/parser/data/v2/common/component_helper_test.go @@ -0,0 +1,196 @@ +package common + +import ( + "github.com/stretchr/testify/assert" + "testing" + + v1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" +) + +func TestIsContainer(t *testing.T) { + + tests := []struct { + name string + component v1.Component + wantIsSupported bool + }{ + { + name: "Container component", + component: v1.Component{ + Name: "name", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{}, + }, + }, + wantIsSupported: true, + }, + { + name: "Not a container component", + component: v1.Component{ + Name: "name", + ComponentUnion: v1.ComponentUnion{ + Openshift: &v1.OpenshiftComponent{}, + }, + }, + wantIsSupported: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + isSupported := IsContainer(tt.component) + if isSupported != tt.wantIsSupported { + t.Errorf("TestIsContainer error: component support mismatch, expected: %v got: %v", tt.wantIsSupported, isSupported) + } + }) + } + +} + +func TestIsVolume(t *testing.T) { + + tests := []struct { + name string + component v1.Component + wantIsSupported bool + }{ + { + name: "Volume component", + component: v1.Component{ + Name: "name", + ComponentUnion: v1.ComponentUnion{ + Volume: &v1.VolumeComponent{ + Volume: v1.Volume{ + Size: "size", + }, + }, + }, + }, + wantIsSupported: true, + }, + { + name: "Not a volume component", + component: v1.Component{ + Name: "name", + ComponentUnion: v1.ComponentUnion{ + Openshift: &v1.OpenshiftComponent{}, + }, + }, + wantIsSupported: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + isSupported := IsVolume(tt.component) + if isSupported != tt.wantIsSupported { + t.Errorf("TestIsVolume error: component support mismatch, expected: %v got: %v", tt.wantIsSupported, isSupported) + } + }) + } + +} + +func TestGetComponentType(t *testing.T) { + cmpTypeErr := "unknown component type" + + tests := []struct { + name string + component v1.Component + wantErr *string + componentType v1.ComponentType + }{ + { + name: "Volume component", + component: v1.Component{ + Name: "name", + ComponentUnion: v1.ComponentUnion{ + Volume: &v1.VolumeComponent{ + Volume: v1.Volume{}, + }, + }, + }, + componentType: v1.VolumeComponentType, + }, + { + name: "Openshift component", + component: v1.Component{ + Name: "name", + ComponentUnion: v1.ComponentUnion{ + Openshift: &v1.OpenshiftComponent{}, + }, + }, + componentType: v1.OpenshiftComponentType, + }, + { + name: "Kubernetes component", + component: v1.Component{ + Name: "name", + ComponentUnion: v1.ComponentUnion{ + Kubernetes: &v1.KubernetesComponent{}, + }, + }, + componentType: v1.KubernetesComponentType, + }, + { + name: "Container component", + component: v1.Component{ + Name: "name", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{}, + }, + }, + componentType: v1.ContainerComponentType, + }, + { + name: "Plugin component", + component: v1.Component{ + Name: "name", + ComponentUnion: v1.ComponentUnion{ + Plugin: &v1.PluginComponent{}, + }, + }, + componentType: v1.PluginComponentType, + }, + { + name: "Image component", + component: v1.Component{ + Name: "name", + ComponentUnion: v1.ComponentUnion{ + Image: &v1.ImageComponent{}, + }, + }, + componentType: v1.ImageComponentType, + }, + { + name: "Custom component", + component: v1.Component{ + Name: "name", + ComponentUnion: v1.ComponentUnion{ + Custom: &v1.CustomComponent{}, + }, + }, + componentType: v1.CustomComponentType, + }, + { + name: "Unknown component", + component: v1.Component{ + Name: "name", + ComponentUnion: v1.ComponentUnion{}, + }, + wantErr: &cmpTypeErr, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := GetComponentType(tt.component) + // Unexpected error + if (err != nil) != (tt.wantErr != nil) { + t.Errorf("TestGetComponentType() unexpected error: %v, wantErr %v", err, tt.wantErr) + } else if err == nil && got != tt.componentType { + t.Errorf("TestGetComponentType error: component type mismatch, expected: %v got: %v", tt.componentType, got) + } else if err != nil { + assert.Regexp(t, *tt.wantErr, err.Error(), "TestGetComponentType(): Error message should match") + } + }) + } + +} diff --git a/pkg/devfile/parser/data/v2/common/errors.go b/pkg/devfile/parser/data/v2/common/errors.go new file mode 100644 index 0000000..ed5050e --- /dev/null +++ b/pkg/devfile/parser/data/v2/common/errors.go @@ -0,0 +1,27 @@ +package common + +import "fmt" + +// FieldAlreadyExistError error returned if tried to add already exisitng field +type FieldAlreadyExistError struct { + // field which already exist + Field string + // field name + Name string +} + +func (e *FieldAlreadyExistError) Error() string { + return fmt.Sprintf("%s %s already exists in devfile", e.Field, e.Name) +} + +// FieldNotFoundError error returned if the field with the name is not found +type FieldNotFoundError struct { + // field which doesn't exist + Field string + // field name + Name string +} + +func (e *FieldNotFoundError) Error() string { + return fmt.Sprintf("%s %s is not found in the devfile", e.Field, e.Name) +} diff --git a/pkg/devfile/parser/data/v2/common/options.go b/pkg/devfile/parser/data/v2/common/options.go new file mode 100644 index 0000000..64c9321 --- /dev/null +++ b/pkg/devfile/parser/data/v2/common/options.go @@ -0,0 +1,69 @@ +package common + +import ( + "reflect" + + v1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + apiAttributes "github.com/devfile/api/v2/pkg/attributes" +) + +// DevfileOptions provides options for Devfile operations +type DevfileOptions struct { + // Filter is a map that lets filter devfile object against their attributes. Interface can be string, float, boolean or a map + Filter map[string]interface{} + + // CommandOptions specifies the various options available to filter commands + CommandOptions CommandOptions + + // ComponentOptions specifies the various options available to filter components + ComponentOptions ComponentOptions + + // ProjectOptions specifies the various options available to filter projects/starterProjects + ProjectOptions ProjectOptions + + // FilterByName specifies the name for the particular devfile object that's been looking for + FilterByName string +} + +// CommandOptions specifies the various options available to filter commands +type CommandOptions struct { + // CommandGroupKind is an option that allows to filter command based on their kind + CommandGroupKind v1.CommandGroupKind + + // CommandType is an option that allows to filter command based on their type + CommandType v1.CommandType +} + +// ComponentOptions specifies the various options available to filter components +type ComponentOptions struct { + + // ComponentType is an option that allows to filter component based on their type + ComponentType v1.ComponentType +} + +// ProjectOptions specifies the various options available to filter projects/starterProjects +type ProjectOptions struct { + + // ProjectSourceType is an option that allows to filter project based on their source type + ProjectSourceType v1.ProjectSourceType +} + +// FilterDevfileObject filters devfile attributes with the given options +func FilterDevfileObject(attributes apiAttributes.Attributes, options DevfileOptions) (bool, error) { + filterIn := true + for key, value := range options.Filter { + var err error + currentFilterIn := false + attrValue := attributes.Get(key, &err) + var keyNotFoundErr = &apiAttributes.KeyNotFoundError{Key: key} + if err != nil && err.Error() != keyNotFoundErr.Error() { + return false, err + } else if reflect.DeepEqual(attrValue, value) { + currentFilterIn = true + } + + filterIn = filterIn && currentFilterIn + } + + return filterIn, nil +} diff --git a/pkg/devfile/parser/data/v2/common/options_test.go b/pkg/devfile/parser/data/v2/common/options_test.go new file mode 100644 index 0000000..0b8451e --- /dev/null +++ b/pkg/devfile/parser/data/v2/common/options_test.go @@ -0,0 +1,70 @@ +package common + +import ( + "testing" + + "github.com/devfile/api/v2/pkg/attributes" +) + +func TestFilterDevfileObject(t *testing.T) { + + tests := []struct { + name string + attributes attributes.Attributes + options DevfileOptions + wantFilter bool + }{ + { + name: "Filter with one key", + attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + "secondString": "secondStringValue", + }), + options: DevfileOptions{ + Filter: map[string]interface{}{ + "firstString": "firstStringValue", + }, + }, + wantFilter: true, + }, + { + name: "Filter with two keys", + attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + "secondString": "secondStringValue", + }), + options: DevfileOptions{ + Filter: map[string]interface{}{ + "firstString": "firstStringValue", + "secondString": "secondStringValue", + }, + }, + wantFilter: true, + }, + { + name: "Filter with missing key", + attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + "secondString": "secondStringValue", + }), + options: DevfileOptions{ + Filter: map[string]interface{}{ + "missingkey": "firstStringValue", + }, + }, + wantFilter: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + filterIn, err := FilterDevfileObject(tt.attributes, tt.options) + // Unexpected error + if err != nil { + t.Errorf("TestFilterDevfileObject() unexpected error: %v", err) + } else if filterIn != tt.wantFilter { + t.Errorf("TestFilterDevfileObject() error: expected %v got %v", tt.wantFilter, filterIn) + } + }) + } +} diff --git a/pkg/devfile/parser/data/v2/common/project_helper.go b/pkg/devfile/parser/data/v2/common/project_helper.go new file mode 100644 index 0000000..b8df56e --- /dev/null +++ b/pkg/devfile/parser/data/v2/common/project_helper.go @@ -0,0 +1,58 @@ +package common + +import ( + "fmt" + + v1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" +) + +// GetDefaultSource get information about primary source +// returns 3 strings: remote name, remote URL, reference(revision) +func GetDefaultSource(ps v1.GitLikeProjectSource) (remoteName string, remoteURL string, revision string, err error) { + // get git checkout information + // if there are multiple remotes we are ignoring them, as we don't need to setup git repository as it is defined here, + // the only thing that we need is to download the content + + if ps.CheckoutFrom != nil && ps.CheckoutFrom.Revision != "" { + revision = ps.CheckoutFrom.Revision + } + if len(ps.Remotes) > 1 { + if ps.CheckoutFrom == nil { + err = fmt.Errorf("there are multiple git remotes but no checkoutFrom information") + return "", "", "", err + } + remoteName = ps.CheckoutFrom.Remote + if val, ok := ps.Remotes[remoteName]; ok { + remoteURL = val + } else { + err = fmt.Errorf("checkoutFrom.Remote is not defined in Remotes") + return "", "", "", err + + } + } else { + // there is only one remote, using range to get it as there are not indexes + for name, url := range ps.Remotes { + remoteName = name + remoteURL = url + } + + } + + return remoteName, remoteURL, revision, err + +} + +// GetProjectSourceType returns the source type of a given project source +func GetProjectSourceType(projectSrc v1.ProjectSource) (v1.ProjectSourceType, error) { + switch { + case projectSrc.Git != nil: + return v1.GitProjectSourceType, nil + case projectSrc.Zip != nil: + return v1.ZipProjectSourceType, nil + case projectSrc.Custom != nil: + return v1.CustomProjectSourceType, nil + + default: + return "", fmt.Errorf("unknown project source type") + } +} diff --git a/pkg/devfile/parser/data/v2/common/project_helper_test.go b/pkg/devfile/parser/data/v2/common/project_helper_test.go new file mode 100644 index 0000000..fe92cd2 --- /dev/null +++ b/pkg/devfile/parser/data/v2/common/project_helper_test.go @@ -0,0 +1,172 @@ +package common + +import ( + "github.com/stretchr/testify/assert" + "testing" + + v1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" +) + +func TestGitLikeProjectSource_GetDefaultSource(t *testing.T) { + checkoutFromRemoteUndefinedErr := "checkoutFrom.Remote is not defined in Remotes" + missingCheckoutFromErr := "there are multiple git remotes but no checkoutFrom information" + + tests := []struct { + name string + gitLikeProjectSource v1.GitLikeProjectSource + want1 string + want2 string + want3 string + wantErr *string + }{ + { + name: "only one remote", + gitLikeProjectSource: v1.GitLikeProjectSource{ + Remotes: map[string]string{ + "origin": "url", + }, + }, + want1: "origin", + want2: "url", + want3: "", + }, + { + name: "multiple remotes, checkoutFrom with only branch", + gitLikeProjectSource: v1.GitLikeProjectSource{ + Remotes: map[string]string{ + "origin": "urlO", + }, + CheckoutFrom: &v1.CheckoutFrom{Revision: "dev"}, + }, + want1: "origin", + want2: "urlO", + want3: "dev", + }, + { + name: "multiple remotes, checkoutFrom without revision", + gitLikeProjectSource: v1.GitLikeProjectSource{ + Remotes: map[string]string{ + "origin": "urlO", + "upstream": "urlU", + }, + CheckoutFrom: &v1.CheckoutFrom{Remote: "upstream"}, + }, + want1: "upstream", + want2: "urlU", + want3: "", + }, + { + name: "multiple remotes, checkoutFrom with revision", + gitLikeProjectSource: v1.GitLikeProjectSource{ + Remotes: map[string]string{ + "origin": "urlO", + "upstream": "urlU", + }, + CheckoutFrom: &v1.CheckoutFrom{Remote: "upstream", Revision: "v1"}, + }, + want1: "upstream", + want2: "urlU", + want3: "v1", + }, + { + name: "multiple remotes, checkoutFrom with unknown remote", + gitLikeProjectSource: v1.GitLikeProjectSource{ + Remotes: map[string]string{ + "origin": "urlO", + "upstream": "urlU", + }, + CheckoutFrom: &v1.CheckoutFrom{Remote: "non"}, + }, + want1: "", + want2: "", + want3: "", + wantErr: &checkoutFromRemoteUndefinedErr, + }, + { + name: "multiple remotes, no checkoutFrom", + gitLikeProjectSource: v1.GitLikeProjectSource{ + Remotes: map[string]string{ + "origin": "urlO", + "upstream": "urlU", + }, + }, + want1: "", + want2: "", + want3: "", + wantErr: &missingCheckoutFromErr, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + got1, got2, got3, err := GetDefaultSource(tt.gitLikeProjectSource) + if (err != nil) != (tt.wantErr != nil) { + t.Errorf("TestGitLikeProjectSource_GetDefaultSource() unexpected error: %v, wantErr %v", err, tt.wantErr) + } else if err == nil { + if got1 != tt.want1 { + t.Errorf("TestGitLikeProjectSource_GetDefaultSource() error: got1 = %v, want %v", got1, tt.want1) + } + if got2 != tt.want2 { + t.Errorf("TestGitLikeProjectSource_GetDefaultSource() error: got2 = %v, want %v", got2, tt.want2) + } + if got3 != tt.want3 { + t.Errorf("TestGitLikeProjectSource_GetDefaultSource() error: got3 = %v, want %v", got3, tt.want3) + } + } else { + assert.Regexp(t, *tt.wantErr, err.Error(), "TestGitLikeProjectSource_GetDefaultSource(): Error message should match") + } + }) + } +} + +func TestGetProjectSrcType(t *testing.T) { + projectSrcTypeErr := "unknown project source type" + + tests := []struct { + name string + projectSrc v1.ProjectSource + wantErr *string + projectSrcType v1.ProjectSourceType + }{ + { + name: "Git project", + projectSrc: v1.ProjectSource{ + Git: &v1.GitProjectSource{}, + }, + projectSrcType: v1.GitProjectSourceType, + }, + { + name: "Zip project", + projectSrc: v1.ProjectSource{ + Zip: &v1.ZipProjectSource{}, + }, + projectSrcType: v1.ZipProjectSourceType, + }, + { + name: "Custom project", + projectSrc: v1.ProjectSource{ + Custom: &v1.CustomProjectSource{}, + }, + projectSrcType: v1.CustomProjectSourceType, + }, + { + name: "Unknown project", + projectSrc: v1.ProjectSource{}, + wantErr: &projectSrcTypeErr, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := GetProjectSourceType(tt.projectSrc) + // Unexpected error + if (err != nil) != (tt.wantErr != nil) { + t.Errorf("TestGetProjectSrcType() unexpected error: %v, wantErr %v", err, tt.wantErr) + } else if err == nil && got != tt.projectSrcType { + t.Errorf("TestGetProjectSrcType() error: project src type mismatch, expected: %v got: %v", tt.projectSrcType, got) + } else if err != nil { + assert.Regexp(t, *tt.wantErr, err.Error(), "TestGetProjectSrcType(): Error message should match") + } + }) + } + +} diff --git a/pkg/devfile/parser/data/v2/components.go b/pkg/devfile/parser/data/v2/components.go new file mode 100644 index 0000000..6b88034 --- /dev/null +++ b/pkg/devfile/parser/data/v2/components.go @@ -0,0 +1,128 @@ +package v2 + +import ( + "fmt" + "reflect" + "strings" + + v1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + "github.com/devfile/library/pkg/devfile/parser/data/v2/common" +) + +// GetComponents returns the slice of Component objects parsed from the Devfile +func (d *DevfileV2) GetComponents(options common.DevfileOptions) ([]v1.Component, error) { + + if reflect.DeepEqual(options, common.DevfileOptions{}) { + return d.Components, nil + } + + var components []v1.Component + for _, component := range d.Components { + // Filter Component Attributes + filterIn, err := common.FilterDevfileObject(component.Attributes, options) + if err != nil { + return nil, err + } else if !filterIn { + continue + } + + // Filter Component Type - Container, Volume, etc. + componentType, err := common.GetComponentType(component) + if err != nil { + return nil, err + } + if options.ComponentOptions.ComponentType != "" && componentType != options.ComponentOptions.ComponentType { + continue + } + + if options.FilterByName == "" || component.Name == options.FilterByName { + components = append(components, component) + } + } + + return components, nil +} + +// GetDevfileContainerComponents iterates through the components in the devfile and returns a list of devfile container components. +// Deprecated, use GetComponents() with the DevfileOptions. +func (d *DevfileV2) GetDevfileContainerComponents(options common.DevfileOptions) ([]v1.Component, error) { + var components []v1.Component + devfileComponents, err := d.GetComponents(options) + if err != nil { + return nil, err + } + for _, comp := range devfileComponents { + if comp.Container != nil { + components = append(components, comp) + } + } + return components, nil +} + +// GetDevfileVolumeComponents iterates through the components in the devfile and returns a list of devfile volume components. +// Deprecated, use GetComponents() with the DevfileOptions. +func (d *DevfileV2) GetDevfileVolumeComponents(options common.DevfileOptions) ([]v1.Component, error) { + var components []v1.Component + devfileComponents, err := d.GetComponents(options) + if err != nil { + return nil, err + } + for _, comp := range devfileComponents { + if comp.Volume != nil { + components = append(components, comp) + } + } + return components, nil +} + +// AddComponents adds the slice of Component objects to the devfile's components +// a component is considered as invalid if it is already defined +// component list passed in will be all processed, and returns a total error of all invalid components +func (d *DevfileV2) AddComponents(components []v1.Component) error { + var errorsList []string + for _, component := range components { + var err error + for _, devfileComponent := range d.Components { + if component.Name == devfileComponent.Name { + err = &common.FieldAlreadyExistError{Name: component.Name, Field: "component"} + errorsList = append(errorsList, err.Error()) + break + } + } + if err == nil { + d.Components = append(d.Components, component) + } + } + if len(errorsList) > 0 { + return fmt.Errorf("errors while adding components:\n%s", strings.Join(errorsList, "\n")) + } + return nil +} + +// UpdateComponent updates the component with the given name +// return an error if the component is not found +func (d *DevfileV2) UpdateComponent(component v1.Component) error { + for i := range d.Components { + if d.Components[i].Name == component.Name { + d.Components[i] = component + return nil + } + } + return fmt.Errorf("update component failed: component %s not found", component.Name) +} + +// DeleteComponent removes the specified component +func (d *DevfileV2) DeleteComponent(name string) error { + + for i := range d.Components { + if d.Components[i].Name == name { + d.Components = append(d.Components[:i], d.Components[i+1:]...) + return nil + } + } + + return &common.FieldNotFoundError{ + Field: "component", + Name: name, + } +} diff --git a/pkg/devfile/parser/data/v2/components_test.go b/pkg/devfile/parser/data/v2/components_test.go new file mode 100644 index 0000000..c0480b1 --- /dev/null +++ b/pkg/devfile/parser/data/v2/components_test.go @@ -0,0 +1,825 @@ +package v2 + +import ( + "fmt" + "github.com/kylelemons/godebug/pretty" + "reflect" + "testing" + + v1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + "github.com/devfile/api/v2/pkg/attributes" + "github.com/devfile/library/pkg/devfile/parser/data/v2/common" + "github.com/devfile/library/pkg/testingutil" + "github.com/stretchr/testify/assert" +) + +func TestDevfile200_AddComponent(t *testing.T) { + multipleDupError := fmt.Sprintf("%s\n%s", "component component1 already exists in devfile", "component component2 already exists in devfile") + + tests := []struct { + name string + currentComponents []v1.Component + newComponents []v1.Component + wantComponents []v1.Component + wantErr *string + }{ + { + name: "successfully add the component", + currentComponents: []v1.Component{ + { + Name: "component1", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{}, + }, + }, + { + Name: "component2", + ComponentUnion: v1.ComponentUnion{ + Volume: &v1.VolumeComponent{}, + }, + }, + }, + newComponents: []v1.Component{ + { + Name: "component3", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{}, + }, + }, + }, + wantComponents: []v1.Component{ + { + Name: "component1", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{}, + }, + }, + { + Name: "component2", + ComponentUnion: v1.ComponentUnion{ + Volume: &v1.VolumeComponent{}, + }, + }, + { + Name: "component3", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{}, + }, + }, + }, + wantErr: nil, + }, + { + name: "error out on duplicate component", + currentComponents: []v1.Component{ + { + Name: "component1", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{}, + }, + }, + { + Name: "component2", + ComponentUnion: v1.ComponentUnion{ + Volume: &v1.VolumeComponent{}, + }, + }, + }, + newComponents: []v1.Component{ + { + Name: "component1", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{}, + }, + }, + { + Name: "component2", + ComponentUnion: v1.ComponentUnion{ + Volume: &v1.VolumeComponent{}, + }, + }, + { + Name: "component3", + ComponentUnion: v1.ComponentUnion{ + Volume: &v1.VolumeComponent{}, + }, + }, + }, + wantComponents: []v1.Component{ + { + Name: "component1", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{}, + }, + }, + { + Name: "component2", + ComponentUnion: v1.ComponentUnion{ + Volume: &v1.VolumeComponent{}, + }, + }, + { + Name: "component3", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{}, + }, + }, + }, + wantErr: &multipleDupError, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := &DevfileV2{ + v1.Devfile{ + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Components: tt.currentComponents, + }, + }, + }, + } + + err := d.AddComponents(tt.newComponents) + // Unexpected error + if (err != nil) != (tt.wantErr != nil) { + t.Errorf("TestDevfile200_AddComponents() unexpected error: %v, wantErr %v", err, tt.wantErr) + } else if tt.wantErr != nil { + assert.Regexp(t, *tt.wantErr, err.Error(), "TestDevfile200_AddComponents(): Error message should match") + } else { + if !reflect.DeepEqual(d.Components, tt.wantComponents) { + t.Errorf("TestDevfile200_AddComponents() wanted: %v, got: %v, difference at %v", tt.wantComponents, d.Components, pretty.Compare(tt.wantComponents, d.Components)) + } + } + }) + } +} + +func TestDevfile200_UpdateComponent(t *testing.T) { + invalidCmpErr := "update component failed: component .* not found" + + tests := []struct { + name string + currentComponents []v1.Component + newComponent v1.Component + wantErr *string + }{ + { + name: "successfully update the component", + currentComponents: []v1.Component{ + { + Name: "Component1", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "image1", + }, + }, + }, + }, + { + Name: "component2", + ComponentUnion: v1.ComponentUnion{ + Volume: &v1.VolumeComponent{}, + }, + }, + }, + newComponent: v1.Component{ + Name: "Component1", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "image2", + }, + }, + }, + }, + }, + { + name: "fail to update the component if not exist", + currentComponents: []v1.Component{ + { + Name: "Component1", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "image1", + }, + }, + }, + }, + }, + newComponent: v1.Component{ + Name: "Component2", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "image2", + }, + }, + }, + }, + wantErr: &invalidCmpErr, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := &DevfileV2{ + v1.Devfile{ + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Components: tt.currentComponents, + }, + }, + }, + } + + err := d.UpdateComponent(tt.newComponent) + // Unexpected error + if (err != nil) != (tt.wantErr != nil) { + t.Errorf("TestDevfile200_UpdateComponent() unexpected error: %v, wantErr %v", err, tt.wantErr) + } else if err == nil { + components, err := d.GetComponents(common.DevfileOptions{}) + if err != nil { + t.Errorf("TestDevfile200_UpdateComponent() unexpected error: %v", err) + return + } + + matched := false + for _, component := range components { + if reflect.DeepEqual(component, tt.newComponent) { + matched = true + break + } + } + + if !matched { + t.Error("TestDevfile200_UpdateComponent() error updating the component") + } + } else { + assert.Regexp(t, *tt.wantErr, err.Error(), "TestDevfile200_UpdateComponent(): Error message should match") + } + }) + } +} + +func TestGetDevfileComponents(t *testing.T) { + invalidCmpType := "unknown component type" + + tests := []struct { + name string + component []v1.Component + wantComponents []string + filterOptions common.DevfileOptions + wantErr *string + }{ + { + name: "Invalid devfile", + component: []v1.Component{}, + }, + { + name: "Get all the components", + component: []v1.Component{ + { + Name: "comp1", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + "secondString": "secondStringValue", + }), + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{}, + }, + }, + { + Name: "comp2", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + "fourthString": "fourthStringValue", + }), + ComponentUnion: v1.ComponentUnion{ + Volume: &v1.VolumeComponent{}, + }, + }, + }, + wantComponents: []string{"comp1", "comp2"}, + }, + { + name: "Get component with the specified filter", + component: []v1.Component{ + { + Name: "comp1", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + "secondString": "secondStringValue", + }), + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{}, + }, + }, + { + Name: "comp2", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + "thirdString": "thirdStringValue", + }), + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{}, + }, + }, + { + Name: "comp3", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + "fourthString": "fourthStringValue", + }), + ComponentUnion: v1.ComponentUnion{ + Volume: &v1.VolumeComponent{}, + }, + }, + { + Name: "comp4", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "fourthString": "fourthStringValue", + }), + ComponentUnion: v1.ComponentUnion{ + Volume: &v1.VolumeComponent{}, + }, + }, + }, + filterOptions: common.DevfileOptions{ + Filter: map[string]interface{}{ + "firstString": "firstStringValue", + }, + CommandOptions: common.CommandOptions{ + CommandGroupKind: v1.BuildCommandGroupKind, + CommandType: v1.CompositeCommandType, + }, + ComponentOptions: common.ComponentOptions{ + ComponentType: v1.VolumeComponentType, + }, + }, + wantComponents: []string{"comp3"}, + }, + { + name: "Get component with the specified name", + component: []v1.Component{ + { + Name: "comp1", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{}, + }, + }, + { + Name: "comp2", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{}, + }, + }, + { + Name: "comp3", + ComponentUnion: v1.ComponentUnion{ + Volume: &v1.VolumeComponent{}, + }, + }, + }, + filterOptions: common.DevfileOptions{ + FilterByName: "comp3", + }, + wantComponents: []string{"comp3"}, + }, + { + name: "component name not found", + component: []v1.Component{ + { + Name: "comp1", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{}, + }, + }, + { + Name: "comp2", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{}, + }, + }, + }, + filterOptions: common.DevfileOptions{ + FilterByName: "comp3", + }, + wantComponents: []string{}, + }, + { + name: "Wrong filter for component", + component: []v1.Component{ + { + Name: "comp1", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + "secondString": "secondStringValue", + }), + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{}, + }, + }, + { + Name: "comp2", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + "thirdString": "thirdStringValue", + }), + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{}, + }, + }, + }, + filterOptions: common.DevfileOptions{ + Filter: map[string]interface{}{ + "firstStringIsWrong": "firstStringValue", + }, + ComponentOptions: common.ComponentOptions{ + ComponentType: v1.ContainerComponentType, + }, + }, + }, + { + name: "Invalid component type", + component: []v1.Component{ + { + Name: "comp1", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + }), + ComponentUnion: v1.ComponentUnion{}, + }, + }, + filterOptions: common.DevfileOptions{ + Filter: map[string]interface{}{ + "firstString": "firstStringValue", + }, + }, + wantErr: &invalidCmpType, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := &DevfileV2{ + v1.Devfile{ + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Components: tt.component, + }, + }, + }, + } + + components, err := d.GetComponents(tt.filterOptions) + if (err != nil) != (tt.wantErr != nil) { + t.Errorf("TestGetDevfileComponents() unexpected error: %v, wantErr %v", err, tt.wantErr) + } else if err == nil { + // confirm the length of actual vs expected + if len(components) != len(tt.wantComponents) { + t.Errorf("TestGetDevfileComponents() error: length of expected components is not the same as the length of actual components") + return + } + + // compare the component slices for content + for _, wantComponent := range tt.wantComponents { + matched := false + for _, component := range components { + if wantComponent == component.Name { + matched = true + } + } + + if !matched { + t.Errorf("TestGetDevfileComponents() error: component %s not found in the devfile", wantComponent) + } + } + } else { + assert.Regexp(t, *tt.wantErr, err.Error(), "TestGetDevfileComponents(): Error message should match") + } + }) + } + +} + +func TestGetDevfileContainerComponents(t *testing.T) { + + tests := []struct { + name string + component []v1.Component + expectedMatchesCount int + filterOptions common.DevfileOptions + }{ + { + name: "Invalid devfile", + component: []v1.Component{}, + expectedMatchesCount: 0, + }, + { + name: "Valid devfile with wrong component type (Openshift)", + component: []v1.Component{ + { + ComponentUnion: v1.ComponentUnion{ + Openshift: &v1.OpenshiftComponent{}, + }, + }, + }, + expectedMatchesCount: 0, + }, + { + name: "Valid devfile with correct component type (Container)", + component: []v1.Component{ + testingutil.GetFakeContainerComponent("comp1"), + testingutil.GetFakeContainerComponent("comp2"), + }, + expectedMatchesCount: 2, + filterOptions: common.DevfileOptions{}, + }, + { + name: "Get Container component with the specified filter", + component: []v1.Component{ + { + Name: "comp1", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + "secondString": "secondStringValue", + }), + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{}, + }, + }, + { + Name: "comp2", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + "thirdString": "thirdStringValue", + }), + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{}, + }, + }, + }, + filterOptions: common.DevfileOptions{ + Filter: map[string]interface{}{ + "firstString": "firstStringValue", + "secondString": "secondStringValue", + }, + }, + expectedMatchesCount: 1, + }, + { + name: "Get Container component with the wrong specified filter", + component: []v1.Component{ + { + Name: "comp1", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + "secondString": "secondStringValue", + }), + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{}, + }, + }, + { + Name: "comp2", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + "thirdString": "thirdStringValue", + }), + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{}, + }, + }, + }, + filterOptions: common.DevfileOptions{ + Filter: map[string]interface{}{ + "firstStringIsWrong": "firstStringValue", + }, + }, + expectedMatchesCount: 0, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := &DevfileV2{ + v1.Devfile{ + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Components: tt.component, + }, + }, + }, + } + + devfileComponents, err := d.GetDevfileContainerComponents(tt.filterOptions) + if err != nil { + t.Errorf("TestGetDevfileContainerComponents() unexpected error: %v", err) + } else if len(devfileComponents) != tt.expectedMatchesCount { + t.Errorf("TestGetDevfileContainerComponents error: wrong number of components matched: expected %v, actual %v", tt.expectedMatchesCount, len(devfileComponents)) + } + }) + } + +} + +func TestGetDevfileVolumeComponents(t *testing.T) { + + tests := []struct { + name string + component []v1.Component + expectedMatchesCount int + filterOptions common.DevfileOptions + }{ + { + name: "Invalid devfile", + component: []v1.Component{}, + expectedMatchesCount: 0, + }, + { + name: "Valid devfile with wrong component type (Kubernetes)", + component: []v1.Component{ + { + ComponentUnion: v1.ComponentUnion{ + Kubernetes: &v1.KubernetesComponent{}, + }, + }, + }, + expectedMatchesCount: 0, + }, + { + name: "Valid devfile with correct component type (Volume)", + component: []v1.Component{ + testingutil.GetFakeContainerComponent("comp1"), + testingutil.GetFakeVolumeComponent("myvol", "4Gi"), + }, + expectedMatchesCount: 1, + }, + { + name: "Get Container component with the specified filter", + component: []v1.Component{ + { + Name: "comp1", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + "secondString": "secondStringValue", + }), + ComponentUnion: v1.ComponentUnion{ + Volume: &v1.VolumeComponent{}, + }, + }, + { + Name: "comp2", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + "thirdString": "thirdStringValue", + }), + ComponentUnion: v1.ComponentUnion{ + Volume: &v1.VolumeComponent{}, + }, + }, + }, + filterOptions: common.DevfileOptions{ + Filter: map[string]interface{}{ + "firstString": "firstStringValue", + }, + }, + expectedMatchesCount: 2, + }, + { + name: "Get Container component with the wrong specified filter", + component: []v1.Component{ + { + Name: "comp1", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + "secondString": "secondStringValue", + }), + ComponentUnion: v1.ComponentUnion{ + Volume: &v1.VolumeComponent{}, + }, + }, + }, + filterOptions: common.DevfileOptions{ + Filter: map[string]interface{}{ + "firstStringIsWrong": "firstStringValue", + }, + }, + expectedMatchesCount: 0, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := &DevfileV2{ + v1.Devfile{ + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Components: tt.component, + }, + }, + }, + } + devfileComponents, err := d.GetDevfileVolumeComponents(tt.filterOptions) + if err != nil { + t.Errorf("TestGetDevfileVolumeComponents() unexpected error: %v", err) + } else if len(devfileComponents) != tt.expectedMatchesCount { + t.Errorf("TestGetDevfileVolumeComponents() error: wrong number of components matched: expected %v, actual %v", tt.expectedMatchesCount, len(devfileComponents)) + } + }) + } + +} + +func TestDeleteComponents(t *testing.T) { + + missingCmpErr := "component .* is not found in the devfile" + + d := &DevfileV2{ + v1.Devfile{ + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Components: []v1.Component{ + { + Name: "comp2", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + VolumeMounts: []v1.VolumeMount{ + testingutil.GetFakeVolumeMount("comp2", "/path"), + testingutil.GetFakeVolumeMount("comp2", "/path2"), + testingutil.GetFakeVolumeMount("comp3", "/path"), + }, + }, + }, + }, + }, + { + Name: "comp2", + ComponentUnion: v1.ComponentUnion{ + Volume: &v1.VolumeComponent{}, + }, + }, + { + Name: "comp3", + ComponentUnion: v1.ComponentUnion{ + Volume: &v1.VolumeComponent{}, + }, + }, + }, + }, + }, + }, + } + + tests := []struct { + name string + componentToDelete string + wantComponents []v1.Component + wantErr *string + }{ + { + name: "Successfully delete a Component", + componentToDelete: "comp3", + wantComponents: []v1.Component{ + { + Name: "comp2", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + VolumeMounts: []v1.VolumeMount{ + testingutil.GetFakeVolumeMount("comp2", "/path"), + testingutil.GetFakeVolumeMount("comp2", "/path2"), + testingutil.GetFakeVolumeMount("comp3", "/path"), + }, + }, + }, + }, + }, + { + Name: "comp2", + ComponentUnion: v1.ComponentUnion{ + Volume: &v1.VolumeComponent{}, + }, + }, + }, + }, + { + name: "Missing Component", + componentToDelete: "comp12", + wantErr: &missingCmpErr, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := d.DeleteComponent(tt.componentToDelete) + if (err != nil) != (tt.wantErr != nil) { + t.Errorf("TestDeleteComponents() unexpected error: %v, wantErr %v", err, tt.wantErr) + } else if err == nil { + assert.Equal(t, tt.wantComponents, d.Components, "TestDeleteComponents(): The two values should be the same.") + } else { + assert.Regexp(t, *tt.wantErr, err.Error(), "TestDeleteComponents(): Error message should match") + } + }) + } + +} diff --git a/pkg/devfile/parser/data/v2/containers.go b/pkg/devfile/parser/data/v2/containers.go new file mode 100644 index 0000000..7671f18 --- /dev/null +++ b/pkg/devfile/parser/data/v2/containers.go @@ -0,0 +1,279 @@ +package v2 + +import ( + "fmt" + "strconv" + "strings" + + v1alpha2 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + "github.com/devfile/library/pkg/devfile/parser/data/v2/common" + corev1 "k8s.io/api/core/v1" +) + +// AddEnvVars accepts a map of container name mapped to an array of the env vars to be set; +// it adds the envirnoment variables to a given container name of the DevfileV2 object +// Example of containerEnvMap : {"runtime": {{Name: "Foo", Value: "Bar"}}} +func (d *DevfileV2) AddEnvVars(containerEnvMap map[string][]v1alpha2.EnvVar) error { + components, err := d.GetComponents(common.DevfileOptions{}) + if err != nil { + return err + } + for _, component := range components { + if component.Container != nil { + component.Container.Env = merge(component.Container.Env, containerEnvMap[component.Name]) + d.UpdateComponent(component) + } + } + return nil +} + +// RemoveEnvVars accepts a map of container name mapped to an array of environment variables to be removed; +// it removes the env vars from the specified container name of the DevfileV2 object +func (d *DevfileV2) RemoveEnvVars(containerEnvMap map[string][]string) error { + components, err := d.GetComponents(common.DevfileOptions{}) + if err != nil { + return err + } + for _, component := range components { + if component.Container != nil { + component.Container.Env, err = removeEnvVarsFromList(component.Container.Env, containerEnvMap[component.Name]) + if err != nil { + return err + } + d.UpdateComponent(component) + } + } + return nil +} + +// SetPorts accepts a map of container name mapped to an array of port numbers to be set; +// it converts ports to endpoints, sets the endpoint to a given container name of the DevfileV2 object +// Example of containerPortsMap: {"runtime": {"8080", "9000"}, "wildfly": {"12956"}} +func (d *DevfileV2) SetPorts(containerPortsMap map[string][]string) error { + components, err := d.GetComponents(common.DevfileOptions{}) + if err != nil { + return err + } + for _, component := range components { + endpoints, err := portsToEndpoints(containerPortsMap[component.Name]...) + if err != nil { + return err + } + if component.Container != nil { + component.Container.Endpoints = addEndpoints(component.Container.Endpoints, endpoints) + d.UpdateComponent(component) + } + } + + return nil +} + +// RemovePorts accepts a map of container name mapped to an array of port numbers to be removed; +// it removes the container endpoints with the specified port numbers of the specified container of the DevfileV2 object +// Example of containerPortsMap: {"runtime": {"8080", "9000"}, "wildfly": {"12956"}} +func (d *DevfileV2) RemovePorts(containerPortsMap map[string][]string) error { + components, err := d.GetComponents(common.DevfileOptions{}) + if err != nil { + return err + } + for _, component := range components { + if component.Container != nil { + component.Container.Endpoints, err = removePortsFromList(component.Container.Endpoints, containerPortsMap[component.Name]) + if err != nil { + return err + } + d.UpdateComponent(component) + } + } + + return nil +} + +// removeEnvVarsFromList removes the env variables based on the keys provided +// and returns a new EnvVarList +func removeEnvVarsFromList(envVarList []v1alpha2.EnvVar, keys []string) ([]v1alpha2.EnvVar, error) { + // convert the array of envVarList to a map such that it can easily search for env var(s) + // to remove from the component + envVarListMap := map[string]bool{} + for _, env := range envVarList { + if !envVarListMap[env.Name] { + envVarListMap[env.Name] = true + } + } + + // convert the array of keys to a map so that it can do a fast search for environment variable(s) + // to remove from the component + envVarToBeRemoved := map[string]bool{} + // now check if the environment variable(s) requested for removal exists in + // the env vars currently set in the component + // if an env var requested for removal is not currently set, then raise an error + // else add the env var to the envVarToBeRemoved map + for _, key := range keys { + if !envVarListMap[key] { + return envVarList, fmt.Errorf("unable to find environment variable %s in the component", key) + } + envVarToBeRemoved[key] = true + } + + // finally, let's remove the environment variables(s) requested by the user + newEnvVarList := []v1alpha2.EnvVar{} + for _, envVar := range envVarList { + // if the env is in the keys(env var(s) to be removed), we skip it + if envVarToBeRemoved[envVar.Name] { + continue + } + newEnvVarList = append(newEnvVarList, envVar) + } + return newEnvVarList, nil +} + +// removePortsFromList removes the ports from a given Endpoint list based on the provided port numbers +// and returns a new list of Endpoint +func removePortsFromList(endpoints []v1alpha2.Endpoint, ports []string) ([]v1alpha2.Endpoint, error) { + // convert the array of Endpoint to a map such that it can easily search for port(s) + // to remove from the component + portInEndpoint := map[string]bool{} + for _, ep := range endpoints { + port := strconv.Itoa(ep.TargetPort) + if !portInEndpoint[port] { + portInEndpoint[port] = true + } + } + + // convert the array of ports to a map so that it can do a fast search for port(s) + // to remove from the component + portsToBeRemoved := map[string]bool{} + + // now check if the port(s) requested for removal exists in + // the ports currently present in the component; + // if a port requested for removal is not currently present, then raise an error + // else add the port to the portsToBeRemoved map + for _, port := range ports { + if !portInEndpoint[port] { + return endpoints, fmt.Errorf("unable to find port %q in the component", port) + } + portsToBeRemoved[port] = true + } + + // finally, let's remove the port(s) requested by the user + newEndpointsList := []v1alpha2.Endpoint{} + for _, ep := range endpoints { + // if the port is in the port(s)(to be removed), we skip it + if portsToBeRemoved[strconv.Itoa(ep.TargetPort)] { + continue + } + newEndpointsList = append(newEndpointsList, ep) + } + return newEndpointsList, nil +} + +// merge merges the other EnvVarlist with keeping last value for duplicate EnvVars +// and returns a new EnvVarList +func merge(original []v1alpha2.EnvVar, other []v1alpha2.EnvVar) []v1alpha2.EnvVar { + + var dedupNewEvl []v1alpha2.EnvVar + newEvl := append(original, other...) + uniqueMap := make(map[string]string) + // last value will be kept in case of duplicate env vars + for _, envVar := range newEvl { + uniqueMap[envVar.Name] = envVar.Value + } + + for key, value := range uniqueMap { + dedupNewEvl = append(dedupNewEvl, v1alpha2.EnvVar{ + Name: key, + Value: value, + }) + } + + return dedupNewEvl + +} + +// portsToEndpoints converts an array of ports to an array of v1alpha2.Endpoint +func portsToEndpoints(ports ...string) ([]v1alpha2.Endpoint, error) { + var endpoints []v1alpha2.Endpoint + conPorts, err := getContainerPortsFromStrings(ports) + if err != nil { + return nil, err + } + for _, port := range conPorts { + + endpoint := v1alpha2.Endpoint{ + Name: fmt.Sprintf("port-%d-%s", port.ContainerPort, strings.ToLower(string(port.Protocol))), + TargetPort: int(port.ContainerPort), + Protocol: v1alpha2.EndpointProtocol(strings.ToLower(string(port.Protocol))), + } + endpoints = append(endpoints, endpoint) + } + return endpoints, nil + +} + +// addEndpoints appends two arrays of v1alpha2.Endpoint objects +func addEndpoints(current []v1alpha2.Endpoint, other []v1alpha2.Endpoint) []v1alpha2.Endpoint { + newList := make([]v1alpha2.Endpoint, len(current)) + copy(newList, current) + for _, ep := range other { + present := false + + for _, presentep := range newList { + + protocol := presentep.Protocol + if protocol == "" { + // endpoint protocol default value is http + protocol = "http" + } + // if the target port and protocol match, we add a case where the protocol is not provided and hence we assume that to be "tcp" + if presentep.TargetPort == ep.TargetPort && (ep.Protocol == protocol) { + present = true + break + } + } + if !present { + newList = append(newList, ep) + } + } + + return newList +} + +// getContainerPortsFromStrings generates ContainerPort values from the array of string port values +// ports is the array containing the string port values +func getContainerPortsFromStrings(ports []string) ([]corev1.ContainerPort, error) { + var containerPorts []corev1.ContainerPort + for _, port := range ports { + splits := strings.Split(port, "/") + if len(splits) < 1 || len(splits) > 2 { + return nil, fmt.Errorf("unable to parse the port string %s", port) + } + + portNumberI64, err := strconv.ParseInt(splits[0], 10, 32) + if err != nil { + return nil, fmt.Errorf("invalid port number %s", splits[0]) + } + portNumber := int32(portNumberI64) + + var portProto corev1.Protocol + if len(splits) == 2 { + switch strings.ToUpper(splits[1]) { + case "TCP": + portProto = corev1.ProtocolTCP + case "UDP": + portProto = corev1.ProtocolUDP + default: + return nil, fmt.Errorf("invalid port protocol %s", splits[1]) + } + } else { + portProto = corev1.ProtocolTCP + } + + port := corev1.ContainerPort{ + Name: fmt.Sprintf("%d-%s", portNumber, strings.ToLower(string(portProto))), + ContainerPort: portNumber, + Protocol: portProto, + } + containerPorts = append(containerPorts, port) + } + return containerPorts, nil +} diff --git a/pkg/devfile/parser/data/v2/containers_test.go b/pkg/devfile/parser/data/v2/containers_test.go new file mode 100644 index 0000000..5f79e23 --- /dev/null +++ b/pkg/devfile/parser/data/v2/containers_test.go @@ -0,0 +1,662 @@ +package v2 + +import ( + "reflect" + "testing" + + "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + "github.com/kylelemons/godebug/pretty" +) + +func TestAddEnvVars(t *testing.T) { + + tests := []struct { + name string + listToAdd map[string][]v1alpha2.EnvVar + currentDevfile *DevfileV2 + wantDevFile *DevfileV2 + }{ + { + name: "add env vars", + listToAdd: map[string][]v1alpha2.EnvVar{ + "loadbalancer": { + { + Name: "DATABASE_PASSWORD", + Value: "苦痛", + }, + }, + }, + currentDevfile: testDevfileData(), + wantDevFile: &DevfileV2{ + Devfile: v1alpha2.Devfile{ + DevWorkspaceTemplateSpec: v1alpha2.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1alpha2.DevWorkspaceTemplateSpecContent{ + Commands: []v1alpha2.Command{ + { + Id: "devbuild", + CommandUnion: v1alpha2.CommandUnion{ + Exec: &v1alpha2.ExecCommand{ + WorkingDir: "/projects/nodejs-starter", + }, + }, + }, + }, + Components: []v1alpha2.Component{ + { + Name: "runtime", + ComponentUnion: v1alpha2.ComponentUnion{ + Container: &v1alpha2.ContainerComponent{ + Container: v1alpha2.Container{ + Image: "quay.io/nodejs-12", + Env: []v1alpha2.EnvVar{ + { + Name: "DATABASE_PASSWORD", + Value: "苦痛", + }, + }, + }, + Endpoints: []v1alpha2.Endpoint{ + { + Name: "port-3030", + TargetPort: 3030, + }, + }, + }, + }, + }, + { + Name: "loadbalancer", + ComponentUnion: v1alpha2.ComponentUnion{ + Container: &v1alpha2.ContainerComponent{ + Container: v1alpha2.Container{ + Image: "quay.io/nginx", + Env: []v1alpha2.EnvVar{ + { + Name: "DATABASE_PASSWORD", + Value: "苦痛", + }, + }, + }, + }, + }, + }, + }, + Events: &v1alpha2.Events{ + DevWorkspaceEvents: v1alpha2.DevWorkspaceEvents{ + PostStop: []string{"post-stop"}, + }, + }, + Projects: []v1alpha2.Project{ + { + ClonePath: "/projects", + Name: "nodejs-starter-build", + }, + }, + StarterProjects: []v1alpha2.StarterProject{ + { + SubDir: "/projects", + Name: "starter-project-2", + }, + }, + }, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + err := tt.currentDevfile.AddEnvVars(tt.listToAdd) + + if err != nil { + t.Errorf("TestAddAndRemoveEnvVars() unexpected error while adding env vars %+v", err.Error()) + } + + if !reflect.DeepEqual(tt.currentDevfile, tt.wantDevFile) { + t.Errorf("TestAddAndRemoveEnvVars() error: wanted: %v, got: %v, difference at %v", tt.wantDevFile, tt.currentDevfile, pretty.Compare(tt.currentDevfile, tt.wantDevFile)) + } + + }) + } + +} + +func TestRemoveEnvVars(t *testing.T) { + + tests := []struct { + name string + listToRemove map[string][]string + currentDevfile *DevfileV2 + wantDevFile *DevfileV2 + wantRemoveErr bool + }{ + { + name: "remove env vars", + listToRemove: map[string][]string{ + "runtime": { + "DATABASE_PASSWORD", + }, + }, + currentDevfile: testDevfileData(), + wantDevFile: &DevfileV2{ + Devfile: v1alpha2.Devfile{ + DevWorkspaceTemplateSpec: v1alpha2.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1alpha2.DevWorkspaceTemplateSpecContent{ + Commands: []v1alpha2.Command{ + { + Id: "devbuild", + CommandUnion: v1alpha2.CommandUnion{ + Exec: &v1alpha2.ExecCommand{ + WorkingDir: "/projects/nodejs-starter", + }, + }, + }, + }, + Components: []v1alpha2.Component{ + { + Name: "runtime", + ComponentUnion: v1alpha2.ComponentUnion{ + Container: &v1alpha2.ContainerComponent{ + Container: v1alpha2.Container{ + Image: "quay.io/nodejs-12", + Env: []v1alpha2.EnvVar{}, + }, + Endpoints: []v1alpha2.Endpoint{ + { + Name: "port-3030", + TargetPort: 3030, + }, + }, + }, + }, + }, + { + Name: "loadbalancer", + ComponentUnion: v1alpha2.ComponentUnion{ + Container: &v1alpha2.ContainerComponent{ + Container: v1alpha2.Container{ + Image: "quay.io/nginx", + Env: []v1alpha2.EnvVar{}, + }, + }, + }, + }, + }, + Events: &v1alpha2.Events{ + DevWorkspaceEvents: v1alpha2.DevWorkspaceEvents{ + PostStop: []string{"post-stop"}, + }, + }, + Projects: []v1alpha2.Project{ + { + ClonePath: "/projects", + Name: "nodejs-starter-build", + }, + }, + StarterProjects: []v1alpha2.StarterProject{ + { + SubDir: "/projects", + Name: "starter-project-2", + }, + }, + }, + }, + }, + }, + }, + { + name: "remove non-existent env vars", + listToRemove: map[string][]string{ + "runtime": { + "NON_EXISTENT_KEY", + }, + }, + currentDevfile: testDevfileData(), + wantDevFile: &DevfileV2{ + Devfile: v1alpha2.Devfile{ + DevWorkspaceTemplateSpec: v1alpha2.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1alpha2.DevWorkspaceTemplateSpecContent{ + Commands: []v1alpha2.Command{ + { + Id: "devbuild", + CommandUnion: v1alpha2.CommandUnion{ + Exec: &v1alpha2.ExecCommand{ + WorkingDir: "/projects/nodejs-starter", + }, + }, + }, + }, + Components: []v1alpha2.Component{ + { + Name: "runtime", + ComponentUnion: v1alpha2.ComponentUnion{ + Container: &v1alpha2.ContainerComponent{ + Container: v1alpha2.Container{ + Image: "quay.io/nodejs-12", + Env: []v1alpha2.EnvVar{ + { + Name: "DATABASE_PASSWORD", + Value: "苦痛", + }, + }, + }, + Endpoints: []v1alpha2.Endpoint{ + { + Name: "port-3030", + TargetPort: 3030, + }, + }, + }, + }, + }, + { + Name: "loadbalancer", + ComponentUnion: v1alpha2.ComponentUnion{ + Container: &v1alpha2.ContainerComponent{ + Container: v1alpha2.Container{ + Image: "quay.io/nginx", + }, + }, + }, + }, + }, + Events: &v1alpha2.Events{ + DevWorkspaceEvents: v1alpha2.DevWorkspaceEvents{ + PostStop: []string{"post-stop"}, + }, + }, + Projects: []v1alpha2.Project{ + { + ClonePath: "/projects", + Name: "nodejs-starter-build", + }, + }, + StarterProjects: []v1alpha2.StarterProject{ + { + SubDir: "/projects", + Name: "starter-project-2", + }, + }, + }, + }, + }, + }, + wantRemoveErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.currentDevfile.RemoveEnvVars(tt.listToRemove) + + if (err != nil) != tt.wantRemoveErr { + t.Errorf("TestAddAndRemoveEnvVars() unexpected error while removing env vars %+v", err.Error()) + } + + if !reflect.DeepEqual(tt.currentDevfile, tt.wantDevFile) { + t.Errorf("TestAddAndRemoveEnvVars() error: wanted: %v, got: %v, difference at %v", tt.wantDevFile, tt.currentDevfile, pretty.Compare(tt.currentDevfile, tt.wantDevFile)) + } + + }) + } + +} + +func TestSetPorts(t *testing.T) { + + tests := []struct { + name string + portToSet map[string][]string + currentDevfile *DevfileV2 + wantDevFile *DevfileV2 + }{ + { + name: "set ports", + portToSet: map[string][]string{"runtime": {"9000"}, "loadbalancer": {"8000"}}, + currentDevfile: testDevfileData(), + wantDevFile: &DevfileV2{ + Devfile: v1alpha2.Devfile{ + DevWorkspaceTemplateSpec: v1alpha2.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1alpha2.DevWorkspaceTemplateSpecContent{ + Commands: []v1alpha2.Command{ + { + Id: "devbuild", + CommandUnion: v1alpha2.CommandUnion{ + Exec: &v1alpha2.ExecCommand{ + WorkingDir: "/projects/nodejs-starter", + }, + }, + }, + }, + Components: []v1alpha2.Component{ + { + Name: "runtime", + ComponentUnion: v1alpha2.ComponentUnion{ + Container: &v1alpha2.ContainerComponent{ + Container: v1alpha2.Container{ + Image: "quay.io/nodejs-12", + Env: []v1alpha2.EnvVar{ + { + Name: "DATABASE_PASSWORD", + Value: "苦痛", + }, + }, + }, + Endpoints: []v1alpha2.Endpoint{ + { + Name: "port-3030", + TargetPort: 3030, + }, + { + Name: "port-9000-tcp", + TargetPort: 9000, + Protocol: "tcp", + }, + }, + }, + }, + }, + { + Name: "loadbalancer", + ComponentUnion: v1alpha2.ComponentUnion{ + Container: &v1alpha2.ContainerComponent{ + Container: v1alpha2.Container{ + Image: "quay.io/nginx", + }, + Endpoints: []v1alpha2.Endpoint{ + { + Name: "port-8000-tcp", + TargetPort: 8000, + Protocol: "tcp", + }, + }, + }, + }, + }, + }, + Events: &v1alpha2.Events{ + DevWorkspaceEvents: v1alpha2.DevWorkspaceEvents{ + PostStop: []string{"post-stop"}, + }, + }, + Projects: []v1alpha2.Project{ + { + ClonePath: "/projects", + Name: "nodejs-starter-build", + }, + }, + StarterProjects: []v1alpha2.StarterProject{ + { + SubDir: "/projects", + Name: "starter-project-2", + }, + }, + }, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + err := tt.currentDevfile.SetPorts(tt.portToSet) + + if err != nil { + t.Errorf("TestSetAndRemovePorts() unexpected error while adding ports %+v", err.Error()) + } + + if !reflect.DeepEqual(tt.currentDevfile, tt.wantDevFile) { + t.Errorf("TestSetAndRemovePorts() error: wanted: %v, got: %v, difference at %v", tt.wantDevFile, tt.currentDevfile, pretty.Compare(tt.currentDevfile, tt.wantDevFile)) + } + }) + } + +} + +func TestRemovePorts(t *testing.T) { + + tests := []struct { + name string + portToRemove map[string][]string + currentDevfile *DevfileV2 + wantDevFile *DevfileV2 + wantRemoveErr bool + }{ + { + name: "remove ports", + portToRemove: map[string][]string{"runtime": {"3030"}}, + currentDevfile: testDevfileData(), + wantDevFile: &DevfileV2{ + Devfile: v1alpha2.Devfile{ + DevWorkspaceTemplateSpec: v1alpha2.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1alpha2.DevWorkspaceTemplateSpecContent{ + Commands: []v1alpha2.Command{ + { + Id: "devbuild", + CommandUnion: v1alpha2.CommandUnion{ + Exec: &v1alpha2.ExecCommand{ + WorkingDir: "/projects/nodejs-starter", + }, + }, + }, + }, + Components: []v1alpha2.Component{ + { + Name: "runtime", + ComponentUnion: v1alpha2.ComponentUnion{ + Container: &v1alpha2.ContainerComponent{ + Container: v1alpha2.Container{ + Image: "quay.io/nodejs-12", + Env: []v1alpha2.EnvVar{ + { + Name: "DATABASE_PASSWORD", + Value: "苦痛", + }, + }, + }, + Endpoints: []v1alpha2.Endpoint{}, + }, + }, + }, + { + Name: "loadbalancer", + ComponentUnion: v1alpha2.ComponentUnion{ + Container: &v1alpha2.ContainerComponent{ + Container: v1alpha2.Container{ + Image: "quay.io/nginx", + }, + Endpoints: []v1alpha2.Endpoint{}, + }, + }, + }, + }, + Events: &v1alpha2.Events{ + DevWorkspaceEvents: v1alpha2.DevWorkspaceEvents{ + PostStop: []string{"post-stop"}, + }, + }, + Projects: []v1alpha2.Project{ + { + ClonePath: "/projects", + Name: "nodejs-starter-build", + }, + }, + StarterProjects: []v1alpha2.StarterProject{ + { + SubDir: "/projects", + Name: "starter-project-2", + }, + }, + }, + }, + }, + }, + }, + { + name: "remove non-existent ports", + portToRemove: map[string][]string{"runtime": {"3050"}}, + currentDevfile: testDevfileData(), + wantDevFile: &DevfileV2{ + Devfile: v1alpha2.Devfile{ + DevWorkspaceTemplateSpec: v1alpha2.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1alpha2.DevWorkspaceTemplateSpecContent{ + Commands: []v1alpha2.Command{ + { + Id: "devbuild", + CommandUnion: v1alpha2.CommandUnion{ + Exec: &v1alpha2.ExecCommand{ + WorkingDir: "/projects/nodejs-starter", + }, + }, + }, + }, + Components: []v1alpha2.Component{ + { + Name: "runtime", + ComponentUnion: v1alpha2.ComponentUnion{ + Container: &v1alpha2.ContainerComponent{ + Container: v1alpha2.Container{ + Image: "quay.io/nodejs-12", + Env: []v1alpha2.EnvVar{ + { + Name: "DATABASE_PASSWORD", + Value: "苦痛", + }, + }, + }, + Endpoints: []v1alpha2.Endpoint{ + { + Name: "port-3030", + TargetPort: 3030, + }, + }, + }, + }, + }, + { + Name: "loadbalancer", + ComponentUnion: v1alpha2.ComponentUnion{ + Container: &v1alpha2.ContainerComponent{ + Container: v1alpha2.Container{ + Image: "quay.io/nginx", + }, + }, + }, + }, + }, + Events: &v1alpha2.Events{ + DevWorkspaceEvents: v1alpha2.DevWorkspaceEvents{ + PostStop: []string{"post-stop"}, + }, + }, + Projects: []v1alpha2.Project{ + { + ClonePath: "/projects", + Name: "nodejs-starter-build", + }, + }, + StarterProjects: []v1alpha2.StarterProject{ + { + SubDir: "/projects", + Name: "starter-project-2", + }, + }, + }, + }, + }, + }, + wantRemoveErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.currentDevfile.RemovePorts(tt.portToRemove) + + if (err != nil) != tt.wantRemoveErr { + t.Errorf("TestSetAndRemovePorts() unexpected error while removing ports %+v", err.Error()) + } + + if !reflect.DeepEqual(tt.currentDevfile, tt.wantDevFile) { + t.Errorf("TestSetAndRemovePorts() error: wanted: %v, got: %v, difference at %v", tt.wantDevFile, tt.currentDevfile, pretty.Compare(tt.currentDevfile, tt.wantDevFile)) + } + }) + } + +} + +func testDevfileData() *DevfileV2 { + return &DevfileV2{ + Devfile: v1alpha2.Devfile{ + DevWorkspaceTemplateSpec: v1alpha2.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1alpha2.DevWorkspaceTemplateSpecContent{ + Commands: []v1alpha2.Command{ + { + Id: "devbuild", + CommandUnion: v1alpha2.CommandUnion{ + Exec: &v1alpha2.ExecCommand{ + WorkingDir: "/projects/nodejs-starter", + }, + }, + }, + }, + Components: []v1alpha2.Component{ + { + Name: "runtime", + ComponentUnion: v1alpha2.ComponentUnion{ + Container: &v1alpha2.ContainerComponent{ + Container: v1alpha2.Container{ + Image: "quay.io/nodejs-12", + Env: []v1alpha2.EnvVar{ + { + Name: "DATABASE_PASSWORD", + Value: "苦痛", + }, + }, + }, + Endpoints: []v1alpha2.Endpoint{ + { + Name: "port-3030", + TargetPort: 3030, + }, + }, + }, + }, + }, + { + Name: "loadbalancer", + ComponentUnion: v1alpha2.ComponentUnion{ + Container: &v1alpha2.ContainerComponent{ + Container: v1alpha2.Container{ + Image: "quay.io/nginx", + }, + }, + }, + }, + }, + Events: &v1alpha2.Events{ + DevWorkspaceEvents: v1alpha2.DevWorkspaceEvents{ + PostStop: []string{"post-stop"}, + }, + }, + Projects: []v1alpha2.Project{ + { + ClonePath: "/projects", + Name: "nodejs-starter-build", + }, + }, + StarterProjects: []v1alpha2.StarterProject{ + { + SubDir: "/projects", + Name: "starter-project-2", + }, + }, + }, + }, + }, + } +} diff --git a/pkg/devfile/parser/data/v2/events.go b/pkg/devfile/parser/data/v2/events.go new file mode 100644 index 0000000..15c88b0 --- /dev/null +++ b/pkg/devfile/parser/data/v2/events.go @@ -0,0 +1,84 @@ +package v2 + +import ( + "fmt" + v1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + "github.com/devfile/library/pkg/devfile/parser/data/v2/common" + "strings" +) + +// GetEvents returns the Events Object parsed from devfile +func (d *DevfileV2) GetEvents() v1.Events { + if d.Events != nil { + return *d.Events + } + return v1.Events{} +} + +// AddEvents adds the Events Object to the devfile's events +// an event field is considered as invalid if it is already defined +// all event fields will be checked and processed, and returns a total error of all event fields +func (d *DevfileV2) AddEvents(events v1.Events) error { + + if d.Events == nil { + d.Events = &v1.Events{} + } + var errorsList []string + if len(events.PreStop) > 0 { + if len(d.Events.PreStop) > 0 { + errorsList = append(errorsList, (&common.FieldAlreadyExistError{Field: "event field", Name: "pre stop"}).Error()) + } else { + d.Events.PreStop = events.PreStop + } + } + + if len(events.PreStart) > 0 { + if len(d.Events.PreStart) > 0 { + errorsList = append(errorsList, (&common.FieldAlreadyExistError{Field: "event field", Name: "pre start"}).Error()) + } else { + d.Events.PreStart = events.PreStart + } + } + + if len(events.PostStop) > 0 { + if len(d.Events.PostStop) > 0 { + errorsList = append(errorsList, (&common.FieldAlreadyExistError{Field: "event field", Name: "post stop"}).Error()) + } else { + d.Events.PostStop = events.PostStop + } + } + + if len(events.PostStart) > 0 { + if len(d.Events.PostStart) > 0 { + errorsList = append(errorsList, (&common.FieldAlreadyExistError{Field: "event field", Name: "post start"}).Error()) + } else { + d.Events.PostStart = events.PostStart + } + } + if len(errorsList) > 0 { + return fmt.Errorf("errors while adding events:\n%s", strings.Join(errorsList, "\n")) + } + return nil +} + +// UpdateEvents updates the devfile's events +// it only updates the events passed to it +func (d *DevfileV2) UpdateEvents(postStart, postStop, preStart, preStop []string) { + + if d.Events == nil { + d.Events = &v1.Events{} + } + + if postStart != nil { + d.Events.PostStart = postStart + } + if postStop != nil { + d.Events.PostStop = postStop + } + if preStart != nil { + d.Events.PreStart = preStart + } + if preStop != nil { + d.Events.PreStop = preStop + } +} diff --git a/pkg/devfile/parser/data/v2/events_test.go b/pkg/devfile/parser/data/v2/events_test.go new file mode 100644 index 0000000..d60b940 --- /dev/null +++ b/pkg/devfile/parser/data/v2/events_test.go @@ -0,0 +1,167 @@ +package v2 + +import ( + "fmt" + "github.com/kylelemons/godebug/pretty" + "github.com/stretchr/testify/assert" + "reflect" + "testing" + + v1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" +) + +func TestDevfile200_AddEvents(t *testing.T) { + multipleDupError := fmt.Sprintf("%s\n%s", "event field pre start already exists in devfile", "event field post stop already exists in devfile") + + tests := []struct { + name string + currentEvents *v1.Events + newEvents v1.Events + wantEvents v1.Events + wantErr *string + }{ + { + name: "successfully add the events", + currentEvents: &v1.Events{ + DevWorkspaceEvents: v1.DevWorkspaceEvents{ + PreStart: []string{"preStart1"}, + }, + }, + newEvents: v1.Events{ + DevWorkspaceEvents: v1.DevWorkspaceEvents{ + PostStart: []string{"postStart1"}, + }, + }, + wantEvents: v1.Events{ + DevWorkspaceEvents: v1.DevWorkspaceEvents{ + PreStart: []string{"preStart1"}, + PostStart: []string{"postStart1"}, + }, + }, + }, + { + name: "successfully add the events to empty devfile event", + newEvents: v1.Events{ + DevWorkspaceEvents: v1.DevWorkspaceEvents{ + PostStart: []string{"postStart1"}, + }, + }, + wantEvents: v1.Events{ + DevWorkspaceEvents: v1.DevWorkspaceEvents{ + PostStart: []string{"postStart1"}, + }, + }, + }, + { + name: "event already present", + currentEvents: &v1.Events{ + DevWorkspaceEvents: v1.DevWorkspaceEvents{ + PreStart: []string{"preStart1"}, + PostStop: []string{"postStop1"}, + }, + }, + newEvents: v1.Events{ + DevWorkspaceEvents: v1.DevWorkspaceEvents{ + PreStart: []string{"preStart2"}, + PostStop: []string{"postStop2"}, + PreStop: []string{"preStop"}, + }, + }, + wantErr: &multipleDupError, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := &DevfileV2{ + v1.Devfile{ + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Events: tt.currentEvents, + }, + }, + }, + } + + err := d.AddEvents(tt.newEvents) + + if (err != nil) != (tt.wantErr != nil) { + t.Errorf("TestDevfile200_AddEvents() unexpected error: %v, wantErr %v", err, tt.wantErr) + } else if tt.wantErr != nil { + assert.Regexp(t, *tt.wantErr, err.Error(), "TestDevfile200_AddEvents(): Error message should match") + } else { + if !reflect.DeepEqual(*d.Events, tt.wantEvents) { + t.Errorf("TestDevfile200_AddEvents() wanted: %v, got: %v, difference at %v", tt.wantEvents, *d.Events, pretty.Compare(tt.wantEvents, *d.Events)) + } + } + + }) + } +} + +func TestDevfile200_UpdateEvents(t *testing.T) { + + tests := []struct { + name string + currentEvents *v1.Events + newEvents v1.Events + }{ + { + name: "successfully add/update the events", + currentEvents: &v1.Events{ + DevWorkspaceEvents: v1.DevWorkspaceEvents{ + PreStart: []string{"preStart1"}, + }, + }, + newEvents: v1.Events{ + DevWorkspaceEvents: v1.DevWorkspaceEvents{ + PreStart: []string{"preStart2"}, + PostStart: []string{"postStart2"}, + }, + }, + }, + { + name: "successfully update the events to empty", + currentEvents: &v1.Events{ + DevWorkspaceEvents: v1.DevWorkspaceEvents{ + PreStart: []string{"preStart1"}, + }, + }, + newEvents: v1.Events{ + DevWorkspaceEvents: v1.DevWorkspaceEvents{ + PreStart: []string{""}, + }, + }, + }, + { + name: "successfully add the events to empty devfile events", + currentEvents: nil, + newEvents: v1.Events{ + DevWorkspaceEvents: v1.DevWorkspaceEvents{ + PreStart: []string{"preStart2"}, + PostStart: []string{"postStart2"}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := &DevfileV2{ + v1.Devfile{ + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Events: tt.currentEvents, + }, + }, + }, + } + + d.UpdateEvents(tt.newEvents.PostStart, tt.newEvents.PostStop, tt.newEvents.PreStart, tt.newEvents.PreStop) + + events := d.GetEvents() + if !reflect.DeepEqual(events, tt.newEvents) { + t.Errorf("TestDevfile200_UpdateEvents() error: events did not get updated. got - %+v, wanted - %+v", events, tt.newEvents) + } + + }) + } +} diff --git a/pkg/devfile/parser/data/v2/header.go b/pkg/devfile/parser/data/v2/header.go new file mode 100644 index 0000000..b005fd4 --- /dev/null +++ b/pkg/devfile/parser/data/v2/header.go @@ -0,0 +1,25 @@ +package v2 + +import ( + devfilepkg "github.com/devfile/api/v2/pkg/devfile" +) + +//GetSchemaVersion gets devfile schema version +func (d *DevfileV2) GetSchemaVersion() string { + return d.SchemaVersion +} + +//SetSchemaVersion sets devfile schema version +func (d *DevfileV2) SetSchemaVersion(version string) { + d.SchemaVersion = version +} + +// GetMetadata returns the DevfileMetadata Object parsed from devfile +func (d *DevfileV2) GetMetadata() devfilepkg.DevfileMetadata { + return d.Metadata +} + +// SetMetadata sets the metadata for devfile +func (d *DevfileV2) SetMetadata(metadata devfilepkg.DevfileMetadata) { + d.Metadata = metadata +} diff --git a/pkg/devfile/parser/data/v2/header_test.go b/pkg/devfile/parser/data/v2/header_test.go new file mode 100644 index 0000000..19fc6ec --- /dev/null +++ b/pkg/devfile/parser/data/v2/header_test.go @@ -0,0 +1,226 @@ +package v2 + +import ( + "reflect" + "testing" + + v1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + "github.com/devfile/api/v2/pkg/attributes" + devfilepkg "github.com/devfile/api/v2/pkg/devfile" +) + +func TestDevfile200_GetSchemaVersion(t *testing.T) { + + tests := []struct { + name string + expectedSchemaVersion string + devfilev2 *DevfileV2 + }{ + { + name: "Get the schema version", + devfilev2: &DevfileV2{ + v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: "2.0.0", + }, + }, + }, + expectedSchemaVersion: "2.0.0", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + version := tt.devfilev2.GetSchemaVersion() + if version != tt.expectedSchemaVersion { + t.Errorf("TestDevfile200_GetSchemaVersion() error: schema version did not match. Expected %s, got %s", tt.expectedSchemaVersion, version) + } + }) + } +} + +func TestDevfile200_SetSchemaVersion(t *testing.T) { + + tests := []struct { + name string + schemaVersion string + devfilev2 *DevfileV2 + expectedDevfilev2 *DevfileV2 + }{ + { + name: "empty header", + devfilev2: &DevfileV2{ + v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{}, + }, + }, + schemaVersion: "2.0.0", + expectedDevfilev2: &DevfileV2{ + v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: "2.0.0", + }, + }, + }, + }, + { + name: "override existing header", + devfilev2: &DevfileV2{ + v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: "1.0.0", + }, + }, + }, + schemaVersion: "2.0.0", + expectedDevfilev2: &DevfileV2{ + v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: "2.0.0", + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.devfilev2.SetSchemaVersion(tt.schemaVersion) + if !reflect.DeepEqual(tt.devfilev2, tt.expectedDevfilev2) { + t.Errorf("TestDevfile200_SetSchemaVersion() error: expected %v, got %v", tt.expectedDevfilev2, tt.devfilev2) + } + }) + } +} + +func TestDevfile200_GetMetadata(t *testing.T) { + + tests := []struct { + name string + devfilev2 *DevfileV2 + expectedName string + expectedVersion string + expectedAttribute string + expectedDockerfilePath string + }{ + { + name: "Get the metadata", + devfilev2: &DevfileV2{ + v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + Metadata: devfilepkg.DevfileMetadata{ + Name: "nodejs", + Version: "1.2.3", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "alpha.build-dockerfile": "/relative/path/to/Dockerfile", + }), + }, + }, + }, + }, + expectedName: "nodejs", + expectedVersion: "1.2.3", + expectedDockerfilePath: "/relative/path/to/Dockerfile", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + metadata := tt.devfilev2.GetMetadata() + if metadata.Name != tt.expectedName { + t.Errorf("TestDevfile200_GetMetadata() error: name mismatch, expected %v, got %v", tt.expectedName, metadata.Name) + } + if metadata.Version != tt.expectedVersion { + t.Errorf("TestDevfile200_GetMetadata() error: version mismatch, expected %v, got %v", tt.expectedVersion, metadata.Version) + } + if metadata.Attributes.GetString("alpha.build-dockerfile", nil) != tt.expectedDockerfilePath { + t.Errorf("TestDevfile200_GetMetadata() error: dockor file path mismatch, expected %v, got %v", tt.expectedDockerfilePath, metadata.Attributes.GetString("alpha.build-dockerfile", nil)) + } + }) + } +} + +func TestDevfile200_SetSetMetadata(t *testing.T) { + + tests := []struct { + name string + metadata devfilepkg.DevfileMetadata + devfilev2 *DevfileV2 + expectedDevfilev2 *DevfileV2 + }{ + { + name: "empty header", + devfilev2: &DevfileV2{ + v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{}, + }, + }, + metadata: devfilepkg.DevfileMetadata{ + Name: "nodejs", + Version: "2.0.0", + }, + expectedDevfilev2: &DevfileV2{ + v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + Metadata: devfilepkg.DevfileMetadata{ + Name: "nodejs", + Version: "2.0.0", + }, + }, + }, + }, + }, + { + name: "override existing header", + devfilev2: &DevfileV2{ + v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: "2.0.0", + }, + }, + }, + metadata: devfilepkg.DevfileMetadata{ + Name: "nodejs", + Version: "2.1.0", + Attributes: attributes.Attributes{}.FromMap(map[string]interface{}{ + "xyz": "xyz", + }, nil), + DisplayName: "display", + Description: "decription", + Tags: []string{"tag1"}, + Icon: "icon", + GlobalMemoryLimit: "globalmemorylimit", + ProjectType: "projectype", + Language: "language", + Website: "website", + }, + expectedDevfilev2: &DevfileV2{ + v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: "2.0.0", + Metadata: devfilepkg.DevfileMetadata{ + Name: "nodejs", + Version: "2.1.0", + Attributes: attributes.Attributes{}.FromMap(map[string]interface{}{ + "xyz": "xyz", + }, nil), + DisplayName: "display", + Description: "decription", + Tags: []string{"tag1"}, + Icon: "icon", + GlobalMemoryLimit: "globalmemorylimit", + ProjectType: "projectype", + Language: "language", + Website: "website", + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.devfilev2.SetMetadata(tt.metadata) + if !reflect.DeepEqual(tt.devfilev2, tt.expectedDevfilev2) { + t.Errorf("TestDevfile200_SetSchemaVersion() error: expected %v, got %v", tt.expectedDevfilev2, tt.devfilev2) + } + }) + } +} diff --git a/pkg/devfile/parser/data/v2/parent.go b/pkg/devfile/parser/data/v2/parent.go new file mode 100644 index 0000000..75df021 --- /dev/null +++ b/pkg/devfile/parser/data/v2/parent.go @@ -0,0 +1,15 @@ +package v2 + +import ( + v1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" +) + +// GetParent returns the Parent object parsed from devfile +func (d *DevfileV2) GetParent() *v1.Parent { + return d.Parent +} + +// SetParent sets the parent for the devfile +func (d *DevfileV2) SetParent(parent *v1.Parent) { + d.Parent = parent +} diff --git a/pkg/devfile/parser/data/v2/parent_test.go b/pkg/devfile/parser/data/v2/parent_test.go new file mode 100644 index 0000000..171cb9e --- /dev/null +++ b/pkg/devfile/parser/data/v2/parent_test.go @@ -0,0 +1,51 @@ +package v2 + +import ( + "reflect" + "testing" + + v1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" +) + +func TestDevfile200_SetParent(t *testing.T) { + + tests := []struct { + name string + parent *v1.Parent + devfilev2 *DevfileV2 + expectedDevfilev2 *DevfileV2 + }{ + { + name: "set parent", + devfilev2: &DevfileV2{ + v1.Devfile{}, + }, + parent: &v1.Parent{ + ImportReference: v1.ImportReference{ + RegistryUrl: "testRegistryUrl", + }, + ParentOverrides: v1.ParentOverrides{}, + }, + expectedDevfilev2: &DevfileV2{ + v1.Devfile{ + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + Parent: &v1.Parent{ + ImportReference: v1.ImportReference{ + RegistryUrl: "testRegistryUrl", + }, + ParentOverrides: v1.ParentOverrides{}, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.devfilev2.SetParent(tt.parent) + if !reflect.DeepEqual(tt.devfilev2, tt.expectedDevfilev2) { + t.Errorf("TestDevfile200_SetParent() error: expected %v, got %v", tt.expectedDevfilev2, tt.devfilev2) + } + }) + } +} diff --git a/pkg/devfile/parser/data/v2/projects.go b/pkg/devfile/parser/data/v2/projects.go new file mode 100644 index 0000000..714ed5b --- /dev/null +++ b/pkg/devfile/parser/data/v2/projects.go @@ -0,0 +1,179 @@ +package v2 + +import ( + "fmt" + v1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + "github.com/devfile/library/pkg/devfile/parser/data/v2/common" + "reflect" + "strings" +) + +// GetProjects returns the Project Object parsed from devfile +func (d *DevfileV2) GetProjects(options common.DevfileOptions) ([]v1.Project, error) { + + if reflect.DeepEqual(options, common.DevfileOptions{}) { + return d.Projects, nil + } + + var projects []v1.Project + for _, project := range d.Projects { + // Filter Project Attributes + filterIn, err := common.FilterDevfileObject(project.Attributes, options) + if err != nil { + return nil, err + } else if !filterIn { + continue + } + + // Filter Project Source Type - Git, Zip, etc. + projectSourceType, err := common.GetProjectSourceType(project.ProjectSource) + if err != nil { + return nil, err + } + if options.ProjectOptions.ProjectSourceType != "" && projectSourceType != options.ProjectOptions.ProjectSourceType { + continue + } + if options.FilterByName == "" || project.Name == options.FilterByName { + projects = append(projects, project) + } + } + + return projects, nil +} + +// AddProjects adss the slice of Devfile projects to the Devfile's project list +// a project is considered as invalid if it is already defined +// project list passed in will be all processed, and returns a total error of all invalid projects +func (d *DevfileV2) AddProjects(projects []v1.Project) error { + projectsMap := make(map[string]bool) + var errorsList []string + for _, project := range d.Projects { + projectsMap[project.Name] = true + } + + for _, project := range projects { + if _, ok := projectsMap[project.Name]; !ok { + d.Projects = append(d.Projects, project) + } else { + errorsList = append(errorsList, (&common.FieldAlreadyExistError{Name: project.Name, Field: "project"}).Error()) + continue + } + } + if len(errorsList) > 0 { + return fmt.Errorf("errors while adding projects:\n%s", strings.Join(errorsList, "\n")) + } + return nil +} + +// UpdateProject updates the slice of Devfile projects parsed from the Devfile +// return an error if the project is not found +func (d *DevfileV2) UpdateProject(project v1.Project) error { + for i := range d.Projects { + if d.Projects[i].Name == project.Name { + d.Projects[i] = project + return nil + } + } + return fmt.Errorf("update project failed: project %s not found", project.Name) +} + +// DeleteProject removes the specified project +func (d *DevfileV2) DeleteProject(name string) error { + + for i := range d.Projects { + if d.Projects[i].Name == name { + d.Projects = append(d.Projects[:i], d.Projects[i+1:]...) + return nil + } + } + + return &common.FieldNotFoundError{ + Field: "project", + Name: name, + } +} + +//GetStarterProjects returns the DevfileStarterProject parsed from devfile +func (d *DevfileV2) GetStarterProjects(options common.DevfileOptions) ([]v1.StarterProject, error) { + + if reflect.DeepEqual(options, common.DevfileOptions{}) { + return d.StarterProjects, nil + } + + var starterProjects []v1.StarterProject + for _, starterProject := range d.StarterProjects { + // Filter Starter Project Attributes + filterIn, err := common.FilterDevfileObject(starterProject.Attributes, options) + if err != nil { + return nil, err + } else if !filterIn { + continue + } + + // Filter Starter Project Source Type - Git, Zip, etc. + starterProjectSourceType, err := common.GetProjectSourceType(starterProject.ProjectSource) + if err != nil { + return nil, err + } + if options.ProjectOptions.ProjectSourceType != "" && starterProjectSourceType != options.ProjectOptions.ProjectSourceType { + continue + } + + if options.FilterByName == "" || starterProject.Name == options.FilterByName { + starterProjects = append(starterProjects, starterProject) + } + } + + return starterProjects, nil +} + +// AddStarterProjects adds the slice of Devfile starter projects to the Devfile's starter project list +// a starterProject is considered as invalid if it is already defined +// starterProject list passed in will be all processed, and returns a total error of all invalid starterProjects +func (d *DevfileV2) AddStarterProjects(projects []v1.StarterProject) error { + projectsMap := make(map[string]bool) + var errorsList []string + for _, project := range d.StarterProjects { + projectsMap[project.Name] = true + } + + for _, project := range projects { + if _, ok := projectsMap[project.Name]; !ok { + d.StarterProjects = append(d.StarterProjects, project) + } else { + errorsList = append(errorsList, (&common.FieldAlreadyExistError{Name: project.Name, Field: "starterProject"}).Error()) + continue + } + } + if len(errorsList) > 0 { + return fmt.Errorf("errors while adding starterProjects:\n%s", strings.Join(errorsList, "\n")) + } + return nil +} + +// UpdateStarterProject updates the slice of Devfile starter projects parsed from the Devfile +func (d *DevfileV2) UpdateStarterProject(project v1.StarterProject) error { + for i := range d.StarterProjects { + if d.StarterProjects[i].Name == project.Name { + d.StarterProjects[i] = project + return nil + } + } + return fmt.Errorf("update starter project failed: starter project %s not found", project.Name) +} + +// DeleteStarterProject removes the specified starter project +func (d *DevfileV2) DeleteStarterProject(name string) error { + + for i := range d.StarterProjects { + if d.StarterProjects[i].Name == name { + d.StarterProjects = append(d.StarterProjects[:i], d.StarterProjects[i+1:]...) + return nil + } + } + + return &common.FieldNotFoundError{ + Field: "starter project", + Name: name, + } +} diff --git a/pkg/devfile/parser/data/v2/projects_test.go b/pkg/devfile/parser/data/v2/projects_test.go new file mode 100644 index 0000000..683181a --- /dev/null +++ b/pkg/devfile/parser/data/v2/projects_test.go @@ -0,0 +1,904 @@ +package v2 + +import ( + "fmt" + "reflect" + "testing" + + v1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + "github.com/devfile/api/v2/pkg/attributes" + "github.com/devfile/library/pkg/devfile/parser/data/v2/common" + "github.com/kylelemons/godebug/pretty" + "github.com/stretchr/testify/assert" +) + +func TestDevfile200_GetProjects(t *testing.T) { + invalidProjectSrcType := "unknown project source type" + + tests := []struct { + name string + currentProjects []v1.Project + filterOptions common.DevfileOptions + wantProjects []string + wantErr *string + }{ + { + name: "Get all the projects", + currentProjects: []v1.Project{ + { + Name: "project1", + ProjectSource: v1.ProjectSource{ + Git: &v1.GitProjectSource{}, + }, + }, + { + Name: "project2", + ProjectSource: v1.ProjectSource{ + Git: &v1.GitProjectSource{}, + }, + }, + }, + filterOptions: common.DevfileOptions{}, + wantProjects: []string{"project1", "project2"}, + }, + { + name: "Get the filtered projects", + currentProjects: []v1.Project{ + { + Name: "project1", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + "secondString": "secondStringValue", + }), + ClonePath: "/project", + ProjectSource: v1.ProjectSource{ + Git: &v1.GitProjectSource{}, + }, + }, + { + Name: "project2", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + "thirdString": "thirdStringValue", + }), + ClonePath: "/project", + ProjectSource: v1.ProjectSource{ + Zip: &v1.ZipProjectSource{}, + }, + }, + }, + filterOptions: common.DevfileOptions{ + Filter: map[string]interface{}{ + "firstString": "firstStringValue", + }, + ProjectOptions: common.ProjectOptions{ + ProjectSourceType: v1.GitProjectSourceType, + }, + }, + wantProjects: []string{"project1"}, + }, + { + name: "Get project with the specified name", + currentProjects: []v1.Project{ + { + Name: "project1", + ProjectSource: v1.ProjectSource{ + Git: &v1.GitProjectSource{}, + }, + }, + { + Name: "project2", + ProjectSource: v1.ProjectSource{ + Zip: &v1.ZipProjectSource{}, + }, + }, + }, + filterOptions: common.DevfileOptions{ + FilterByName: "project2", + }, + wantProjects: []string{"project2"}, + }, + { + name: "project name not found", + currentProjects: []v1.Project{ + { + Name: "project1", + ProjectSource: v1.ProjectSource{ + Git: &v1.GitProjectSource{}, + }, + }, + { + Name: "project2", + ProjectSource: v1.ProjectSource{ + Zip: &v1.ZipProjectSource{}, + }, + }, + }, + filterOptions: common.DevfileOptions{ + FilterByName: "project3", + }, + wantProjects: []string{}, + }, + { + name: "Wrong filter for projects", + currentProjects: []v1.Project{ + { + Name: "project1", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + "secondString": "secondStringValue", + }), + ClonePath: "/project", + ProjectSource: v1.ProjectSource{ + Git: &v1.GitProjectSource{}, + }, + }, + { + Name: "project2", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + "thirdString": "thirdStringValue", + }), + ClonePath: "/project", + ProjectSource: v1.ProjectSource{ + Zip: &v1.ZipProjectSource{}, + }, + }, + }, + filterOptions: common.DevfileOptions{ + Filter: map[string]interface{}{ + "firstStringIsWrong": "firstStringValue", + }, + }, + }, + { + name: "Invalid project src type", + currentProjects: []v1.Project{ + { + Name: "project1", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + }), + ProjectSource: v1.ProjectSource{}, + }, + }, + filterOptions: common.DevfileOptions{ + Filter: map[string]interface{}{ + "firstString": "firstStringValue", + }, + }, + wantErr: &invalidProjectSrcType, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := &DevfileV2{ + v1.Devfile{ + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Projects: tt.currentProjects, + }, + }, + }, + } + + projects, err := d.GetProjects(tt.filterOptions) + if (err != nil) != (tt.wantErr != nil) { + t.Errorf("TestDevfile200_GetProjects() unexpected error: %v, wantErr %v", err, tt.wantErr) + } else if err == nil { + // confirm the length of actual vs expected + if len(projects) != len(tt.wantProjects) { + t.Errorf("TestDevfile200_GetProjects() error: length of expected projects is not the same as the length of actual projects") + return + } + + // compare the project slices for content + for _, wantProject := range tt.wantProjects { + matched := false + for _, project := range projects { + if wantProject == project.Name { + matched = true + } + } + + if !matched { + t.Errorf("TestDevfile200_GetProjects() error: project %s not found in the devfile", wantProject) + } + } + } else { + assert.Regexp(t, *tt.wantErr, err.Error(), "TestDevfile200_GetProjects(): Error message should match") + } + }) + } +} + +func TestDevfile200_AddProjects(t *testing.T) { + currentProject := []v1.Project{ + { + Name: "java-starter", + ClonePath: "/project", + }, + { + Name: "quarkus-starter", + ClonePath: "/test", + }, + } + + d := &DevfileV2{ + v1.Devfile{ + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Projects: currentProject, + }, + }, + }, + } + + multipleDupError := fmt.Sprintf("%s\n%s", "project java-starter already exists in devfile", "project quarkus-starter already exists in devfile") + + tests := []struct { + name string + args []v1.Project + wantProjects []v1.Project + wantErr *string + }{ + { + name: "It should add project", + args: []v1.Project{ + { + Name: "nodejs", + }, + { + Name: "spring-pet-clinic", + }, + }, + wantProjects: []v1.Project{ + { + Name: "java-starter", + ClonePath: "/project", + }, + { + Name: "quarkus-starter", + ClonePath: "/test", + }, + { + Name: "nodejs", + }, + { + Name: "spring-pet-clinic", + }, + }, + wantErr: nil, + }, + + { + name: "It should give error if tried to add already present starter project", + args: []v1.Project{ + { + Name: "java-starter", + }, + { + Name: "quarkus-starter", + }, + }, + wantProjects: []v1.Project{ + { + Name: "java-starter", + ClonePath: "/project", + }, + { + Name: "quarkus-starter", + ClonePath: "/test", + }, + }, + wantErr: &multipleDupError, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := d.AddProjects(tt.args) + if (err != nil) != (tt.wantErr != nil) { + t.Errorf("TestDevfile200_AddProjects() unexpected error: %v, wantErr %v", err, tt.wantErr) + } else if tt.wantErr != nil { + assert.Regexp(t, *tt.wantErr, err.Error(), "TestDevfile200_AddProjects(): Error message should match") + } else if err == nil { + if !reflect.DeepEqual(d.Projects, tt.wantProjects) { + t.Errorf("TestDevfile200_AddProjects() error: wanted: %v, got: %v, difference at %v", tt.wantProjects, d.Projects, pretty.Compare(tt.wantProjects, d.Projects)) + } + } + }) + } + +} + +func TestDevfile200_UpdateProject(t *testing.T) { + + missingProjectErr := "update project failed: project .* not found" + + tests := []struct { + name string + args v1.Project + devfilev2 *DevfileV2 + expectedDevfilev2 *DevfileV2 + wantErr *string + }{ + { + name: "It should update project for existing project", + args: v1.Project{ + Name: "nodejs", + ClonePath: "/test", + }, + devfilev2: &DevfileV2{ + v1.Devfile{ + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Projects: []v1.Project{ + { + Name: "nodejs", + ClonePath: "/project", + }, + { + Name: "java-starter", + ClonePath: "/project", + }, + }, + }, + }, + }, + }, + expectedDevfilev2: &DevfileV2{ + v1.Devfile{ + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Projects: []v1.Project{ + { + Name: "nodejs", + ClonePath: "/test", + }, + { + Name: "java-starter", + ClonePath: "/project", + }, + }, + }, + }, + }, + }, + }, + { + name: "It should fail to update project for non existing project", + args: v1.Project{ + Name: "quarkus-starter", + ClonePath: "/project", + }, + devfilev2: &DevfileV2{ + v1.Devfile{ + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Projects: []v1.Project{ + { + Name: "nodejs", + ClonePath: "/project", + }, + { + Name: "java-starter", + ClonePath: "/project", + }, + }, + }, + }, + }, + }, + wantErr: &missingProjectErr, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.devfilev2.UpdateProject(tt.args) + // Unexpected error + if (err != nil) != (tt.wantErr != nil) { + t.Errorf("TestDevfile200_UpdateProject() unexpected error: %v, wantErr %v", err, tt.wantErr) + } else if err == nil && !reflect.DeepEqual(tt.devfilev2, tt.expectedDevfilev2) { + t.Errorf("TestDevfile200_UpdateProject() error: wanted: %v, got: %v, difference at %v", tt.expectedDevfilev2, tt.devfilev2, pretty.Compare(tt.expectedDevfilev2, tt.devfilev2)) + } else if err != nil { + assert.Regexp(t, *tt.wantErr, err.Error(), "TestDevfile200_UpdateProject(): Error message should match") + } + }) + } +} + +func TestDevfile200_DeleteProject(t *testing.T) { + missingProjectErr := "project .* is not found in the devfile" + + d := &DevfileV2{ + v1.Devfile{ + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Projects: []v1.Project{ + { + Name: "nodejs", + ClonePath: "/project", + }, + { + Name: "java", + ClonePath: "/project2", + }, + }, + }, + }, + }, + } + + tests := []struct { + name string + projectToDelete string + wantProjects []v1.Project + wantErr *string + }{ + { + name: "Project successfully deleted", + projectToDelete: "nodejs", + wantProjects: []v1.Project{ + { + Name: "java", + ClonePath: "/project2", + }, + }, + }, + { + name: "Project not found", + projectToDelete: "nodejs1", + wantErr: &missingProjectErr, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := d.DeleteProject(tt.projectToDelete) + if (err != nil) != (tt.wantErr != nil) { + t.Errorf("TestDevfile200_DeleteProject() unexpected error: %v, wantErr %v", err, tt.wantErr) + } else if err == nil { + assert.Equal(t, tt.wantProjects, d.Projects, "TestDevfile200_DeleteProject(): The two values should be the same.") + } else { + assert.Regexp(t, *tt.wantErr, err.Error(), "TestDevfile200_DeleteProject(): Error message should match") + } + }) + } + +} + +func TestDevfile200_GetStarterProjects(t *testing.T) { + + invalidStarterProjectSrcTypeErr := "unknown project source type" + + tests := []struct { + name string + currentStarterProjects []v1.StarterProject + filterOptions common.DevfileOptions + wantStarterProjects []string + wantErr *string + }{ + { + name: "Get all the starter projects", + currentStarterProjects: []v1.StarterProject{ + { + Name: "project1", + ProjectSource: v1.ProjectSource{ + Git: &v1.GitProjectSource{}, + }, + }, + { + Name: "project2", + ProjectSource: v1.ProjectSource{ + Git: &v1.GitProjectSource{}, + }, + }, + }, + filterOptions: common.DevfileOptions{}, + wantStarterProjects: []string{"project1", "project2"}, + }, + { + name: "Get the filtered starter projects", + currentStarterProjects: []v1.StarterProject{ + { + Name: "project1", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + "secondString": "secondStringValue", + }), + ProjectSource: v1.ProjectSource{ + Git: &v1.GitProjectSource{}, + }, + }, + { + Name: "project2", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + "thirdString": "thirdStringValue", + }), + ProjectSource: v1.ProjectSource{ + Zip: &v1.ZipProjectSource{}, + }, + }, + { + Name: "project3", + ProjectSource: v1.ProjectSource{ + Git: &v1.GitProjectSource{}, + }, + }, + }, + filterOptions: common.DevfileOptions{ + ProjectOptions: common.ProjectOptions{ + ProjectSourceType: v1.GitProjectSourceType, + }, + }, + wantStarterProjects: []string{"project1", "project3"}, + }, + { + name: "Get starter project with specified name", + currentStarterProjects: []v1.StarterProject{ + { + Name: "project1", + ProjectSource: v1.ProjectSource{ + Git: &v1.GitProjectSource{}, + }, + }, + { + Name: "project2", + ProjectSource: v1.ProjectSource{ + Zip: &v1.ZipProjectSource{}, + }, + }, + }, + filterOptions: common.DevfileOptions{ + FilterByName: "project2", + }, + wantStarterProjects: []string{"project2"}, + }, + { + name: "starter project name not found", + currentStarterProjects: []v1.StarterProject{ + { + Name: "project1", + ProjectSource: v1.ProjectSource{ + Git: &v1.GitProjectSource{}, + }, + }, + { + Name: "project2", + ProjectSource: v1.ProjectSource{ + Zip: &v1.ZipProjectSource{}, + }, + }, + }, + filterOptions: common.DevfileOptions{ + FilterByName: "project3", + }, + wantStarterProjects: []string{}, + }, + { + name: "Wrong filter for starter projects", + currentStarterProjects: []v1.StarterProject{ + { + Name: "project1", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + "secondString": "secondStringValue", + }), + ProjectSource: v1.ProjectSource{ + Git: &v1.GitProjectSource{}, + }, + }, + { + Name: "project2", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + "thirdString": "thirdStringValue", + }), + ProjectSource: v1.ProjectSource{ + Zip: &v1.ZipProjectSource{}, + }, + }, + }, + filterOptions: common.DevfileOptions{ + Filter: map[string]interface{}{ + "firstStringIsWrong": "firstStringValue", + }, + }, + }, + { + name: "Invalid starter project src type", + currentStarterProjects: []v1.StarterProject{ + { + Name: "project1", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + }), + ProjectSource: v1.ProjectSource{}, + }, + }, + filterOptions: common.DevfileOptions{ + Filter: map[string]interface{}{ + "firstString": "firstStringValue", + }, + }, + wantErr: &invalidStarterProjectSrcTypeErr, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := &DevfileV2{ + v1.Devfile{ + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + StarterProjects: tt.currentStarterProjects, + }, + }, + }, + } + + starterProjects, err := d.GetStarterProjects(tt.filterOptions) + if (err != nil) != (tt.wantErr != nil) { + t.Errorf("TestDevfile200_GetStarterProjects() unexpected error: %v, wantErr %v", err, tt.wantErr) + } else if err == nil { + // confirm the length of actual vs expected + if len(starterProjects) != len(tt.wantStarterProjects) { + t.Errorf("TestDevfile200_GetStarterProjects() error: length of expected starter projects is not the same as the length of actual starter projects") + return + } + + // compare the starter project slices for content + for _, wantProject := range tt.wantStarterProjects { + matched := false + + for _, starterProject := range starterProjects { + if wantProject == starterProject.Name { + matched = true + } + } + + if !matched { + t.Errorf("TestDevfile200_GetStarterProjects() error: starter project %s not found in the devfile", wantProject) + } + } + } else { + assert.Regexp(t, *tt.wantErr, err.Error(), "TestDevfile200_GetStarterProjects(): Error message should match") + } + }) + } +} + +func TestDevfile200_AddStarterProjects(t *testing.T) { + currentProject := []v1.StarterProject{ + { + Name: "java-starter", + Description: "starter project for java", + }, + { + Name: "quarkus-starter", + Description: "starter project for quarkus", + }, + } + + d := &DevfileV2{ + v1.Devfile{ + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + StarterProjects: currentProject, + }, + }, + }, + } + multipleDupError := fmt.Sprintf("%s\n%s", "starterProject java-starter already exists in devfile", "starterProject quarkus-starter already exists in devfile") + + tests := []struct { + name string + args []v1.StarterProject + wantErr *string + }{ + { + name: "It should add starter project", + args: []v1.StarterProject{ + { + Name: "nodejs", + Description: "starter project for nodejs", + }, + { + Name: "spring-pet-clinic", + Description: "starter project for springboot", + }, + }, + }, + + { + name: "It should give error if tried to add already present starter project", + args: []v1.StarterProject{ + { + Name: "java-starter", + Description: "starter project for java", + }, + { + Name: "quarkus-starter", + Description: "starter project for quarkus", + }, + }, + wantErr: &multipleDupError, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := d.AddStarterProjects(tt.args) + if (err != nil) != (tt.wantErr != nil) { + t.Errorf("TestDevfile200_AddStarterProjects() unexpected error: %v, wantErr %v", err, tt.wantErr) + } else if tt.wantErr != nil { + assert.Regexp(t, *tt.wantErr, err.Error(), "TestDevfile200_AddStarterProjects(): Error message should match") + } else if err == nil { + wantProjects := append(currentProject, tt.args...) + + if !reflect.DeepEqual(d.StarterProjects, wantProjects) { + t.Errorf("TestDevfile200_AddStarterProjects() error: wanted: %v, got: %v, difference at %v", wantProjects, d.StarterProjects, pretty.Compare(wantProjects, d.StarterProjects)) + } + } + }) + } + +} + +func TestDevfile200_UpdateStarterProject(t *testing.T) { + + missingStarterProjectErr := "update starter project failed: starter project .* not found" + + tests := []struct { + name string + args v1.StarterProject + devfilev2 *DevfileV2 + expectedDevfilev2 *DevfileV2 + wantErr *string + }{ + { + name: "It should update project for existing project", + args: v1.StarterProject{ + Name: "nodejs", + SubDir: "/test", + }, + devfilev2: &DevfileV2{ + v1.Devfile{ + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + StarterProjects: []v1.StarterProject{ + { + Name: "nodejs", + SubDir: "/project", + }, + { + Name: "java-starter", + SubDir: "/project", + }, + }, + }, + }, + }, + }, + expectedDevfilev2: &DevfileV2{ + v1.Devfile{ + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + StarterProjects: []v1.StarterProject{ + { + Name: "nodejs", + SubDir: "/test", + }, + { + Name: "java-starter", + SubDir: "/project", + }, + }, + }, + }, + }, + }, + }, + { + name: "It should fail to update project for non existing project", + args: v1.StarterProject{ + Name: "quarkus-starter", + SubDir: "/project", + }, + devfilev2: &DevfileV2{ + v1.Devfile{ + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + StarterProjects: []v1.StarterProject{ + { + Name: "nodejs", + SubDir: "/project", + }, + { + Name: "java-starter", + SubDir: "/project", + }, + }, + }, + }, + }, + }, + wantErr: &missingStarterProjectErr, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.devfilev2.UpdateStarterProject(tt.args) + // Unexpected error + if (err != nil) != (tt.wantErr != nil) { + t.Errorf("TestDevfile200_UpdateStarterProject() unexpected error: %v, wantErr %v", err, tt.wantErr) + } else if err == nil && !reflect.DeepEqual(tt.devfilev2, tt.expectedDevfilev2) { + t.Errorf("TestDevfile200_UpdateStarterProject() error: wanted: %v, got: %v, difference at %v", tt.expectedDevfilev2, tt.devfilev2, pretty.Compare(tt.expectedDevfilev2, tt.devfilev2)) + } else if err != nil { + assert.Regexp(t, *tt.wantErr, err.Error(), "TestDevfile200_UpdateStarterProject(): Error message should match") + } + }) + } +} + +func TestDevfile200_DeleteStarterProject(t *testing.T) { + + d := &DevfileV2{ + v1.Devfile{ + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + StarterProjects: []v1.StarterProject{ + { + Name: "nodejs", + SubDir: "/project", + }, + { + Name: "java", + SubDir: "/project2", + }, + }, + }, + }, + }, + } + + missingStarterProjectErr := "starter project .* is not found in the devfile" + + tests := []struct { + name string + starterProjectToDelete string + wantStarterProjects []v1.StarterProject + wantErr *string + }{ + { + name: "Starter Project successfully deleted", + starterProjectToDelete: "nodejs", + wantStarterProjects: []v1.StarterProject{ + { + Name: "java", + SubDir: "/project2", + }, + }, + }, + { + name: "Starter Project not found", + starterProjectToDelete: "nodejs1", + wantErr: &missingStarterProjectErr, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := d.DeleteStarterProject(tt.starterProjectToDelete) + if (err != nil) != (tt.wantErr != nil) { + t.Errorf("TestDevfile200_DeleteStarterProject() unexpected error: %v, wantErr %v", err, tt.wantErr) + } else if err == nil { + assert.Equal(t, tt.wantStarterProjects, d.StarterProjects, "TestDevfile200_DeleteStarterProject(): The two values should be the same.") + } else { + assert.Regexp(t, *tt.wantErr, err.Error(), "TestDevfile200_DeleteStarterProject(): Error message should match") + } + }) + } + +} diff --git a/pkg/devfile/parser/data/v2/types.go b/pkg/devfile/parser/data/v2/types.go new file mode 100644 index 0000000..7a8505b --- /dev/null +++ b/pkg/devfile/parser/data/v2/types.go @@ -0,0 +1,10 @@ +package v2 + +import ( + v1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" +) + +// DevfileV2 is the devfile go struct from devfile/api +type DevfileV2 struct { + v1.Devfile +} diff --git a/pkg/devfile/parser/data/v2/volumes.go b/pkg/devfile/parser/data/v2/volumes.go new file mode 100644 index 0000000..37a29fc --- /dev/null +++ b/pkg/devfile/parser/data/v2/volumes.go @@ -0,0 +1,102 @@ +package v2 + +import ( + "fmt" + "strings" + + v1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + "github.com/devfile/library/pkg/devfile/parser/data/v2/common" +) + +// AddVolumeMounts adds the volume mounts to the specified container component +func (d *DevfileV2) AddVolumeMounts(containerName string, volumeMounts []v1.VolumeMount) error { + var pathErrorContainers []string + found := false + for _, component := range d.Components { + if component.Container != nil && component.Name == containerName { + found = true + for _, devfileVolumeMount := range component.Container.VolumeMounts { + for _, volumeMount := range volumeMounts { + if devfileVolumeMount.Path == volumeMount.Path { + pathErrorContainers = append(pathErrorContainers, fmt.Sprintf("unable to mount volume %s, as another volume %s is mounted to the same path %s in the container %s", volumeMount.Name, devfileVolumeMount.Name, volumeMount.Path, component.Name)) + } + } + } + if len(pathErrorContainers) == 0 { + component.Container.VolumeMounts = append(component.Container.VolumeMounts, volumeMounts...) + } + } + } + + if !found { + return &common.FieldNotFoundError{ + Field: "container component", + Name: containerName, + } + } + + if len(pathErrorContainers) > 0 { + return fmt.Errorf("errors while adding volume mounts:\n%s", strings.Join(pathErrorContainers, "\n")) + } + + return nil +} + +// DeleteVolumeMount deletes the volume mount from container components +func (d *DevfileV2) DeleteVolumeMount(name string) error { + found := false + for i := range d.Components { + if d.Components[i].Container != nil && d.Components[i].Name != name { + // Volume Mounts can have multiple instances of a volume mounted at different paths + // As arrays are rearraged/shifted for deletion, we lose one element every time there is a match + // Looping backward is efficient, otherwise we would have to manually decrement counter + // if we looped forward + for j := len(d.Components[i].Container.VolumeMounts) - 1; j >= 0; j-- { + if d.Components[i].Container.VolumeMounts[j].Name == name { + found = true + d.Components[i].Container.VolumeMounts = append(d.Components[i].Container.VolumeMounts[:j], d.Components[i].Container.VolumeMounts[j+1:]...) + } + } + } + } + + if !found { + return &common.FieldNotFoundError{ + Field: "volume mount", + Name: name, + } + } + + return nil +} + +// GetVolumeMountPaths gets all the mount paths of the specified volume mount from the specified container component. +// A container can mount at different paths for a given volume. +func (d *DevfileV2) GetVolumeMountPaths(mountName, containerName string) ([]string, error) { + componentFound := false + var mountPaths []string + + for _, component := range d.Components { + if component.Container != nil && component.Name == containerName { + componentFound = true + for _, volumeMount := range component.Container.VolumeMounts { + if volumeMount.Name == mountName { + mountPaths = append(mountPaths, volumeMount.Path) + } + } + } + } + + if !componentFound { + return mountPaths, &common.FieldNotFoundError{ + Field: "container component", + Name: containerName, + } + } + + if len(mountPaths) == 0 { + return mountPaths, fmt.Errorf("volume %s not mounted to component %s", mountName, containerName) + } + + return mountPaths, nil +} diff --git a/pkg/devfile/parser/data/v2/volumes_test.go b/pkg/devfile/parser/data/v2/volumes_test.go new file mode 100644 index 0000000..9f0e84c --- /dev/null +++ b/pkg/devfile/parser/data/v2/volumes_test.go @@ -0,0 +1,385 @@ +package v2 + +import ( + "testing" + + v1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + "github.com/devfile/library/pkg/testingutil" + "github.com/stretchr/testify/assert" +) + +func TestDevfile200_AddVolumeMount(t *testing.T) { + image0 := "some-image-0" + + container0 := "container0" + container1 := "container1" + + volume0 := "volume0" + volume1 := "volume1" + + samePathPresentErr := "unable to mount volume .*, as another volume .* is mounted to the same path .* in the container .*" + missingContainerErr := "container component .* is not found in the devfile" + + type args struct { + componentName string + volumeMounts []v1.VolumeMount + } + tests := []struct { + name string + currentComponents []v1.Component + wantComponents []v1.Component + args args + wantErr *string + }{ + { + name: "add the volume mount when other mounts are present", + currentComponents: []v1.Component{ + { + Name: container0, + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: image0, + VolumeMounts: []v1.VolumeMount{ + testingutil.GetFakeVolumeMount(volume1, "/data"), + }, + }, + }, + }, + }, + { + Name: container1, + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: image0, + VolumeMounts: []v1.VolumeMount{ + testingutil.GetFakeVolumeMount(volume1, "/data"), + }, + }, + }, + }, + }, + }, + args: args{ + volumeMounts: []v1.VolumeMount{ + testingutil.GetFakeVolumeMount(volume0, "/path0"), + }, + componentName: container0, + }, + wantComponents: []v1.Component{ + { + Name: container0, + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: image0, + VolumeMounts: []v1.VolumeMount{ + testingutil.GetFakeVolumeMount(volume1, "/data"), + testingutil.GetFakeVolumeMount(volume0, "/path0"), + }, + }, + }, + }, + }, + { + Name: container1, + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: image0, + VolumeMounts: []v1.VolumeMount{ + testingutil.GetFakeVolumeMount(volume1, "/data"), + }, + }, + }, + }, + }, + }, + }, + { + name: "error out when same path is present in the container", + currentComponents: []v1.Component{ + { + Name: container0, + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: image0, + VolumeMounts: []v1.VolumeMount{ + testingutil.GetFakeVolumeMount(volume0, "/data0"), + testingutil.GetFakeVolumeMount(volume1, "/data1"), + }, + }, + }, + }, + }, + }, + args: args{ + volumeMounts: []v1.VolumeMount{ + testingutil.GetFakeVolumeMount(volume0, "/data1"), + testingutil.GetFakeVolumeMount(volume1, "/data0"), + }, + componentName: container0, + }, + wantErr: &samePathPresentErr, + }, + { + name: "error out when the specified container is not found", + currentComponents: []v1.Component{ + { + Name: container0, + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: image0, + VolumeMounts: []v1.VolumeMount{ + testingutil.GetFakeVolumeMount(volume1, "/data"), + }, + }, + }, + }, + }, + }, + args: args{ + volumeMounts: []v1.VolumeMount{ + testingutil.GetFakeVolumeMount(volume0, "/data"), + }, + componentName: container1, + }, + wantErr: &missingContainerErr, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := &DevfileV2{ + v1.Devfile{ + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Components: tt.currentComponents, + }, + }, + }, + } + + err := d.AddVolumeMounts(tt.args.componentName, tt.args.volumeMounts) + if (err != nil) != (tt.wantErr != nil) { + t.Errorf("TestDevfile200_AddVolumeMount() unexpected error: %v, wantErr %v", err, tt.wantErr) + } else if err == nil { + assert.Equal(t, tt.wantComponents, d.Components, "TestDevfile200_AddVolumeMount(): The two values should be the same.") + } else { + assert.Regexp(t, *tt.wantErr, err.Error(), "TestDevfile200_AddVolumeMount(): Error message should match") + } + }) + } +} + +func TestDevfile200_DeleteVolumeMounts(t *testing.T) { + + d := &DevfileV2{ + v1.Devfile{ + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Components: []v1.Component{ + { + Name: "comp1", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + VolumeMounts: []v1.VolumeMount{ + testingutil.GetFakeVolumeMount("comp2", "/path"), + testingutil.GetFakeVolumeMount("comp2", "/path2"), + testingutil.GetFakeVolumeMount("comp3", "/path"), + testingutil.GetFakeVolumeMount("comp2", "/path3"), + }, + }, + }, + }, + }, + { + Name: "comp4", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + VolumeMounts: []v1.VolumeMount{ + testingutil.GetFakeVolumeMount("comp2", "/path"), + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + missingMountErr := "volume mount .* is not found in the devfile" + + tests := []struct { + name string + volMountToDelete string + wantComponents []v1.Component + wantErr *string + }{ + { + name: "Volume Component with mounts", + volMountToDelete: "comp2", + wantComponents: []v1.Component{ + { + Name: "comp1", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + VolumeMounts: []v1.VolumeMount{ + testingutil.GetFakeVolumeMount("comp3", "/path"), + }, + }, + }, + }, + }, + { + Name: "comp4", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + VolumeMounts: []v1.VolumeMount{}, + }, + }, + }, + }, + }, + }, + { + name: "Missing mount name", + volMountToDelete: "comp1", + wantErr: &missingMountErr, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := d.DeleteVolumeMount(tt.volMountToDelete) + if (err != nil) != (tt.wantErr != nil) { + t.Errorf("TestDevfile200_DeleteVolumeMounts() unexpected error: %v, wantErr %v", err, tt.wantErr) + } else if err == nil { + assert.Equal(t, tt.wantComponents, d.Components, "TestDevfile200_DeleteVolumeMounts(): The two values should be the same.") + } else { + assert.Regexp(t, *tt.wantErr, err.Error(), "TestDevfile200_DeleteVolumeMounts(): Error message should match") + } + }) + } + +} + +func TestDevfile200_GetVolumeMountPaths(t *testing.T) { + + volumeNotMountedErr := "volume .* not mounted to component .*" + missingContainerErr := "container component .* is not found in the devfile" + + tests := []struct { + name string + currentComponents []v1.Component + mountName string + componentName string + wantPaths []string + wantErr *string + }{ + { + name: "vol is mounted on the specified container component", + currentComponents: []v1.Component{ + { + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + VolumeMounts: []v1.VolumeMount{ + testingutil.GetFakeVolumeMount("volume1", "/path"), + testingutil.GetFakeVolumeMount("volume1", "/path2"), + }, + }, + }, + }, + Name: "component1", + }, + }, + wantPaths: []string{"/path", "/path2"}, + mountName: "volume1", + componentName: "component1", + }, + { + name: "vol is not mounted on the specified container component", + currentComponents: []v1.Component{ + { + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + VolumeMounts: []v1.VolumeMount{ + testingutil.GetFakeVolumeMount("volume1", "/path"), + }, + }, + }, + }, + Name: "component1", + }, + }, + mountName: "volume2", + componentName: "component1", + wantErr: &volumeNotMountedErr, + }, + { + name: "invalid specified container", + currentComponents: []v1.Component{ + { + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + VolumeMounts: []v1.VolumeMount{ + testingutil.GetFakeVolumeMount("volume1", "/path"), + }, + }, + }, + }, + Name: "component1", + }, + }, + mountName: "volume1", + componentName: "component2", + wantErr: &missingContainerErr, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := &DevfileV2{ + v1.Devfile{ + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Components: tt.currentComponents, + }, + }, + }, + } + gotPaths, err := d.GetVolumeMountPaths(tt.mountName, tt.componentName) + if (err != nil) != (tt.wantErr != nil) { + t.Errorf("TestDevfile200_GetVolumeMountPaths() unexpected error: %v, wantErr %v", err, tt.wantErr) + } else if err == nil { + if len(gotPaths) != len(tt.wantPaths) { + t.Errorf("TestDevfile200_GetVolumeMountPaths() error: mount paths length mismatch, expected %v, actual %v", len(tt.wantPaths), len(gotPaths)) + } + + for _, wantPath := range tt.wantPaths { + matched := false + for _, gotPath := range gotPaths { + if wantPath == gotPath { + matched = true + } + } + + if !matched { + t.Errorf("TestDevfile200_GetVolumeMountPaths() error: unable to find the wanted mount path %s in the actual mount paths slice", wantPath) + } + } + } else { + assert.Regexp(t, *tt.wantErr, err.Error(), "TestDevfile200_DeleteVolumeMounts(): Error message should match") + } + }) + } +} diff --git a/pkg/devfile/parser/data/v2/workspace.go b/pkg/devfile/parser/data/v2/workspace.go new file mode 100644 index 0000000..6cf94d5 --- /dev/null +++ b/pkg/devfile/parser/data/v2/workspace.go @@ -0,0 +1,25 @@ +package v2 + +import ( + v1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" +) + +// GetDevfileWorkspaceSpecContent returns the workspace spec content for the devfile +func (d *DevfileV2) GetDevfileWorkspaceSpecContent() *v1.DevWorkspaceTemplateSpecContent { + + return &d.DevWorkspaceTemplateSpecContent +} + +// SetDevfileWorkspaceSpecContent sets the workspace spec content +func (d *DevfileV2) SetDevfileWorkspaceSpecContent(content v1.DevWorkspaceTemplateSpecContent) { + d.DevWorkspaceTemplateSpecContent = content +} + +func (d *DevfileV2) GetDevfileWorkspaceSpec() *v1.DevWorkspaceTemplateSpec { + return &d.DevWorkspaceTemplateSpec +} + +// SetDevfileWorkspaceSpec sets the workspace spec +func (d *DevfileV2) SetDevfileWorkspaceSpec(spec v1.DevWorkspaceTemplateSpec) { + d.DevWorkspaceTemplateSpec = spec +} diff --git a/pkg/devfile/parser/data/v2/workspace_test.go b/pkg/devfile/parser/data/v2/workspace_test.go new file mode 100644 index 0000000..52f88d0 --- /dev/null +++ b/pkg/devfile/parser/data/v2/workspace_test.go @@ -0,0 +1,107 @@ +package v2 + +import ( + "reflect" + "testing" + + v1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" +) + +var devworkspaceContent = v1.DevWorkspaceTemplateSpecContent{ + Components: []v1.Component{ + { + Name: "component1", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{}, + }, + }, + { + Name: "component2", + ComponentUnion: v1.ComponentUnion{ + Volume: &v1.VolumeComponent{}, + }, + }, + }, +} + +func TestDevfile200_SetDevfileWorkspaceSpecContent(t *testing.T) { + + devfilev2 := &DevfileV2{ + v1.Devfile{}, + } + + tests := []struct { + name string + workspaceSpecContent v1.DevWorkspaceTemplateSpecContent + expectedDevfilev2 *DevfileV2 + }{ + { + name: "set workspace", + workspaceSpecContent: devworkspaceContent, + expectedDevfilev2: &DevfileV2{ + v1.Devfile{ + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: devworkspaceContent, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + devfilev2.SetDevfileWorkspaceSpecContent(tt.workspaceSpecContent) + if !reflect.DeepEqual(devfilev2, tt.expectedDevfilev2) { + t.Errorf("TestDevfile200_SetDevfileWorkspaceSpecContent() error: expected %v, got %v", tt.expectedDevfilev2, devfilev2) + } + }) + } +} + +func TestDevfile200_SetDevfileWorkspaceSpec(t *testing.T) { + + devfilev2 := &DevfileV2{ + v1.Devfile{}, + } + + tests := []struct { + name string + workspaceSpec v1.DevWorkspaceTemplateSpec + expectedDevfilev2 *DevfileV2 + }{ + { + name: "set workspace spec", + workspaceSpec: v1.DevWorkspaceTemplateSpec{ + Parent: &v1.Parent{ + ImportReference: v1.ImportReference{ + ImportReferenceUnion: v1.ImportReferenceUnion{ + Uri: "uri", + }, + }, + }, + DevWorkspaceTemplateSpecContent: devworkspaceContent, + }, + expectedDevfilev2: &DevfileV2{ + v1.Devfile{ + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + Parent: &v1.Parent{ + ImportReference: v1.ImportReference{ + ImportReferenceUnion: v1.ImportReferenceUnion{ + Uri: "uri", + }, + }, + }, + DevWorkspaceTemplateSpecContent: devworkspaceContent, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + devfilev2.SetDevfileWorkspaceSpec(tt.workspaceSpec) + if !reflect.DeepEqual(devfilev2, tt.expectedDevfilev2) { + t.Errorf("TestDevfile200_SetDevfileWorkspaceSpec() error: expected %v, got %v", tt.expectedDevfilev2, devfilev2) + } + }) + } +} diff --git a/pkg/devfile/parser/data/versions.go b/pkg/devfile/parser/data/versions.go new file mode 100644 index 0000000..e85a026 --- /dev/null +++ b/pkg/devfile/parser/data/versions.go @@ -0,0 +1,48 @@ +package data + +import ( + "reflect" + + v2 "github.com/devfile/library/pkg/devfile/parser/data/v2" + v200 "github.com/devfile/library/pkg/devfile/parser/data/v2/2.0.0" + v210 "github.com/devfile/library/pkg/devfile/parser/data/v2/2.1.0" + v220 "github.com/devfile/library/pkg/devfile/parser/data/v2/2.2.0" +) + +// SupportedApiVersions stores the supported devfile API versions +type supportedApiVersion string + +// Supported devfile API versions +const ( + APISchemaVersion200 supportedApiVersion = "2.0.0" + APISchemaVersion210 supportedApiVersion = "2.1.0" + APISchemaVersion220 supportedApiVersion = "2.2.0" + APIVersionAlpha2 supportedApiVersion = "v1alpha2" +) + +// ------------- Init functions ------------- // + +// apiVersionToDevfileStruct maps supported devfile API versions to their corresponding devfile structs +var apiVersionToDevfileStruct map[supportedApiVersion]reflect.Type + +// Initializes a map of supported devfile api versions and devfile structs +func init() { + apiVersionToDevfileStruct = make(map[supportedApiVersion]reflect.Type) + apiVersionToDevfileStruct[APISchemaVersion200] = reflect.TypeOf(v2.DevfileV2{}) + apiVersionToDevfileStruct[APISchemaVersion210] = reflect.TypeOf(v2.DevfileV2{}) + apiVersionToDevfileStruct[APISchemaVersion220] = reflect.TypeOf(v2.DevfileV2{}) + apiVersionToDevfileStruct[APIVersionAlpha2] = reflect.TypeOf(v2.DevfileV2{}) +} + +// Map to store mappings between supported devfile API versions and respective devfile JSON schemas +var devfileApiVersionToJSONSchema map[supportedApiVersion]string + +// init initializes a map of supported devfile apiVersions with it's respective devfile JSON schema +func init() { + devfileApiVersionToJSONSchema = make(map[supportedApiVersion]string) + devfileApiVersionToJSONSchema[APISchemaVersion200] = v200.JsonSchema200 + devfileApiVersionToJSONSchema[APISchemaVersion210] = v210.JsonSchema210 + devfileApiVersionToJSONSchema[APISchemaVersion220] = v220.JsonSchema220 + // should use hightest v2 schema version since it is expected to be backward compatible with the same api version + devfileApiVersionToJSONSchema[APIVersionAlpha2] = v220.JsonSchema220 +} diff --git a/pkg/devfile/parser/devfileobj.go b/pkg/devfile/parser/devfileobj.go new file mode 100644 index 0000000..65c669c --- /dev/null +++ b/pkg/devfile/parser/devfileobj.go @@ -0,0 +1,21 @@ +package parser + +import ( + devfileCtx "github.com/devfile/library/pkg/devfile/parser/context" + "github.com/devfile/library/pkg/devfile/parser/data" +) + +// Default filenames for create devfile +const ( + OutputDevfileYamlPath = "devfile.yaml" +) + +// DevfileObj is the runtime devfile object +type DevfileObj struct { + + // Ctx has devfile context info + Ctx devfileCtx.DevfileCtx + + // Data has the devfile data + Data data.DevfileData +} diff --git a/pkg/devfile/parser/parse.go b/pkg/devfile/parser/parse.go new file mode 100644 index 0000000..0fa5012 --- /dev/null +++ b/pkg/devfile/parser/parse.go @@ -0,0 +1,598 @@ +package parser + +import ( + "context" + "encoding/json" + "fmt" + "github.com/devfile/library/pkg/util" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/clientcmd" + "net/url" + "path" + "sigs.k8s.io/controller-runtime/pkg/client" + "strings" + + devfileCtx "github.com/devfile/library/pkg/devfile/parser/context" + "github.com/devfile/library/pkg/devfile/parser/data" + "github.com/devfile/library/pkg/devfile/parser/data/v2/common" + "k8s.io/klog" + + "reflect" + + v1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + apiOverride "github.com/devfile/api/v2/pkg/utils/overriding" + "github.com/devfile/api/v2/pkg/validation" + versionpkg "github.com/hashicorp/go-version" + "github.com/pkg/errors" +) + +// ParseDevfile func validates the devfile integrity. +// Creates devfile context and runtime objects +func parseDevfile(d DevfileObj, resolveCtx *resolutionContextTree, tool resolverTools, flattenedDevfile bool) (DevfileObj, error) { + + // Validate devfile + err := d.Ctx.Validate() + if err != nil { + return d, err + } + + // Create a new devfile data object + d.Data, err = data.NewDevfileData(d.Ctx.GetApiVersion()) + if err != nil { + return d, err + } + + // Unmarshal devfile content into devfile struct + err = json.Unmarshal(d.Ctx.GetDevfileContent(), &d.Data) + if err != nil { + return d, errors.Wrapf(err, "failed to decode devfile content") + } + + if flattenedDevfile { + err = parseParentAndPlugin(d, resolveCtx, tool) + if err != nil { + return DevfileObj{}, err + } + } + + // Successful + return d, nil +} + +// ParserArgs is the struct to pass into parser functions which contains required info for parsing devfile. +// It accepts devfile path, devfile URL or devfile content in []byte format. +type ParserArgs struct { + // Path is a relative or absolute devfile path. + Path string + // URL is the URL address of the specific devfile. + URL string + // Data is the devfile content in []byte format. + Data []byte + // FlattenedDevfile defines if the returned devfileObj is flattened content (true) or raw content (false). + // The value is default to be true. + FlattenedDevfile *bool + // RegistryURLs is a list of registry hosts which parser should pull parent devfile from. + // If registryUrl is defined in devfile, this list will be ignored. + RegistryURLs []string + // DefaultNamespace is the default namespace to use + // If namespace is defined under devfile's parent kubernetes object, this namespace will be ignored. + DefaultNamespace string + // Context is the context used for making Kubernetes requests + Context context.Context + // K8sClient is the Kubernetes client instance used for interacting with a cluster + K8sClient client.Client +} + +// ParseDevfile func populates the devfile data, parses and validates the devfile integrity. +// Creates devfile context and runtime objects +func ParseDevfile(args ParserArgs) (d DevfileObj, err error) { + if args.Data != nil { + d.Ctx, err = devfileCtx.NewByteContentDevfileCtx(args.Data) + if err != nil { + return d, errors.Wrap(err, "failed to set devfile content from bytes") + } + } else if args.Path != "" { + d.Ctx = devfileCtx.NewDevfileCtx(args.Path) + } else if args.URL != "" { + d.Ctx = devfileCtx.NewURLDevfileCtx(args.URL) + } else { + return d, errors.Wrap(err, "the devfile source is not provided") + } + + tool := resolverTools{ + defaultNamespace: args.DefaultNamespace, + registryURLs: args.RegistryURLs, + context: args.Context, + k8sClient: args.K8sClient, + } + + flattenedDevfile := true + if args.FlattenedDevfile != nil { + flattenedDevfile = *args.FlattenedDevfile + } + + d, err = populateAndParseDevfile(d, &resolutionContextTree{}, tool, flattenedDevfile) + if err != nil { + return d, errors.Wrap(err, "failed to populateAndParseDevfile") + } + + //set defaults only if we are flattening parent and parsing succeeded + if flattenedDevfile && err == nil { + err = setDefaults(d) + if err != nil { + return d, errors.Wrap(err, "failed to setDefaults") + } + } + + return d, err +} + +// resolverTools contains required structs and data for resolving remote components of a devfile (plugins and parents) +type resolverTools struct { + // DefaultNamespace is the default namespace to use for resolving Kubernetes ImportReferences that do not include one + defaultNamespace string + // RegistryURLs is a list of registry hosts which parser should pull parent devfile from. + // If registryUrl is defined in devfile, this list will be ignored. + registryURLs []string + // Context is the context used for making Kubernetes or HTTP requests + context context.Context + // K8sClient is the Kubernetes client instance used for interacting with a cluster + k8sClient client.Client +} + +func populateAndParseDevfile(d DevfileObj, resolveCtx *resolutionContextTree, tool resolverTools, flattenedDevfile bool) (DevfileObj, error) { + var err error + if err = resolveCtx.hasCycle(); err != nil { + return DevfileObj{}, err + } + // Fill the fields of DevfileCtx struct + if d.Ctx.GetURL() != "" { + err = d.Ctx.PopulateFromURL() + } else if d.Ctx.GetDevfileContent() != nil { + err = d.Ctx.PopulateFromRaw() + } else { + err = d.Ctx.Populate() + } + if err != nil { + return d, err + } + + return parseDevfile(d, resolveCtx, tool, flattenedDevfile) +} + +// Parse func populates the flattened devfile data, parses and validates the devfile integrity. +// Creates devfile context and runtime objects +// Deprecated, use ParseDevfile() instead +func Parse(path string) (d DevfileObj, err error) { + + // NewDevfileCtx + d.Ctx = devfileCtx.NewDevfileCtx(path) + return populateAndParseDevfile(d, &resolutionContextTree{}, resolverTools{}, true) +} + +// ParseRawDevfile populates the raw devfile data without overriding and merging +// Deprecated, use ParseDevfile() instead +func ParseRawDevfile(path string) (d DevfileObj, err error) { + // NewDevfileCtx + d.Ctx = devfileCtx.NewDevfileCtx(path) + return populateAndParseDevfile(d, &resolutionContextTree{}, resolverTools{}, false) +} + +// ParseFromURL func parses and validates the devfile integrity. +// Creates devfile context and runtime objects +// Deprecated, use ParseDevfile() instead +func ParseFromURL(url string) (d DevfileObj, err error) { + d.Ctx = devfileCtx.NewURLDevfileCtx(url) + return populateAndParseDevfile(d, &resolutionContextTree{}, resolverTools{}, true) +} + +// ParseFromData func parses and validates the devfile integrity. +// Creates devfile context and runtime objects +// Deprecated, use ParseDevfile() instead +func ParseFromData(data []byte) (d DevfileObj, err error) { + d.Ctx, err = devfileCtx.NewByteContentDevfileCtx(data) + if err != nil { + return d, errors.Wrap(err, "failed to set devfile content from bytes") + } + return populateAndParseDevfile(d, &resolutionContextTree{}, resolverTools{}, true) +} + +func parseParentAndPlugin(d DevfileObj, resolveCtx *resolutionContextTree, tool resolverTools) (err error) { + flattenedParent := &v1.DevWorkspaceTemplateSpecContent{} + var mainDevfileVersion, parentDevfileVerson, pluginDevfileVerson *versionpkg.Version + var devfileVersion string + if devfileVersion = d.Ctx.GetApiVersion(); devfileVersion == "" { + devfileVersion = d.Data.GetSchemaVersion() + } + + if devfileVersion != "" { + mainDevfileVersion, err = versionpkg.NewVersion(devfileVersion) + if err != nil { + return fmt.Errorf("fail to parse version of the main devfile") + } + } + parent := d.Data.GetParent() + if parent != nil { + if !reflect.DeepEqual(parent, &v1.Parent{}) { + + var parentDevfileObj DevfileObj + switch { + case parent.Uri != "": + parentDevfileObj, err = parseFromURI(parent.ImportReference, d.Ctx, resolveCtx, tool) + case parent.Id != "": + parentDevfileObj, err = parseFromRegistry(parent.ImportReference, resolveCtx, tool) + case parent.Kubernetes != nil: + parentDevfileObj, err = parseFromKubeCRD(parent.ImportReference, resolveCtx, tool) + default: + return fmt.Errorf("devfile parent does not define any resources") + } + if err != nil { + return err + } + var devfileVersion string + if devfileVersion = parentDevfileObj.Ctx.GetApiVersion(); devfileVersion == "" { + devfileVersion = parentDevfileObj.Data.GetSchemaVersion() + } + + if devfileVersion != "" { + parentDevfileVerson, err = versionpkg.NewVersion(devfileVersion) + if err != nil { + return fmt.Errorf("fail to parse version of parent devfile from: %v", resolveImportReference(parent.ImportReference)) + } + if parentDevfileVerson.GreaterThan(mainDevfileVersion) { + return fmt.Errorf("the parent devfile version from %v is greater than the child devfile version from %v", resolveImportReference(parent.ImportReference), resolveImportReference(resolveCtx.importReference)) + } + } + parentWorkspaceContent := parentDevfileObj.Data.GetDevfileWorkspaceSpecContent() + // add attribute to parent elements + err = addSourceAttributesForOverrideAndMerge(parent.ImportReference, parentWorkspaceContent) + if err != nil { + return err + } + if !reflect.DeepEqual(parent.ParentOverrides, v1.ParentOverrides{}) { + // add attribute to parentOverrides elements + curNodeImportReference := resolveCtx.importReference + err = addSourceAttributesForOverrideAndMerge(curNodeImportReference, &parent.ParentOverrides) + if err != nil { + return err + } + flattenedParent, err = apiOverride.OverrideDevWorkspaceTemplateSpec(parentWorkspaceContent, parent.ParentOverrides) + if err != nil { + return err + } + } else { + flattenedParent = parentWorkspaceContent + } + + klog.V(4).Infof("adding data of devfile with URI: %v", parent.Uri) + } + } + + flattenedPlugins := []*v1.DevWorkspaceTemplateSpecContent{} + components, err := d.Data.GetComponents(common.DevfileOptions{}) + if err != nil { + return err + } + for _, component := range components { + if component.Plugin != nil && !reflect.DeepEqual(component.Plugin, &v1.PluginComponent{}) { + plugin := component.Plugin + var pluginDevfileObj DevfileObj + switch { + case plugin.Uri != "": + pluginDevfileObj, err = parseFromURI(plugin.ImportReference, d.Ctx, resolveCtx, tool) + case plugin.Id != "": + pluginDevfileObj, err = parseFromRegistry(plugin.ImportReference, resolveCtx, tool) + case plugin.Kubernetes != nil: + pluginDevfileObj, err = parseFromKubeCRD(plugin.ImportReference, resolveCtx, tool) + default: + return fmt.Errorf("plugin %s does not define any resources", component.Name) + } + if err != nil { + return err + } + var devfileVersion string + if devfileVersion = pluginDevfileObj.Ctx.GetApiVersion(); devfileVersion == "" { + devfileVersion = pluginDevfileObj.Data.GetSchemaVersion() + } + + if devfileVersion != "" { + pluginDevfileVerson, err = versionpkg.NewVersion(devfileVersion) + if err != nil { + return fmt.Errorf("fail to parse version of plugin devfile from: %v", resolveImportReference(component.Plugin.ImportReference)) + } + if pluginDevfileVerson.GreaterThan(mainDevfileVersion) { + return fmt.Errorf("the plugin devfile version from %v is greater than the child devfile version from %v", resolveImportReference(component.Plugin.ImportReference), resolveImportReference(resolveCtx.importReference)) + } + } + pluginWorkspaceContent := pluginDevfileObj.Data.GetDevfileWorkspaceSpecContent() + // add attribute to plugin elements + err = addSourceAttributesForOverrideAndMerge(plugin.ImportReference, pluginWorkspaceContent) + if err != nil { + return err + } + flattenedPlugin := pluginWorkspaceContent + if !reflect.DeepEqual(plugin.PluginOverrides, v1.PluginOverrides{}) { + // add attribute to pluginOverrides elements + curNodeImportReference := resolveCtx.importReference + err = addSourceAttributesForOverrideAndMerge(curNodeImportReference, &plugin.PluginOverrides) + if err != nil { + return err + } + flattenedPlugin, err = apiOverride.OverrideDevWorkspaceTemplateSpec(pluginWorkspaceContent, plugin.PluginOverrides) + if err != nil { + return err + } + } + flattenedPlugins = append(flattenedPlugins, flattenedPlugin) + } + } + + mergedContent, err := apiOverride.MergeDevWorkspaceTemplateSpec(d.Data.GetDevfileWorkspaceSpecContent(), flattenedParent, flattenedPlugins...) + if err != nil { + return err + } + d.Data.SetDevfileWorkspaceSpecContent(*mergedContent) + // remove parent from flatterned devfile + d.Data.SetParent(nil) + + return nil +} + +func parseFromURI(importReference v1.ImportReference, curDevfileCtx devfileCtx.DevfileCtx, resolveCtx *resolutionContextTree, tool resolverTools) (DevfileObj, error) { + uri := importReference.Uri + // validate URI + err := validation.ValidateURI(uri) + if err != nil { + return DevfileObj{}, err + } + // NewDevfileCtx + var d DevfileObj + absoluteURL := strings.HasPrefix(uri, "http://") || strings.HasPrefix(uri, "https://") + var newUri string + + // relative path on disk + if !absoluteURL && curDevfileCtx.GetAbsPath() != "" { + newUri = path.Join(path.Dir(curDevfileCtx.GetAbsPath()), uri) + d.Ctx = devfileCtx.NewDevfileCtx(newUri) + } else if absoluteURL { + // absolute URL address + newUri = uri + d.Ctx = devfileCtx.NewURLDevfileCtx(newUri) + } else if curDevfileCtx.GetURL() != "" { + // relative path to a URL + u, err := url.Parse(curDevfileCtx.GetURL()) + if err != nil { + return DevfileObj{}, err + } + u.Path = path.Join(path.Dir(u.Path), uri) + newUri = u.String() + d.Ctx = devfileCtx.NewURLDevfileCtx(newUri) + } + importReference.Uri = newUri + newResolveCtx := resolveCtx.appendNode(importReference) + + return populateAndParseDevfile(d, newResolveCtx, tool, true) +} + +func parseFromRegistry(importReference v1.ImportReference, resolveCtx *resolutionContextTree, tool resolverTools) (d DevfileObj, err error) { + id := importReference.Id + registryURL := importReference.RegistryUrl + if registryURL != "" { + devfileContent, err := getDevfileFromRegistry(id, registryURL, importReference.Version) + if err != nil { + return DevfileObj{}, err + } + d.Ctx, err = devfileCtx.NewByteContentDevfileCtx(devfileContent) + if err != nil { + return d, errors.Wrap(err, "failed to set devfile content from bytes") + } + newResolveCtx := resolveCtx.appendNode(importReference) + + return populateAndParseDevfile(d, newResolveCtx, tool, true) + + } else if tool.registryURLs != nil { + for _, registryURL := range tool.registryURLs { + devfileContent, err := getDevfileFromRegistry(id, registryURL, importReference.Version) + if devfileContent != nil && err == nil { + d.Ctx, err = devfileCtx.NewByteContentDevfileCtx(devfileContent) + if err != nil { + return d, errors.Wrap(err, "failed to set devfile content from bytes") + } + importReference.RegistryUrl = registryURL + newResolveCtx := resolveCtx.appendNode(importReference) + + return populateAndParseDevfile(d, newResolveCtx, tool, true) + } + } + } else { + return DevfileObj{}, fmt.Errorf("failed to fetch from registry, registry URL is not provided") + } + + return DevfileObj{}, fmt.Errorf("failed to get id: %s from registry URLs provided", id) +} + +func getDevfileFromRegistry(id, registryURL, version string) ([]byte, error) { + if !strings.HasPrefix(registryURL, "http://") && !strings.HasPrefix(registryURL, "https://") { + return nil, fmt.Errorf("the provided registryURL: %s is not a valid URL", registryURL) + } + param := util.HTTPRequestParams{ + URL: fmt.Sprintf("%s/devfiles/%s/%s", registryURL, id, version), + } + return util.HTTPGetRequest(param, 0) +} + +func parseFromKubeCRD(importReference v1.ImportReference, resolveCtx *resolutionContextTree, tool resolverTools) (d DevfileObj, err error) { + + if tool.k8sClient == nil || tool.context == nil { + return DevfileObj{}, fmt.Errorf("Kubernetes client and context are required to parse from Kubernetes CRD") + } + namespace := importReference.Kubernetes.Namespace + + if namespace == "" { + // if namespace is not set in devfile, use default namespace provided in by consumer + if tool.defaultNamespace != "" { + namespace = tool.defaultNamespace + } else { + // use current namespace if namespace is not set in devfile and not provided by consumer + loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() + configOverrides := &clientcmd.ConfigOverrides{} + config := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides) + namespace, _, err = config.Namespace() + if err != nil { + return DevfileObj{}, fmt.Errorf("kubernetes namespace is not provided, and cannot get current running cluster's namespace: %v", err) + } + } + } + + var dwTemplate v1.DevWorkspaceTemplate + namespacedName := types.NamespacedName{ + Name: importReference.Kubernetes.Name, + Namespace: namespace, + } + err = tool.k8sClient.Get(tool.context, namespacedName, &dwTemplate) + if err != nil { + return DevfileObj{}, err + } + + d, err = convertDevWorskapceTemplateToDevObj(dwTemplate) + if err != nil { + return DevfileObj{}, err + } + + importReference.Kubernetes.Namespace = namespace + newResolveCtx := resolveCtx.appendNode(importReference) + + err = parseParentAndPlugin(d, newResolveCtx, tool) + return d, err + +} + +func convertDevWorskapceTemplateToDevObj(dwTemplate v1.DevWorkspaceTemplate) (d DevfileObj, err error) { + // APIVersion: group/version + // for example: APIVersion: "workspace.devfile.io/v1alpha2" uses api version v1alpha2, and match to v2 schemas + tempList := strings.Split(dwTemplate.APIVersion, "/") + apiversion := tempList[len(tempList)-1] + d.Data, err = data.NewDevfileData(apiversion) + if err != nil { + return DevfileObj{}, err + } + d.Data.SetDevfileWorkspaceSpec(dwTemplate.Spec) + + return d, nil + +} + +//setDefaults sets the default values for nil boolean properties after the merging of devWorkspaceTemplateSpec is complete +func setDefaults(d DevfileObj) (err error) { + + var devfileVersion string + if devfileVersion = d.Ctx.GetApiVersion(); devfileVersion == "" { + devfileVersion = d.Data.GetSchemaVersion() + } + + commands, err := d.Data.GetCommands(common.DevfileOptions{}) + + if err != nil { + return err + } + + //set defaults on the commands + var cmdGroup *v1.CommandGroup + for i := range commands { + command := commands[i] + cmdGroup = nil + + if command.Exec != nil { + exec := command.Exec + val := exec.GetHotReloadCapable() + exec.HotReloadCapable = &val + cmdGroup = exec.Group + + } else if command.Composite != nil { + composite := command.Composite + val := composite.GetParallel() + composite.Parallel = &val + cmdGroup = composite.Group + + } else if command.Apply != nil { + cmdGroup = command.Apply.Group + } + + if cmdGroup != nil { + setIsDefault(cmdGroup) + } + + } + + //set defaults on the components + + components, err := d.Data.GetComponents(common.DevfileOptions{}) + + if err != nil { + return err + } + + var endpoints []v1.Endpoint + for i := range components { + component := components[i] + endpoints = nil + + if component.Container != nil { + container := component.Container + val := container.GetDedicatedPod() + container.DedicatedPod = &val + + msVal := container.GetMountSources() + container.MountSources = &msVal + + endpoints = container.Endpoints + + } else if component.Kubernetes != nil { + endpoints = component.Kubernetes.Endpoints + if devfileVersion != string(data.APISchemaVersion200) && devfileVersion != string(data.APISchemaVersion210) { + val := component.Kubernetes.GetDeployByDefault() + component.Kubernetes.DeployByDefault = &val + } + } else if component.Openshift != nil { + endpoints = component.Openshift.Endpoints + if devfileVersion != string(data.APISchemaVersion200) && devfileVersion != string(data.APISchemaVersion210) { + val := component.Openshift.GetDeployByDefault() + component.Openshift.DeployByDefault = &val + } + + } else if component.Volume != nil && devfileVersion != string(data.APISchemaVersion200) { + volume := component.Volume + val := volume.GetEphemeral() + volume.Ephemeral = &val + + } else if component.Image != nil { //we don't need to do a schema version check since Image in v2.2.0. If used in older specs, a parser error would occur + dockerImage := component.Image.Dockerfile + if dockerImage != nil { + val := dockerImage.GetRootRequired() + dockerImage.RootRequired = &val + } + val := component.Image.GetAutoBuild() + component.Image.AutoBuild = &val + } + + if endpoints != nil { + setEndpoints(endpoints) + } + } + + return nil +} + +///setIsDefault sets the default value of CommandGroup.IsDefault if nil +func setIsDefault(cmdGroup *v1.CommandGroup) { + val := cmdGroup.GetIsDefault() + cmdGroup.IsDefault = &val +} + +//setEndpoints sets the default value of Endpoint.Secure if nil +func setEndpoints(endpoints []v1.Endpoint) { + for i := range endpoints { + val := endpoints[i].GetSecure() + endpoints[i].Secure = &val + } +} diff --git a/pkg/devfile/parser/parse_test.go b/pkg/devfile/parser/parse_test.go new file mode 100644 index 0000000..1c47669 --- /dev/null +++ b/pkg/devfile/parser/parse_test.go @@ -0,0 +1,4754 @@ +package parser + +import ( + "context" + "fmt" + v1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + "github.com/devfile/api/v2/pkg/attributes" + devfilepkg "github.com/devfile/api/v2/pkg/devfile" + devfileCtx "github.com/devfile/library/pkg/devfile/parser/context" + "github.com/devfile/library/pkg/devfile/parser/data" + v2 "github.com/devfile/library/pkg/devfile/parser/data/v2" + "github.com/devfile/library/pkg/devfile/parser/data/v2/common" + "github.com/devfile/library/pkg/testingutil" + "github.com/kylelemons/godebug/pretty" + "github.com/stretchr/testify/assert" + "io/ioutil" + kubev1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "net" + "net/http" + "net/http/httptest" + "os" + "path" + "reflect" + "sigs.k8s.io/yaml" + "strings" + "testing" +) + +const schemaVersion = string(data.APISchemaVersion220) + +var isTrue bool = true +var isFalse bool = false +var apiSchemaVersions = []string{data.APISchemaVersion200.String(), data.APISchemaVersion210.String(), data.APISchemaVersion220.String()} + +var defaultDiv testingutil.DockerImageValues = testingutil.DockerImageValues{ + ImageName: "image:latest", + Uri: "/local/image", + BuildContext: "/src", +} + +func Test_parseParentAndPluginFromURI(t *testing.T) { + const uri1 = "127.0.0.1:8080" + const uri2 = "127.0.0.1:9090" + importFromUri1 := attributes.Attributes{}.PutString(importSourceAttribute, fmt.Sprintf("uri: http://%s", uri1)) + importFromUri2 := attributes.Attributes{}.PutString(importSourceAttribute, fmt.Sprintf("uri: http://%s", uri2)) + parentOverridesFromMainDevfile := attributes.Attributes{}.PutString(importSourceAttribute, + fmt.Sprintf("uri: http://%s", uri1)).PutString(parentOverrideAttribute, "main devfile") + pluginOverridesFromMainDevfile := attributes.Attributes{}.PutString(importSourceAttribute, + fmt.Sprintf("uri: http://%s", uri2)).PutString(pluginOverrideAttribute, "main devfile") + + divRRTrue := defaultDiv + divRRTrue.RootRequired = &isTrue + + divRRFalse := divRRTrue + divRRFalse.RootRequired = &isFalse + + parentDevfile := DevfileObj{ + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Commands: []v1.Command{ + { + Id: "devrun", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{ + WorkingDir: "/projects", + CommandLine: "npm run", + HotReloadCapable: &isTrue, + }, + }, + }, + { + Id: "testrun", + CommandUnion: v1.CommandUnion{ + Apply: &v1.ApplyCommand{ + LabeledCommand: v1.LabeledCommand{ + BaseCommand: v1.BaseCommand{ + Group: &v1.CommandGroup{ + Kind: v1.TestCommandGroupKind, + IsDefault: &isTrue, + }, + }, + }, + }, + }, + }, + { + Id: "allcmds", + CommandUnion: v1.CommandUnion{ + Composite: &v1.CompositeCommand{ + Commands: []string{"testrun", "devrun"}, + Parallel: &isTrue, + }, + }, + }, + }, + Components: []v1.Component{ + { + Name: "nodejs", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Annotation: &v1.Annotation{ + Deployment: map[string]string{ + "deploy-key1": "deploy-value1", + "deploy-key2": "deploy-value2", + }, + Service: map[string]string{ + "svc-key1": "svc-value1", + "svc-key2": "svc-value2", + }, + }, + Image: "quay.io/nodejs-10", + DedicatedPod: &isTrue, + }, + Endpoints: []v1.Endpoint{ + { + Name: "log", + TargetPort: 443, + Secure: &isFalse, + Annotations: map[string]string{ + "ingress-key1": "ingress-value1", + "ingress-key2": "ingress-value2", + }, + }, + }, + }, + }, + }, + { + Name: "volume", + ComponentUnion: v1.ComponentUnion{ + Volume: &v1.VolumeComponent{ + Volume: v1.Volume{ + Size: "2Gi", + Ephemeral: &isFalse, + }, + }, + }, + }, + { + Name: "openshift", + ComponentUnion: v1.ComponentUnion{ + Openshift: &v1.OpenshiftComponent{ + K8sLikeComponent: v1.K8sLikeComponent{ + K8sLikeComponentLocation: v1.K8sLikeComponentLocation{ + Uri: "https://xyz.com/dir/file.yaml", + }, + Endpoints: []v1.Endpoint{ + { + Name: "metrics", + TargetPort: 8080, + }, + }, + }, + }, + }, + }, + testingutil.GetDockerImageTestComponent(divRRTrue, nil, nil), + }, + Events: &v1.Events{ + DevWorkspaceEvents: v1.DevWorkspaceEvents{ + PostStart: []string{"post-start-0"}, + }, + }, + Projects: []v1.Project{ + { + ClonePath: "/data", + ProjectSource: v1.ProjectSource{ + Git: &v1.GitProjectSource{ + GitLikeProjectSource: v1.GitLikeProjectSource{ + Remotes: map[string]string{ + "master": "https://githube.com/somerepo/someproject.git", + }, + }, + }, + }, + Name: "nodejs-starter", + }, + }, + }, + }, + }, + }, + } + + parentCmdAlreadyDefinedErr := "Some Commands are already defined in parent.* If you want to override them, you should do it in the parent scope." + parentCmpAlreadyDefinedErr := "Some Components are already defined in parent.* If you want to override them, you should do it in the parent scope." + parentProjectAlreadyDefinedErr := "Some Projects are already defined in parent.* If you want to override them, you should do it in the parent scope." + pluginCmdAlreadyDefinedErr := "Some Commands are already defined in plugin.* If you want to override them, you should do it in the plugin scope." + pluginCmpAlreadyDefinedErr := "Some Components are already defined in plugin.* If you want to override them, you should do it in the plugin scope." + pluginProjectAlreadyDefinedErr := "Some Projects are already defined in plugin.* If you want to override them, you should do it in the plugin scope." + newCmdErr := "Some Commands do not override any existing element.* They should be defined in the main body, as new elements, not in the overriding section" + newCmpErr := "Some Components do not override any existing element.* They should be defined in the main body, as new elements, not in the overriding section" + newProjectErr := "Some Projects do not override any existing element.* They should be defined in the main body, as new elements, not in the overriding section" + importCycleErr := "devfile has an cycle in references: main devfile -> .*" + parentDevfileVersionErr := "the parent devfile version from .* is greater than the child devfile version from main devfile" + pluginDevfileVersionErr := "the plugin devfile version from .* is greater than the child devfile version from main devfile" + + type args struct { + devFileObj DevfileObj + } + tests := []struct { + name string + args args + parentDevfile DevfileObj + pluginDevfile DevfileObj + pluginOverride v1.PluginOverrides + wantDevFile DevfileObj + wantErr []string + testRecursiveReference bool + }{ + { + name: "it should override the requested parent's data and add the local devfile's data", + args: args{ + devFileObj: DevfileObj{ + Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + Parent: &v1.Parent{ + ParentOverrides: v1.ParentOverrides{ + Commands: []v1.CommandParentOverride{ + { + Id: "devrun", + CommandUnionParentOverride: v1.CommandUnionParentOverride{ + Exec: &v1.ExecCommandParentOverride{ + WorkingDir: "/projects/nodejs-starter", + HotReloadCapable: &isFalse, + }, + }, + }, + { + Id: "testrun", + CommandUnionParentOverride: v1.CommandUnionParentOverride{ + Apply: &v1.ApplyCommandParentOverride{ + LabeledCommandParentOverride: v1.LabeledCommandParentOverride{ + BaseCommandParentOverride: v1.BaseCommandParentOverride{ + Group: &v1.CommandGroupParentOverride{ + Kind: v1.CommandGroupKindParentOverride(v1.BuildCommandGroupKind), + IsDefault: &isFalse, + }, + }, + }, + }, + }, + }, + { + Id: "allcmds", + CommandUnionParentOverride: v1.CommandUnionParentOverride{ + Composite: &v1.CompositeCommandParentOverride{ + Parallel: &isFalse, + }, + }, + }, + }, + Components: []v1.ComponentParentOverride{ + { + Name: "nodejs", + ComponentUnionParentOverride: v1.ComponentUnionParentOverride{ + Container: &v1.ContainerComponentParentOverride{ + ContainerParentOverride: v1.ContainerParentOverride{ + Annotation: &v1.AnnotationParentOverride{ + Deployment: map[string]string{ + "deploy-key2": "deploy-value3", + "deploy-key3": "deploy-value3", + }, + Service: map[string]string{ + "svc-key2": "svc-value3", + "svc-key3": "svc-value3", + }, + }, + Image: "quay.io/nodejs-12", + DedicatedPod: &isFalse, + MountSources: &isTrue, //overrides an unset value to true + }, + Endpoints: []v1.EndpointParentOverride{ + { + Name: "log", + TargetPort: 443, + Secure: &isTrue, + Annotations: map[string]string{ + "ingress-key2": "ingress-value3", + "ingress-key3": "ingress-value3", + }, + }, + }, + }, + }, + }, + { + Name: "volume", + ComponentUnionParentOverride: v1.ComponentUnionParentOverride{ + Volume: &v1.VolumeComponentParentOverride{ + VolumeParentOverride: v1.VolumeParentOverride{ + Size: "2Gi", + Ephemeral: &isTrue, + }, + }, + }, + }, + { + Name: "openshift", + ComponentUnionParentOverride: v1.ComponentUnionParentOverride{ + Openshift: &v1.OpenshiftComponentParentOverride{ + K8sLikeComponentParentOverride: v1.K8sLikeComponentParentOverride{ + Endpoints: []v1.EndpointParentOverride{ + { + Name: "metrics", + TargetPort: 8080, + Secure: &isFalse, + }, + }, + }, + }, + }, + }, + testingutil.GetDockerImageTestComponentParentOverride(divRRFalse), + }, + Projects: []v1.ProjectParentOverride{ + { + ClonePath: "/projects", + Name: "nodejs-starter", + }, + }, + }, + }, + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Commands: []v1.Command{ + { + Id: "devbuild", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{ + WorkingDir: "/projects/nodejs-starter", + }, + }, + }, + }, + Components: []v1.Component{ + { + Name: "runtime", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "quay.io/nodejs-12", + }, + }, + }, + }, + }, + Events: &v1.Events{ + DevWorkspaceEvents: v1.DevWorkspaceEvents{ + PostStop: []string{"post-stop"}, + }, + }, + Projects: []v1.Project{ + { + ClonePath: "/projects", + Name: "nodejs-starter-build", + }, + }, + }, + }, + }, + }, + }, + }, + parentDevfile: parentDevfile, + wantDevFile: DevfileObj{ + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Commands: []v1.Command{ + { + Attributes: parentOverridesFromMainDevfile, + Id: "devrun", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{ + CommandLine: "npm run", + WorkingDir: "/projects/nodejs-starter", + HotReloadCapable: &isFalse, + }, + }, + }, + { + Attributes: parentOverridesFromMainDevfile, + Id: "testrun", + CommandUnion: v1.CommandUnion{ + Apply: &v1.ApplyCommand{ + LabeledCommand: v1.LabeledCommand{ + BaseCommand: v1.BaseCommand{ + Group: &v1.CommandGroup{ + Kind: v1.BuildCommandGroupKind, + IsDefault: &isFalse, + }, + }, + }, + }, + }, + }, + { + Attributes: parentOverridesFromMainDevfile, + Id: "allcmds", + CommandUnion: v1.CommandUnion{ + Composite: &v1.CompositeCommand{ + Commands: []string{"testrun", "devrun"}, + Parallel: &isFalse, + }, + }, + }, + { + Id: "devbuild", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{ + WorkingDir: "/projects/nodejs-starter", + }, + }, + }, + }, + Components: []v1.Component{ + { + Attributes: parentOverridesFromMainDevfile, + Name: "nodejs", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Annotation: &v1.Annotation{ + Deployment: map[string]string{ + "deploy-key1": "deploy-value1", + "deploy-key2": "deploy-value3", + "deploy-key3": "deploy-value3", + }, + Service: map[string]string{ + "svc-key1": "svc-value1", + "svc-key2": "svc-value3", + "svc-key3": "svc-value3", + }, + }, + Image: "quay.io/nodejs-12", + DedicatedPod: &isFalse, + MountSources: &isTrue, + }, + Endpoints: []v1.Endpoint{ + { + Name: "log", + TargetPort: 443, + Secure: &isTrue, + Annotations: map[string]string{ + "ingress-key1": "ingress-value1", + "ingress-key2": "ingress-value3", + "ingress-key3": "ingress-value3", + }, + }, + }, + }, + }, + }, + { + Attributes: parentOverridesFromMainDevfile, + Name: "volume", + ComponentUnion: v1.ComponentUnion{ + Volume: &v1.VolumeComponent{ + Volume: v1.Volume{ + Size: "2Gi", + Ephemeral: &isTrue, + }, + }, + }, + }, + { + Attributes: parentOverridesFromMainDevfile, + Name: "openshift", + ComponentUnion: v1.ComponentUnion{ + Openshift: &v1.OpenshiftComponent{ + K8sLikeComponent: v1.K8sLikeComponent{ + K8sLikeComponentLocation: v1.K8sLikeComponentLocation{ + Uri: "https://xyz.com/dir/file.yaml", + }, + Endpoints: []v1.Endpoint{ + { + Name: "metrics", + TargetPort: 8080, + Secure: &isFalse, + }, + }, + }, + }, + }, + }, + testingutil.GetDockerImageTestComponent(divRRFalse, nil, parentOverridesFromMainDevfile), + { + Name: "runtime", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "quay.io/nodejs-12", + }, + }, + }, + }, + }, + Events: &v1.Events{ + DevWorkspaceEvents: v1.DevWorkspaceEvents{ + PostStart: []string{"post-start-0"}, + PostStop: []string{"post-stop"}, + PreStop: []string{}, + PreStart: []string{}, + }, + }, + Projects: []v1.Project{ + { + Attributes: parentOverridesFromMainDevfile, + ClonePath: "/projects", + ProjectSource: v1.ProjectSource{ + Git: &v1.GitProjectSource{ + GitLikeProjectSource: v1.GitLikeProjectSource{ + Remotes: map[string]string{ + "master": "https://githube.com/somerepo/someproject.git", + }, + }, + }, + }, + Name: "nodejs-starter", + }, + { + ClonePath: "/projects", + Name: "nodejs-starter-build", + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "handle a parent's data without any local override and add the local devfile's data", + args: args{ + devFileObj: DevfileObj{ + Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Commands: []v1.Command{ + { + Id: "devbuild", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{ + WorkingDir: "/projects/nodejs-starter", + }, + }, + }, + }, + Components: []v1.Component{ + { + Name: "runtime", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "quay.io/nodejs-12", + }, + }, + }, + }, + { + Name: "Kubernetes", + ComponentUnion: v1.ComponentUnion{ + Kubernetes: &v1.KubernetesComponent{ + K8sLikeComponent: v1.K8sLikeComponent{ + K8sLikeComponentLocation: v1.K8sLikeComponentLocation{ + Uri: "/devfiles", + }, + Endpoints: []v1.Endpoint{ + { + Name: "messages", + TargetPort: 8080, + Secure: &isTrue, + }, + }, + }, + }, + }, + }, + }, + Events: &v1.Events{ + DevWorkspaceEvents: v1.DevWorkspaceEvents{ + PostStop: []string{"post-stop"}, + }, + }, + Projects: []v1.Project{ + { + ClonePath: "/projects", + Name: "nodejs-starter-build", + }, + }, + }, + }, + }, + }, + }, + }, + parentDevfile: parentDevfile, + wantDevFile: DevfileObj{ + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Commands: []v1.Command{ + { + Attributes: importFromUri1, + Id: "devrun", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{ + CommandLine: "npm run", + WorkingDir: "/projects", + HotReloadCapable: &isTrue, + }, + }, + }, + { + Attributes: importFromUri1, + Id: "testrun", + CommandUnion: v1.CommandUnion{ + Apply: &v1.ApplyCommand{ + LabeledCommand: v1.LabeledCommand{ + BaseCommand: v1.BaseCommand{ + Group: &v1.CommandGroup{ + Kind: v1.TestCommandGroupKind, + IsDefault: &isTrue, + }, + }, + }, + }, + }, + }, + { + Attributes: importFromUri1, + Id: "allcmds", + CommandUnion: v1.CommandUnion{ + Composite: &v1.CompositeCommand{ + Commands: []string{"testrun", "devrun"}, + Parallel: &isTrue, + }, + }, + }, + { + Id: "devbuild", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{ + WorkingDir: "/projects/nodejs-starter", + }, + }, + }, + }, + Components: []v1.Component{ + { + Attributes: importFromUri1, + Name: "nodejs", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Annotation: &v1.Annotation{ + Deployment: map[string]string{ + "deploy-key1": "deploy-value1", + "deploy-key2": "deploy-value2", + }, + Service: map[string]string{ + "svc-key1": "svc-value1", + "svc-key2": "svc-value2", + }, + }, + Image: "quay.io/nodejs-10", + DedicatedPod: &isTrue, + }, + Endpoints: []v1.Endpoint{ + { + Name: "log", + TargetPort: 443, + Secure: &isFalse, + Annotations: map[string]string{ + "ingress-key1": "ingress-value1", + "ingress-key2": "ingress-value2", + }, + }, + }, + }, + }, + }, + { + Attributes: importFromUri1, + Name: "volume", + ComponentUnion: v1.ComponentUnion{ + Volume: &v1.VolumeComponent{ + Volume: v1.Volume{ + Size: "2Gi", + Ephemeral: &isFalse, + }, + }, + }, + }, + { + Attributes: importFromUri1, + Name: "openshift", + ComponentUnion: v1.ComponentUnion{ + Openshift: &v1.OpenshiftComponent{ + K8sLikeComponent: v1.K8sLikeComponent{ + K8sLikeComponentLocation: v1.K8sLikeComponentLocation{ + Uri: "https://xyz.com/dir/file.yaml", + }, + Endpoints: []v1.Endpoint{ + { + Name: "metrics", + TargetPort: 8080, + }, + }, + }, + }, + }, + }, + //no overrides so expected values are the same as the parent + testingutil.GetDockerImageTestComponent(divRRTrue, nil, importFromUri1), + { + Name: "runtime", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "quay.io/nodejs-12", + }, + }, + }, + }, + { + Name: "Kubernetes", + ComponentUnion: v1.ComponentUnion{ + Kubernetes: &v1.KubernetesComponent{ + K8sLikeComponent: v1.K8sLikeComponent{ + K8sLikeComponentLocation: v1.K8sLikeComponentLocation{ + Uri: "/devfiles", + }, + Endpoints: []v1.Endpoint{ + { + Name: "messages", + TargetPort: 8080, + Secure: &isTrue, + }, + }, + }, + }, + }, + }, + }, + Events: &v1.Events{ + DevWorkspaceEvents: v1.DevWorkspaceEvents{ + PostStart: []string{"post-start-0"}, + PostStop: []string{"post-stop"}, + PreStop: []string{}, + PreStart: []string{}, + }, + }, + Projects: []v1.Project{ + { + Attributes: importFromUri1, + ClonePath: "/data", + ProjectSource: v1.ProjectSource{ + Git: &v1.GitProjectSource{ + GitLikeProjectSource: v1.GitLikeProjectSource{ + Remotes: map[string]string{ + "master": "https://githube.com/somerepo/someproject.git", + }, + }, + }, + }, + Name: "nodejs-starter", + }, + { + ClonePath: "/projects", + Name: "nodejs-starter-build", + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "it should error out when the override is invalid", + args: args{ + devFileObj: DevfileObj{ + Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + Parent: &v1.Parent{ + ParentOverrides: v1.ParentOverrides{ + Commands: []v1.CommandParentOverride{ + { + Id: "devrun", + CommandUnionParentOverride: v1.CommandUnionParentOverride{ + Exec: &v1.ExecCommandParentOverride{ + WorkingDir: "/projects/nodejs-starter", + }, + }, + }, + }, + Components: []v1.ComponentParentOverride{ + { + Name: "nodejs", + ComponentUnionParentOverride: v1.ComponentUnionParentOverride{ + Container: &v1.ContainerComponentParentOverride{ + ContainerParentOverride: v1.ContainerParentOverride{ + Image: "quay.io/nodejs-12", + }, + }, + }, + }, + }, + Projects: []v1.ProjectParentOverride{ + { + ClonePath: "/projects", + Name: "nodejs-starter", + }, + }, + }, + }, + }, + }, + }, + }, + }, + parentDevfile: DevfileObj{ + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Commands: []v1.Command{}, + Components: []v1.Component{}, + Projects: []v1.Project{}, + }, + }, + }, + }, + }, + wantDevFile: DevfileObj{ + Data: &v2.DevfileV2{}, + }, + wantErr: []string{newCmpErr, newCmdErr, newProjectErr}, + }, + { + name: "error out if the same parent command is defined again in the local devfile", + args: args{ + devFileObj: DevfileObj{ + Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Commands: []v1.Command{ + { + Id: "devbuild", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{ + WorkingDir: "/projects/nodejs-starter", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + parentDevfile: DevfileObj{ + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Commands: []v1.Command{ + { + Id: "devbuild", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{ + WorkingDir: "/projects/nodejs-starter", + }, + }, + }, + }, + }, + }, + }, + }, + }, + wantDevFile: DevfileObj{ + Data: &v2.DevfileV2{}, + }, + wantErr: []string{parentCmdAlreadyDefinedErr}, + }, + { + name: "error out if the same parent component is defined again in the local devfile", + args: args{ + devFileObj: DevfileObj{ + Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Components: []v1.Component{ + { + Name: "runtime", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "quay.io/nodejs-12", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + parentDevfile: DevfileObj{ + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Components: []v1.Component{ + { + Name: "runtime", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "quay.io/nodejs-12", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + wantDevFile: DevfileObj{ + Data: &v2.DevfileV2{}, + }, + wantErr: []string{parentCmpAlreadyDefinedErr}, + }, + { + name: "should not have error if the same event is defined again in the local devfile", + args: args{ + devFileObj: DevfileObj{ + Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Events: &v1.Events{ + DevWorkspaceEvents: v1.DevWorkspaceEvents{ + PostStop: []string{"post-stop"}, + }, + }, + }, + }, + }, + }, + }, + }, + parentDevfile: DevfileObj{ + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Events: &v1.Events{ + DevWorkspaceEvents: v1.DevWorkspaceEvents{ + PostStop: []string{"post-stop"}, + }, + }, + }, + }, + }, + }, + }, + wantDevFile: DevfileObj{ + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Events: &v1.Events{ + DevWorkspaceEvents: v1.DevWorkspaceEvents{ + PostStop: []string{"post-stop"}, + PreStart: []string{}, + PreStop: []string{}, + PostStart: []string{}, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "error out if the parent project is defined again in the local devfile", + args: args{ + devFileObj: DevfileObj{ + Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Projects: []v1.Project{ + { + ClonePath: "/projects", + Name: "nodejs-starter-build", + ProjectSource: v1.ProjectSource{ + Git: &v1.GitProjectSource{ + GitLikeProjectSource: v1.GitLikeProjectSource{ + Remotes: map[string]string{ + "origin": "url", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + parentDevfile: DevfileObj{ + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Projects: []v1.Project{ + { + ClonePath: "/projects", + Name: "nodejs-starter-build", + ProjectSource: v1.ProjectSource{ + Git: &v1.GitProjectSource{ + GitLikeProjectSource: v1.GitLikeProjectSource{ + Remotes: map[string]string{ + "origin": "url", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + wantDevFile: DevfileObj{ + Data: &v2.DevfileV2{}, + }, + wantErr: []string{parentProjectAlreadyDefinedErr}, + }, + { + name: "error out if the parent devfile version is greater than main devfile version", + args: args{ + devFileObj: DevfileObj{ + Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: "2.0.0", + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{}, + }, + }, + }, + }, + }, + parentDevfile: DevfileObj{ + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{}, + }, + }, + }, + }, + wantDevFile: DevfileObj{ + Data: &v2.DevfileV2{}, + }, + wantErr: []string{parentDevfileVersionErr}, + }, + { + name: "it should merge the plugin's uri data and add the local devfile's data", + args: args{ + devFileObj: DevfileObj{ + Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Commands: []v1.Command{ + { + Id: "devbuild", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{ + WorkingDir: "/projects/nodejs-starter", + }, + }, + }, + }, + Components: []v1.Component{ + { + Name: "runtime", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "quay.io/nodejs-12", + }, + }, + }, + }, + }, + Events: &v1.Events{ + DevWorkspaceEvents: v1.DevWorkspaceEvents{ + PostStop: []string{"post-stop"}, + }, + }, + Projects: []v1.Project{ + { + ClonePath: "/projects", + Name: "nodejs-starter-build", + }, + }, + }, + }, + }, + }, + }, + }, + pluginDevfile: DevfileObj{ + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Commands: []v1.Command{ + { + Id: "devrun", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{ + WorkingDir: "/projects", + CommandLine: "npm run", + }, + }, + }, + }, + Components: []v1.Component{ + { + Name: "nodejs", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Annotation: &v1.Annotation{ + Deployment: map[string]string{ + "deploy-key1": "deploy-value1", + "deploy-key2": "deploy-value2", + }, + Service: map[string]string{ + "svc-key1": "svc-value1", + "svc-key2": "svc-value2", + }, + }, + Image: "quay.io/nodejs-10", + }, + }, + }, + }, + }, + Events: &v1.Events{ + DevWorkspaceEvents: v1.DevWorkspaceEvents{ + PostStart: []string{"post-start-0"}, + }, + }, + Projects: []v1.Project{ + { + ClonePath: "/data", + ProjectSource: v1.ProjectSource{ + Git: &v1.GitProjectSource{ + GitLikeProjectSource: v1.GitLikeProjectSource{ + Remotes: map[string]string{ + "master": "https://githube.com/somerepo/someproject.git", + }, + }, + }, + }, + Name: "nodejs-starter", + }, + }, + }, + }, + }, + }, + }, + wantDevFile: DevfileObj{ + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Commands: []v1.Command{ + { + Attributes: importFromUri2, + Id: "devrun", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{ + CommandLine: "npm run", + WorkingDir: "/projects", + }, + }, + }, + { + Id: "devbuild", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{ + WorkingDir: "/projects/nodejs-starter", + }, + }, + }, + }, + Components: []v1.Component{ + { + Attributes: importFromUri2, + Name: "nodejs", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Annotation: &v1.Annotation{ + Deployment: map[string]string{ + "deploy-key1": "deploy-value1", + "deploy-key2": "deploy-value2", + }, + Service: map[string]string{ + "svc-key1": "svc-value1", + "svc-key2": "svc-value2", + }, + }, + Image: "quay.io/nodejs-10", + }, + }, + }, + }, + { + Name: "runtime", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "quay.io/nodejs-12", + }, + }, + }, + }, + }, + Events: &v1.Events{ + DevWorkspaceEvents: v1.DevWorkspaceEvents{ + PostStart: []string{"post-start-0"}, + PostStop: []string{"post-stop"}, + PreStop: []string{}, + PreStart: []string{}, + }, + }, + Projects: []v1.Project{ + { + Attributes: importFromUri2, + ClonePath: "/data", + ProjectSource: v1.ProjectSource{ + Git: &v1.GitProjectSource{ + GitLikeProjectSource: v1.GitLikeProjectSource{ + Remotes: map[string]string{ + "master": "https://githube.com/somerepo/someproject.git", + }, + }, + }, + }, + Name: "nodejs-starter", + }, + { + ClonePath: "/projects", + Name: "nodejs-starter-build", + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "it should override the plugin's data with local overrides and add the local devfile's data", + args: args{ + devFileObj: DevfileObj{ + Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Commands: []v1.Command{ + { + Id: "devbuild", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{ + WorkingDir: "/projects/nodejs-starter", + }, + }, + }, + }, + Components: []v1.Component{ + { + Name: "runtime", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "quay.io/nodejs-12", + }, + }, + }, + }, + }, + Events: &v1.Events{ + DevWorkspaceEvents: v1.DevWorkspaceEvents{ + PostStop: []string{"post-stop-1"}, + }, + }, + Projects: []v1.Project{ + { + ClonePath: "/projects", + Name: "nodejs-starter-build", + }, + }, + }, + }, + }, + }, + }, + }, + pluginDevfile: DevfileObj{ + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Commands: []v1.Command{ + { + Id: "devrun", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{ + WorkingDir: "/projects", + CommandLine: "npm run", + }, + }, + }, + }, + Components: []v1.Component{ + { + Name: "nodejs", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Annotation: &v1.Annotation{ + Deployment: map[string]string{ + "deploy-key1": "deploy-value1", + "deploy-key2": "deploy-value2", + }, + Service: map[string]string{ + "svc-key1": "svc-value1", + "svc-key2": "svc-value2", + }, + }, + Image: "quay.io/nodejs-10", + }, + Endpoints: []v1.Endpoint{ + { + Annotations: map[string]string{ + "ingress-key1": "ingress-value1", + "ingress-key2": "ingress-value2", + }, + Name: "url", + TargetPort: 8080, + }, + }, + }, + }, + }, + testingutil.GetDockerImageTestComponent(divRRFalse, nil, nil), + }, + Events: &v1.Events{ + DevWorkspaceEvents: v1.DevWorkspaceEvents{ + PostStart: []string{"post-start-0"}, + PostStop: []string{"post-stop-2"}, + }, + }, + Projects: []v1.Project{ + { + ClonePath: "/data", + ProjectSource: v1.ProjectSource{ + Git: &v1.GitProjectSource{ + GitLikeProjectSource: v1.GitLikeProjectSource{ + Remotes: map[string]string{ + "master": "https://githube.com/somerepo/someproject.git", + }, + }, + }, + }, + Name: "nodejs-starter", + }, + }, + }, + }, + }, + }, + }, + pluginOverride: v1.PluginOverrides{ + OverridesBase: v1.OverridesBase{}, + Components: []v1.ComponentPluginOverride{ + { + Name: "nodejs", + ComponentUnionPluginOverride: v1.ComponentUnionPluginOverride{ + Container: &v1.ContainerComponentPluginOverride{ + ContainerPluginOverride: v1.ContainerPluginOverride{ + Annotation: &v1.AnnotationPluginOverride{ + Deployment: map[string]string{ + "deploy-key2": "deploy-value3", + "deploy-key3": "deploy-value3", + }, + Service: map[string]string{ + "svc-key2": "svc-value3", + "svc-key3": "svc-value3", + }, + }, + Image: "quay.io/nodejs-12", + }, + Endpoints: []v1.EndpointPluginOverride{ + { + Annotations: map[string]string{ + "ingress-key2": "ingress-value3", + "ingress-key3": "ingress-value3", + }, + Name: "url", + TargetPort: 9090, + }, + }, + }, + }, + }, + testingutil.GetDockerImageTestComponentPluginOverride(divRRTrue), + }, + Commands: []v1.CommandPluginOverride{ + { + Id: "devrun", + CommandUnionPluginOverride: v1.CommandUnionPluginOverride{ + Exec: &v1.ExecCommandPluginOverride{ + WorkingDir: "/projects-new", + CommandLine: "npm build", + }, + }, + }, + }, + }, + wantDevFile: DevfileObj{ + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Commands: []v1.Command{ + { + Attributes: pluginOverridesFromMainDevfile, + Id: "devrun", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{ + CommandLine: "npm build", + WorkingDir: "/projects-new", + }, + }, + }, + { + Id: "devbuild", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{ + WorkingDir: "/projects/nodejs-starter", + }, + }, + }, + }, + Components: []v1.Component{ + { + Attributes: pluginOverridesFromMainDevfile, + Name: "nodejs", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Annotation: &v1.Annotation{ + Deployment: map[string]string{ + "deploy-key1": "deploy-value1", + "deploy-key2": "deploy-value3", + "deploy-key3": "deploy-value3", + }, + Service: map[string]string{ + "svc-key1": "svc-value1", + "svc-key2": "svc-value3", + "svc-key3": "svc-value3", + }, + }, + Image: "quay.io/nodejs-12", + }, + Endpoints: []v1.Endpoint{ + { + Annotations: map[string]string{ + "ingress-key1": "ingress-value1", + "ingress-key2": "ingress-value3", + "ingress-key3": "ingress-value3", + }, + Name: "url", + TargetPort: 9090, + }, + }, + }, + }, + }, + testingutil.GetDockerImageTestComponent(divRRTrue, nil, pluginOverridesFromMainDevfile), + { + Name: "runtime", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "quay.io/nodejs-12", + }, + }, + }, + }, + }, + Events: &v1.Events{ + DevWorkspaceEvents: v1.DevWorkspaceEvents{ + PostStart: []string{"post-start-0"}, + PostStop: []string{"post-stop-1", "post-stop-2"}, + PreStop: []string{}, + PreStart: []string{}, + }, + }, + Projects: []v1.Project{ + { + Attributes: importFromUri2, + ClonePath: "/data", + ProjectSource: v1.ProjectSource{ + Git: &v1.GitProjectSource{ + GitLikeProjectSource: v1.GitLikeProjectSource{ + Remotes: map[string]string{ + "master": "https://githube.com/somerepo/someproject.git", + }, + }, + }, + }, + Name: "nodejs-starter", + }, + { + ClonePath: "/projects", + Name: "nodejs-starter-build", + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "it should error out when the plugin devfile is invalid", + args: args{ + devFileObj: DevfileObj{ + Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{}, + }, + }, + }, + }, + pluginDevfile: DevfileObj{ + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Commands: []v1.Command{}, + Components: []v1.Component{}, + Projects: []v1.Project{}, + }, + }, + }, + }, + }, + pluginOverride: v1.PluginOverrides{ + Commands: []v1.CommandPluginOverride{ + { + Id: "devrun", + CommandUnionPluginOverride: v1.CommandUnionPluginOverride{ + Exec: &v1.ExecCommandPluginOverride{ + WorkingDir: "/projects/nodejs-starter", + }, + }, + }, + }, + }, + wantDevFile: DevfileObj{ + Data: &v2.DevfileV2{}, + }, + wantErr: []string{newCmdErr}, + }, + { + name: "error out if the same plugin command is defined again in the local devfile", + args: args{ + devFileObj: DevfileObj{ + Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Commands: []v1.Command{ + { + Id: "devbuild", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{ + WorkingDir: "/projects/nodejs-starter", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + pluginDevfile: DevfileObj{ + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Commands: []v1.Command{ + { + Id: "devbuild", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{ + WorkingDir: "/projects/nodejs-starter", + }, + }, + }, + }, + }, + }, + }, + }, + }, + wantDevFile: DevfileObj{ + Data: &v2.DevfileV2{}, + }, + wantErr: []string{pluginCmdAlreadyDefinedErr}, + }, + { + name: "error out if the same plugin component is defined again in the local devfile", + args: args{ + devFileObj: DevfileObj{ + Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Components: []v1.Component{ + { + Name: "runtime", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "quay.io/nodejs-12", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + pluginDevfile: DevfileObj{ + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Components: []v1.Component{ + { + Name: "runtime", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "quay.io/nodejs-12", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + wantDevFile: DevfileObj{ + Data: &v2.DevfileV2{}, + }, + wantErr: []string{pluginCmpAlreadyDefinedErr}, + }, + { + name: "error out if the plugin project is defined again in the local devfile", + args: args{ + devFileObj: DevfileObj{ + Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Projects: []v1.Project{ + { + ClonePath: "/projects", + Name: "nodejs-starter-build", + ProjectSource: v1.ProjectSource{ + Git: &v1.GitProjectSource{ + GitLikeProjectSource: v1.GitLikeProjectSource{ + Remotes: map[string]string{ + "origin": "url", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + pluginDevfile: DevfileObj{ + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Projects: []v1.Project{ + { + ClonePath: "/projects", + Name: "nodejs-starter-build", + ProjectSource: v1.ProjectSource{ + Git: &v1.GitProjectSource{ + GitLikeProjectSource: v1.GitLikeProjectSource{ + Remotes: map[string]string{ + "origin": "url", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + wantDevFile: DevfileObj{ + Data: &v2.DevfileV2{}, + }, + wantErr: []string{pluginProjectAlreadyDefinedErr}, + }, + { + name: "error out if the same project is defined in the both plugin devfile and parent", + args: args{ + devFileObj: DevfileObj{ + Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Projects: []v1.Project{ + { + ClonePath: "/projects", + Name: "nodejs-starter-build", + ProjectSource: v1.ProjectSource{ + Git: &v1.GitProjectSource{ + GitLikeProjectSource: v1.GitLikeProjectSource{ + Remotes: map[string]string{ + "origin": "url", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + pluginDevfile: DevfileObj{ + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Projects: []v1.Project{ + { + ClonePath: "/projects", + Name: "nodejs-starter", + ProjectSource: v1.ProjectSource{ + Git: &v1.GitProjectSource{ + GitLikeProjectSource: v1.GitLikeProjectSource{ + Remotes: map[string]string{ + "origin": "url", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + parentDevfile: DevfileObj{ + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Projects: []v1.Project{ + { + ClonePath: "/projects", + Name: "nodejs-starter", + ProjectSource: v1.ProjectSource{ + Git: &v1.GitProjectSource{ + GitLikeProjectSource: v1.GitLikeProjectSource{ + Remotes: map[string]string{ + "origin": "url", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + wantDevFile: DevfileObj{ + Data: &v2.DevfileV2{}, + }, + wantErr: []string{pluginProjectAlreadyDefinedErr}, + }, + { + name: "error out if the same command is defined in both plugin devfile and parent devfile", + args: args{ + devFileObj: DevfileObj{ + Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Commands: []v1.Command{ + { + Id: "devbuild", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{ + WorkingDir: "/projects/nodejs-starter", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + pluginDevfile: DevfileObj{ + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Commands: []v1.Command{ + { + Id: "devrun", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{ + WorkingDir: "/projects/nodejs-starter", + }, + }, + }, + }, + }, + }, + }, + }, + }, + parentDevfile: DevfileObj{ + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Commands: []v1.Command{ + { + Id: "devrun", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{ + WorkingDir: "/projects/nodejs-starter", + }, + }, + }, + }, + }, + }, + }, + }, + }, + wantDevFile: DevfileObj{ + Data: &v2.DevfileV2{}, + }, + wantErr: []string{pluginCmdAlreadyDefinedErr}, + }, + { + name: "error out if the same component is defined in both plugin devfile and parent devfile", + args: args{ + devFileObj: DevfileObj{ + Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Components: []v1.Component{ + { + Name: "runtime", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "quay.io/nodejs-12", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + pluginDevfile: DevfileObj{ + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Components: []v1.Component{ + { + Name: "build", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "quay.io/nodejs-12", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + parentDevfile: DevfileObj{ + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Components: []v1.Component{ + { + Name: "build", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "quay.io/nodejs-10", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + wantDevFile: DevfileObj{ + Data: &v2.DevfileV2{}, + }, + wantErr: []string{pluginCmpAlreadyDefinedErr}, + }, + { + name: "it should override the requested parent's data and plugin's data, and add the local devfile's data", + args: args{ + devFileObj: DevfileObj{ + Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + Parent: &v1.Parent{ + ParentOverrides: v1.ParentOverrides{ + Commands: []v1.CommandParentOverride{ + { + Id: "devrun", + CommandUnionParentOverride: v1.CommandUnionParentOverride{ + Exec: &v1.ExecCommandParentOverride{ + WorkingDir: "/projects/nodejs-starter", + }, + }, + }, + }, + Projects: []v1.ProjectParentOverride{ + { + ClonePath: "/projects", + Name: "nodejs-starter", + }, + }, + }, + }, + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Commands: []v1.Command{ + { + Id: "devbuild", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{ + WorkingDir: "/projects/nodejs-starter", + }, + }, + }, + }, + Components: []v1.Component{ + { + Name: "runtime", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "quay.io/nodejs-12", + }, + }, + }, + }, + }, + Events: &v1.Events{ + DevWorkspaceEvents: v1.DevWorkspaceEvents{ + PostStop: []string{"post-stop"}, + }, + }, + Projects: []v1.Project{ + { + ClonePath: "/projects", + Name: "nodejs-starter-build", + }, + }, + }, + }, + }, + }, + }, + }, + parentDevfile: DevfileObj{ + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Commands: []v1.Command{ + { + Id: "devrun", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{ + WorkingDir: "/projects", + CommandLine: "npm run", + }, + }, + }, + }, + Events: &v1.Events{ + DevWorkspaceEvents: v1.DevWorkspaceEvents{ + PostStart: []string{"post-start-0"}, + }, + }, + Projects: []v1.Project{ + { + ClonePath: "/data", + ProjectSource: v1.ProjectSource{ + Git: &v1.GitProjectSource{ + GitLikeProjectSource: v1.GitLikeProjectSource{ + Remotes: map[string]string{ + "master": "https://githube.com/somerepo/someproject.git", + }, + }, + }, + }, + Name: "nodejs-starter", + }, + }, + }, + }, + }, + }, + }, + pluginDevfile: DevfileObj{ + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Commands: []v1.Command{ + { + Id: "devdebug", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{ + WorkingDir: "/projects", + CommandLine: "npm debug", + }, + }, + }, + }, + Components: []v1.Component{ + { + Name: "nodejs", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "quay.io/nodejs-10", + }, + }, + }, + }, + }, + Events: &v1.Events{ + DevWorkspaceEvents: v1.DevWorkspaceEvents{ + PreStart: []string{"pre-start-0"}, + }, + }, + }, + }, + }, + }, + }, + pluginOverride: v1.PluginOverrides{ + Components: []v1.ComponentPluginOverride{ + { + Name: "nodejs", + ComponentUnionPluginOverride: v1.ComponentUnionPluginOverride{ + Container: &v1.ContainerComponentPluginOverride{ + ContainerPluginOverride: v1.ContainerPluginOverride{ + Image: "quay.io/nodejs-12", + }, + }, + }, + }, + }, + }, + wantDevFile: DevfileObj{ + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Commands: []v1.Command{ + { + Attributes: parentOverridesFromMainDevfile, + Id: "devrun", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{ + CommandLine: "npm run", + WorkingDir: "/projects/nodejs-starter", + }, + }, + }, + { + Attributes: importFromUri2, + Id: "devdebug", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{ + WorkingDir: "/projects", + CommandLine: "npm debug", + }, + }, + }, + { + Id: "devbuild", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{ + WorkingDir: "/projects/nodejs-starter", + }, + }, + }, + }, + Components: []v1.Component{ + { + Attributes: pluginOverridesFromMainDevfile, + Name: "nodejs", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "quay.io/nodejs-12", + }, + }, + }, + }, + { + Name: "runtime", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "quay.io/nodejs-12", + }, + }, + }, + }, + }, + Events: &v1.Events{ + DevWorkspaceEvents: v1.DevWorkspaceEvents{ + PostStart: []string{"post-start-0"}, + PostStop: []string{"post-stop"}, + PreStop: []string{}, + PreStart: []string{"pre-start-0"}, + }, + }, + Projects: []v1.Project{ + { + Attributes: parentOverridesFromMainDevfile, + ClonePath: "/projects", + ProjectSource: v1.ProjectSource{ + Git: &v1.GitProjectSource{ + GitLikeProjectSource: v1.GitLikeProjectSource{ + Remotes: map[string]string{ + "master": "https://githube.com/somerepo/someproject.git", + }, + }, + }, + }, + Name: "nodejs-starter", + }, + { + ClonePath: "/projects", + Name: "nodejs-starter-build", + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "error out if the plugin component is defined with a different component type in the local devfile", + args: args{ + devFileObj: DevfileObj{ + Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Components: []v1.Component{ + { + Name: "runtime", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "quay.io/nodejs-12", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + pluginDevfile: DevfileObj{ + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Components: []v1.Component{ + { + Name: "runtime", + ComponentUnion: v1.ComponentUnion{ + Volume: &v1.VolumeComponent{ + Volume: v1.Volume{ + Size: "500Mi", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + wantDevFile: DevfileObj{ + Data: &v2.DevfileV2{}, + }, + wantErr: []string{pluginCmpAlreadyDefinedErr}, + }, + { + name: "it should override with no errors if the plugin component is defined with a different component type in the plugin override", + args: args{ + devFileObj: DevfileObj{ + Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + }, + }, + }, + }, + pluginOverride: v1.PluginOverrides{ + Components: []v1.ComponentPluginOverride{ + { + Name: "runtime", + ComponentUnionPluginOverride: v1.ComponentUnionPluginOverride{ + Container: &v1.ContainerComponentPluginOverride{ + ContainerPluginOverride: v1.ContainerPluginOverride{ + Image: "quay.io/nodejs-12", + }, + }, + }, + }, + }, + }, + pluginDevfile: DevfileObj{ + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Components: []v1.Component{ + { + Name: "runtime", + ComponentUnion: v1.ComponentUnion{ + Volume: &v1.VolumeComponent{ + Volume: v1.Volume{ + Size: "500Mi", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + wantDevFile: DevfileObj{ + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Components: []v1.Component{ + { + Attributes: pluginOverridesFromMainDevfile, + Name: "runtime", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "quay.io/nodejs-12", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "error out if the parent component is defined with a different component type in the local devfile", + args: args{ + devFileObj: DevfileObj{ + Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Components: []v1.Component{ + { + Name: "runtime", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "quay.io/nodejs-12", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + parentDevfile: DevfileObj{ + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Components: []v1.Component{ + { + Name: "runtime", + ComponentUnion: v1.ComponentUnion{ + Volume: &v1.VolumeComponent{ + Volume: v1.Volume{ + Size: "500Mi", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + wantDevFile: DevfileObj{ + Data: &v2.DevfileV2{}, + }, + wantErr: []string{parentCmpAlreadyDefinedErr}, + }, + { + name: "it should override with no errors if the parent component is defined with a different component type in the parent override", + args: args{ + devFileObj: DevfileObj{ + Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + Parent: &v1.Parent{ + ParentOverrides: v1.ParentOverrides{ + Components: []v1.ComponentParentOverride{ + { + Name: "runtime", + ComponentUnionParentOverride: v1.ComponentUnionParentOverride{ + Container: &v1.ContainerComponentParentOverride{ + ContainerParentOverride: v1.ContainerParentOverride{ + Image: "quay.io/nodejs-12", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + parentDevfile: DevfileObj{ + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Components: []v1.Component{ + { + Name: "runtime", + ComponentUnion: v1.ComponentUnion{ + Volume: &v1.VolumeComponent{ + Volume: v1.Volume{ + Size: "500Mi", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + wantDevFile: DevfileObj{ + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Components: []v1.Component{ + { + Attributes: parentOverridesFromMainDevfile, + Name: "runtime", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "quay.io/nodejs-12", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "error out if the URI is recursively referenced", + args: args{ + devFileObj: DevfileObj{ + Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{}, + }, + }, + }, + pluginDevfile: DevfileObj{ + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + Parent: &v1.Parent{ + ImportReference: v1.ImportReference{ + ImportReferenceUnion: v1.ImportReferenceUnion{ + Uri: "http://" + uri2, + }, + }, + }, + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Components: []v1.Component{ + { + Name: "runtime", + ComponentUnion: v1.ComponentUnion{ + Volume: &v1.VolumeComponent{ + Volume: v1.Volume{ + Size: "500Mi", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + wantDevFile: DevfileObj{ + Data: &v2.DevfileV2{}, + }, + wantErr: []string{importCycleErr}, + testRecursiveReference: true, + }, + { + name: "error out if the plugin devfile is greater than main devfile version", + args: args{ + devFileObj: DevfileObj{ + Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: "2.0.0", + }, + }, + }, + }, + }, + pluginDevfile: DevfileObj{ + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{}, + }, + }, + }, + wantDevFile: DevfileObj{ + Data: &v2.DevfileV2{}, + }, + wantErr: []string{pluginDevfileVersionErr}, + testRecursiveReference: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var parentTestServer *httptest.Server + var pluginTestServer *httptest.Server + if !reflect.DeepEqual(tt.parentDevfile, DevfileObj{}) { + parentTestServer = httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + data, err := yaml.Marshal(tt.parentDevfile.Data) + if err != nil { + t.Errorf("Test_parseParentAndPluginFromURI() unexpected error while doing yaml marshal: %v", err) + } + _, err = w.Write(data) + if err != nil { + t.Errorf("Test_parseParentAndPluginFromURI() unexpected error while writing data: %v", err) + } + })) + // create a listener with the desired port. + l1, err := net.Listen("tcp", uri1) + if err != nil { + t.Errorf("Test_parseParentAndPluginFromURI() unexpected error while creating listener: %v", err) + } + + // NewUnstartedServer creates a listener. Close that listener and replace + // with the one we created. + parentTestServer.Listener.Close() + parentTestServer.Listener = l1 + + parentTestServer.Start() + defer parentTestServer.Close() + + parent := tt.args.devFileObj.Data.GetParent() + if parent == nil { + parent = &v1.Parent{} + } + parent.Uri = parentTestServer.URL + + tt.args.devFileObj.Data.SetParent(parent) + } + if !reflect.DeepEqual(tt.pluginDevfile, DevfileObj{}) { + + pluginTestServer = httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + data, err := yaml.Marshal(tt.pluginDevfile.Data) + if err != nil { + t.Errorf("Test_parseParentAndPluginFromURI() unexpected error while doing yaml marshal: %v", err) + } + _, err = w.Write(data) + if err != nil { + t.Errorf("Test_parseParentAndPluginFromURI() unexpected error while writing data: %v", err) + } + })) + l, err := net.Listen("tcp", uri2) + if err != nil { + t.Errorf("Test_parseParentAndPluginFromURI() unexpected error while creating listener: %v", err) + } + + // NewUnstartedServer creates a listener. Close that listener and replace + // with the one we created. + pluginTestServer.Listener.Close() + pluginTestServer.Listener = l + + pluginTestServer.Start() + defer pluginTestServer.Close() + + plugincomp := []v1.Component{ + { + Name: "plugincomp", + ComponentUnion: v1.ComponentUnion{ + Plugin: &v1.PluginComponent{ + ImportReference: v1.ImportReference{ + ImportReferenceUnion: v1.ImportReferenceUnion{ + Uri: pluginTestServer.URL, + }, + }, + PluginOverrides: tt.pluginOverride, + }, + }, + }, + } + tt.args.devFileObj.Data.AddComponents(plugincomp) + + } + err := parseParentAndPlugin(tt.args.devFileObj, &resolutionContextTree{}, resolverTools{}) + + // Unexpected error + if (err != nil) != (tt.wantErr != nil) { + t.Errorf("Test_parseParentAndPluginFromURI() unexpected error: %v, wantErr %v", err, tt.wantErr) + } else if err == nil && !reflect.DeepEqual(tt.args.devFileObj.Data, tt.wantDevFile.Data) { + t.Errorf("Test_parseParentAndPluginFromURI() error: wanted: %v, got: %v, difference at %v", tt.wantDevFile.Data, tt.args.devFileObj.Data, pretty.Compare(tt.args.devFileObj.Data, tt.wantDevFile.Data)) + } else if err != nil { + for _, wantErr := range tt.wantErr { + assert.Regexp(t, wantErr, err.Error(), "Test_parseParentAndPluginFromURI(): Error message should match") + } + } + }) + } +} + +func Test_parseParentAndPlugin_RecursivelyReference(t *testing.T) { + const uri1 = "127.0.0.1:8080" + const uri2 = "127.0.0.1:8090" + const httpPrefix = "http://" + const name = "testcrd" + const namespace = "defaultnamespace" + + devFileObj := DevfileObj{ + Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + Parent: &v1.Parent{ + ImportReference: v1.ImportReference{ + ImportReferenceUnion: v1.ImportReferenceUnion{ + Uri: httpPrefix + uri1, + }, + }, + }, + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Components: []v1.Component{ + { + Name: "runtime2", + ComponentUnion: v1.ComponentUnion{ + Volume: &v1.VolumeComponent{ + Volume: v1.Volume{ + Size: "500Mi", + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + parentDevfile1 := DevfileObj{ + Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + Parent: &v1.Parent{ + ImportReference: v1.ImportReference{ + ImportReferenceUnion: v1.ImportReferenceUnion{ + Kubernetes: &v1.KubernetesCustomResourceImportReference{ + Name: name, + Namespace: namespace, + }, + }, + }, + }, + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Components: []v1.Component{ + { + Name: "runtime", + ComponentUnion: v1.ComponentUnion{ + Volume: &v1.VolumeComponent{ + Volume: v1.Volume{ + Size: "500Mi", + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + parentDevfile2 := DevfileObj{ + Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + Parent: &v1.Parent{ + ImportReference: v1.ImportReference{ + ImportReferenceUnion: v1.ImportReferenceUnion{ + Uri: httpPrefix + uri1, + }, + }, + }, + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Components: []v1.Component{ + { + Name: "test", + ComponentUnion: v1.ComponentUnion{ + Volume: &v1.VolumeComponent{ + Volume: v1.Volume{ + Size: "500Mi", + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + testServer1 := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + data, err := yaml.Marshal(parentDevfile1.Data) + if err != nil { + t.Errorf("Test_parseParentAndPlugin_RecursivelyReference() unexpected error while doing yaml marshal: %v", err) + } + _, err = w.Write(data) + if err != nil { + t.Errorf("Test_parseParentAndPlugin_RecursivelyReference() unexpected error while writing data: %v", err) + } + })) + // create a listener with the desired port. + l1, err := net.Listen("tcp", uri1) + if err != nil { + t.Errorf("Test_parseParentAndPlugin_RecursivelyReference() unexpected error while creating listener: %v", err) + } + + // NewUnstartedServer creates a listener. Close that listener and replace + // with the one we created. + testServer1.Listener.Close() + testServer1.Listener = l1 + + testServer1.Start() + defer testServer1.Close() + + testServer2 := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var data []byte + if strings.Contains(r.URL.Path, "/devfiles/nodejs") { + data, err = yaml.Marshal(parentDevfile2.Data) + } else { + w.WriteHeader(http.StatusNotFound) + return + } + if err != nil { + t.Errorf("Test_parseParentAndPlugin_RecursivelyReference() unexpected error while writing data: %v", err) + } + _, err = w.Write(data) + if err != nil { + t.Errorf("Test_parseParentAndPlugin_RecursivelyReference() unexpected error while writing data: %v", err) + } + })) + // create a listener with the desired port. + l3, err := net.Listen("tcp", uri2) + if err != nil { + t.Errorf("Test_parseParentAndPlugin_RecursivelyReference() unexpected error while creating listener: %v", err) + } + + // NewUnstartedServer creates a listener. Close that listener and replace + // with the one we created. + testServer2.Listener.Close() + testServer2.Listener = l3 + + testServer2.Start() + defer testServer2.Close() + + parentSpec := v1.DevWorkspaceTemplateSpec{ + Parent: &v1.Parent{ + ImportReference: v1.ImportReference{ + ImportReferenceUnion: v1.ImportReferenceUnion{ + Id: "nodejs", + }, + RegistryUrl: httpPrefix + uri2, + }, + }, + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Components: []v1.Component{ + { + Name: "crdcomponent", + ComponentUnion: v1.ComponentUnion{ + Volume: &v1.VolumeComponent{ + Volume: v1.Volume{ + Size: "500Mi", + }, + }, + }, + }, + }, + }, + } + devWorkspaceResources := map[string]v1.DevWorkspaceTemplate{ + name: { + TypeMeta: kubev1.TypeMeta{ + Kind: "DevWorkspaceTemplate", + APIVersion: "testgroup/v1alpha2", + }, + Spec: parentSpec, + }, + } + + t.Run("it should error out if import reference has a cycle", func(t *testing.T) { + testK8sClient := &testingutil.FakeK8sClient{ + DevWorkspaceResources: devWorkspaceResources, + } + tool := resolverTools{ + k8sClient: testK8sClient, + context: context.Background(), + } + + err := parseParentAndPlugin(devFileObj, &resolutionContextTree{}, tool) + // devfile has a cycle in references: main devfile -> uri: http://127.0.0.1:8080 -> name: testcrd, namespace: defaultnamespace -> id: nodejs, registryURL: http://127.0.0.1:8090 -> uri: http://127.0.0.1:8080 + expectedErr := fmt.Sprintf("devfile has an cycle in references: main devfile -> uri: %s%s -> name: %s, namespace: %s -> id: nodejs, registryURL: %s%s -> uri: %s%s", httpPrefix, uri1, name, namespace, + httpPrefix, uri2, httpPrefix, uri1) + // Unexpected error + if err == nil || !reflect.DeepEqual(expectedErr, err.Error()) { + t.Errorf("Test_parseParentAndPlugin_RecursivelyReference() unexpected error: %v", err) + + return + } + + }) +} + +func Test_parseParentFromRegistry(t *testing.T) { + const validRegistry = "127.0.0.1:8080" + const invalidRegistry = "invalid-registry.io" + tool := resolverTools{ + registryURLs: []string{"http://" + validRegistry}, + } + + invalidURLErr := "the provided registryURL: .* is not a valid URL" + idNotFoundErr := "failed to get id: .* from registry URLs provided" + + parentDevfile := DevfileObj{ + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Components: []v1.Component{ + { + Name: "parent-runtime", + ComponentUnion: v1.ComponentUnion{ + Volume: &v1.VolumeComponent{ + Volume: v1.Volume{ + Size: "500Mi", + }, + }, + }, + }, + testingutil.GetDockerImageTestComponent(defaultDiv, nil, nil), + }, + }, + }, + }, + }, + } + testServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + var data []byte + var err error + if strings.Contains(r.URL.Path, "/devfiles/nodejs") { + data, err = yaml.Marshal(parentDevfile.Data) + } else { + w.WriteHeader(http.StatusNotFound) + return + } + if err != nil { + t.Errorf("Test_parseParentFromRegistry() unexpected error while doing yaml marshal: %v", err) + return + } + _, err = w.Write(data) + if err != nil { + t.Errorf("Test_parseParentFromRegistry() unexpected error while writing data: %v", err) + } + })) + // create a listener with the desired port. + l, err := net.Listen("tcp", validRegistry) + if err != nil { + t.Errorf("Test_parseParentFromRegistry() unexpected error while creating listener: %v", err) + return + } + + // NewUnstartedServer creates a listener. Close that listener and replace + // with the one we created. + testServer.Listener.Close() + testServer.Listener = l + + testServer.Start() + defer testServer.Close() + + div := defaultDiv + div.RootRequired = &isTrue + + mainDevfileContent := v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + Parent: &v1.Parent{ + ImportReference: v1.ImportReference{ + RegistryUrl: "http://" + validRegistry, + ImportReferenceUnion: v1.ImportReferenceUnion{ + Id: "nodejs", + }, + }, + ParentOverrides: v1.ParentOverrides{ + Components: []v1.ComponentParentOverride{ + { + Name: "parent-runtime", + ComponentUnionParentOverride: v1.ComponentUnionParentOverride{ + Container: &v1.ContainerComponentParentOverride{ + ContainerParentOverride: v1.ContainerParentOverride{ + Image: "quay.io/nodejs-12", + }, + }, + }, + }, + testingutil.GetDockerImageTestComponentParentOverride(div), + }, + }, + }, + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Commands: []v1.Command{ + { + Id: "devbuild", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{ + WorkingDir: "/projects/nodejs-starter", + }, + }, + }, + }, + Components: []v1.Component{ + { + Name: "runtime2", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "quay.io/nodejs-12", + }, + }, + }, + }, + }, + Events: &v1.Events{ + DevWorkspaceEvents: v1.DevWorkspaceEvents{ + PostStop: []string{"post-stop"}, + }, + }, + Projects: []v1.Project{ + { + ClonePath: "/projects", + Name: "nodejs-starter-build", + }, + }, + }, + }, + } + + importFromRegistry := attributes.Attributes{}.PutString(importSourceAttribute, resolveImportReference(mainDevfileContent.Parent.ImportReference)) + parentOverridesFromMainDevfile := attributes.Attributes{}.PutString(importSourceAttribute, + resolveImportReference(mainDevfileContent.Parent.ImportReference)).PutString(parentOverrideAttribute, "main devfile") + + wantDevfileContent := v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Commands: []v1.Command{ + { + Id: "devbuild", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{ + WorkingDir: "/projects/nodejs-starter", + }, + }, + }, + }, + Components: []v1.Component{ + { + Attributes: parentOverridesFromMainDevfile, + Name: "parent-runtime", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "quay.io/nodejs-12", + }, + }, + }, + }, + testingutil.GetDockerImageTestComponent(div, nil, parentOverridesFromMainDevfile), + { + Name: "runtime2", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "quay.io/nodejs-12", + }, + }, + }, + }, + }, + Events: &v1.Events{ + DevWorkspaceEvents: v1.DevWorkspaceEvents{ + PostStart: []string{}, + PostStop: []string{"post-stop"}, + PreStop: []string{}, + PreStart: []string{}, + }, + }, + Projects: []v1.Project{ + { + ClonePath: "/projects", + Name: "nodejs-starter-build", + }, + }, + }, + }, + } + + tests := []struct { + name string + mainDevfile DevfileObj + registryURI string + wantDevFile DevfileObj + wantErr *string + testRecursiveReference bool + }{ + { + name: "it should override the requested parent's data from provided registryURL and add the local devfile's data", + mainDevfile: DevfileObj{ + Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), + Data: &v2.DevfileV2{ + Devfile: mainDevfileContent, + }, + }, + wantDevFile: DevfileObj{ + Data: &v2.DevfileV2{ + Devfile: wantDevfileContent, + }, + }, + }, + { + name: "it should override the requested parent's data from registryURLs set in context and add the local devfile's data", + mainDevfile: DevfileObj{ + Data: &v2.DevfileV2{ + Devfile: mainDevfileContent, + }, + }, + wantDevFile: DevfileObj{ + Data: &v2.DevfileV2{ + Devfile: wantDevfileContent, + }, + }, + }, + { + name: "it should merge the requested parent's data from provided registryURL if no override is set", + mainDevfile: DevfileObj{ + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + Parent: &v1.Parent{ + ImportReference: v1.ImportReference{ + RegistryUrl: "http://" + validRegistry, + ImportReferenceUnion: v1.ImportReferenceUnion{ + Id: "nodejs", + }, + }, + }, + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Commands: []v1.Command{ + { + Id: "devbuild", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{ + WorkingDir: "/projects/nodejs-starter", + }, + }, + }, + }, + Components: []v1.Component{ + { + Name: "runtime2", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "quay.io/nodejs-12", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + wantDevFile: DevfileObj{ + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Commands: []v1.Command{ + { + Id: "devbuild", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{ + WorkingDir: "/projects/nodejs-starter", + }, + }, + }, + }, + Components: []v1.Component{ + { + Attributes: importFromRegistry, + Name: "parent-runtime", + ComponentUnion: v1.ComponentUnion{ + Volume: &v1.VolumeComponent{ + Volume: v1.Volume{ + Size: "500Mi", + }, + }, + }, + }, + testingutil.GetDockerImageTestComponent(defaultDiv, nil, importFromRegistry), + { + Name: "runtime2", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "quay.io/nodejs-12", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "it should error out with invalid registry provided", + mainDevfile: DevfileObj{ + Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + Parent: &v1.Parent{ + ImportReference: v1.ImportReference{ + ImportReferenceUnion: v1.ImportReferenceUnion{ + Id: "nodejs", + }, + RegistryUrl: invalidRegistry, + }, + }, + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{}, + }, + }, + }, + }, + wantErr: &invalidURLErr, + }, + { + name: "it should error out with non-exist registry id provided", + mainDevfile: DevfileObj{ + Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + Parent: &v1.Parent{ + ImportReference: v1.ImportReference{ + ImportReferenceUnion: v1.ImportReferenceUnion{ + Id: "not-exist", + }, + }, + }, + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{}, + }, + }, + }, + }, + wantErr: &idNotFoundErr, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + err := parseParentAndPlugin(tt.mainDevfile, &resolutionContextTree{}, tool) + + // Unexpected error + if (err != nil) != (tt.wantErr != nil) { + t.Errorf("Test_parseParentFromRegistry() unexpected error: %v, wantErr %v", err, tt.wantErr) + } else if err == nil && !reflect.DeepEqual(tt.mainDevfile.Data, tt.wantDevFile.Data) { + t.Errorf("Test_parseParentFromRegistry() error: wanted: %v, got: %v, difference at %v", tt.wantDevFile.Data, tt.mainDevfile.Data, pretty.Compare(tt.mainDevfile.Data, tt.wantDevFile.Data)) + } else if err != nil { + assert.Regexp(t, *tt.wantErr, err.Error(), "Test_parseParentFromRegistry(): Error message should match") + } + + }) + } +} + +func Test_parseParentFromKubeCRD(t *testing.T) { + + const ( + namespace = "default" + name = "test-parent-k8s" + apiVersion = "testgroup/v1alpha2" + ) + + kubeCRDReference := v1.ImportReference{ + ImportReferenceUnion: v1.ImportReferenceUnion{ + Kubernetes: &v1.KubernetesCustomResourceImportReference{ + Name: name, + Namespace: namespace, + }, + }, + } + + importFromKubeCRD := attributes.Attributes{}.PutString(importSourceAttribute, resolveImportReference(kubeCRDReference)) + parentOverridesFromMainDevfile := attributes.Attributes{}.PutString(importSourceAttribute, + resolveImportReference(kubeCRDReference)).PutString(parentOverrideAttribute, "main devfile") + + parentSpec := v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Components: []v1.Component{ + { + Name: "parent-runtime", + ComponentUnion: v1.ComponentUnion{ + Volume: &v1.VolumeComponent{ + Volume: v1.Volume{ + Size: "500Mi", + }, + }, + }, + }, + testingutil.GetDockerImageTestComponent(defaultDiv, nil, nil), + }, + }, + } + + //this is a copy of parentSpec which can't be reused because defaults are being set on the SrcType and ImageType properties in the override code. + parentSpec2 := v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Components: []v1.Component{ + { + Name: "parent-runtime", + ComponentUnion: v1.ComponentUnion{ + Volume: &v1.VolumeComponent{ + Volume: v1.Volume{ + Size: "500Mi", + }, + }, + }, + }, + testingutil.GetDockerImageTestComponent(defaultDiv, nil, nil), + }, + }, + } + + crdNotFoundErr := "not found" + + //override all properties + div := testingutil.DockerImageValues{ + ImageName: "image:next", + Uri: "/local/image2", + BuildContext: "/src2", + RootRequired: &isTrue, + } + + tests := []struct { + name string + devWorkspaceResources map[string]v1.DevWorkspaceTemplate + errors map[string]string + mainDevfile DevfileObj + wantDevFile DevfileObj + wantErr *string + }{ + { + name: "should successfully override the parent data", + mainDevfile: DevfileObj{ + Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + Parent: &v1.Parent{ + ImportReference: kubeCRDReference, + ParentOverrides: v1.ParentOverrides{ + Components: []v1.ComponentParentOverride{ + { + Name: "parent-runtime", + ComponentUnionParentOverride: v1.ComponentUnionParentOverride{ + Container: &v1.ContainerComponentParentOverride{ + ContainerParentOverride: v1.ContainerParentOverride{ + Image: "quay.io/nodejs-12", + }, + }, + }, + }, + testingutil.GetDockerImageTestComponentParentOverride(div), + }, + }, + }, + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Commands: []v1.Command{ + { + Id: "devbuild", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{ + WorkingDir: "/projects/nodejs-starter", + }, + }, + }, + }, + Components: []v1.Component{ + { + Name: "runtime", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "quay.io/nodejs-12", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + wantDevFile: DevfileObj{ + Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Commands: []v1.Command{ + { + Id: "devbuild", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{ + WorkingDir: "/projects/nodejs-starter", + }, + }, + }, + }, + Components: []v1.Component{ + { + Attributes: parentOverridesFromMainDevfile, + Name: "parent-runtime", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "quay.io/nodejs-12", + }, + }, + }, + }, + testingutil.GetDockerImageTestComponent(div, nil, parentOverridesFromMainDevfile), + { + Name: "runtime", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "quay.io/nodejs-12", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + devWorkspaceResources: map[string]v1.DevWorkspaceTemplate{ + name: { + TypeMeta: kubev1.TypeMeta{ + Kind: "DevWorkspaceTemplate", + APIVersion: apiVersion, + }, + Spec: parentSpec, + }, + }, + }, + { + name: "should successfully merge the parent data without override defined", + mainDevfile: DevfileObj{ + Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + Parent: &v1.Parent{ + ImportReference: kubeCRDReference, + }, + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Commands: []v1.Command{ + { + Id: "devbuild", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{ + WorkingDir: "/projects/nodejs-starter", + }, + }, + }, + }, + Components: []v1.Component{ + { + Name: "runtime", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "quay.io/nodejs-12", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + wantDevFile: DevfileObj{ + Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Commands: []v1.Command{ + { + Id: "devbuild", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{ + WorkingDir: "/projects/nodejs-starter", + }, + }, + }, + }, + Components: []v1.Component{ + { + Attributes: importFromKubeCRD, + Name: "parent-runtime", + ComponentUnion: v1.ComponentUnion{ + Volume: &v1.VolumeComponent{ + Volume: v1.Volume{ + Size: "500Mi", + }, + }, + }, + }, + testingutil.GetDockerImageTestComponent(defaultDiv, nil, importFromKubeCRD), + { + Name: "runtime", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "quay.io/nodejs-12", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + devWorkspaceResources: map[string]v1.DevWorkspaceTemplate{ + name: { + TypeMeta: kubev1.TypeMeta{ + Kind: "DevWorkspaceTemplate", + APIVersion: apiVersion, + }, + Spec: parentSpec2, + }, + }, + }, + { + name: "should fail if kclient get returns error", + mainDevfile: DevfileObj{ + Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + Parent: &v1.Parent{ + ImportReference: kubeCRDReference, + }, + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{}, + }, + }, + }, + }, + devWorkspaceResources: map[string]v1.DevWorkspaceTemplate{}, + errors: map[string]string{ + name: crdNotFoundErr, + }, + wantErr: &crdNotFoundErr, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testK8sClient := &testingutil.FakeK8sClient{ + DevWorkspaceResources: tt.devWorkspaceResources, + Errors: tt.errors, + } + tool := resolverTools{ + k8sClient: testK8sClient, + context: context.Background(), + } + err := parseParentAndPlugin(tt.mainDevfile, &resolutionContextTree{}, tool) + // Unexpected error + if (err != nil) != (tt.wantErr != nil) { + t.Errorf("Test_parseParentFromKubeCRD() unexpected error: %v, wantErr %v", err, tt.wantErr) + } else if err == nil && !reflect.DeepEqual(tt.mainDevfile.Data, tt.wantDevFile.Data) { + t.Errorf("Test_parseParentFromKubeCRD() error: wanted: %v, got: %v, difference at %v", tt.wantDevFile.Data, tt.mainDevfile.Data, pretty.Compare(tt.mainDevfile.Data, tt.wantDevFile.Data)) + } else if err != nil { + assert.Regexp(t, *tt.wantErr, err.Error(), "Test_parseParentFromKubeCRD(): Error message should match") + } + + }) + } +} + +func Test_parseFromURI(t *testing.T) { + const ( + uri1 = "127.0.0.1:8080" + httpPrefix = "http://" + localRelativeURI = "testTmp/dir/devfile.yaml" + notExistURI = "notexist/devfile.yaml" + invalidURL = "http//invalid.com" + ) + uri2 := path.Join(uri1, localRelativeURI) + + localDevfile := DevfileObj{ + Ctx: devfileCtx.NewDevfileCtx(localRelativeURI), + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Components: []v1.Component{ + { + Name: "runtime", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "nodejs", + DedicatedPod: &isFalse, + MountSources: &isTrue, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + invalidFilePathErr := "the provided path is not a valid yaml filepath, and devfile.yaml or .devfile.yaml not found in the provided path.*" + readDevfileErr := "failed to read devfile from path.*" + URLNotFoundErr := "error getting devfile info from url: failed to retrieve .*, 404: Not Found" + invalidURLErr := "parse .* invalid URI for request" + + // prepare for local file + err := os.MkdirAll(path.Dir(localRelativeURI), 0755) + if err != nil { + fmt.Errorf("Test_parseFromURI() error: failed to create folder: %v, error: %v", path.Dir(localRelativeURI), err) + } + yamlData, err := yaml.Marshal(localDevfile.Data) + if err != nil { + fmt.Errorf("Test_parseFromURI() error: failed to marshall devfile data: %v", err) + } + err = ioutil.WriteFile(localRelativeURI, yamlData, 0644) + if err != nil { + fmt.Errorf("Test_parseFromURI() error: fail to write to file: %v", err) + } + + if err != nil { + t.Error(err) + } + + defer os.RemoveAll("testTmp/") + + parentDevfile := DevfileObj{ + Ctx: devfileCtx.NewURLDevfileCtx(httpPrefix + uri1), + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + Parent: &v1.Parent{ + ImportReference: v1.ImportReference{ + ImportReferenceUnion: v1.ImportReferenceUnion{ + Uri: localRelativeURI, + }, + }, + }, + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Components: []v1.Component{ + { + Name: "runtime2", + ComponentUnion: v1.ComponentUnion{ + Volume: &v1.VolumeComponent{ + Volume: v1.Volume{ + Size: "500Mi", + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + relativeParentDevfile := DevfileObj{ + Ctx: devfileCtx.NewURLDevfileCtx(httpPrefix + uri2), + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Components: []v1.Component{ + { + Name: "runtime", + ComponentUnion: v1.ComponentUnion{ + Volume: &v1.VolumeComponent{ + Volume: v1.Volume{ + Size: "500Mi", + Ephemeral: &isFalse, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + testServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.Contains(r.URL.Path, "notexist") { + w.WriteHeader(http.StatusNotFound) + return + } + var data []byte + var err error + if strings.Contains(r.URL.Path, "devfile.yaml") { + data, err = yaml.Marshal(relativeParentDevfile.Data) + } else { + data, err = yaml.Marshal(parentDevfile.Data) + } + if err != nil { + t.Errorf("Test_parseFromURI() unexpected while doing yaml marshal: %v", err) + return + } + _, err = w.Write(data) + if err != nil { + t.Errorf("Test_parseFromURI() unexpected error while writing data: %v", err) + } + })) + // create a listener with the desired port. + l, err := net.Listen("tcp", uri1) + if err != nil { + t.Errorf("Test_parseFromURI() unexpected error while creating listener: %v", err) + return + } + + // NewUnstartedServer creates a listener. Close that listener and replace + // with the one we created. + testServer.Listener.Close() + testServer.Listener = l + + testServer.Start() + defer testServer.Close() + + tests := []struct { + name string + curDevfileCtx devfileCtx.DevfileCtx + importReference v1.ImportReference + wantDevFile DevfileObj + wantErr *string + }{ + { + name: "should be able to parse from relative uri on local disk", + curDevfileCtx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), + wantDevFile: localDevfile, + importReference: v1.ImportReference{ + ImportReferenceUnion: v1.ImportReferenceUnion{ + Uri: localRelativeURI, + }, + }, + }, + { + name: "should be able to parse relative uri from URL", + curDevfileCtx: parentDevfile.Ctx, + wantDevFile: relativeParentDevfile, + importReference: v1.ImportReference{ + ImportReferenceUnion: v1.ImportReferenceUnion{ + Uri: localRelativeURI, + }, + }, + }, + { + name: "should fail if no path or url has been set for devfile ctx", + curDevfileCtx: devfileCtx.DevfileCtx{}, + wantErr: &invalidFilePathErr, + }, + { + name: "should fail if file not exist", + curDevfileCtx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), + importReference: v1.ImportReference{ + ImportReferenceUnion: v1.ImportReferenceUnion{ + Uri: notExistURI, + }, + }, + wantErr: &readDevfileErr, + }, + { + name: "should fail if url not exist", + curDevfileCtx: devfileCtx.NewURLDevfileCtx(httpPrefix + uri1), + importReference: v1.ImportReference{ + ImportReferenceUnion: v1.ImportReferenceUnion{ + Uri: notExistURI, + }, + }, + wantErr: &URLNotFoundErr, + }, + { + name: "should fail if with invalid URI format", + curDevfileCtx: devfileCtx.NewURLDevfileCtx(OutputDevfileYamlPath), + importReference: v1.ImportReference{ + ImportReferenceUnion: v1.ImportReferenceUnion{ + Uri: invalidURL, + }, + }, + wantErr: &invalidURLErr, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // if the main devfile is from local, need to set absolute path + if tt.curDevfileCtx.GetURL() == "" { + err := tt.curDevfileCtx.SetAbsPath() + if err != nil { + t.Errorf("Test_parseFromURI() unexpected error: %v", err) + return + } + } + got, err := parseFromURI(tt.importReference, tt.curDevfileCtx, &resolutionContextTree{}, resolverTools{}) + if (err != nil) != (tt.wantErr != nil) { + t.Errorf("Test_parseFromURI() unexpected error: %v, wantErr %v", err, tt.wantErr) + } else if err == nil && !reflect.DeepEqual(got.Data, tt.wantDevFile.Data) { + t.Errorf("Test_parseFromURI() error: wanted: %v, got: %v, difference at %v", tt.wantDevFile, got, pretty.Compare(tt.wantDevFile, got)) + } else if err != nil { + assert.Regexp(t, *tt.wantErr, err.Error(), "Test_parseFromURI(): Error message should match") + } + }) + } +} + +func Test_parseFromRegistry(t *testing.T) { + const ( + registry = "127.0.0.1:8080" + httpPrefix = "http://" + notExistId = "notexist" + invalidRegistry = "http//invalid.com" + registryId = "nodejs" + ) + + parentDevfile := DevfileObj{ + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Components: []v1.Component{ + { + Name: "runtime2", + ComponentUnion: v1.ComponentUnion{ + Volume: &v1.VolumeComponent{ + Volume: v1.Volume{ + Size: "500Mi", + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + latestParentDevfile := DevfileObj{ + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Components: []v1.Component{ + { + Name: "runtime-latest", + ComponentUnion: v1.ComponentUnion{ + Volume: &v1.VolumeComponent{ + Volume: v1.Volume{ + Size: "500Mi", + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + wantDevfile := DevfileObj{ + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Components: []v1.Component{ + { + Name: "runtime2", + ComponentUnion: v1.ComponentUnion{ + Volume: &v1.VolumeComponent{ + Volume: v1.Volume{ + Size: "500Mi", + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + latestWantDevfile := DevfileObj{ + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Components: []v1.Component{ + { + Name: "runtime-latest", + ComponentUnion: v1.ComponentUnion{ + Volume: &v1.VolumeComponent{ + Volume: v1.Volume{ + Size: "500Mi", + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + invalidURLErr := "the provided registryURL: .* is not a valid URL" + URLNotFoundErr := "failed to retrieve .*, 404: Not Found" + missingRegistryURLErr := "failed to fetch from registry, registry URL is not provided" + invalidRegistryURLErr := "Get .* dial tcp: lookup http: .*" + + testServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var data []byte + var err error + if strings.Contains(r.URL.Path, "/devfiles/"+registryId) { + if strings.Contains(r.URL.Path, "latest") { + data, err = yaml.Marshal(latestParentDevfile.Data) + } else if strings.Contains(r.URL.Path, "1.1.0") { + data, err = yaml.Marshal(parentDevfile.Data) + } else if r.URL.Path == fmt.Sprintf("/devfiles/%s/", registryId) { + data, err = yaml.Marshal(parentDevfile.Data) + } else { + w.WriteHeader(http.StatusNotFound) + return + } + } else { + w.WriteHeader(http.StatusNotFound) + return + } + if err != nil { + t.Errorf("Test_parseFromRegistry() unexpected error while doing yaml marshal: %v", err) + return + } + _, err = w.Write(data) + if err != nil { + t.Errorf("Test_parseFromRegistry() unexpected error while writing data: %v", err) + } + })) + // create a listener with the desired port. + l, err := net.Listen("tcp", registry) + if err != nil { + t.Errorf("Test_parseFromRegistry() unexpected error while creating listener: %v", err) + return + } + + // NewUnstartedServer creates a listener. Close that listener and replace + // with the one we created. + testServer.Listener.Close() + testServer.Listener = l + + testServer.Start() + defer testServer.Close() + + tests := []struct { + name string + curDevfileCtx devfileCtx.DevfileCtx + importReference v1.ImportReference + tool resolverTools + wantDevFile DevfileObj + wantErr *string + }{ + { + name: "should fail if provided registryUrl does not have protocol prefix", + wantDevFile: wantDevfile, + importReference: v1.ImportReference{ + ImportReferenceUnion: v1.ImportReferenceUnion{ + Id: registryId, + }, + RegistryUrl: registry, + }, + wantErr: &invalidURLErr, + }, + { + name: "should be able to parse from provided registryUrl with prefix", + wantDevFile: wantDevfile, + importReference: v1.ImportReference{ + ImportReferenceUnion: v1.ImportReferenceUnion{ + Id: registryId, + }, + RegistryUrl: httpPrefix + registry, + }, + }, + { + name: "should be able to parse from registry URL defined in tool", + wantDevFile: wantDevfile, + importReference: v1.ImportReference{ + ImportReferenceUnion: v1.ImportReferenceUnion{ + Id: registryId, + }, + }, + tool: resolverTools{ + registryURLs: []string{"http://" + registry}, + }, + }, + { + name: "should be able to parse from provided registryUrl with latest version specified", + wantDevFile: latestWantDevfile, + importReference: v1.ImportReference{ + ImportReferenceUnion: v1.ImportReferenceUnion{ + Id: registryId, + }, + Version: "latest", + RegistryUrl: httpPrefix + registry, + }, + }, + { + name: "should be able to parse from provided registryUrl with version specified", + wantDevFile: wantDevfile, + importReference: v1.ImportReference{ + ImportReferenceUnion: v1.ImportReferenceUnion{ + Id: registryId, + }, + Version: "1.1.0", + RegistryUrl: httpPrefix + registry, + }, + }, + { + name: "should fail if version does not exist", + importReference: v1.ImportReference{ + ImportReferenceUnion: v1.ImportReferenceUnion{ + Id: registryId, + }, + Version: "999.9.9", + RegistryUrl: httpPrefix + registry, + }, + wantErr: &URLNotFoundErr, + }, + { + name: "should fail if registryId does not exist", + importReference: v1.ImportReference{ + ImportReferenceUnion: v1.ImportReferenceUnion{ + Id: notExistId, + }, + RegistryUrl: httpPrefix + registry, + }, + wantErr: &URLNotFoundErr, + }, + { + name: "should fail if registryUrl is not provided, and no registry URLs has been set in tool", + importReference: v1.ImportReference{ + ImportReferenceUnion: v1.ImportReferenceUnion{ + Id: registryId, + }, + }, + wantErr: &missingRegistryURLErr, + }, + { + name: "should fail if registryUrl is invalid", + importReference: v1.ImportReference{ + ImportReferenceUnion: v1.ImportReferenceUnion{ + Id: notExistId, + }, + RegistryUrl: httpPrefix + invalidRegistry, + }, + wantErr: &invalidRegistryURLErr, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parseFromRegistry(tt.importReference, &resolutionContextTree{}, tt.tool) + if (err != nil) != (tt.wantErr != nil) { + t.Errorf("Test_parseFromRegistry() unexpected error: %v, wantErr %v", err, tt.wantErr) + } else if err == nil && !reflect.DeepEqual(got.Data, tt.wantDevFile.Data) { + t.Errorf("Test_parseFromRegistry() error: wanted: %v, got: %v, difference at %v", tt.wantDevFile, got, pretty.Compare(tt.wantDevFile, got)) + } else if err != nil { + assert.Regexp(t, *tt.wantErr, err.Error(), "Test_parseFromRegistry(): Error message should match") + } + }) + } +} + +func Test_parseFromKubeCRD(t *testing.T) { + const ( + namespace = "default" + name = "test-parent-k8s" + apiVersion = "testgroup/v1alpha2" + ) + parentSpec := v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Components: []v1.Component{ + { + Name: "runtime", + ComponentUnion: v1.ComponentUnion{ + Volume: &v1.VolumeComponent{ + Volume: v1.Volume{ + Size: "500Mi", + }, + }, + }, + }, + }, + }, + } + parentDevfile := DevfileObj{ + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevWorkspaceTemplateSpec: parentSpec, + }, + }, + } + + crdNotFoundErr := "not found" + + tests := []struct { + name string + curDevfileCtx devfileCtx.DevfileCtx + importReference v1.ImportReference + devWorkspaceResources map[string]v1.DevWorkspaceTemplate + errors map[string]string + wantDevFile DevfileObj + wantErr *string + }{ + { + name: "should successfully parse the parent with namespace specified in devfile", + wantDevFile: parentDevfile, + importReference: v1.ImportReference{ + ImportReferenceUnion: v1.ImportReferenceUnion{ + Kubernetes: &v1.KubernetesCustomResourceImportReference{ + Name: name, + Namespace: namespace, + }, + }, + }, + devWorkspaceResources: map[string]v1.DevWorkspaceTemplate{ + name: { + TypeMeta: kubev1.TypeMeta{ + Kind: "DevWorkspaceTemplate", + APIVersion: apiVersion, + }, + Spec: parentSpec, + }, + }, + }, + { + name: "should fail if kclient get returns error", + wantDevFile: parentDevfile, + importReference: v1.ImportReference{ + ImportReferenceUnion: v1.ImportReferenceUnion{ + Kubernetes: &v1.KubernetesCustomResourceImportReference{ + Name: name, + Namespace: namespace, + }, + }, + }, + devWorkspaceResources: map[string]v1.DevWorkspaceTemplate{}, + errors: map[string]string{ + name: crdNotFoundErr, + }, + wantErr: &crdNotFoundErr, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testK8sClient := &testingutil.FakeK8sClient{ + DevWorkspaceResources: tt.devWorkspaceResources, + Errors: tt.errors, + } + tool := resolverTools{ + k8sClient: testK8sClient, + context: context.Background(), + } + got, err := parseFromKubeCRD(tt.importReference, &resolutionContextTree{}, tool) + if (err != nil) != (tt.wantErr != nil) { + t.Errorf("Test_parseFromKubeCRD() unexpected error: %v, wantErr %v", err, tt.wantErr) + } else if err == nil && !reflect.DeepEqual(got.Data, tt.wantDevFile.Data) { + t.Errorf("Test_parseFromKubeCRD() error: wanted: %v, got: %v, difference at %v", tt.wantDevFile, got, pretty.Compare(tt.wantDevFile, got)) + } else if err != nil { + assert.Regexp(t, *tt.wantErr, err.Error(), "Test_parseFromKubeCRD(): Error message should match") + } + }) + } +} + +func Test_setDefaults(t *testing.T) { + type testType struct { + name string + dataObj data.DevfileData + wantDevFile data.DevfileData + } + + var tests []testType + var version string + + // set up tests for unset boolean properties + for i := range apiSchemaVersions { + version = apiSchemaVersions[i] + testName := fmt.Sprintf("Verify defaults on unset boolean properties for devfile %s", version) + want, err := getBooleanDevfileTestData(version, true) + if err != nil { + t.Errorf("GetBooleanDevfileTestData() unexpected error %v ", err) + } + obj, err := getUnsetBooleanDevfileTestData(version) + if err != nil { + t.Errorf("GetUnsetBooleanDevfileTestData() unexpected error %v ", err) + } + tests = append(tests, testType{ + name: testName, + dataObj: obj, + wantDevFile: want, + }) + } + + //repeat tests on set boolean properties + for i := range apiSchemaVersions { + version = apiSchemaVersions[i] + testName := fmt.Sprintf("Verify defaults on set boolean properties for devfile %s", version) + obj, err := getBooleanDevfileTestData(version, false) + if err != nil { + t.Errorf("GetBooleanDevfileTestData() unexpected error %v ", err) + } + + tests = append(tests, testType{ + name: testName, + dataObj: obj, + wantDevFile: obj, //setDefaults should not alter properties that are explicitly set, so "want" structure should be identical + }) + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := DevfileObj{Data: tt.dataObj} + err := setDefaults(d) + if err != nil { + t.Errorf("Test_setDefaults() unexpected error setting defaults %v ", err) + } else if err == nil && !reflect.DeepEqual(d.Data, tt.wantDevFile) { + t.Errorf("Test_setDefaults() error: wanted: %v, got: %v, difference at %v/ ", tt.wantDevFile, d.Data, pretty.Compare(tt.wantDevFile, tt.dataObj)) + } + + }) + } +} + +// getUnsetBooleanDevfileObj returns a DevfileData object that contains unset boolean properties +func getUnsetBooleanDevfileTestData(apiVersion string) (devfileData data.DevfileData, err error) { + devfileData = &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: apiVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Commands: []v1.Command{ + { + Id: "devrun", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{ + CommandLine: "npm run", + WorkingDir: "/projects/nodejs-starter", + }, + }, + }, + { + Id: "testrun", + CommandUnion: v1.CommandUnion{ + Apply: &v1.ApplyCommand{ + LabeledCommand: v1.LabeledCommand{ + BaseCommand: v1.BaseCommand{ + Group: &v1.CommandGroup{ + Kind: v1.BuildCommandGroupKind, + }, + }, + }, + }, + }, + }, + { + Id: "allcmds", + CommandUnion: v1.CommandUnion{ + Composite: &v1.CompositeCommand{ + Commands: []string{"testrun", "devrun"}, + }, + }, + }, + }, + Components: []v1.Component{ + { + Name: "nodejs", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Annotation: &v1.Annotation{ + Deployment: map[string]string{ + "deploy-key1": "deploy-value1", + }, + Service: map[string]string{ + "svc-key1": "svc-value1", + "svc-key2": "svc-value3", + }, + }, + Image: "quay.io/nodejs-12", + }, + Endpoints: []v1.Endpoint{ + { + Name: "log", + TargetPort: 443, + Annotations: map[string]string{ + "ingress-key1": "ingress-value1", + "ingress-key2": "ingress-value3", + }, + }, + }, + }, + }, + }, + testingutil.GetFakeVolumeComponent("volume", "2Gi"), + { + Name: "openshift", + ComponentUnion: v1.ComponentUnion{ + Openshift: &v1.OpenshiftComponent{ + K8sLikeComponent: v1.K8sLikeComponent{ + K8sLikeComponentLocation: v1.K8sLikeComponentLocation{ + Uri: "https://xyz.com/dir/file.yaml", + }, + Endpoints: []v1.Endpoint{ + { + Name: "metrics", + TargetPort: 8080, + }, + }, + }, + }, + }, + }, + }, + Events: &v1.Events{ + DevWorkspaceEvents: v1.DevWorkspaceEvents{ + PostStart: []string{"post-start-0"}, + PostStop: []string{"post-stop"}, + PreStop: []string{}, + PreStart: []string{}, + }, + }, + }, + }, + }, + } + + if apiVersion != string(data.APISchemaVersion200) && apiVersion != string(data.APISchemaVersion210) { + comp := []v1.Component{testingutil.GetDockerImageTestComponent(testingutil.DockerImageValues{}, nil, nil)} + err = devfileData.AddComponents(comp) + } + + return devfileData, err + +} + +//getBooleanDevfileTestData returns a DevfileData object that contains set values for the boolean properties. If setDefault is true, an object with the default boolean values will be returned +func getBooleanDevfileTestData(apiVersion string, setDefault bool) (devfileData data.DevfileData, err error) { + + type boolValues struct { + hotReloadCapable *bool + secure *bool + parallel *bool + dedicatedPod *bool + mountSources *bool + isDefault *bool + rootRequired *bool + ephemeral *bool + autoBuild *bool + deployByDefaul *bool + } + + //default values according to spec + defaultBools := boolValues{&isFalse, &isFalse, &isFalse, &isFalse, &isTrue, &isFalse, &isFalse, &isFalse, &isFalse, &isFalse} + //set values will be a mix of default and inverse values + setBools := boolValues{&isTrue, &isTrue, &isFalse, &isTrue, &isFalse, &isFalse, &isTrue, &isFalse, &isTrue, &isFalse} + + var values boolValues + + if setDefault { + values = defaultBools + } else { + values = setBools + } + + devfileData = &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: apiVersion, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Commands: []v1.Command{ + { + Id: "devrun", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{ + CommandLine: "npm run", + WorkingDir: "/projects/nodejs-starter", + HotReloadCapable: values.hotReloadCapable, + }, + }, + }, + { + Id: "testrun", + CommandUnion: v1.CommandUnion{ + Apply: &v1.ApplyCommand{ + LabeledCommand: v1.LabeledCommand{ + BaseCommand: v1.BaseCommand{ + Group: &v1.CommandGroup{ + Kind: v1.BuildCommandGroupKind, + IsDefault: values.isDefault, + }, + }, + }, + }, + }, + }, + { + Id: "allcmds", + CommandUnion: v1.CommandUnion{ + Composite: &v1.CompositeCommand{ + Commands: []string{"testrun", "devrun"}, + Parallel: values.parallel, + }, + }, + }, + }, + Components: []v1.Component{ + { + Name: "nodejs", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Annotation: &v1.Annotation{ + Deployment: map[string]string{ + "deploy-key1": "deploy-value1", + }, + Service: map[string]string{ + "svc-key1": "svc-value1", + "svc-key2": "svc-value3", + }, + }, + Image: "quay.io/nodejs-12", + DedicatedPod: values.dedicatedPod, + MountSources: values.mountSources, + }, + Endpoints: []v1.Endpoint{ + { + Name: "log", + TargetPort: 443, + Annotations: map[string]string{ + "ingress-key1": "ingress-value1", + "ingress-key2": "ingress-value3", + }, + Secure: values.secure, + }, + }, + }, + }, + }, + testingutil.GetFakeVolumeComponent("volume", "2Gi"), + { + Name: "openshift", + ComponentUnion: v1.ComponentUnion{ + Openshift: &v1.OpenshiftComponent{ + K8sLikeComponent: v1.K8sLikeComponent{ + K8sLikeComponentLocation: v1.K8sLikeComponentLocation{ + Uri: "https://xyz.com/dir/file.yaml", + }, + Endpoints: []v1.Endpoint{ + { + Name: "metrics", + TargetPort: 8080, + Secure: values.secure, + }, + }, + }, + }, + }, + }, + }, + Events: &v1.Events{ + DevWorkspaceEvents: v1.DevWorkspaceEvents{ + PostStart: []string{"post-start-0"}, + PostStop: []string{"post-stop"}, + PreStop: []string{}, + PreStart: []string{}, + }, + }, + }, + }, + }, + } + + if apiVersion != string(data.APISchemaVersion200) { + volComponent, _ := devfileData.GetComponents(common.DevfileOptions{ComponentOptions: common.ComponentOptions{ + ComponentType: v1.VolumeComponentType, + }}) + + volComponent[0].Volume.Ephemeral = values.ephemeral + } + + if apiVersion != string(data.APISchemaVersion200) && apiVersion != string(data.APISchemaVersion210) { + comp := []v1.Component{testingutil.GetDockerImageTestComponent(testingutil.DockerImageValues{RootRequired: values.rootRequired}, values.autoBuild, nil)} + err = devfileData.AddComponents(comp) + + openshiftComponent, _ := devfileData.GetComponents(common.DevfileOptions{ComponentOptions: common.ComponentOptions{ + ComponentType: v1.OpenshiftComponentType, + }}) + openshiftComponent[0].Openshift.DeployByDefault = values.deployByDefaul + + } + + return devfileData, err +} diff --git a/pkg/devfile/parser/resolutionContext.go b/pkg/devfile/parser/resolutionContext.go new file mode 100644 index 0000000..5bb0d4b --- /dev/null +++ b/pkg/devfile/parser/resolutionContext.go @@ -0,0 +1,64 @@ +package parser + +import ( + "fmt" + "reflect" + + v1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" +) + +// resolutionContextTree is a recursive structure representing information about the devfile that is +// lost when flattening (e.g. plugins, parents) +type resolutionContextTree struct { + importReference v1.ImportReference + parentNode *resolutionContextTree +} + +// appendNode adds a new node to the resolution context. +func (t *resolutionContextTree) appendNode(importReference v1.ImportReference) *resolutionContextTree { + newNode := &resolutionContextTree{ + importReference: importReference, + parentNode: t, + } + return newNode +} + +// hasCycle checks if the current resolutionContextTree has a cycle +func (t *resolutionContextTree) hasCycle() error { + var seenRefs []v1.ImportReference + currNode := t + hasCycle := false + cycle := resolveImportReference(t.importReference) + + for currNode.parentNode != nil { + for _, seenRef := range seenRefs { + if reflect.DeepEqual(seenRef, currNode.importReference) { + hasCycle = true + } + } + seenRefs = append(seenRefs, currNode.importReference) + currNode = currNode.parentNode + cycle = fmt.Sprintf("%s -> %s", resolveImportReference(currNode.importReference), cycle) + } + + if hasCycle { + return fmt.Errorf("devfile has an cycle in references: %v", cycle) + } + return nil +} + +func resolveImportReference(importReference v1.ImportReference) string { + if !reflect.DeepEqual(importReference, v1.ImportReference{}) { + switch { + case importReference.Uri != "": + return fmt.Sprintf("uri: %s", importReference.Uri) + case importReference.Id != "": + return fmt.Sprintf("id: %s, registryURL: %s", importReference.Id, importReference.RegistryUrl) + case importReference.Kubernetes != nil: + return fmt.Sprintf("name: %s, namespace: %s", importReference.Kubernetes.Name, importReference.Kubernetes.Namespace) + } + + } + // the first node + return "main devfile" +} diff --git a/pkg/devfile/parser/sourceAttribute.go b/pkg/devfile/parser/sourceAttribute.go new file mode 100644 index 0000000..dd7608f --- /dev/null +++ b/pkg/devfile/parser/sourceAttribute.go @@ -0,0 +1,115 @@ +package parser + +import ( + "fmt" + v1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + "github.com/devfile/api/v2/pkg/attributes" + "github.com/devfile/api/v2/pkg/validation" +) + +const ( + importSourceAttribute = validation.ImportSourceAttribute + parentOverrideAttribute = validation.ParentOverrideAttribute + pluginOverrideAttribute = validation.PluginOverrideAttribute +) + +// addSourceAttributesForParentOverride adds an attribute 'api.devfile.io/imported-from=' +// to all elements of template spec content that support attributes. +func addSourceAttributesForTemplateSpecContent(sourceImportReference v1.ImportReference, template *v1.DevWorkspaceTemplateSpecContent) { + for idx, component := range template.Components { + if component.Attributes == nil { + template.Components[idx].Attributes = attributes.Attributes{} + } + template.Components[idx].Attributes.PutString(importSourceAttribute, resolveImportReference(sourceImportReference)) + } + for idx, command := range template.Commands { + if command.Attributes == nil { + template.Commands[idx].Attributes = attributes.Attributes{} + } + template.Commands[idx].Attributes.PutString(importSourceAttribute, resolveImportReference(sourceImportReference)) + } + for idx, project := range template.Projects { + if project.Attributes == nil { + template.Projects[idx].Attributes = attributes.Attributes{} + } + template.Projects[idx].Attributes.PutString(importSourceAttribute, resolveImportReference(sourceImportReference)) + } + for idx, project := range template.StarterProjects { + if project.Attributes == nil { + template.StarterProjects[idx].Attributes = attributes.Attributes{} + } + template.StarterProjects[idx].Attributes.PutString(importSourceAttribute, resolveImportReference(sourceImportReference)) + } +} + +// addSourceAttributesForParentOverride adds an attribute 'api.devfile.io/parent-override-from=' +// to all elements of parent override that support attributes. +func addSourceAttributesForParentOverride(sourceImportReference v1.ImportReference, parentOverrides *v1.ParentOverrides) { + for idx, component := range parentOverrides.Components { + if component.Attributes == nil { + parentOverrides.Components[idx].Attributes = attributes.Attributes{} + } + parentOverrides.Components[idx].Attributes.PutString(parentOverrideAttribute, resolveImportReference(sourceImportReference)) + } + for idx, command := range parentOverrides.Commands { + if command.Attributes == nil { + parentOverrides.Commands[idx].Attributes = attributes.Attributes{} + } + parentOverrides.Commands[idx].Attributes.PutString(parentOverrideAttribute, resolveImportReference(sourceImportReference)) + } + for idx, project := range parentOverrides.Projects { + if project.Attributes == nil { + parentOverrides.Projects[idx].Attributes = attributes.Attributes{} + } + parentOverrides.Projects[idx].Attributes.PutString(parentOverrideAttribute, resolveImportReference(sourceImportReference)) + } + for idx, project := range parentOverrides.StarterProjects { + if project.Attributes == nil { + parentOverrides.StarterProjects[idx].Attributes = attributes.Attributes{} + } + parentOverrides.StarterProjects[idx].Attributes.PutString(parentOverrideAttribute, resolveImportReference(sourceImportReference)) + } + +} + +// addSourceAttributesForPluginOverride adds an attribute 'api.devfile.io/plugin-override-from=' +// to all elements of plugin override that support attributes. +func addSourceAttributesForPluginOverride(sourceImportReference v1.ImportReference, pluginOverrides *v1.PluginOverrides) { + for idx, component := range pluginOverrides.Components { + if component.Attributes == nil { + pluginOverrides.Components[idx].Attributes = attributes.Attributes{} + } + pluginOverrides.Components[idx].Attributes.PutString(pluginOverrideAttribute, resolveImportReference(sourceImportReference)) + } + for idx, command := range pluginOverrides.Commands { + if command.Attributes == nil { + pluginOverrides.Commands[idx].Attributes = attributes.Attributes{} + } + pluginOverrides.Commands[idx].Attributes.PutString(pluginOverrideAttribute, resolveImportReference(sourceImportReference)) + } + +} + +// addSourceAttributesForOverrideAndMerge adds an attribute record the import reference to all elements of template that support attributes. +func addSourceAttributesForOverrideAndMerge(sourceImportReference v1.ImportReference, template interface{}) error { + if template == nil { + return fmt.Errorf("cannot add source attributes to nil") + } + + mainContent, isMainContent := template.(*v1.DevWorkspaceTemplateSpecContent) + parentOverride, isParentOverride := template.(*v1.ParentOverrides) + pluginOverride, isPluginOverride := template.(*v1.PluginOverrides) + + switch { + case isMainContent: + addSourceAttributesForTemplateSpecContent(sourceImportReference, mainContent) + case isParentOverride: + addSourceAttributesForParentOverride(sourceImportReference, parentOverride) + case isPluginOverride: + addSourceAttributesForPluginOverride(sourceImportReference, pluginOverride) + default: + return fmt.Errorf("unknown template type") + } + + return nil +} diff --git a/pkg/devfile/parser/sourceAttribute_test.go b/pkg/devfile/parser/sourceAttribute_test.go new file mode 100644 index 0000000..6701efb --- /dev/null +++ b/pkg/devfile/parser/sourceAttribute_test.go @@ -0,0 +1,158 @@ +package parser + +import ( + v1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + "github.com/devfile/api/v2/pkg/attributes" + "github.com/kylelemons/godebug/pretty" + "github.com/stretchr/testify/assert" + "reflect" + "testing" +) + +func TestAddSourceAttributesForOverrideAndMerge(t *testing.T) { + importReference := v1.ImportReference{ + ImportReferenceUnion: v1.ImportReferenceUnion{ + Uri: "127.0.0.1:8080", + }, + } + uriImportAttribute := attributes.Attributes{}.PutString(importSourceAttribute, resolveImportReference(importReference)) + pluginOverrideImportAttribute := attributes.Attributes{}.PutString(pluginOverrideAttribute, "main devfile") + parentOverrideImportAttribute := attributes.Attributes{}.PutString(parentOverrideAttribute, "main devfile") + + nilTemplateErr := "cannot add source attributes to nil" + invalidTemplateTypeErr := "unknown template type" + + tests := []struct { + name string + wantErr *string + importReference v1.ImportReference + template interface{} + wantResult interface{} + }{ + { + name: "should fail if template is nil", + template: nil, + wantErr: &nilTemplateErr, + }, + { + name: "should fail if template is a not support type", + template: "invalid template", + wantErr: &invalidTemplateTypeErr, + }, + { + name: "template is with type *DevWorkspaceTemplateSpecContent", + importReference: importReference, + template: &v1.DevWorkspaceTemplateSpecContent{ + Components: []v1.Component{ + { + Name: "nodejs", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "quay.io/nodejs-10", + }, + }, + }, + }, + }, + }, + wantResult: &v1.DevWorkspaceTemplateSpecContent{ + Components: []v1.Component{ + { + Attributes: uriImportAttribute, + Name: "nodejs", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "quay.io/nodejs-10", + }, + }, + }, + }, + }, + }, + }, + { + name: "template is with type *PluginOverrides", + importReference: v1.ImportReference{}, + template: &v1.PluginOverrides{ + Components: []v1.ComponentPluginOverride{ + { + Name: "nodejs", + ComponentUnionPluginOverride: v1.ComponentUnionPluginOverride{ + Container: &v1.ContainerComponentPluginOverride{ + ContainerPluginOverride: v1.ContainerPluginOverride{ + Image: "quay.io/nodejs-10", + }, + }, + }, + }, + }, + }, + wantResult: &v1.PluginOverrides{ + Components: []v1.ComponentPluginOverride{ + { + Name: "nodejs", + Attributes: pluginOverrideImportAttribute, + ComponentUnionPluginOverride: v1.ComponentUnionPluginOverride{ + Container: &v1.ContainerComponentPluginOverride{ + ContainerPluginOverride: v1.ContainerPluginOverride{ + Image: "quay.io/nodejs-10", + }, + }, + }, + }, + }, + }, + }, + { + name: "template is with type *ParentOverrides", + importReference: v1.ImportReference{}, + template: &v1.ParentOverrides{ + Components: []v1.ComponentParentOverride{ + { + Name: "nodejs", + ComponentUnionParentOverride: v1.ComponentUnionParentOverride{ + Container: &v1.ContainerComponentParentOverride{ + ContainerParentOverride: v1.ContainerParentOverride{ + Image: "quay.io/nodejs-10", + }, + }, + }, + }, + }, + }, + wantResult: &v1.ParentOverrides{ + Components: []v1.ComponentParentOverride{ + { + Name: "nodejs", + Attributes: parentOverrideImportAttribute, + ComponentUnionParentOverride: v1.ComponentUnionParentOverride{ + Container: &v1.ContainerComponentParentOverride{ + ContainerParentOverride: v1.ContainerParentOverride{ + Image: "quay.io/nodejs-10", + }, + }, + }, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := addSourceAttributesForOverrideAndMerge(tt.importReference, tt.template) + + if (err != nil) != (tt.wantErr != nil) { + t.Errorf("Test_AddSourceAttributesForOverrideAndMerge() unexpected error: %v, wantErr %v", err, tt.wantErr) + } else if err == nil && !reflect.DeepEqual(tt.template, tt.wantResult) { + t.Errorf("TestAddSourceAttributesForOverrideAndMerge() error: wanted: %v, got: %v, difference at %v", tt.wantResult, tt.template, pretty.Compare(tt.template, tt.wantResult)) + } else if err != nil { + assert.Regexp(t, *tt.wantErr, err.Error(), "TestAddSourceAttributesForOverrideAndMerge(): Error message should match") + } + + }) + } + +} diff --git a/pkg/devfile/parser/writer.go b/pkg/devfile/parser/writer.go new file mode 100644 index 0000000..78abd8f --- /dev/null +++ b/pkg/devfile/parser/writer.go @@ -0,0 +1,32 @@ +package parser + +import ( + "sigs.k8s.io/yaml" + + "github.com/devfile/library/pkg/testingutil/filesystem" + "github.com/pkg/errors" + "k8s.io/klog" +) + +// WriteYamlDevfile creates a devfile.yaml file +func (d *DevfileObj) WriteYamlDevfile() error { + + // Encode data into YAML format + yamlData, err := yaml.Marshal(d.Data) + if err != nil { + return errors.Wrapf(err, "failed to marshal devfile object into yaml") + } + // Write to devfile.yaml + fs := d.Ctx.GetFs() + if fs == nil { + fs = filesystem.DefaultFs{} + } + err = fs.WriteFile(d.Ctx.GetAbsPath(), yamlData, 0644) + if err != nil { + return errors.Wrapf(err, "failed to create devfile yaml file") + } + + // Successful + klog.V(2).Infof("devfile yaml created at: '%s'", OutputDevfileYamlPath) + return nil +} diff --git a/pkg/devfile/parser/writer_test.go b/pkg/devfile/parser/writer_test.go new file mode 100644 index 0000000..9601a25 --- /dev/null +++ b/pkg/devfile/parser/writer_test.go @@ -0,0 +1,50 @@ +package parser + +import ( + "testing" + + v1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + devfilepkg "github.com/devfile/api/v2/pkg/devfile" + devfileCtx "github.com/devfile/library/pkg/devfile/parser/context" + v2 "github.com/devfile/library/pkg/devfile/parser/data/v2" + "github.com/devfile/library/pkg/testingutil/filesystem" +) + +func TestWriteYamlDevfile(t *testing.T) { + + var ( + schemaVersion = "2.0.0" + testName = "TestName" + ) + + t.Run("write yaml devfile", func(t *testing.T) { + + // Use fakeFs + fs := filesystem.NewFakeFs() + + // DevfileObj + devfileObj := DevfileObj{ + Ctx: devfileCtx.FakeContext(fs, OutputDevfileYamlPath), + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaVersion, + Metadata: devfilepkg.DevfileMetadata{ + Name: testName, + }, + }, + }, + }, + } + + // test func() + err := devfileObj.WriteYamlDevfile() + if err != nil { + t.Errorf("TestWriteYamlDevfile() unexpected error: '%v'", err) + } + + if _, err := fs.Stat(OutputDevfileYamlPath); err != nil { + t.Errorf("TestWriteYamlDevfile() unexpected error: '%v'", err) + } + }) +} diff --git a/pkg/devfile/validate/validate.go b/pkg/devfile/validate/validate.go new file mode 100644 index 0000000..a1fa002 --- /dev/null +++ b/pkg/devfile/validate/validate.go @@ -0,0 +1,67 @@ +package validate + +import ( + "fmt" + v2Validation "github.com/devfile/api/v2/pkg/validation" + devfileData "github.com/devfile/library/pkg/devfile/parser/data" + v2 "github.com/devfile/library/pkg/devfile/parser/data/v2" + "github.com/devfile/library/pkg/devfile/parser/data/v2/common" + "github.com/hashicorp/go-multierror" +) + +// ValidateDevfileData validates whether sections of devfile are compatible +func ValidateDevfileData(data devfileData.DevfileData) error { + + commands, err := data.GetCommands(common.DevfileOptions{}) + if err != nil { + return err + } + components, err := data.GetComponents(common.DevfileOptions{}) + if err != nil { + return err + } + projects, err := data.GetProjects(common.DevfileOptions{}) + if err != nil { + return err + } + starterProjects, err := data.GetStarterProjects(common.DevfileOptions{}) + if err != nil { + return err + } + + var returnedErr error + switch d := data.(type) { + case *v2.DevfileV2: + // validate components + err = v2Validation.ValidateComponents(components) + if err != nil { + returnedErr = multierror.Append(returnedErr, err) + } + + // validate commands + err = v2Validation.ValidateCommands(commands, components) + if err != nil { + returnedErr = multierror.Append(returnedErr, err) + } + + err = v2Validation.ValidateEvents(data.GetEvents(), commands) + if err != nil { + returnedErr = multierror.Append(returnedErr, err) + } + + err = v2Validation.ValidateProjects(projects) + if err != nil { + returnedErr = multierror.Append(returnedErr, err) + } + + err = v2Validation.ValidateStarterProjects(starterProjects) + if err != nil { + returnedErr = multierror.Append(returnedErr, err) + } + + return returnedErr + + default: + return fmt.Errorf("unknown devfile type %T", d) + } +} diff --git a/pkg/testingutil/containers.go b/pkg/testingutil/containers.go new file mode 100644 index 0000000..3c9433b --- /dev/null +++ b/pkg/testingutil/containers.go @@ -0,0 +1,10 @@ +package testingutil + +import corev1 "k8s.io/api/core/v1" + +// CreateFakeContainer creates a container with the given containerName +func CreateFakeContainer(containerName string) corev1.Container { + return corev1.Container{ + Name: containerName, + } +} diff --git a/pkg/testingutil/devfile.go b/pkg/testingutil/devfile.go new file mode 100644 index 0000000..74831a2 --- /dev/null +++ b/pkg/testingutil/devfile.go @@ -0,0 +1,223 @@ +package testingutil + +import ( + v1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + "github.com/devfile/api/v2/pkg/attributes" +) + +var ( + isFalse = false + isTrue = true +) + +// GetFakeContainerComponent returns a fake container component for testing. +// Deprecated: use GenerateDummyContainerComponent instead +func GetFakeContainerComponent(name string) v1.Component { + volumeName := "myvolume1" + volumePath := "/my/volume/mount/path1" + VolumeMounts := []v1.VolumeMount{ + { + Name: volumeName, + Path: volumePath, + }, + } + return GenerateDummyContainerComponent(name, VolumeMounts, nil, []v1.EnvVar{}, v1.Annotation{}, nil) +} + +// GetFakeVolumeComponent returns a fake volume component for testing +func GetFakeVolumeComponent(name, size string) v1.Component { + + return v1.Component{ + Name: name, + ComponentUnion: v1.ComponentUnion{ + Volume: &v1.VolumeComponent{ + Volume: v1.Volume{ + Size: size, + }, + }, + }, + } + +} + +// GetFakeExecRunCommands returns fake commands for testing +func GetFakeExecRunCommands() []v1.ExecCommand { + return []v1.ExecCommand{ + { + CommandLine: "ls -a", + Component: "alias1", + LabeledCommand: v1.LabeledCommand{ + BaseCommand: v1.BaseCommand{ + Group: &v1.CommandGroup{ + Kind: v1.RunCommandGroupKind, + }, + }, + }, + + WorkingDir: "/root", + }, + } +} + +// GetFakeEnv returns a fake env for testing +func GetFakeEnv(name, value string) v1.EnvVar { + return v1.EnvVar{ + Name: name, + Value: value, + } +} + +// GetFakeEnvParentOverride returns a fake envParentOverride for testing +func GetFakeEnvParentOverride(name, value string) v1.EnvVarParentOverride { + return v1.EnvVarParentOverride{ + Name: name, + Value: value, + } +} + +// GetFakeVolumeMount returns a fake volume mount for testing +func GetFakeVolumeMount(name, path string) v1.VolumeMount { + return v1.VolumeMount{ + Name: name, + Path: path, + } +} + +// GetFakeVolumeMountParentOverride returns a fake volumeMountParentOverride for testing +func GetFakeVolumeMountParentOverride(name, path string) v1.VolumeMountParentOverride { + return v1.VolumeMountParentOverride{ + Name: name, + Path: path, + } +} + +// GenerateDummyContainerComponent returns a dummy container component for testing +func GenerateDummyContainerComponent(name string, volMounts []v1.VolumeMount, endpoints []v1.Endpoint, envs []v1.EnvVar, annotation v1.Annotation, dedicatedPod *bool) v1.Component { + image := "docker.io/maven:latest" + mountSources := true + + return v1.Component{ + Name: name, + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: image, + Annotation: &annotation, + Env: envs, + VolumeMounts: volMounts, + MountSources: &mountSources, + DedicatedPod: dedicatedPod, + }, + Endpoints: endpoints, + }}} +} + +//DockerImageValues struct can be used to set override or main component struct values +type DockerImageValues struct { + //maps to Image.ImageName + ImageName string + //maps to Image.Dockerfile.DockerfileSrc.Uri + Uri string + //maps to Image.Dockerfile.BuildContext + BuildContext string + //maps to Image.Dockerfile.RootRequired + RootRequired *bool +} + +//GetDockerImageTestComponent returns a docker image component that is used for testing. +//The parameters allow customization of the content. If they are set to nil, then the properties will not be set +func GetDockerImageTestComponent(div DockerImageValues, autobuild *bool, attr attributes.Attributes) v1.Component { + comp := v1.Component{ + Name: "image", + ComponentUnion: v1.ComponentUnion{ + Image: &v1.ImageComponent{ + Image: v1.Image{ + ImageName: div.ImageName, + ImageUnion: v1.ImageUnion{ + AutoBuild: autobuild, + Dockerfile: &v1.DockerfileImage{ + DockerfileSrc: v1.DockerfileSrc{ + Uri: div.Uri, + }, + Dockerfile: v1.Dockerfile{ + BuildContext: div.BuildContext, + }, + }, + }, + }, + }, + }, + } + + if div.RootRequired != nil { + comp.Image.Dockerfile.RootRequired = div.RootRequired + } + + if attr != nil { + comp.Attributes = attr + } + + return comp +} + +//GetDockerImageTestComponentParentOverride returns a docker image parent override component that is used for testing. +//The parameters allow customization of the content. If they are set to nil, then the properties will not be set +func GetDockerImageTestComponentParentOverride(div DockerImageValues) v1.ComponentParentOverride { + comp := v1.ComponentParentOverride{ + Name: "image", + ComponentUnionParentOverride: v1.ComponentUnionParentOverride{ + Image: &v1.ImageComponentParentOverride{ + ImageParentOverride: v1.ImageParentOverride{ + ImageName: div.ImageName, + ImageUnionParentOverride: v1.ImageUnionParentOverride{ + Dockerfile: &v1.DockerfileImageParentOverride{ + DockerfileSrcParentOverride: v1.DockerfileSrcParentOverride{ + Uri: div.Uri, + }, + DockerfileParentOverride: v1.DockerfileParentOverride{ + BuildContext: div.BuildContext, + }, + }, + }, + }, + }, + }, + } + + if div.RootRequired != nil { + comp.Image.Dockerfile.RootRequired = div.RootRequired + } + + return comp +} + +//GetDockerImageTestComponentPluginOverride returns a docker image parent override component that is used for testing. +//The parameters allow customization of the content. If they are set to nil, then the properties will not be set +func GetDockerImageTestComponentPluginOverride(div DockerImageValues) v1.ComponentPluginOverride { + comp := v1.ComponentPluginOverride{ + Name: "image", + ComponentUnionPluginOverride: v1.ComponentUnionPluginOverride{ + Image: &v1.ImageComponentPluginOverride{ + ImagePluginOverride: v1.ImagePluginOverride{ + ImageName: div.ImageName, + ImageUnionPluginOverride: v1.ImageUnionPluginOverride{ + Dockerfile: &v1.DockerfileImagePluginOverride{ + DockerfileSrcPluginOverride: v1.DockerfileSrcPluginOverride{ + Uri: div.Uri, + }, + DockerfilePluginOverride: v1.DockerfilePluginOverride{ + BuildContext: div.BuildContext, + }, + }, + }, + }, + }, + }, + } + + if div.RootRequired != nil { + comp.Image.Dockerfile.RootRequired = div.RootRequired + } + + return comp +} diff --git a/pkg/testingutil/filesystem/default_fs.go b/pkg/testingutil/filesystem/default_fs.go new file mode 100644 index 0000000..9eea63f --- /dev/null +++ b/pkg/testingutil/filesystem/default_fs.go @@ -0,0 +1,179 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* + This package is a FORK of https://github.com/kubernetes/kubernetes/blob/master/pkg/util/filesystem/defaultfs.go + See above license +*/ + +package filesystem + +import ( + "io/ioutil" + "os" + "path/filepath" + "time" +) + +// DefaultFs implements Filesystem using same-named functions from "os" and "io/ioutil" +type DefaultFs struct{} + +var _ Filesystem = DefaultFs{} + +// Stat via os.Stat +func (DefaultFs) Stat(name string) (os.FileInfo, error) { + return os.Stat(name) +} + +// Create via os.Create +func (DefaultFs) Create(name string) (File, error) { + file, err := os.Create(name) + if err != nil { + return nil, err + } + return &defaultFile{file}, nil +} + +// Open via os.Open +func (DefaultFs) Open(name string) (File, error) { + file, err := os.Open(name) + if err != nil { + return nil, err + } + + return &defaultFile{file}, nil +} + +// OpenFile via os.OpenFile +func (DefaultFs) OpenFile(name string, flag int, perm os.FileMode) (File, error) { + file, err := os.OpenFile(name, flag, perm) + if err != nil { + return nil, err + } + + return &defaultFile{file}, nil +} + +// Rename via os.Rename +func (DefaultFs) Rename(oldpath, newpath string) error { + return os.Rename(oldpath, newpath) +} + +// MkdirAll via os.MkdirAll +func (DefaultFs) MkdirAll(path string, perm os.FileMode) error { + return os.MkdirAll(path, perm) +} + +// Chtimes via os.Chtimes +func (DefaultFs) Chtimes(name string, atime time.Time, mtime time.Time) error { + return os.Chtimes(name, atime, mtime) +} + +// RemoveAll via os.RemoveAll +func (DefaultFs) RemoveAll(path string) error { + return os.RemoveAll(path) +} + +// Remove via os.RemoveAll +func (DefaultFs) Remove(name string) error { + return os.Remove(name) +} + +// Getwd via os.Getwd +func (DefaultFs) Getwd() (dir string, err error) { + return os.Getwd() +} + +// ReadFile via ioutil.ReadFile +func (DefaultFs) ReadFile(filename string) ([]byte, error) { + return ioutil.ReadFile(filename) +} + +// WriteFile via ioutil.WriteFile +func (DefaultFs) WriteFile(filename string, data []byte, perm os.FileMode) error { + return ioutil.WriteFile(filename, data, perm) +} + +// TempDir via ioutil.TempDir +func (DefaultFs) TempDir(dir, prefix string) (string, error) { + return ioutil.TempDir(dir, prefix) +} + +// TempFile via ioutil.TempFile +func (DefaultFs) TempFile(dir, prefix string) (File, error) { + file, err := ioutil.TempFile(dir, prefix) + if err != nil { + return nil, err + } + return &defaultFile{file}, nil +} + +// ReadDir via ioutil.ReadDir +func (DefaultFs) ReadDir(dirname string) ([]os.FileInfo, error) { + return ioutil.ReadDir(dirname) +} + +// Walk via filepath.Walk +func (DefaultFs) Walk(root string, walkFn filepath.WalkFunc) error { + return filepath.Walk(root, walkFn) +} + +// Chmod via os.Chmod +func (f DefaultFs) Chmod(name string, mode os.FileMode) error { + return os.Chmod(name, mode) +} + +// defaultFile implements File using same-named functions from "os" +type defaultFile struct { + file *os.File +} + +// Name via os.File.Name +func (file *defaultFile) Name() string { + return file.file.Name() +} + +// Write via os.File.Write +func (file *defaultFile) Write(b []byte) (n int, err error) { + return file.file.Write(b) +} + +// WriteString via File.WriteString +func (file *defaultFile) WriteString(s string) (int, error) { + return file.file.WriteString(s) +} + +// Sync via os.File.Sync +func (file *defaultFile) Sync() error { + return file.file.Sync() +} + +// Close via os.File.Close +func (file *defaultFile) Close() error { + return file.file.Close() +} + +func (file *defaultFile) Readdir(n int) ([]os.FileInfo, error) { + return file.file.Readdir(n) +} + +func (file *defaultFile) Read(b []byte) (n int, err error) { + return file.file.Read(b) +} + +func (file *defaultFile) Chmod(name string, mode os.FileMode) error { + return file.file.Chmod(mode) +} diff --git a/pkg/testingutil/filesystem/fake_fs.go b/pkg/testingutil/filesystem/fake_fs.go new file mode 100644 index 0000000..fbf016d --- /dev/null +++ b/pkg/testingutil/filesystem/fake_fs.go @@ -0,0 +1,178 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* + This package is a FORK of https://github.com/kubernetes/kubernetes/blob/master/pkg/util/filesystem/fakefs.go + See above license +*/ + +package filesystem + +import ( + "os" + "path/filepath" + "time" + + "github.com/spf13/afero" +) + +// fakeFs is implemented in terms of afero +type fakeFs struct { + a afero.Afero +} + +// NewFakeFs returns a fake Filesystem that exists in-memory, useful for unit tests +func NewFakeFs() Filesystem { + return &fakeFs{a: afero.Afero{Fs: afero.NewMemMapFs()}} +} + +// Stat via afero.Fs.Stat +func (fs *fakeFs) Stat(name string) (os.FileInfo, error) { + return fs.a.Fs.Stat(name) +} + +// Create via afero.Fs.Create +func (fs *fakeFs) Create(name string) (File, error) { + file, err := fs.a.Fs.Create(name) + if err != nil { + return nil, err + } + return &fakeFile{file}, nil +} + +// Open via afero.Fs.Open +func (fs *fakeFs) Open(name string) (File, error) { + file, err := fs.a.Fs.Open(name) + if err != nil { + return nil, err + } + return &fakeFile{file}, nil +} + +// OpenFile via afero.Fs.OpenFile +func (fs *fakeFs) OpenFile(name string, flag int, perm os.FileMode) (File, error) { + file, err := fs.a.Fs.OpenFile(name, flag, perm) + if err != nil { + return nil, err + } + return &fakeFile{file}, nil +} + +// Rename via afero.Fs.Rename +func (fs *fakeFs) Rename(oldpath, newpath string) error { + return fs.a.Fs.Rename(oldpath, newpath) +} + +// MkdirAll via afero.Fs.MkdirAll +func (fs *fakeFs) MkdirAll(path string, perm os.FileMode) error { + return fs.a.Fs.MkdirAll(path, perm) +} + +// Chtimes via afero.Fs.Chtimes +func (fs *fakeFs) Chtimes(name string, atime time.Time, mtime time.Time) error { + return fs.a.Fs.Chtimes(name, atime, mtime) +} + +// ReadFile via afero.ReadFile +func (fs *fakeFs) ReadFile(filename string) ([]byte, error) { + return fs.a.ReadFile(filename) +} + +// WriteFile via afero.WriteFile +func (fs *fakeFs) WriteFile(filename string, data []byte, perm os.FileMode) error { + return fs.a.WriteFile(filename, data, perm) +} + +// TempDir via afero.TempDir +func (fs *fakeFs) TempDir(dir, prefix string) (string, error) { + return fs.a.TempDir(dir, prefix) +} + +// TempFile via afero.TempFile +func (fs *fakeFs) TempFile(dir, prefix string) (File, error) { + file, err := fs.a.TempFile(dir, prefix) + if err != nil { + return nil, err + } + return &fakeFile{file}, nil +} + +// ReadDir via afero.ReadDir +func (fs *fakeFs) ReadDir(dirname string) ([]os.FileInfo, error) { + return fs.a.ReadDir(dirname) +} + +// Walk via afero.Walk +func (fs *fakeFs) Walk(root string, walkFn filepath.WalkFunc) error { + return fs.a.Walk(root, walkFn) +} + +// RemoveAll via afero.RemoveAll +func (fs *fakeFs) RemoveAll(path string) error { + return fs.a.RemoveAll(path) +} + +func (fs *fakeFs) Getwd() (dir string, err error) { + return ".", nil +} + +// Remove via afero.RemoveAll +func (fs *fakeFs) Remove(name string) error { + return fs.a.Remove(name) +} + +// Chmod via afero.Chmod +func (fs *fakeFs) Chmod(name string, mode os.FileMode) error { + return fs.a.Chmod(name, mode) +} + +// fakeFile implements File; for use with fakeFs +type fakeFile struct { + file afero.File +} + +// Name via afero.File.Name +func (file *fakeFile) Name() string { + return file.file.Name() +} + +// Write via afero.File.Write +func (file *fakeFile) Write(b []byte) (n int, err error) { + return file.file.Write(b) +} + +// WriteString via afero.File.WriteString +func (file *fakeFile) WriteString(s string) (n int, err error) { + return file.file.WriteString(s) +} + +// Sync via afero.File.Sync +func (file *fakeFile) Sync() error { + return file.file.Sync() +} + +// Close via afero.File.Close +func (file *fakeFile) Close() error { + return file.file.Close() +} + +func (file *fakeFile) Readdir(n int) ([]os.FileInfo, error) { + return file.file.Readdir(n) +} + +func (file *fakeFile) Read(b []byte) (n int, err error) { + return file.file.Read(b) +} diff --git a/pkg/testingutil/filesystem/filesystem.go b/pkg/testingutil/filesystem/filesystem.go new file mode 100644 index 0000000..e71b6ff --- /dev/null +++ b/pkg/testingutil/filesystem/filesystem.go @@ -0,0 +1,65 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* + This package is a FORK of https://github.com/kubernetes/kubernetes/blob/master/pkg/util/filesystem/filesystem.go + See above license +*/ + +package filesystem + +import ( + "os" + "path/filepath" + "time" +) + +// Filesystem is an interface that we can use to mock various filesystem operations +type Filesystem interface { + // from "os" + Stat(name string) (os.FileInfo, error) + Create(name string) (File, error) + Open(name string) (File, error) + OpenFile(name string, flag int, perm os.FileMode) (File, error) + Rename(oldpath, newpath string) error + MkdirAll(path string, perm os.FileMode) error + Chtimes(name string, atime time.Time, mtime time.Time) error + RemoveAll(path string) error + Remove(name string) error + Chmod(name string, mode os.FileMode) error + Getwd() (dir string, err error) + + // from "io/ioutil" + ReadFile(filename string) ([]byte, error) + WriteFile(filename string, data []byte, perm os.FileMode) error + TempDir(dir, prefix string) (string, error) + TempFile(dir, prefix string) (File, error) + ReadDir(dirname string) ([]os.FileInfo, error) + Walk(root string, walkFn filepath.WalkFunc) error +} + +// File is an interface that we can use to mock various filesystem operations typically +// accessed through the File object from the "os" package +type File interface { + // for now, the only os.File methods used are those below, add more as necessary + Name() string + Write(b []byte) (n int, err error) + WriteString(s string) (n int, err error) + Sync() error + Close() error + Read(b []byte) (n int, err error) + Readdir(n int) ([]os.FileInfo, error) +} diff --git a/pkg/testingutil/filesystem/singleton.go b/pkg/testingutil/filesystem/singleton.go new file mode 100644 index 0000000..a8dcea8 --- /dev/null +++ b/pkg/testingutil/filesystem/singleton.go @@ -0,0 +1,10 @@ +package filesystem + +var singleFs Filesystem + +func Get() Filesystem { + if singleFs == nil { + singleFs = &DefaultFs{} + } + return singleFs +} diff --git a/pkg/testingutil/filesystem/watcher.go b/pkg/testingutil/filesystem/watcher.go new file mode 100644 index 0000000..af93ffb --- /dev/null +++ b/pkg/testingutil/filesystem/watcher.go @@ -0,0 +1,88 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* + This package is a FORK of https://github.com/kubernetes/kubernetes/blob/master/pkg/util/filesystem/watcher.go + See above license +*/ + +package filesystem + +import ( + "github.com/fsnotify/fsnotify" +) + +// FSWatcher is a callback-based filesystem watcher abstraction for fsnotify. +type FSWatcher interface { + // Initializes the watcher with the given watch handlers. + // Called before all other methods. + Init(FSEventHandler, FSErrorHandler) error + + // Starts listening for events and errors. + // When an event or error occurs, the corresponding handler is called. + Run() + + // Add a filesystem path to watch + AddWatch(path string) error +} + +// FSEventHandler is called when a fsnotify event occurs. +type FSEventHandler func(event fsnotify.Event) + +// FSErrorHandler is called when a fsnotify error occurs. +type FSErrorHandler func(err error) + +type fsnotifyWatcher struct { + watcher *fsnotify.Watcher + eventHandler FSEventHandler + errorHandler FSErrorHandler +} + +var _ FSWatcher = &fsnotifyWatcher{} + +func (w *fsnotifyWatcher) AddWatch(path string) error { + return w.watcher.Add(path) +} + +func (w *fsnotifyWatcher) Init(eventHandler FSEventHandler, errorHandler FSErrorHandler) error { + var err error + w.watcher, err = fsnotify.NewWatcher() + if err != nil { + return err + } + + w.eventHandler = eventHandler + w.errorHandler = errorHandler + return nil +} + +func (w *fsnotifyWatcher) Run() { + go func() { + defer w.watcher.Close() + for { + select { + case event := <-w.watcher.Events: + if w.eventHandler != nil { + w.eventHandler(event) + } + case err := <-w.watcher.Errors: + if w.errorHandler != nil { + w.errorHandler(err) + } + } + } + }() +} diff --git a/pkg/testingutil/k8sClient.go b/pkg/testingutil/k8sClient.go new file mode 100644 index 0000000..93394fd --- /dev/null +++ b/pkg/testingutil/k8sClient.go @@ -0,0 +1,32 @@ +package testingutil + +import ( + "context" + "errors" + "fmt" + + "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type FakeK8sClient struct { + client.Client // To satisfy interface; override all used methods + DevWorkspaceResources map[string]v1alpha2.DevWorkspaceTemplate + Errors map[string]string +} + +func (client *FakeK8sClient) Get(_ context.Context, namespacedName client.ObjectKey, obj client.Object) error { + template, ok := obj.(*v1alpha2.DevWorkspaceTemplate) + if !ok { + return fmt.Errorf("called Get() in fake client with non-DevWorkspaceTemplate") + } + if element, ok := client.DevWorkspaceResources[namespacedName.Name]; ok { + *template = element + return nil + } + + if err, ok := client.Errors[namespacedName.Name]; ok { + return errors.New(err) + } + return fmt.Errorf("test does not define an entry for %s", namespacedName.Name) +} diff --git a/pkg/testingutil/resources.go b/pkg/testingutil/resources.go new file mode 100644 index 0000000..e6495a7 --- /dev/null +++ b/pkg/testingutil/resources.go @@ -0,0 +1,37 @@ +package testingutil + +import ( + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" +) + +// FakeResourceRequirements creates a fake resource requirements from cpu and memory +func FakeResourceRequirements(cpu, memory string) (corev1.ResourceRequirements, error) { + var resReq corev1.ResourceRequirements + + limits := make(corev1.ResourceList) + var err error + limits[corev1.ResourceCPU], err = resource.ParseQuantity(cpu) + if err != nil { + return resReq, err + } + limits[corev1.ResourceMemory], err = resource.ParseQuantity(memory) + if err != nil { + return resReq, err + } + resReq.Limits = limits + + requests := make(corev1.ResourceList) + requests[corev1.ResourceCPU], err = resource.ParseQuantity(cpu) + if err != nil { + return resReq, err + } + requests[corev1.ResourceMemory], err = resource.ParseQuantity(memory) + if err != nil { + return resReq, err + } + + resReq.Requests = requests + + return resReq, nil +} diff --git a/pkg/util/httpcache.go b/pkg/util/httpcache.go new file mode 100644 index 0000000..8c14060 --- /dev/null +++ b/pkg/util/httpcache.go @@ -0,0 +1,29 @@ +package util + +import ( + "io/ioutil" + "os" + "path/filepath" + "time" + + "k8s.io/klog" +) + +// cleanHttpCache checks cacheDir and deletes all files that were modified more than cacheTime back +func cleanHttpCache(cacheDir string, cacheTime time.Duration) error { + cacheFiles, err := ioutil.ReadDir(cacheDir) + if err != nil { + return err + } + + for _, f := range cacheFiles { + if f.ModTime().Add(cacheTime).Before(time.Now()) { + klog.V(4).Infof("Removing cache file %s, because it is older than %s", f.Name(), cacheTime.String()) + err := os.Remove(filepath.Join(cacheDir, f.Name())) + if err != nil { + return err + } + } + } + return nil +} diff --git a/pkg/util/util.go b/pkg/util/util.go new file mode 100644 index 0000000..7d10a9b --- /dev/null +++ b/pkg/util/util.go @@ -0,0 +1,1242 @@ +package util + +import ( + "archive/zip" + "bufio" + "bytes" + "crypto/rand" + "fmt" + "io" + "io/ioutil" + "math/big" + "net" + "net/http" + "net/url" + "os" + "os/exec" + "os/signal" + "os/user" + "path" + "path/filepath" + "regexp" + "runtime" + "sort" + "strconv" + "strings" + "syscall" + "time" + + "github.com/devfile/library/pkg/testingutil/filesystem" + "github.com/fatih/color" + "github.com/gobwas/glob" + "github.com/gregjones/httpcache" + "github.com/gregjones/httpcache/diskcache" + "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + kvalidation "k8s.io/apimachinery/pkg/util/validation" + "k8s.io/client-go/util/homedir" + "k8s.io/klog" +) + +const ( + HTTPRequestTimeout = 30 * time.Second // HTTPRequestTimeout configures timeout of all HTTP requests + ResponseHeaderTimeout = 30 * time.Second // ResponseHeaderTimeout is the timeout to retrieve the server's response headers + ModeReadWriteFile = 0600 // default Permission for a file + CredentialPrefix = "odo-" // CredentialPrefix is the prefix of the credential that uses to access secure registry +) + +// httpCacheDir determines directory where odo will cache HTTP respones +var httpCacheDir = filepath.Join(os.TempDir(), "odohttpcache") + +var letterRunes = []rune("abcdefghijklmnopqrstuvwxyz") + +// 63 is the max length of a DeploymentConfig in Openshift and we also have to take into account +// that each component also gets a volume that uses the component name suffixed with -s2idata +const maxAllowedNamespacedStringLength = 63 - len("-s2idata") - 1 + +// This value can be provided to set a seperate directory for users 'homedir' resolution +// note for mocking purpose ONLY +var customHomeDir = os.Getenv("CUSTOM_HOMEDIR") + +const defaultGithubRef = "master" + +// ResourceRequirementInfo holds resource quantity before transformation into its appropriate form in container spec +type ResourceRequirementInfo struct { + ResourceType corev1.ResourceName + MinQty resource.Quantity + MaxQty resource.Quantity +} + +// HTTPRequestParams holds parameters of forming http request +type HTTPRequestParams struct { + URL string + Token string +} + +// DownloadParams holds parameters of forming file download request +type DownloadParams struct { + Request HTTPRequestParams + Filepath string +} + +// ConvertLabelsToSelector converts the given labels to selector +func ConvertLabelsToSelector(labels map[string]string) string { + var selector string + isFirst := true + for k, v := range labels { + if isFirst { + isFirst = false + if v == "" { + selector = selector + fmt.Sprintf("%v", k) + } else { + selector = fmt.Sprintf("%v=%v", k, v) + } + } else { + if v == "" { + selector = selector + fmt.Sprintf(",%v", k) + } else { + selector = selector + fmt.Sprintf(",%v=%v", k, v) + } + } + } + return selector +} + +// GenerateRandomString generates a random string of lower case characters of +// the given size +func GenerateRandomString(n int) string { + b := make([]rune, n) + + for i := range b { + // this error is ignored because it fails only when the 2nd arg of Int() is less then 0 + // which wont happen + n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letterRunes)))) + b[i] = letterRunes[n.Int64()] + } + return string(b) +} + +// In checks if the value is in the array +func In(arr []string, value string) bool { + for _, item := range arr { + if item == value { + return true + } + } + return false +} + +// NamespaceOpenShiftObject hyphenates applicationName and componentName +func NamespaceOpenShiftObject(componentName string, applicationName string) (string, error) { + + // Error if it's blank + if componentName == "" { + return "", errors.New("namespacing: component name cannot be blank") + } + + // Error if it's blank + if applicationName == "" { + return "", errors.New("namespacing: application name cannot be blank") + } + + // Return the hyphenated namespaced name + originalName := fmt.Sprintf("%s-%s", strings.Replace(componentName, "/", "-", -1), applicationName) + truncatedName := TruncateString(originalName, maxAllowedNamespacedStringLength) + if originalName != truncatedName { + klog.V(4).Infof("The combination of application %s and component %s was too long so the final name was truncated to %s", + applicationName, componentName, truncatedName) + } + return truncatedName, nil +} + +// ExtractComponentType returns only component type part from passed component type(default unqualified, fully qualified, versioned, etc...and their combinations) for use as component name +// Possible types of parameters: +// 1. "myproject/python:3.5" -- Return python +// 2. "python:3.5" -- Return python +// 3. nodejs -- Return nodejs +func ExtractComponentType(namespacedVersionedComponentType string) string { + s := strings.Split(namespacedVersionedComponentType, "/") + versionedString := s[0] + if len(s) == 2 { + versionedString = s[1] + } + s = strings.Split(versionedString, ":") + return s[0] +} + +// ParseComponentImageName returns +// 1. image name +// 2. component type i.e, builder image name +// 3. component name default value is component type else the user requested component name +// 4. component version which is by default latest else version passed with builder image name +func ParseComponentImageName(imageName string) (string, string, string, string) { + // We don't have to check it anymore, Args check made sure that args has at least one item + // and no more than two + + // "Default" values + componentImageName := imageName + componentType := imageName + componentName := ExtractComponentType(componentType) + componentVersion := "latest" + + // Check if componentType includes ":", if so, then we need to spit it into using versions + if strings.ContainsAny(componentImageName, ":") { + versionSplit := strings.Split(imageName, ":") + componentType = versionSplit[0] + componentName = ExtractComponentType(componentType) + componentVersion = versionSplit[1] + } + return componentImageName, componentType, componentName, componentVersion +} + +// WIN represent the windows OS +const WIN = "windows" + +// ReadFilePath Reads file path form URL file:///C:/path/to/file to C:\path\to\file +func ReadFilePath(u *url.URL, os string) string { + location := u.Path + if os == WIN { + location = strings.Replace(u.Path, "/", "\\", -1) + location = location[1:] + } + return location +} + +// GenFileURL Converts file path on windows to /C:/path/to/file to work in URL +func GenFileURL(location string, os ...string) string { + // param os is made variadic only for the purpose of UTs but need not be passed mandatorily + currOS := runtime.GOOS + if len(os) > 0 { + currOS = os[0] + } + urlPath := location + if currOS == WIN { + urlPath = "/" + strings.Replace(location, "\\", "/", -1) + } + return "file://" + urlPath +} + +// ConvertKeyValueStringToMap converts String Slice of Parameters to a Map[String]string +// Each value of the slice is expected to be in the key=value format +// Values that do not conform to this "spec", will be ignored +func ConvertKeyValueStringToMap(params []string) map[string]string { + result := make(map[string]string, len(params)) + for _, param := range params { + str := strings.Split(param, "=") + if len(str) != 2 { + klog.Fatalf("Parameter %s is not in the expected key=value format", param) + } else { + result[str[0]] = str[1] + } + } + return result +} + +// TruncateString truncates passed string to given length +// Note: if -1 is passed, the original string is returned +func TruncateString(str string, maxLen int) string { + if maxLen == -1 { + return str + } + if len(str) > maxLen { + return str[:maxLen] + } + return str +} + +// GetAbsPath returns absolute path from passed file path resolving even ~ to user home dir and any other such symbols that are only +// shell expanded can also be handled here +func GetAbsPath(path string) (string, error) { + // Only shell resolves `~` to home so handle it specially + var dir string + if strings.HasPrefix(path, "~") { + if len(customHomeDir) > 0 { + dir = customHomeDir + } else { + usr, err := user.Current() + if err != nil { + return path, errors.Wrapf(err, "unable to resolve %s to absolute path", path) + } + dir = usr.HomeDir + } + + if len(path) > 1 { + path = filepath.Join(dir, path[1:]) + } else { + path = dir + } + } + + path, err := filepath.Abs(path) + if err != nil { + return path, errors.Wrapf(err, "unable to resolve %s to absolute path", path) + } + return path, nil +} + +// GetRandomName returns a randomly generated name which can be used for naming odo and/or openshift entities +// prefix: Desired prefix part of the name +// prefixMaxLen: Desired maximum length of prefix part of random name; if -1 is passed, no limit on length will be enforced +// existList: List to verify that the returned name does not already exist +// retries: number of retries to try generating a unique name +// Returns: +// 1. randomname: is prefix-suffix, where: +// prefix: string passed as prefix or fetched current directory of length same as the passed prefixMaxLen +// suffix: 4 char random string +// 2. error: if requested number of retries also failed to generate unique name +func GetRandomName(prefix string, prefixMaxLen int, existList []string, retries int) (string, error) { + prefix = TruncateString(GetDNS1123Name(strings.ToLower(prefix)), prefixMaxLen) + name := fmt.Sprintf("%s-%s", prefix, GenerateRandomString(4)) + + //Create a map of existing names for efficient iteration to find if the newly generated name is same as any of the already existing ones + existingNames := make(map[string]bool) + for _, existingName := range existList { + existingNames[existingName] = true + } + + // check if generated name is already used in the existList + if _, ok := existingNames[name]; ok { + prevName := name + trial := 0 + // keep generating names until generated name is not unique. So, loop terminates when name is unique and hence for condition is false + for ok { + trial = trial + 1 + prevName = name + // Attempt unique name generation from prefix-suffix by concatenating prefix-suffix withrandom string of length 4 + prevName = fmt.Sprintf("%s-%s", prevName, GenerateRandomString(4)) + _, ok = existingNames[prevName] + if trial >= retries { + // Avoid infinite loops and fail after passed number of retries + return "", fmt.Errorf("failed to generate a unique name even after %d retrials", retries) + } + } + // If found to be unique, set name as generated name + name = prevName + } + // return name + return name, nil +} + +// GetDNS1123Name Converts passed string into DNS-1123 string +func GetDNS1123Name(str string) string { + nonAllowedCharsRegex := regexp.MustCompile(`[^a-zA-Z0-9_-]+`) + withReplacedChars := strings.Replace( + nonAllowedCharsRegex.ReplaceAllString(str, "-"), + "--", "-", -1) + return removeNonAlphaSuffix(removeNonAlphaPrefix(withReplacedChars)) +} + +func removeNonAlphaPrefix(input string) string { + regex := regexp.MustCompile("^[^a-zA-Z0-9]+(.*)$") + return regex.ReplaceAllString(input, "$1") +} + +func removeNonAlphaSuffix(input string) string { + suffixRegex := regexp.MustCompile("^(.*?)[^a-zA-Z0-9]+$") //regex that strips all trailing non alpha-numeric chars + matches := suffixRegex.FindStringSubmatch(input) + matchesLength := len(matches) + if matchesLength == 0 { + // in this case the string does not contain a non-alphanumeric suffix + return input + } else { + // in this case we return the smallest match which in the last element in the array + return matches[matchesLength-1] + } +} + +// SliceDifference returns the values of s2 that do not exist in s1 +func SliceDifference(s1 []string, s2 []string) []string { + mb := map[string]bool{} + for _, x := range s1 { + mb[x] = true + } + difference := []string{} + for _, x := range s2 { + if _, ok := mb[x]; !ok { + difference = append(difference, x) + } + } + return difference +} + +// OpenBrowser opens the URL within the users default browser +func OpenBrowser(url string) error { + var err error + + switch runtime.GOOS { + case "linux": + err = exec.Command("xdg-open", url).Start() + case "windows": + err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start() + case "darwin": + err = exec.Command("open", url).Start() + default: + err = fmt.Errorf("unsupported platform") + } + if err != nil { + return err + } + + return nil +} + +// FetchResourceQuantity takes passed min, max and requested resource quantities and returns min and max resource requests +func FetchResourceQuantity(resourceType corev1.ResourceName, min string, max string, request string) (*ResourceRequirementInfo, error) { + if min == "" && max == "" && request == "" { + return nil, nil + } + // If minimum and maximum both are passed they carry highest priority + // Otherwise, use the request as min and max + var minResource resource.Quantity + var maxResource resource.Quantity + if min != "" { + resourceVal, err := resource.ParseQuantity(min) + if err != nil { + return nil, err + } + minResource = resourceVal + } + if max != "" { + resourceVal, err := resource.ParseQuantity(max) + if err != nil { + return nil, err + } + maxResource = resourceVal + } + if request != "" && (min == "" || max == "") { + resourceVal, err := resource.ParseQuantity(request) + if err != nil { + return nil, err + } + minResource = resourceVal + maxResource = resourceVal + } + return &ResourceRequirementInfo{ + ResourceType: resourceType, + MinQty: minResource, + MaxQty: maxResource, + }, nil +} + +// CheckPathExists checks if a path exists or not +func CheckPathExists(path string) bool { + if _, err := os.Stat(path); !os.IsNotExist(err) { + // path to file does exist + return true + } + klog.V(4).Infof("path %s doesn't exist, skipping it", path) + return false +} + +// GetHostWithPort parses provided url and returns string formated as +// host:port even if port was not specifically specified in the origin url. +// If port is not specified, standart port corresponding to url schema is provided. +// example: for url https://example.com function will return "example.com:443" +// for url https://example.com:8443 function will return "example:8443" +func GetHostWithPort(inputURL string) (string, error) { + u, err := url.Parse(inputURL) + if err != nil { + return "", errors.Wrapf(err, "error while getting port for url %s ", inputURL) + } + + port := u.Port() + address := u.Host + // if port is not specified try to detect it based on provided scheme + if port == "" { + portInt, err := net.LookupPort("tcp", u.Scheme) + if err != nil { + return "", errors.Wrapf(err, "error while getting port for url %s ", inputURL) + } + port = strconv.Itoa(portInt) + address = fmt.Sprintf("%s:%s", u.Host, port) + } + return address, nil +} + +// GetIgnoreRulesFromDirectory reads the .odoignore file, if present, and reads the rules from it +// if the .odoignore file is not found, then .gitignore is searched for the rules +// if both are not found, return empty array +// directory is the name of the directory to look into for either of the files +// rules is the array of rules (in string form) +func GetIgnoreRulesFromDirectory(directory string) ([]string, error) { + rules := []string{".git"} + // checking for presence of .odoignore file + pathIgnore := filepath.Join(directory, ".odoignore") + if _, err := os.Stat(pathIgnore); os.IsNotExist(err) || err != nil { + // .odoignore doesn't exist + // checking presence of .gitignore file + pathIgnore = filepath.Join(directory, ".gitignore") + if _, err := os.Stat(pathIgnore); os.IsNotExist(err) || err != nil { + // both doesn't exist, return empty array + return rules, nil + } + } + + file, err := os.Open(pathIgnore) + if err != nil { + return nil, err + } + + defer file.Close() // #nosec G307 + + scanner := bufio.NewReader(file) + for { + line, _, err := scanner.ReadLine() + if err != nil { + if err == io.EOF { + break + } + + return rules, err + } + spaceTrimmedLine := strings.TrimSpace(string(line)) + if len(spaceTrimmedLine) > 0 && !strings.HasPrefix(string(line), "#") && !strings.HasPrefix(string(line), ".git") { + rules = append(rules, string(line)) + } + } + + return rules, nil +} + +// GetAbsGlobExps converts the relative glob expressions into absolute glob expressions +// returns the absolute glob expressions +func GetAbsGlobExps(directory string, globExps []string) []string { + absGlobExps := []string{} + for _, globExp := range globExps { + // for glob matching with the library + // the relative paths in the glob expressions need to be converted to absolute paths + absGlobExps = append(absGlobExps, filepath.Join(directory, globExp)) + } + return absGlobExps +} + +// GetSortedKeys retrieves the alphabetically-sorted keys of the specified map +func GetSortedKeys(mapping map[string]string) []string { + keys := make([]string, len(mapping)) + + i := 0 + for k := range mapping { + keys[i] = k + i++ + } + + sort.Strings(keys) + + return keys +} + +// GetSplitValuesFromStr returns a slice containing the split string, using ',' as a separator +func GetSplitValuesFromStr(inputStr string) []string { + if len(inputStr) == 0 { + return []string{} + } + + result := strings.Split(inputStr, ",") + for i, value := range result { + result[i] = strings.TrimSpace(value) + } + return result +} + +// GetContainerPortsFromStrings generates ContainerPort values from the array of string port values +// ports is the array containing the string port values +func GetContainerPortsFromStrings(ports []string) ([]corev1.ContainerPort, error) { + var containerPorts []corev1.ContainerPort + for _, port := range ports { + splits := strings.Split(port, "/") + if len(splits) < 1 || len(splits) > 2 { + return nil, fmt.Errorf("unable to parse the port string %s", port) + } + + portNumberI64, err := strconv.ParseInt(splits[0], 10, 32) + if err != nil { + return nil, fmt.Errorf("invalid port number %s", splits[0]) + } + portNumber := int32(portNumberI64) + + var portProto corev1.Protocol + if len(splits) == 2 { + switch strings.ToUpper(splits[1]) { + case "TCP": + portProto = corev1.ProtocolTCP + case "UDP": + portProto = corev1.ProtocolUDP + default: + return nil, fmt.Errorf("invalid port protocol %s", splits[1]) + } + } else { + portProto = corev1.ProtocolTCP + } + + port := corev1.ContainerPort{ + Name: fmt.Sprintf("%d-%s", portNumber, strings.ToLower(string(portProto))), + ContainerPort: portNumber, + Protocol: portProto, + } + containerPorts = append(containerPorts, port) + } + return containerPorts, nil +} + +// IsGlobExpMatch compiles strToMatch against each of the passed globExps +// Parameters: +// strToMatch : a string for matching against the rules +// globExps : a list of glob patterns to match strToMatch with +// Returns: true if there is any match else false the error (if any) +// Notes: +// Source as well as glob expression to match is changed to forward +// slashes due to supporting Windows as well as support with the +// "github.com/gobwas/glob" library that we use. +func IsGlobExpMatch(strToMatch string, globExps []string) (bool, error) { + + // Replace all backslashes with forward slashes in order for + // glob / expression matching to work correctly with + // the "github.com/gobwas/glob" library + strToMatch = strings.Replace(strToMatch, "\\", "/", -1) + + for _, globExp := range globExps { + + // We replace backslashes with forward slashes for + // glob expression / matching support + globExp = strings.Replace(globExp, "\\", "/", -1) + + pattern, err := glob.Compile(globExp) + if err != nil { + return false, err + } + matched := pattern.Match(strToMatch) + if matched { + klog.V(4).Infof("ignoring path %s because of glob rule %s", strToMatch, globExp) + return true, nil + } + } + return false, nil +} + +// CheckOutputFlag returns true if specified output format is supported +func CheckOutputFlag(outputFlag string) bool { + if outputFlag == "json" || outputFlag == "" { + return true + } + return false +} + +// RemoveDuplicates goes through a string slice and removes all duplicates. +// Reference: https://siongui.github.io/2018/04/14/go-remove-duplicates-from-slice-or-array/ +func RemoveDuplicates(s []string) []string { + + // Make a map and go through each value to see if it's a duplicate or not + m := make(map[string]bool) + for _, item := range s { + if _, ok := m[item]; !ok { + m[item] = true + } + } + + // Append to the unique string + var result []string + for item := range m { + result = append(result, item) + } + return result +} + +// RemoveRelativePathFromFiles removes a specified path from a list of files +func RemoveRelativePathFromFiles(files []string, path string) ([]string, error) { + + removedRelativePathFiles := []string{} + for _, file := range files { + rel, err := filepath.Rel(path, file) + if err != nil { + return []string{}, err + } + removedRelativePathFiles = append(removedRelativePathFiles, rel) + } + + return removedRelativePathFiles, nil +} + +// DeletePath deletes a file/directory if it exists and doesn't throw error if it doesn't exist +func DeletePath(path string) error { + _, err := os.Stat(path) + + // reason for double negative is os.IsExist() would be blind to EMPTY FILE. + if !os.IsNotExist(err) { + return os.Remove(path) + } + return nil +} + +// HTTPGetFreePort gets a free port from the system +func HTTPGetFreePort() (int, error) { + listener, err := net.Listen("tcp", "localhost:0") + if err != nil { + return -1, err + } + freePort := listener.Addr().(*net.TCPAddr).Port + err = listener.Close() + if err != nil { + return -1, err + } + return freePort, nil +} + +// IsEmpty checks to see if a directory is empty +// shamelessly taken from: https://stackoverflow.com/questions/30697324/how-to-check-if-directory-on-path-is-empty +// this helps detect any edge cases where an empty directory is copied over +func IsEmpty(name string) (bool, error) { + f, err := os.Open(name) + if err != nil { + return false, err + } + defer f.Close() // #nosec G307 + + _, err = f.Readdirnames(1) // Or f.Readdir(1) + if err == io.EOF { + return true, nil + } + return false, err // Either not empty or error, suits both cases +} + +// GetRemoteFilesMarkedForDeletion returns the list of remote files marked for deletion +func GetRemoteFilesMarkedForDeletion(delSrcRelPaths []string, remoteFolder string) []string { + var rmPaths []string + for _, delRelPath := range delSrcRelPaths { + // since the paths inside the container are linux oriented + // so we convert the paths accordingly + rmPaths = append(rmPaths, filepath.ToSlash(filepath.Join(remoteFolder, delRelPath))) + } + return rmPaths +} + +// HTTPGetRequest gets resource contents given URL and token (if applicable) +// cacheFor determines how long the response should be cached (in minutes), 0 for no caching +func HTTPGetRequest(request HTTPRequestParams, cacheFor int) ([]byte, error) { + // Build http request + req, err := http.NewRequest("GET", request.URL, nil) + if err != nil { + return nil, err + } + if request.Token != "" { + bearer := "Bearer " + request.Token + req.Header.Add("Authorization", bearer) + } + + httpClient := &http.Client{ + Transport: &http.Transport{ + ResponseHeaderTimeout: ResponseHeaderTimeout, + }, + Timeout: HTTPRequestTimeout, + } + + klog.V(4).Infof("HTTPGetRequest: %s", req.URL.String()) + + if cacheFor > 0 { + // if there is an error during cache setup we show warning and continue without using cache + cacheError := false + httpCacheTime := time.Duration(cacheFor) * time.Minute + + // make sure that cache directory exists + err = os.MkdirAll(httpCacheDir, 0750) + if err != nil { + cacheError = true + klog.WarningDepth(4, "Unable to setup cache: ", err) + } + err = cleanHttpCache(httpCacheDir, httpCacheTime) + if err != nil { + cacheError = true + klog.WarningDepth(4, "Unable to clean up cache directory: ", err) + } + + if !cacheError { + httpClient.Transport = httpcache.NewTransport(diskcache.New(httpCacheDir)) + klog.V(4).Infof("Response will be cached in %s for %s", httpCacheDir, httpCacheTime) + } else { + klog.V(4).Info("Response won't be cached.") + } + } + + resp, err := httpClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.Header.Get(httpcache.XFromCache) != "" { + klog.V(4).Infof("Cached response used.") + } + + // We have a non 1xx / 2xx status, return an error + if (resp.StatusCode - 300) > 0 { + return nil, errors.Errorf("failed to retrieve %s, %v: %s", request.URL, resp.StatusCode, http.StatusText(resp.StatusCode)) + } + + // Process http response + bytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + return bytes, err +} + +// FilterIgnores applies the glob rules on the filesChanged and filesDeleted and filters them +// returns the filtered results which match any of the glob rules +func FilterIgnores(filesChanged, filesDeleted, absIgnoreRules []string) (filesChangedFiltered, filesDeletedFiltered []string) { + for _, file := range filesChanged { + match, err := IsGlobExpMatch(file, absIgnoreRules) + if err != nil { + continue + } + if !match { + filesChangedFiltered = append(filesChangedFiltered, file) + } + } + + for _, file := range filesDeleted { + match, err := IsGlobExpMatch(file, absIgnoreRules) + if err != nil { + continue + } + if !match { + filesDeletedFiltered = append(filesDeletedFiltered, file) + } + } + return filesChangedFiltered, filesDeletedFiltered +} + +// IsValidProjectDir checks that the folder to download the project from devfile is +// either empty or only contains the devfile used. +func IsValidProjectDir(path string, devfilePath string) error { + files, err := ioutil.ReadDir(path) + if err != nil { + return err + } + + if len(files) > 1 { + return errors.Errorf("Folder %s is not empty. It can only contain the devfile used.", path) + } else if len(files) == 1 { + file := files[0] + if file.IsDir() { + return errors.Errorf("Folder %s is not empty. It contains a subfolder.", path) + } + fileName := files[0].Name() + devfilePath = strings.TrimPrefix(devfilePath, "./") + if fileName != devfilePath { + return errors.Errorf("Folder %s contains one element and it's not the devfile used.", path) + } + } + + return nil +} + +// Converts Git ssh remote to https +func ConvertGitSSHRemoteToHTTPS(remote string) string { + remote = strings.Replace(remote, ":", "/", 1) + remote = strings.Replace(remote, "git@", "https://", 1) + return remote +} + +// GetAndExtractZip downloads a zip file from a URL with a http prefix or +// takes an absolute path prefixed with file:// and extracts it to a destination. +// pathToUnzip specifies the path within the zip folder to extract +func GetAndExtractZip(zipURL string, destination string, pathToUnzip string) error { + if zipURL == "" { + return errors.Errorf("Empty zip url: %s", zipURL) + } + + var pathToZip string + if strings.HasPrefix(zipURL, "file://") { + pathToZip = strings.TrimPrefix(zipURL, "file:/") + if runtime.GOOS == "windows" { + pathToZip = strings.Replace(pathToZip, "\\", "/", -1) + } + } else if strings.HasPrefix(zipURL, "http://") || strings.HasPrefix(zipURL, "https://") { + // Generate temporary zip file location + time := time.Now().Format(time.RFC3339) + time = strings.Replace(time, ":", "-", -1) // ":" is illegal char in windows + pathToZip = path.Join(os.TempDir(), "_"+time+".zip") + + params := DownloadParams{ + Request: HTTPRequestParams{ + URL: zipURL, + }, + Filepath: pathToZip, + } + err := DownloadFile(params) + if err != nil { + return err + } + + defer func() { + if err := DeletePath(pathToZip); err != nil { + klog.Errorf("Could not delete temporary directory for zip file. Error: %s", err) + } + }() + } else { + return errors.Errorf("Invalid Zip URL: %s . Should either be prefixed with file://, http:// or https://", zipURL) + } + + filenames, err := Unzip(pathToZip, destination, pathToUnzip) + if err != nil { + return err + } + + if len(filenames) == 0 { + return errors.New("no files were unzipped, ensure that the project repo is not empty or that sparseCheckoutDir has a valid path") + } + + return nil +} + +// Unzip will decompress a zip archive, moving specified files and folders +// within the zip file (parameter 1) to an output directory (parameter 2) +// Source: https://golangcode.com/unzip-files-in-go/ +// pathToUnzip (parameter 3) is the path within the zip folder to extract +func Unzip(src, dest, pathToUnzip string) ([]string, error) { + var filenames []string + + r, err := zip.OpenReader(src) + if err != nil { + return filenames, err + } + defer r.Close() + + // change path separator to correct character + pathToUnzip = filepath.FromSlash(pathToUnzip) + + // removes first slash of pathToUnzip if present + pathToUnzip = strings.TrimPrefix(pathToUnzip, string(os.PathSeparator)) + + for _, f := range r.File { + // Store filename/path for returning and using later on + index := strings.Index(f.Name, "/") + filename := filepath.FromSlash(f.Name[index+1:]) + if filename == "" { + continue + } + + // if sparseCheckoutDir has a pattern + match, err := filepath.Match(pathToUnzip, filename) + if err != nil { + return filenames, err + } + + // destination filepath before trim + fpath := filepath.Join(dest, filename) + + // used for pattern matching + fpathDir := filepath.Dir(fpath) + + // check for prefix or match + if strings.HasPrefix(filename, pathToUnzip) { + filename = strings.TrimPrefix(filename, pathToUnzip) + } else if !strings.HasPrefix(filename, pathToUnzip) && !match && !sliceContainsString(fpathDir, filenames) { + continue + } + // adds trailing slash to destination if needed as filepath.Join removes it + if (len(filename) == 1 && os.IsPathSeparator(filename[0])) || filename == "" { + fpath = dest + string(os.PathSeparator) + } else { + fpath = filepath.Join(dest, filename) + } + // Check for ZipSlip. More Info: http://bit.ly/2MsjAWE + if !strings.HasPrefix(fpath, filepath.Clean(dest)+string(os.PathSeparator)) { + return filenames, fmt.Errorf("%s: illegal file path", fpath) + } + + filenames = append(filenames, fpath) + + if f.FileInfo().IsDir() { + // Make Folder + if err = os.MkdirAll(fpath, os.ModePerm); err != nil { + return filenames, err + } + continue + } + + // Make File + if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil { + return filenames, err + } + + outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, ModeReadWriteFile) + if err != nil { + return filenames, err + } + + rc, err := f.Open() + if err != nil { + return filenames, err + } + + // limit the number of bytes copied from a file + // This is set to the limit of file size in Github + // which is 100MB + limited := io.LimitReader(rc, 100*1024*1024) + + _, err = io.Copy(outFile, limited) + + // Close the file without defer to close before next iteration of loop + outFile.Close() + rc.Close() + + if err != nil { + return filenames, err + } + } + return filenames, nil +} + +// DownloadFileWithCache downloads the file to the filepath given URL and token (if applicable) +// cacheFor determines how long the response should be cached (in minutes), 0 for no caching +func DownloadFileWithCache(params DownloadParams, cacheFor int) error { + // Get the data + data, err := HTTPGetRequest(params.Request, cacheFor) + if err != nil { + return err + } + + // Create the file + out, err := os.Create(params.Filepath) + if err != nil { + return err + } + defer out.Close() // #nosec G307 + + // Write the data to file + _, err = out.Write(data) + if err != nil { + return err + } + + return nil +} + +// DownloadFile downloads the file to the filepath given URL and token (if applicable) +func DownloadFile(params DownloadParams) error { + return DownloadFileWithCache(params, 0) +} + +// DownloadFileInMemory uses the url to download the file and return bytes +func DownloadFileInMemory(url string) ([]byte, error) { + var httpClient = &http.Client{Transport: &http.Transport{ + ResponseHeaderTimeout: ResponseHeaderTimeout, + }, Timeout: HTTPRequestTimeout} + resp, err := httpClient.Get(url) + if err != nil { + return nil, err + } + // We have a non 1xx / 2xx status, return an error + if (resp.StatusCode - 300) > 0 { + return nil, errors.Errorf("failed to retrieve %s, %v: %s", url, resp.StatusCode, http.StatusText(resp.StatusCode)) + } + defer resp.Body.Close() + + return ioutil.ReadAll(resp.Body) +} + +// ValidateK8sResourceName sanitizes kubernetes resource name with the following requirements: +// - Contain at most 63 characters +// - Contain only lowercase alphanumeric characters or ‘-’ +// - Start with an alphanumeric character +// - End with an alphanumeric character +// - Must not contain all numeric values +func ValidateK8sResourceName(key string, value string) error { + requirements := ` +- Contain at most 63 characters +- Contain only lowercase alphanumeric characters or ‘-’ +- Start with an alphanumeric character +- End with an alphanumeric character +- Must not contain all numeric values + ` + err1 := kvalidation.IsDNS1123Label(value) + _, err2 := strconv.ParseFloat(value, 64) + + if err1 != nil || err2 == nil { + return errors.Errorf("%s \"%s\" is not valid, %s should conform the following requirements: %s", key, value, key, requirements) + } + + return nil +} + +// CheckKubeConfigExist checks for existence of kubeconfig +func CheckKubeConfigExist() bool { + + var kubeconfig string + + if os.Getenv("KUBECONFIG") != "" { + kubeconfig = os.Getenv("KUBECONFIG") + } else { + if home := homedir.HomeDir(); home != "" { + kubeconfig = filepath.Join(home, ".kube", "config") + klog.V(4).Infof("using default kubeconfig path %s", kubeconfig) + } else { + klog.V(4).Infof("no KUBECONFIG provided and cannot fallback to default") + return false + } + } + + if CheckPathExists(kubeconfig) { + return true + } + + return false +} + +// ValidateURL validates the URL +func ValidateURL(sourceURL string) error { + u, err := url.Parse(sourceURL) + if err != nil { + return err + } + + if len(u.Host) == 0 || len(u.Scheme) == 0 { + return errors.New("URL is invalid") + } + + return nil +} + +// ValidateFile validates the file +func ValidateFile(filePath string) error { + // Check if the file path exist + file, err := os.Stat(filePath) + if err != nil { + return err + } + + if file.IsDir() { + return errors.Errorf("%s exists but it's not a file", filePath) + } + + return nil +} + +// CopyFile copies file from source path to destination path +func CopyFile(srcPath string, dstPath string, info os.FileInfo) error { + // In order to avoid file overriding issue, do nothing if source path is equal to destination path + if PathEqual(srcPath, dstPath) { + return nil + } + // Check if the source file path exists + err := ValidateFile(srcPath) + if err != nil { + return err + } + + // Open source file + srcFile, err := os.Open(srcPath) + if err != nil { + return err + } + defer srcFile.Close() // #nosec G307 + + // Create destination file + dstFile, err := os.Create(dstPath) + if err != nil { + return err + } + defer dstFile.Close() // #nosec G307 + + // Ensure destination file has the same file mode with source file + err = os.Chmod(dstFile.Name(), info.Mode()) + if err != nil { + return err + } + + // Copy file + _, err = io.Copy(dstFile, srcFile) + if err != nil { + return err + } + + return nil +} + +// PathEqual compare the paths to determine if they are equal +func PathEqual(firstPath string, secondPath string) bool { + firstAbsPath, _ := GetAbsPath(firstPath) + secondAbsPath, _ := GetAbsPath(secondPath) + return firstAbsPath == secondAbsPath +} + +// sliceContainsString checks for existence of given string in given slice +func sliceContainsString(str string, slice []string) bool { + for _, b := range slice { + if b == str { + return true + } + } + return false +} + +// AddFileToIgnoreFile adds a file to the gitignore file. It only does that if the file doesn't exist +func AddFileToIgnoreFile(gitIgnoreFile, filename string) error { + return addFileToIgnoreFile(gitIgnoreFile, filename, filesystem.DefaultFs{}) +} + +func addFileToIgnoreFile(gitIgnoreFile, filename string, fs filesystem.Filesystem) error { + var data []byte + file, err := fs.OpenFile(gitIgnoreFile, os.O_APPEND|os.O_RDWR, ModeReadWriteFile) + if err != nil { + return errors.Wrap(err, "failed to open .gitignore file") + } + defer file.Close() + + if data, err = fs.ReadFile(gitIgnoreFile); err != nil { + return errors.Wrap(err, fmt.Sprintf("failed reading data from %v file", gitIgnoreFile)) + } + // check whether .odo/odo-file-index.json is already in the .gitignore file + if !strings.Contains(string(data), filename) { + if _, err := file.WriteString("\n" + filename); err != nil { + return errors.Wrapf(err, "failed to add %v to %v file", filepath.Base(filename), gitIgnoreFile) + } + } + return nil +} + +// DisplayLog displays logs to user stdout with some color formatting +func DisplayLog(followLog bool, rd io.ReadCloser, compName string) (err error) { + + defer rd.Close() + + // Copy to stdout (in yellow) + color.Set(color.FgYellow) + defer color.Unset() + + // If we are going to followLog, we'll be copying it to stdout + // else, we copy it to a buffer + if followLog { + + c := make(chan os.Signal) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + go func() { + <-c + color.Unset() + os.Exit(1) + }() + + if _, err = io.Copy(os.Stdout, rd); err != nil { + return errors.Wrapf(err, "error followLoging logs for %s", compName) + } + + } else { + + // Copy to buffer (we aren't going to be followLoging the logs..) + buf := new(bytes.Buffer) + _, err = io.Copy(buf, rd) + if err != nil { + return errors.Wrapf(err, "unable to copy followLog to buffer") + } + + // Copy to stdout + if _, err = io.Copy(os.Stdout, buf); err != nil { + return errors.Wrapf(err, "error copying logs to stdout") + } + + } + return + +} diff --git a/tests/examples/source/devfiles/nodejs/devfile-2.1.0.yaml b/tests/examples/source/devfiles/nodejs/devfile-2.1.0.yaml new file mode 100644 index 0000000..26af497 --- /dev/null +++ b/tests/examples/source/devfiles/nodejs/devfile-2.1.0.yaml @@ -0,0 +1,61 @@ +commands: +- exec: + commandLine: npm install + component: runtime + group: + isDefault: true + kind: build + workingDir: /project + id: install +- exec: + commandLine: npm start + component: runtime + group: + isDefault: true + kind: run + workingDir: /project + id: run +- exec: + commandLine: npm run debug + component: runtime + group: + isDefault: true + kind: debug + workingDir: /project + id: debug +- exec: + commandLine: npm test + component: runtime + group: + isDefault: true + kind: test + workingDir: /project + id: test +components: +- container: + endpoints: + - name: http-3000 + targetPort: 3000 + image: registry.access.redhat.com/ubi8/nodejs-14:latest + memoryLimit: 1024Mi + mountSources: true + sourceMapping: /project + name: runtime +metadata: + description: Stack with Node.js 14 + displayName: Node.js Runtime + icon: https://nodejs.org/static/images/logos/nodejs-new-pantone-black.svg + language: javascript + name: nodejs-prj1-api-abhz + projectType: nodejs + tags: + - NodeJS + - Express + - ubi8 + version: 1.0.1 +schemaVersion: 2.1.0 +starterProjects: +- git: + remotes: + origin: https://github.com/odo-devfiles/nodejs-ex.git + name: nodejs-starter diff --git a/tests/examples/source/devfiles/nodejs/devfile-deploy-with-k8s-uri.yaml b/tests/examples/source/devfiles/nodejs/devfile-deploy-with-k8s-uri.yaml new file mode 100644 index 0000000..9f1c6b4 --- /dev/null +++ b/tests/examples/source/devfiles/nodejs/devfile-deploy-with-k8s-uri.yaml @@ -0,0 +1,89 @@ +schemaVersion: 2.2.0 +metadata: + description: Stack with Node.js 16 + displayName: Node.js Runtime + icon: https://nodejs.org/static/images/logos/nodejs-new-pantone-black.svg + language: javascript + name: nodejs-prj1-api-abhz + projectType: nodejs + tags: + - NodeJS + - Express + - ubi8 + version: 1.0.1 + +starterProjects: + - git: + remotes: + origin: https://github.com/odo-devfiles/nodejs-ex.git + name: nodejs-starter + +variables: + CONTAINER_IMAGE: quay.io/unknown-account/myimage + +commands: +- exec: + commandLine: npm install + component: runtime + group: + isDefault: true + kind: build + workingDir: $PROJECT_SOURCE + id: install +- exec: + commandLine: npm start + component: runtime + group: + isDefault: true + kind: run + workingDir: $PROJECT_SOURCE + id: run +- exec: + commandLine: npm run debug + component: runtime + group: + isDefault: true + kind: debug + workingDir: $PROJECT_SOURCE + id: debug +- exec: + commandLine: npm test + component: runtime + group: + isDefault: true + kind: test + workingDir: $PROJECT_SOURCE + id: test +- id: build-image + apply: + component: outerloop-build +- id: deployk8s + apply: + component: outerloop-deploy +- id: deploy + composite: + commands: + - build-image + - deployk8s + group: + kind: deploy + isDefault: true + +components: +- name: runtime + container: + endpoints: + - name: http-3000 + targetPort: 3000 + image: registry.access.redhat.com/ubi8/nodejs-16-minimal:latest + memoryLimit: 1024Mi + mountSources: true + sourceMapping: $PROJECT_SOURCE +- name: outerloop-build + image: + imageName: "{{CONTAINER_IMAGE}}" + dockerfile: + uri: ./Dockerfile +- name: outerloop-deploy + kubernetes: + uri: 'kubernetes/devfile-deploy-with-k8s-uri/outerloop-deployment.yaml' diff --git a/tests/examples/source/devfiles/nodejs/devfile-deploy-with-multiple-resources-and-k8s-uri.yaml b/tests/examples/source/devfiles/nodejs/devfile-deploy-with-multiple-resources-and-k8s-uri.yaml new file mode 100644 index 0000000..698d01e --- /dev/null +++ b/tests/examples/source/devfiles/nodejs/devfile-deploy-with-multiple-resources-and-k8s-uri.yaml @@ -0,0 +1,96 @@ +commands: + - exec: + commandLine: npm install + component: runtime + group: + isDefault: true + kind: build + workingDir: /project + id: install + - exec: + commandLine: npm start + component: runtime + group: + isDefault: true + kind: run + workingDir: /project + id: run + - exec: + commandLine: npm run debug + component: runtime + group: + isDefault: true + kind: debug + workingDir: /project + id: debug + - exec: + commandLine: npm test + component: runtime + group: + isDefault: true + kind: test + workingDir: /project + id: test + - id: build-image + apply: + component: outerloop-build + - id: deployk8s + apply: + component: outerloop-deploy + - id: deployservice + apply: + component: outerloop-deploy-2 + - id: deploy + composite: + commands: + - build-image + - deployk8s + - deployservice + group: + kind: deploy + isDefault: true +components: + - container: + endpoints: + - name: http-3000 + targetPort: 3000 + image: registry.access.redhat.com/ubi8/nodejs-14:latest + memoryLimit: 1024Mi + mountSources: true + sourceMapping: /project + name: runtime + - name: outerloop-build + image: + imageName: "{{CONTAINER_IMAGE}}" + dockerfile: + uri: ./Dockerfile + buildContext: ${PROJECTS_ROOT} + rootRequired: false + + - name: outerloop-deploy + kubernetes: + uri: kubernetes/devfile-deploy-with-multiple-resources-and-k8s-uri/outerloop-deploy.yaml + - name: outerloop-deploy-2 + kubernetes: + uri: kubernetes/devfile-deploy-with-multiple-resources-and-k8s-uri/outerloop-deploy-2.yaml + +metadata: + description: Stack with Node.js 14 + displayName: Node.js Runtime + icon: https://nodejs.org/static/images/logos/nodejs-new-pantone-black.svg + language: javascript + name: mynodejs + projectType: nodejs + tags: + - NodeJS + - Express + - ubi8 + version: 1.0.1 +schemaVersion: 2.2.0 +starterProjects: + - git: + remotes: + origin: https://github.com/odo-devfiles/nodejs-ex.git + name: nodejs-starter +variables: + CONTAINER_IMAGE: quay.io/unknown-account/myimage diff --git a/tests/examples/source/devfiles/nodejs/devfile-no-endpoints.yaml b/tests/examples/source/devfiles/nodejs/devfile-no-endpoints.yaml new file mode 100644 index 0000000..45730fc --- /dev/null +++ b/tests/examples/source/devfiles/nodejs/devfile-no-endpoints.yaml @@ -0,0 +1,31 @@ +schemaVersion: 2.0.0 +metadata: + name: nodejs +starterProjects: + - name: nodejs-starter + git: + remotes: + origin: "https://github.com/odo-devfiles/nodejs-ex.git" +components: + - name: runtime + container: + image: registry.access.redhat.com/ubi8/nodejs-12:1-36 + memoryLimit: 1024Mi + mountSources: true +commands: + - id: devbuild + exec: + component: runtime + commandLine: npm install + workingDir: ${PROJECTS_ROOT} + group: + kind: build + isDefault: true + - id: devrun + exec: + component: runtime + commandLine: npm start + workingDir: ${PROJECTS_ROOT} + group: + kind: run + isDefault: true \ No newline at end of file diff --git a/tests/examples/source/devfiles/nodejs/devfile-outerloop-args.yaml b/tests/examples/source/devfiles/nodejs/devfile-outerloop-args.yaml index e8c2038..4c81160 100644 --- a/tests/examples/source/devfiles/nodejs/devfile-outerloop-args.yaml +++ b/tests/examples/source/devfiles/nodejs/devfile-outerloop-args.yaml @@ -50,7 +50,7 @@ components: args: - --unknown-flag - value - name: component-built-from-dockerfile + name: deployed metadata: description: Stack with Node.js 14 displayName: Node.js Runtime diff --git a/tests/examples/source/devfiles/nodejs/devfile-outerloop.yaml b/tests/examples/source/devfiles/nodejs/devfile-outerloop.yaml index 7e2ad8f..af2affc 100644 --- a/tests/examples/source/devfiles/nodejs/devfile-outerloop.yaml +++ b/tests/examples/source/devfiles/nodejs/devfile-outerloop.yaml @@ -47,7 +47,7 @@ components: uri: ./Dockerfile buildContext: ${PROJECTS_ROOT} rootRequired: false - name: component-built-from-dockerfile + name: deployed metadata: description: Stack with Node.js 14 displayName: Node.js Runtime diff --git a/tests/examples/source/devfiles/nodejs/devfile-with-MR-CL-CR-modified.yaml b/tests/examples/source/devfiles/nodejs/devfile-with-MR-CL-CR-modified.yaml new file mode 100644 index 0000000..099ff16 --- /dev/null +++ b/tests/examples/source/devfiles/nodejs/devfile-with-MR-CL-CR-modified.yaml @@ -0,0 +1,53 @@ +schemaVersion: 2.2.0 +metadata: + name: nodejs + projectType: nodejs + language: nodejs +starterProjects: + - name: nodejs-starter + git: + remotes: + origin: "https://github.com/odo-devfiles/nodejs-ex.git" +components: + - name: runtime + container: + image: registry.access.redhat.com/ubi8/nodejs-12:1-36 + memoryLimit: 1024Mi + memoryRequest: 550Mi + cpuLimit: 700m + cpuRequest: 250m + endpoints: + - name: "3000-tcp" + targetPort: 3000 + mountSources: true +commands: + - id: devbuild + exec: + component: runtime + commandLine: npm install + workingDir: ${PROJECTS_ROOT} + group: + kind: build + isDefault: true + - id: build + exec: + component: runtime + commandLine: npm install + workingDir: ${PROJECTS_ROOT} + group: + kind: build + - id: devrun + exec: + component: runtime + commandLine: npm start + workingDir: ${PROJECTS_ROOT} + group: + kind: run + isDefault: true + - id: run + exec: + component: runtime + commandLine: npm start + workingDir: ${PROJECTS_ROOT} + group: + kind: run diff --git a/tests/examples/source/devfiles/nodejs/devfile-with-MR-CL-CR.yaml b/tests/examples/source/devfiles/nodejs/devfile-with-MR-CL-CR.yaml new file mode 100644 index 0000000..71fe568 --- /dev/null +++ b/tests/examples/source/devfiles/nodejs/devfile-with-MR-CL-CR.yaml @@ -0,0 +1,53 @@ +schemaVersion: 2.2.0 +metadata: + name: nodejs + projectType: nodejs + language: nodejs +starterProjects: + - name: nodejs-starter + git: + remotes: + origin: "https://github.com/odo-devfiles/nodejs-ex.git" +components: + - name: runtime + container: + image: registry.access.redhat.com/ubi8/nodejs-12:1-36 + memoryLimit: 1024Mi + memoryRequest: 512Mi + cpuLimit: '1' + cpuRequest: 200m + endpoints: + - name: "3000-tcp" + targetPort: 3000 + mountSources: true +commands: + - id: devbuild + exec: + component: runtime + commandLine: npm install + workingDir: ${PROJECTS_ROOT} + group: + kind: build + isDefault: true + - id: build + exec: + component: runtime + commandLine: npm install + workingDir: ${PROJECTS_ROOT} + group: + kind: build + - id: devrun + exec: + component: runtime + commandLine: npm start + workingDir: ${PROJECTS_ROOT} + group: + kind: run + isDefault: true + - id: run + exec: + component: runtime + commandLine: npm start + workingDir: ${PROJECTS_ROOT} + group: + kind: run diff --git a/tests/examples/source/devfiles/nodejs/devfile-with-command-env-with-space.yaml b/tests/examples/source/devfiles/nodejs/devfile-with-command-env-with-space.yaml new file mode 100644 index 0000000..34430be --- /dev/null +++ b/tests/examples/source/devfiles/nodejs/devfile-with-command-env-with-space.yaml @@ -0,0 +1,40 @@ +schemaVersion: 2.0.0 +metadata: + name: nodejs +starterProjects: + - name: nodejs-starter + git: + remotes: + origin: "https://github.com/odo-devfiles/nodejs-ex.git" +components: + - name: runtime + container: + image: registry.access.redhat.com/ubi8/nodejs-12:1-36 + memoryLimit: 1024Mi + endpoints: + - name: "3000-tcp" + targetPort: 3000 + mountSources: true +commands: + - id: buildenvwithspace + exec: + component: runtime + commandLine: npm install && mkdir "$BUILD_ENV1" + workingDir: ${PROJECTS_ROOT} + env: + - name: BUILD_ENV1 + value: "build env variable with space" + group: + kind: build + isDefault: true + - id: envwithspace + exec: + component: runtime + commandLine: mkdir "$ENV1" + env: + - name: ENV1 + value: "env with space" + workingDir: ${PROJECTS_ROOT} + group: + kind: run + isDefault: true diff --git a/tests/examples/source/devfiles/nodejs/devfile-with-command-multiple-envs.yaml b/tests/examples/source/devfiles/nodejs/devfile-with-command-multiple-envs.yaml new file mode 100644 index 0000000..861faeb --- /dev/null +++ b/tests/examples/source/devfiles/nodejs/devfile-with-command-multiple-envs.yaml @@ -0,0 +1,44 @@ +schemaVersion: 2.0.0 +metadata: + name: nodejs +starterProjects: + - name: nodejs-starter + git: + remotes: + origin: "https://github.com/odo-devfiles/nodejs-ex.git" +components: + - name: runtime + container: + image: registry.access.redhat.com/ubi8/nodejs-12:1-36 + memoryLimit: 1024Mi + endpoints: + - name: "3000-tcp" + targetPort: 3000 + mountSources: true +commands: + - id: buildwithmultipleenv + exec: + component: runtime + commandLine: "sh -c 'mkdir $BUILD_ENV1 $BUILD_ENV2' && npm install" + workingDir: ${PROJECTS_ROOT} + env: + - name: BUILD_ENV1 + value: "test_build_env_variable1" + - name: BUILD_ENV2 + value: "test_build_env_variable2" + group: + kind: build + isDefault: true + - id: multipleenv + exec: + component: runtime + commandLine: "sh -c 'mkdir $ENV1 $ENV2'" + env: + - name: ENV1 + value: "test_env_variable1" + - name: ENV2 + value: "test_env_variable2" + workingDir: ${PROJECTS_ROOT} + group: + kind: run + isDefault: true diff --git a/tests/examples/source/devfiles/nodejs/devfile-with-command-single-env.yaml b/tests/examples/source/devfiles/nodejs/devfile-with-command-single-env.yaml new file mode 100644 index 0000000..af2b00f --- /dev/null +++ b/tests/examples/source/devfiles/nodejs/devfile-with-command-single-env.yaml @@ -0,0 +1,40 @@ +schemaVersion: 2.0.0 +metadata: + name: nodejs +starterProjects: + - name: nodejs-starter + git: + remotes: + origin: "https://github.com/odo-devfiles/nodejs-ex.git" +components: + - name: runtime + container: + image: registry.access.redhat.com/ubi8/nodejs-12:1-36 + memoryLimit: 1024Mi + endpoints: + - name: "3000-tcp" + targetPort: 3000 + mountSources: true +commands: + - id: buildwithenv + exec: + component: runtime + commandLine: npm install && mkdir $BUILD_ENV1 + workingDir: ${PROJECTS_ROOT} + env: + - name: BUILD_ENV1 + value: "test_build_env_variable" + group: + kind: build + isDefault: true + - id: singleenv + exec: + component: runtime + commandLine: mkdir $ENV1 + env: + - name: ENV1 + value: "test_env_variable" + workingDir: ${PROJECTS_ROOT} + group: + kind: run + isDefault: true diff --git a/tests/examples/source/devfiles/nodejs/devfile-with-debugrun.yaml b/tests/examples/source/devfiles/nodejs/devfile-with-debugrun.yaml new file mode 100644 index 0000000..144bbd5 --- /dev/null +++ b/tests/examples/source/devfiles/nodejs/devfile-with-debugrun.yaml @@ -0,0 +1,51 @@ +schemaVersion: 2.0.0 +metadata: + name: nodejs +starterProjects: + - name: nodejs-starter + git: + remotes: + origin: "https://github.com/odo-devfiles/nodejs-ex.git" +components: + - name: runtime + container: + image: registry.access.redhat.com/ubi8/nodejs-12:1-36 + memoryLimit: 1024Mi + endpoints: + - name: "3000-tcp" + targetPort: 3000 + - name: "5858-tcp" + targetPort: 5858 + mountSources: true +commands: + - id: devbuild + exec: + component: runtime + commandLine: npm install + workingDir: ${PROJECTS_ROOT} + group: + kind: build + isDefault: true + - id: devrun + exec: + component: runtime + commandLine: npm start + workingDir: ${PROJECTS_ROOT} + group: + kind: run + isDefault: true + - id: debugrun + exec: + component: runtime + commandLine: npm run debug + workingDir: ${PROJECTS_ROOT} + group: + kind: debug + isDefault: true + - id: debug + exec: + component: runtime + commandLine: npm run debug + workingDir: ${PROJECTS_ROOT} + group: + kind: debug diff --git a/tests/examples/source/devfiles/nodejs/devfile-with-hotReload.yaml b/tests/examples/source/devfiles/nodejs/devfile-with-hotReload.yaml new file mode 100644 index 0000000..f75900b --- /dev/null +++ b/tests/examples/source/devfiles/nodejs/devfile-with-hotReload.yaml @@ -0,0 +1,40 @@ +schemaVersion: 2.0.0 +metadata: + name: nodejs +starterProjects: + - name: nodejs-starter + git: + remotes: + origin: "https://github.com/odo-devfiles/nodejs-ex.git" +components: + - name: runtime + container: + image: registry.access.redhat.com/ubi8/nodejs-12:1-36 + memoryLimit: 1024Mi + mountSources: true +commands: + - id: devbuild + exec: + component: runtime + commandLine: npm install + workingDir: ${PROJECTS_ROOT} + group: + kind: build + isDefault: true + - id: devrun + exec: + component: runtime + commandLine: npm start + workingDir: ${PROJECTS_ROOT} + hotReloadCapable: true + group: + kind: run + isDefault: true + - id: debugrun + exec: + component: runtime + commandLine: npm run debug + workingDir: ${PROJECTS_ROOT} + group: + kind: debug + isDefault: true diff --git a/tests/examples/source/devfiles/nodejs/devfile-with-invalid-events.yaml b/tests/examples/source/devfiles/nodejs/devfile-with-invalid-events.yaml new file mode 100644 index 0000000..a6efe27 --- /dev/null +++ b/tests/examples/source/devfiles/nodejs/devfile-with-invalid-events.yaml @@ -0,0 +1,120 @@ +schemaVersion: 2.0.0 +metadata: + name: nodejs +starterProjects: + - name: nodejs-starter + git: + remotes: + origin: "https://github.com/odo-devfiles/nodejs-ex.git" +components: + - name: runtime + container: + image: quay.io/eclipse/che-nodejs10-ubi:next + memoryLimit: 1024Mi + endpoints: + - name: "3000-tcp" + targetPort: 3000 + mountSources: true + - name: "tools" + container: + image: quay.io/eclipse/che-nodejs10-ubi:next + mountSources: true + memoryLimit: 1024Mi +commands: + - id: myprestart + exec: + commandLine: echo hello test2 >> $PROJECTS_ROOT/test.txt + component: tools + - id: secondprestart + exec: + commandLine: echo hello test >> $PROJECTS_ROOT/test.txt + component: runtime + workingDir: / + - id: prestartcomp + composite: + label: pre start composite + commands: + - mypreStart + - secondpreStart + parallel: true + - id: mypoststart + exec: + commandLine: echo I am a PostStart + component: tools + workingDir: / + - id: secondpoststart + exec: + commandLine: echo I am also a PostStart + component: runtime + workingDir: / + - id: wrongpoststart + exec: + commandLine: echo I am also a PostStart + component: runtime #wrongruntime #do not delete comment, tests rely on it for search & replace + workingDir: / + - id: myprestop + exec: + commandLine: echo I am a PreStop + component: tools + workingDir: / + - id: secondprestop + exec: + commandLine: echo I am also a PreStop + component: runtime + workingDir: / + - id: thirdprestop + exec: + commandLine: echo I am a third PreStop + component: runtime + workingDir: / + - id: mycompcmd + composite: + label: Build and Mkdir + commands: + - secondpreStop + - thirdpreStop + parallel: true + - id: mywrongcompcmd + composite: + label: Build and Mkdir + commands: + - secondprestop #secondprestopiswrong #do not delete comment, tests rely on it for search & replace + - thirdprestop + parallel: true + - id: devbuild + exec: + component: runtime + commandLine: npm install + workingDir: ${PROJECTS_ROOT} + group: + kind: build + isDefault: true + - id: build + exec: + component: runtime + commandLine: npm install + workingDir: ${PROJECTS_ROOT} + group: + kind: build + - id: devrun + exec: + component: runtime + commandLine: npm start + workingDir: ${PROJECTS_ROOT} + group: + kind: run + isDefault: true + - id: run + exec: + component: runtime + commandLine: npm start + workingDir: ${PROJECTS_ROOT} + group: + kind: run +events: + postStart: + - "mypoststart" + - "secondpoststart12345" + preStop: + - "mycompcmd" + - "myprestop" diff --git a/tests/examples/source/devfiles/nodejs/devfile-with-invalid-volmount.yaml b/tests/examples/source/devfiles/nodejs/devfile-with-invalid-volmount.yaml new file mode 100644 index 0000000..fb6ab4d --- /dev/null +++ b/tests/examples/source/devfiles/nodejs/devfile-with-invalid-volmount.yaml @@ -0,0 +1,50 @@ +schemaVersion: 2.0.0 +metadata: + name: test-devfile +starterProjects: + - name: nodejs-starter + git: + remotes: + origin: "https://github.com/che-samples/web-nodejs-sample.git" +components: + - name: runtime + container: + image: quay.io/eclipse/che-nodejs10-ubi:next + endpoints: + - name: "3000-tcp" + targetPort: 3000 + mountSources: true + volumeMounts: + - name: invalidvol1 + path: /data + - name: runtime2 + container: + image: quay.io/eclipse/che-nodejs10-ubi:next + mountSources: false + volumeMounts: + - name: invalidvol2 + path: /data + - name: secondvol + path: /data2 + - name: firstvol + volume: {} + - name: secondvol + volume: + size: 3Gi +commands: + - id: devbuild + exec: + component: runtime + commandLine: "echo hello >> myfile.log" + workingDir: /data + group: + kind: build + isDefault: true + - id: devrun + exec: + component: runtime2 + commandLine: "cat myfile.log" + workingDir: /data + group: + kind: run + isDefault: true diff --git a/tests/examples/source/devfiles/nodejs/devfile-with-link.yaml b/tests/examples/source/devfiles/nodejs/devfile-with-link.yaml new file mode 100644 index 0000000..6e7cae6 --- /dev/null +++ b/tests/examples/source/devfiles/nodejs/devfile-with-link.yaml @@ -0,0 +1,127 @@ +commands: +- exec: + commandLine: npm install + component: runtime + group: + isDefault: true + kind: build + workingDir: /project + id: install +- exec: + commandLine: npm start + component: runtime + group: + isDefault: true + kind: run + workingDir: /project + id: run +- exec: + commandLine: npm run debug + component: runtime + group: + isDefault: true + kind: debug + workingDir: /project + id: debug +- exec: + commandLine: npm test + component: runtime + group: + isDefault: true + kind: test + workingDir: /project + id: test +components: +- container: + endpoints: + - name: http-3000 + targetPort: 3000 + image: registry.access.redhat.com/ubi8/nodejs-14:latest + memoryLimit: 300Mi + mountSources: true + sourceMapping: /project + name: runtime +- kubernetes: + inlined: | + apiVersion: redis.redis.opstreelabs.in/v1beta1 + kind: Redis + metadata: + name: myredis + annotations: + service.binding/name: path={.metadata.name} + spec: + redisExporter: + enabled: true + image: 'quay.io/opstree/redis-exporter:1.0' + imagePullPolicy: Always + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 100m + memory: 128Mi + kubernetesConfig: + image: 'quay.io/opstree/redis:v6.2' + imagePullPolicy: IfNotPresent + resources: + requests: + cpu: 101m + memory: 128Mi + limits: + cpu: 101m + memory: 128Mi + redisSecret: + name: redis-secret + key: password + serviceType: LoadBalancer + redisConfig: {} + storage: + volumeClaimTemplate: + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi + name: myredis +- kubernetes: + inlined: | + apiVersion: binding.operators.coreos.com/v1alpha1 + kind: ServiceBinding + metadata: + creationTimestamp: null + name: redis-link + spec: + application: + group: apps + name: api-app + resource: deployments + version: v1 + bindAsFiles: true + detectBindingResources: true + services: + - group: redis.redis.opstreelabs.in + kind: Redis + name: myredis + version: v1beta1 + status: + secret: "" + name: redis-link +metadata: + description: Stack with Node.js 14 + displayName: Node.js Runtime + language: nodejs + name: nodejs + projectType: nodejs + tags: + - NodeJS + - Express + - ubi8 + version: 1.0.1 +schemaVersion: 2.0.0 +starterProjects: +- git: + remotes: + origin: https://github.com/odo-devfiles/nodejs-ex.git + name: nodejs-starter diff --git a/tests/examples/source/devfiles/nodejs/devfile-with-multiple-defaults.yaml b/tests/examples/source/devfiles/nodejs/devfile-with-multiple-defaults.yaml new file mode 100644 index 0000000..02eccdf --- /dev/null +++ b/tests/examples/source/devfiles/nodejs/devfile-with-multiple-defaults.yaml @@ -0,0 +1,127 @@ +schemaVersion: 2.0.0 +metadata: + name: nodejs +starterProjects: + - name: nodejs-starter + git: + remotes: + origin: "https://github.com/odo-devfiles/nodejs-ex.git" +components: + - name: runtime + container: + image: registry.access.redhat.com/ubi8/nodejs-12:1-36 + memoryLimit: 1024Mi + endpoints: + - name: "3000-tcp" + targetPort: 3000 + mountSources: true +commands: + - id: firstbuild + exec: + component: runtime + commandLine: npm install + workingDir: ${PROJECTS_ROOT} + group: + kind: build + isDefault: true + - id: secondbuild + exec: + component: runtime + commandLine: npm install + workingDir: ${PROJECTS_ROOT} + group: + kind: build + - id: thirdbuild + exec: + component: runtime + commandLine: npm install + workingDir: ${PROJECTS_ROOT} + - id: buildwithenv + exec: + component: runtime + commandLine: npm install && mkdir $BUILD_ENV1 + workingDir: ${PROJECTS_ROOT} + env: + - name: BUILD_ENV1 + value: "test_build_env_variable" + - id: buildwithmultipleenv + exec: + component: runtime + commandLine: "sh -c 'mkdir $BUILD_ENV1 $BUILD_ENV2' && npm install" + workingDir: ${PROJECTS_ROOT} + env: + - name: BUILD_ENV1 + value: "test_build_env_variable1" + - name: BUILD_ENV2 + value: "test_build_env_variable2" + - id: buildenvwithspace + exec: + component: runtime + commandLine: npm install && mkdir "$BUILD_ENV1" + workingDir: ${PROJECTS_ROOT} + env: + - name: BUILD_ENV1 + value: "build env variable with space" + - id: firstrun + exec: + component: runtime + commandLine: npm start + workingDir: ${PROJECTS_ROOT} + group: + kind: run + isDefault: true + - id: secondrun + exec: + component: runtime + commandLine: npm start + workingDir: ${PROJECTS_ROOT} + group: + kind: run + - id: singleenv + exec: + component: runtime + commandLine: mkdir $ENV1 + env: + - name: ENV1 + value: "test_env_variable" + workingDir: ${PROJECTS_ROOT} + group: + kind: run + - id: multipleenv + exec: + component: runtime + commandLine: "sh -c 'mkdir $ENV1 $ENV2'" + env: + - name: ENV1 + value: "test_env_variable1" + - name: ENV2 + value: "test_env_variable2" + workingDir: ${PROJECTS_ROOT} + group: + kind: run + - id: envwithspace + exec: + component: runtime + commandLine: mkdir "$ENV1" + env: + - name: ENV1 + value: "env with space" + workingDir: ${PROJECTS_ROOT} + group: + kind: run + - id: test1 + exec: + component: runtime + commandLine: "mkdir test1" + workingDir: ${PROJECTS_ROOT} + group: + kind: test + isDefault: true + - id: test2 + exec: + component: runtime + commandLine: "mkdir test2" + workingDir: ${PROJECTS_ROOT} + group: + kind: test + isDefault: true diff --git a/tests/examples/source/devfiles/nodejs/devfile-with-multiple-endpoints.yaml b/tests/examples/source/devfiles/nodejs/devfile-with-multiple-endpoints.yaml new file mode 100644 index 0000000..1b7dde4 --- /dev/null +++ b/tests/examples/source/devfiles/nodejs/devfile-with-multiple-endpoints.yaml @@ -0,0 +1,67 @@ +commands: +- exec: + commandLine: npm install + component: runtime + group: + isDefault: true + kind: build + workingDir: ${PROJECT_SOURCE} + id: install +- exec: + commandLine: npm start + component: runtime + group: + isDefault: true + kind: run + workingDir: ${PROJECT_SOURCE} + id: run +- exec: + commandLine: npm run debug + component: runtime + group: + isDefault: true + kind: debug + workingDir: ${PROJECT_SOURCE} + id: debug +- exec: + commandLine: npm test + component: runtime + group: + isDefault: true + kind: test + workingDir: ${PROJECT_SOURCE} + id: test +components: +- container: + endpoints: + - name: http-3000 + targetPort: 3000 + exposure: public + - name: http-4567 + targetPort: 4567 + exposure: internal + - name: http-7890 + targetPort: 7890 + exposure: none + image: registry.access.redhat.com/ubi8/nodejs-14:latest + memoryLimit: 1024Mi + mountSources: true + name: runtime +metadata: + description: Stack with Node.js 14 + displayName: Node.js Runtime + icon: https://nodejs.org/static/images/logos/nodejs-new-pantone-black.svg + language: javascript + name: nodejs-ex + projectType: nodejs + tags: + - NodeJS + - Express + - ubi8 + version: 1.0.1 +schemaVersion: 2.0.0 +starterProjects: +- git: + remotes: + origin: https://github.com/odo-devfiles/nodejs-ex.git + name: nodejs-starter diff --git a/tests/examples/source/devfiles/nodejs/devfile-with-multiple-projects.yaml b/tests/examples/source/devfiles/nodejs/devfile-with-multiple-projects.yaml new file mode 100644 index 0000000..9948fc5 --- /dev/null +++ b/tests/examples/source/devfiles/nodejs/devfile-with-multiple-projects.yaml @@ -0,0 +1,44 @@ +schemaVersion: 2.0.0 +metadata: + name: nodejs +projects: + - name: nodeshift + clonePath: webapp/ + git: + remotes: + origin: "https://github.com/nodeshift/nodeshift" + - name: nodejs-health-check + git: + remotes: + origin: "https://github.com/nodeshift-starters/nodejs-health-check" +starterProjects: + - name: nodejs-starter + git: + remotes: + origin: "https://github.com/odo-devfiles/nodejs-ex.git" +components: + - name: runtime + container: + image: registry.access.redhat.com/ubi8/nodejs-12:1-36 + memoryLimit: 1024Mi + endpoints: + - name: "3000-tcp" + targetPort: 3000 + mountSources: true +commands: + - id: devbuild + exec: + component: runtime + commandLine: npm install + workingDir: ${PROJECTS_ROOT}/webapp + group: + kind: build + isDefault: true + - id: devrun + exec: + component: runtime + commandLine: npm start + workingDir: ${PROJECT_SOURCE} + group: + kind: run + isDefault: true diff --git a/tests/examples/source/devfiles/nodejs/devfile-with-no-default.yaml b/tests/examples/source/devfiles/nodejs/devfile-with-no-default.yaml new file mode 100644 index 0000000..77b83d7 --- /dev/null +++ b/tests/examples/source/devfiles/nodejs/devfile-with-no-default.yaml @@ -0,0 +1,47 @@ +schemaVersion: 2.0.0 +metadata: + name: nodejs +starterProjects: + - name: nodejs-starter + git: + remotes: + origin: "https://github.com/odo-devfiles/nodejs-ex.git" +components: + - name: runtime + container: + image: registry.access.redhat.com/ubi8/nodejs-12:1-36 + memoryLimit: 1024Mi + endpoints: + - name: "3000-tcp" + targetPort: 3000 + mountSources: true +commands: + - id: firstbuild + exec: + component: runtime + commandLine: npm install + workingDir: ${PROJECTS_ROOT} + group: + kind: build + isDefault: true + - id: secondbuild + exec: + component: runtime + commandLine: npm install + workingDir: ${PROJECTS_ROOT} + group: + kind: build + - id: firstrun + exec: + component: runtime + commandLine: npm start + workingDir: ${PROJECTS_ROOT} + group: + kind: run + - id: secondrun + exec: + component: runtime + commandLine: npm start + workingDir: ${PROJECTS_ROOT} + group: + kind: run diff --git a/tests/examples/source/devfiles/nodejs/devfile-with-no-group-kind.yaml b/tests/examples/source/devfiles/nodejs/devfile-with-no-group-kind.yaml new file mode 100644 index 0000000..5cc4537 --- /dev/null +++ b/tests/examples/source/devfiles/nodejs/devfile-with-no-group-kind.yaml @@ -0,0 +1,28 @@ +schemaVersion: 2.0.0 +metadata: + name: nodejs +starterProjects: + - name: nodejs-starter + git: + remotes: + origin: "https://github.com/odo-devfiles/nodejs-ex.git" +components: + - name: runtime + container: + image: registry.access.redhat.com/ubi8/nodejs-12:1-36 + memoryLimit: 1024Mi + endpoints: + - name: "3000-tcp" + targetPort: 3000 + mountSources: true +commands: + - id: devbuild + exec: + component: runtime + commandLine: npm install + workingDir: ${PROJECTS_ROOT} + - id: devrun + exec: + component: runtime + commandLine: npm start + workingDir: ${PROJECTS_ROOT} diff --git a/tests/examples/source/devfiles/nodejs/devfile-with-parent.yaml b/tests/examples/source/devfiles/nodejs/devfile-with-parent.yaml new file mode 100644 index 0000000..216b575 --- /dev/null +++ b/tests/examples/source/devfiles/nodejs/devfile-with-parent.yaml @@ -0,0 +1,11 @@ + +schemaVersion: 2.1.0 +metadata: + name: new-ol-version + description: Child devfile to test OL image updating +parent: + uri: https://github.com/OpenLiberty/application-stack/releases/download/maven-0.7.0/devfile.yaml + components: + - name: dev + container: + image: maven:3.6-adoptopenjdk-11-openj9 \ No newline at end of file diff --git a/tests/examples/source/devfiles/nodejs/devfile-with-pod.yaml b/tests/examples/source/devfiles/nodejs/devfile-with-pod.yaml new file mode 100644 index 0000000..dd6409a --- /dev/null +++ b/tests/examples/source/devfiles/nodejs/devfile-with-pod.yaml @@ -0,0 +1,85 @@ +schemaVersion: 2.0.0 +metadata: + description: Stack with Node.js 14 + displayName: Node.js Runtime + icon: https://nodejs.org/static/images/logos/nodejs-new-pantone-black.svg + language: nodejs + name: nodejs + projectType: nodejs + tags: + - NodeJS + - Express + - ubi8 + version: 1.0.1 +starterProjects: + - git: + remotes: + origin: https://github.com/odo-devfiles/nodejs-ex.git + name: nodejs-starter +components: + - container: + image: registry.access.redhat.com/ubi8/nodejs-14:latest + memoryLimit: 1024Mi + mountSources: true + sourceMapping: /project + name: runtime + - kubernetes: + inlined: | + apiVersion: etcd.database.coreos.com/v1beta2 + kind: EtcdCluster + metadata: + annotations: + etcd.database.coreos.com/scope: clusterwide + name: etcdcluster + spec: + size: 3 + version: 3.2.13 + name: etcdcluster + - kubernetes: + inlined: | + apiVersion: v1 + kind: Pod + metadata: + labels: + name: nginx + name: nginx + spec: + containers: + - image: quay.io/bitnami/nginx + name: nginx + ports: + - containerPort: 80 + name: nginx +commands: + - exec: + commandLine: npm install + component: runtime + group: + isDefault: true + kind: build + workingDir: /project + id: install + - exec: + commandLine: npm start + component: runtime + group: + isDefault: true + kind: run + workingDir: /project + id: run + - exec: + commandLine: npm run debug + component: runtime + group: + isDefault: true + kind: debug + workingDir: /project + id: debug + - exec: + commandLine: npm test + component: runtime + group: + isDefault: true + kind: test + workingDir: /project + id: test diff --git a/tests/examples/source/devfiles/nodejs/devfile-with-preStart.yaml b/tests/examples/source/devfiles/nodejs/devfile-with-preStart.yaml new file mode 100644 index 0000000..90b96fd --- /dev/null +++ b/tests/examples/source/devfiles/nodejs/devfile-with-preStart.yaml @@ -0,0 +1,123 @@ +schemaVersion: 2.0.0 +metadata: + name: nodejs +starterProjects: + - name: nodejs-starter + git: + remotes: + origin: "https://github.com/odo-devfiles/nodejs-ex.git" +components: + - name: runtime + container: + image: quay.io/eclipse/che-nodejs10-ubi:next + memoryLimit: 1024Mi + endpoints: + - name: "3000-tcp" + targetPort: 3000 + mountSources: true + - name: "tools" + container: + image: quay.io/eclipse/che-nodejs10-ubi:next + mountSources: true + memoryLimit: 1024Mi +commands: + - id: myprestart + exec: + commandLine: echo hello test2 >> $PROJECTS_ROOT/test.txt + component: tools + - id: secondprestart + exec: + commandLine: echo hello test >> $PROJECTS_ROOT/test.txt + component: runtime + workingDir: / + - id: prestartcomp + composite: + label: pre start composite + commands: + - myPreStart + - secondPreStart + parallel: true + - id: mypoststart + exec: + commandLine: echo I am a PostStart + component: tools + workingDir: / + - id: secondpoststart + exec: + commandLine: echo I am also a PostStart + component: runtime + workingDir: / + - id: wrongpoststart + exec: + commandLine: echo I am also a PostStart + component: runtime #wrongruntime #do not delete comment, tests rely on it for search & replace + workingDir: / + - id: myprestop + exec: + commandLine: echo I am a PreStop + component: tools + workingDir: / + - id: secondprestop + exec: + commandLine: echo I am also a PreStop + component: runtime + workingDir: / + - id: thirdprestop + exec: + commandLine: echo I am a third PreStop + component: runtime + workingDir: / + - id: mycompcmd + composite: + label: Build and Mkdir + commands: + - secondPreStop + - thirdPreStop + parallel: true + - id: mywrongcompcmd + composite: + label: Build and Mkdir + commands: + - secondprestop #secondprestopiswrong #do not delete comment, tests rely on it for search & replace + - thirdprestop + parallel: true + - id: devbuild + exec: + component: runtime + commandLine: npm install + workingDir: ${PROJECTS_ROOT} + group: + kind: build + isDefault: true + - id: build + exec: + component: runtime + commandLine: npm install + workingDir: ${PROJECTS_ROOT} + group: + kind: build + - id: devrun + exec: + component: runtime + commandLine: npm start + workingDir: ${PROJECTS_ROOT} + group: + kind: run + isDefault: true + - id: run + exec: + component: runtime + commandLine: npm start + workingDir: ${PROJECTS_ROOT} + group: + kind: run +events: + preStart: + - "myprestart" + - "prestartcomp" + postStart: + - "mypoststart" + - "secondpoststart" + preStop: + - "mycompcmd" + - "myprestop" diff --git a/tests/examples/source/devfiles/nodejs/devfile-with-remote-attributes.yaml b/tests/examples/source/devfiles/nodejs/devfile-with-remote-attributes.yaml new file mode 100644 index 0000000..80437b3 --- /dev/null +++ b/tests/examples/source/devfiles/nodejs/devfile-with-remote-attributes.yaml @@ -0,0 +1,52 @@ +schemaVersion: 2.0.0 +metadata: + name: nodejs +starterProjects: + - name: nodejs-starter + git: + remotes: + origin: "https://github.com/odo-devfiles/nodejs-ex.git" +components: + - name: runtime + container: + image: registry.access.redhat.com/ubi8/nodejs-12:1-36 + memoryLimit: 1024Mi + endpoints: + - name: "3000-tcp" + targetPort: 3000 + mountSources: true +commands: + - id: devbuild + exec: + component: runtime + commandLine: npm install + workingDir: ${PROJECTS_ROOT} + group: + kind: build + isDefault: true + - id: build + exec: + component: runtime + commandLine: npm install + workingDir: ${PROJECTS_ROOT} + group: + kind: build + - id: devrun + attributes: + "dev.odo.push.path:server.js": "server/server.js" + "dev.odo.push.path:test": "server/test" + "dev.odo.push.path:package.json": "package.json" + exec: + component: runtime + commandLine: npm start + workingDir: ${PROJECTS_ROOT} + group: + kind: run + isDefault: true + - id: run + exec: + component: runtime + commandLine: npm start + workingDir: ${PROJECTS_ROOT} + group: + kind: run diff --git a/tests/examples/source/devfiles/nodejs/devfile-with-testgroup.yaml b/tests/examples/source/devfiles/nodejs/devfile-with-testgroup.yaml new file mode 100644 index 0000000..ec4976e --- /dev/null +++ b/tests/examples/source/devfiles/nodejs/devfile-with-testgroup.yaml @@ -0,0 +1,55 @@ +schemaVersion: 2.0.0 +metadata: + name: nodejs +starterProjects: + - name: nodejs-starter + git: + remotes: + origin: "https://github.com/odo-devfiles/nodejs-ex.git" +components: + - name: runtime + container: + image: registry.access.redhat.com/ubi8/nodejs-12:1-36 + memoryLimit: 1024Mi + mountSources: true +commands: + - id: devbuild + exec: + component: runtime + commandLine: npm install + workingDir: ${PROJECTS_ROOT} + group: + kind: build + isDefault: true + - id: devrun + exec: + component: runtime + commandLine: npm start + workingDir: ${PROJECTS_ROOT} + group: + kind: run + isDefault: true + - id: test1 + exec: + component: runtime + commandLine: "mkdir test1" + workingDir: ${PROJECTS_ROOT} + group: + kind: test + isDefault: true + - id: test2 + exec: + component: runtime + commandLine: "mkdir test2" + workingDir: ${PROJECTS_ROOT} + group: + kind: test + - id: compositetest + composite: + label: Build and Mkdir + commands: + - test1 + - test2 + parallel: false + group: + kind: test \ No newline at end of file diff --git a/tests/examples/source/devfiles/nodejs/devfile-with-valid-events.yaml b/tests/examples/source/devfiles/nodejs/devfile-with-valid-events.yaml new file mode 100644 index 0000000..363f93c --- /dev/null +++ b/tests/examples/source/devfiles/nodejs/devfile-with-valid-events.yaml @@ -0,0 +1,120 @@ +schemaVersion: 2.0.0 +metadata: + name: nodejs +starterProjects: + - name: nodejs-starter + git: + remotes: + origin: "https://github.com/odo-devfiles/nodejs-ex.git" +components: + - name: runtime + container: + image: quay.io/eclipse/che-nodejs10-ubi:next + memoryLimit: 1024Mi + endpoints: + - name: "3000-tcp" + targetPort: 3000 + mountSources: true + - name: "tools" + container: + image: quay.io/eclipse/che-nodejs10-ubi:next + mountSources: true + memoryLimit: 1024Mi +commands: + - id: myprestart + exec: + commandLine: echo hello test2 >> $PROJECTS_ROOT/test.txt + component: tools + - id: secondprestart + exec: + commandLine: echo hello test >> $PROJECTS_ROOT/test.txt + component: runtime + workingDir: / + - id: prestartcomp + composite: + label: pre start composite + commands: + - mypreStart + - secondpreStart + parallel: true + - id: mypoststart + exec: + commandLine: echo I am a PostStart + component: tools + workingDir: / + - id: secondpoststart + exec: + commandLine: echo I am also a PostStart + component: runtime + workingDir: / + - id: wrongpoststart + exec: + commandLine: echo I am also a PostStart + component: runtime #wrongruntime #do not delete comment, tests rely on it for search & replace + workingDir: / + - id: myprestop + exec: + commandLine: echo I am a PreStop + component: tools + workingDir: / + - id: secondprestop + exec: + commandLine: echo I am also a PreStop + component: runtime + workingDir: / + - id: thirdprestop + exec: + commandLine: echo I am a third PreStop + component: runtime + workingDir: / + - id: mycompcmd + composite: + label: Build and Mkdir + commands: + - secondpreStop + - thirdpreStop + parallel: true + - id: mywrongcompcmd + composite: + label: Build and Mkdir + commands: + - secondprestop #secondprestopiswrong #do not delete comment, tests rely on it for search & replace + - thirdprestop + parallel: true + - id: devbuild + exec: + component: runtime + commandLine: npm install + workingDir: ${PROJECTS_ROOT} + group: + kind: build + isDefault: true + - id: build + exec: + component: runtime + commandLine: npm install + workingDir: ${PROJECTS_ROOT} + group: + kind: build + - id: devrun + exec: + component: runtime + commandLine: npm start + workingDir: ${PROJECTS_ROOT} + group: + kind: run + isDefault: true + - id: run + exec: + component: runtime + commandLine: npm start + workingDir: ${PROJECTS_ROOT} + group: + kind: run +events: + postStart: + - "mypoststart" + - "secondpoststart" + preStop: + - "mycompcmd" + - "myprestop" diff --git a/tests/examples/source/devfiles/nodejs/devfile-with-volume-components.yaml b/tests/examples/source/devfiles/nodejs/devfile-with-volume-components.yaml new file mode 100644 index 0000000..000f25f --- /dev/null +++ b/tests/examples/source/devfiles/nodejs/devfile-with-volume-components.yaml @@ -0,0 +1,56 @@ +schemaVersion: 2.0.0 +metadata: + name: test-devfile +starterProjects: + - name: nodejs-starter + git: + remotes: + origin: "https://github.com/che-samples/web-nodejs-sample.git" +components: + - name: runtime + container: + image: quay.io/eclipse/che-nodejs10-ubi:next + memoryLimit: 1024Mi + env: + - name: FOO + value: "bar" + endpoints: + - name: "3000-tcp" + targetPort: 3000 + mountSources: true + volumeMounts: + - name: firstvol + path: /data + - name: secondvol + - name: runtime2 + container: + image: quay.io/eclipse/che-nodejs10-ubi:next + memoryLimit: 1024Mi + mountSources: false + volumeMounts: + - name: firstvol + path: /data + - name: secondvol + path: /data2 + - name: firstvol + volume: {} + - name: secondvol + volume: + size: 3Gi +commands: + - id: devbuild + exec: + component: runtime + commandLine: "echo hello >> myfile.log" + workingDir: /data + group: + kind: build + isDefault: true + - id: devrun + exec: + component: runtime2 + commandLine: "cat myfile.log" + workingDir: /data + group: + kind: run + isDefault: true diff --git a/tests/examples/source/devfiles/nodejs/devfile-with-volumes.yaml b/tests/examples/source/devfiles/nodejs/devfile-with-volumes.yaml new file mode 100644 index 0000000..da1476e --- /dev/null +++ b/tests/examples/source/devfiles/nodejs/devfile-with-volumes.yaml @@ -0,0 +1,54 @@ +schemaVersion: 2.0.0 +metadata: + name: nodejs +starterProjects: + - name: nodejs-starter + git: + remotes: + origin: "https://github.com/che-samples/web-nodejs-sample.git" +components: + - name: runtime + container: + image: quay.io/eclipse/che-nodejs10-ubi:next + memoryLimit: 1024Mi + env: + - name: FOO + value: "bar" + endpoints: + - name: "3000-tcp" + targetPort: 3000 + mountSources: true + volumeMounts: + - name: myvol + path: /data + - name: runtime2 + container: + image: quay.io/eclipse/che-nodejs10-ubi:next + memoryLimit: 1024Mi + mountSources: false + volumeMounts: + - name: myvol + - name: myvol2 + path: /data2 + - name: myvol + volume: + size: 3Gi + - name: myvol2 + volume: {} +commands: + - id: devbuild + exec: + component: runtime + commandLine: "echo hello >> myfile.log" + workingDir: /data + group: + kind: build + isDefault: true + - id: devrun + exec: + component: runtime2 + commandLine: "cat myfile.log" + workingDir: /myvol + group: + kind: run + isDefault: true diff --git a/tests/examples/source/devfiles/nodejs/devfile-without-debugrun.yaml b/tests/examples/source/devfiles/nodejs/devfile-without-debugrun.yaml new file mode 100644 index 0000000..b909a4e --- /dev/null +++ b/tests/examples/source/devfiles/nodejs/devfile-without-debugrun.yaml @@ -0,0 +1,36 @@ +schemaVersion: 2.0.0 +metadata: + name: nodejs +starterProjects: + - name: nodejs-starter + git: + remotes: + origin: "https://github.com/odo-devfiles/nodejs-ex.git" +components: + - name: runtime + container: + image: registry.access.redhat.com/ubi8/nodejs-12:1-36 + memoryLimit: 1024Mi + endpoints: + - name: "3000-tcp" + targetPort: 3000 + - name: "5858-tcp" + targetPort: 5858 + mountSources: true +commands: + - id: devbuild + exec: + component: runtime + commandLine: npm install + workingDir: ${PROJECTS_ROOT} + group: + kind: build + isDefault: true + - id: devrun + exec: + component: runtime + commandLine: npm start + workingDir: ${PROJECTS_ROOT} + group: + kind: run + isDefault: true diff --git a/tests/examples/source/devfiles/nodejs/devfile-without-devbuild.yaml b/tests/examples/source/devfiles/nodejs/devfile-without-devbuild.yaml new file mode 100644 index 0000000..127dda8 --- /dev/null +++ b/tests/examples/source/devfiles/nodejs/devfile-without-devbuild.yaml @@ -0,0 +1,29 @@ +schemaVersion: 2.0.0 +metadata: + name: nodejs +starterProjects: + - name: nodejs-starter + git: + remotes: + origin: "https://github.com/odo-devfiles/nodejs-ex.git" +components: + - name: runtime + container: + image: registry.access.redhat.com/ubi8/nodejs-12:1-36 + memoryLimit: 1024Mi + env: + - name: FOO + value: "bar" + endpoints: + - name: "3000-tcp" + targetPort: 3000 + mountSources: true +commands: + - id: devrun + exec: + component: runtime + commandLine: "npm install && npm start" + workingDir: ${PROJECTS_ROOT} + group: + kind: run + isDefault: true diff --git a/tests/examples/source/devfiles/nodejs/devfile-without-devinit.yaml b/tests/examples/source/devfiles/nodejs/devfile-without-devinit.yaml new file mode 100644 index 0000000..f0f5c28 --- /dev/null +++ b/tests/examples/source/devfiles/nodejs/devfile-without-devinit.yaml @@ -0,0 +1,37 @@ +schemaVersion: 2.0.0 +metadata: + name: nodejs +starterProjects: + - name: nodejs-starter + git: + remotes: + origin: "https://github.com/odo-devfiles/nodejs-ex.git" +components: + - name: runtime + container: + image: quay.io/eclipse/che-nodejs10-ubi:next + memoryLimit: 1024Mi + env: + - name: FOO + value: "bar" + endpoints: + - name: "3000-tcp" + targetPort: 3000 + mountSources: true +commands: + - id: devbuild + exec: + component: runtime + commandLine: npm install + workingDir: ${PROJECTS_ROOT} + group: + kind: build + isDefault: true + - id: devrun + exec: + component: runtime + commandLine: npm start + workingDir: ${PROJECTS_ROOT} + group: + kind: run + isDefault: true diff --git a/tests/examples/source/devfiles/nodejs/devfileCompositeCommands.yaml b/tests/examples/source/devfiles/nodejs/devfileCompositeCommands.yaml new file mode 100644 index 0000000..0bb4259 --- /dev/null +++ b/tests/examples/source/devfiles/nodejs/devfileCompositeCommands.yaml @@ -0,0 +1,53 @@ +schemaVersion: 2.0.0 +metadata: + name: nodejs + version: 1.0.0 +starterProjects: + - name: nodejs-starter + git: + remotes: + origin: "https://github.com/odo-devfiles/nodejs-ex.git" +components: + - name: runtime + container: + image: registry.access.redhat.com/ubi8/nodejs-12:1-36 + memoryLimit: 1024Mi + mountSources: true + endpoints: + - name: http-3000 + targetPort: 3000 +commands: + - id: install + exec: + component: runtime + commandLine: npm install + workingDir: ${PROJECTS_ROOT} + group: + kind: build + isDefault: false + - id: mkdir + exec: + component: runtime + commandLine: mkdir /projects/testfolder + workingDir: ${PROJECTS_ROOT} + group: + kind: build + isDefault: false + - id: run + exec: + component: runtime + commandLine: npm start + workingDir: ${PROJECTS_ROOT} + group: + kind: run + isDefault: true + - id: buildandmkdir + composite: + label: Build and Mkdir + commands: + - mkdir + - install + parallel: false + group: + kind: build + isDefault: true diff --git a/tests/examples/source/devfiles/nodejs/devfileCompositeCommandsParallel.yaml b/tests/examples/source/devfiles/nodejs/devfileCompositeCommandsParallel.yaml new file mode 100644 index 0000000..1713622 --- /dev/null +++ b/tests/examples/source/devfiles/nodejs/devfileCompositeCommandsParallel.yaml @@ -0,0 +1,53 @@ +schemaVersion: 2.0.0 +metadata: + name: nodejs + version: 1.0.0 +starterProjects: + - name: nodejs-starter + git: + remotes: + origin: "https://github.com/odo-devfiles/nodejs-ex.git" +components: + - name: runtime + container: + image: registry.access.redhat.com/ubi8/nodejs-12:1-36 + memoryLimit: 1024Mi + mountSources: true + endpoints: + - name: http-3000 + targetPort: 3000 +commands: + - id: install + exec: + component: runtime + commandLine: npm install + workingDir: ${PROJECTS_ROOT} + group: + kind: build + isDefault: false + - id: mkdir + exec: + component: runtime + commandLine: mkdir /projects/testfolder + workingDir: ${PROJECTS_ROOT} + group: + kind: build + isDefault: false + - id: run + exec: + component: runtime + commandLine: npm start + workingDir: ${PROJECTS_ROOT} + group: + kind: run + isDefault: true + - id: buildandmkdir + composite: + label: Build and Mkdir + commands: + - mkdir + - install + parallel: true + group: + kind: build + isDefault: true diff --git a/tests/examples/source/devfiles/nodejs/devfileCompositeInvalidComponent.yaml b/tests/examples/source/devfiles/nodejs/devfileCompositeInvalidComponent.yaml new file mode 100644 index 0000000..3c1a120 --- /dev/null +++ b/tests/examples/source/devfiles/nodejs/devfileCompositeInvalidComponent.yaml @@ -0,0 +1,53 @@ +schemaVersion: 2.0.0 +metadata: + name: nodejs + version: 1.0.0 +projects: + - name: nodejs-starter + git: + remotes: + origin: "https://github.com/odo-devfiles/nodejs-ex.git" +components: + - name: runtime + container: + image: registry.access.redhat.com/ubi8/nodejs-12:1-36 + memoryLimit: 1024Mi + mountSources: true + endpoints: + - name: http-3000 + targetPort: 3000 +commands: + - id: buildandmkdir + composite: + label: Build and Mkdir + commands: + - mkdir + - install + parallel: false + group: + kind: build + isDefault: true + - id: install + exec: + component: runtime + commandLine: npm install + workingDir: ${CHE_PROJECTS_ROOT}/nodejs-starter + group: + kind: build + isDefault: false + - id: mkdir + exec: + component: fakecomponent + commandLine: mkdir /projects/testfolder + workingDir: ${CHE_PROJECTS_ROOT}/nodejs-starter + group: + kind: build + isDefault: false + - id: run + exec: + component: runtime + commandLine: npm start + workingDir: ${CHE_PROJECTS_ROOT}/nodejs-starter + group: + kind: run + isDefault: true diff --git a/tests/examples/source/devfiles/nodejs/devfileCompositeNonExistent.yaml b/tests/examples/source/devfiles/nodejs/devfileCompositeNonExistent.yaml new file mode 100644 index 0000000..cf7f1f0 --- /dev/null +++ b/tests/examples/source/devfiles/nodejs/devfileCompositeNonExistent.yaml @@ -0,0 +1,53 @@ +schemaVersion: 2.0.0 +metadata: + name: nodejs + version: 1.0.0 +starterProjects: + - name: nodejs-starter + git: + remotes: + origin: "https://github.com/odo-devfiles/nodejs-ex.git" +components: + - name: runtime + container: + image: registry.access.redhat.com/ubi8/nodejs-12:1-36 + memoryLimit: 1024Mi + mountSources: true + endpoints: + - name: http-3000 + targetPort: 3000 +commands: + - id: install + exec: + component: runtime + commandLine: npm install + workingDir: ${PROJECTS_ROOT} + group: + kind: build + isDefault: false + - id: mkdir + exec: + component: runtime + commandLine: mkdir /projects/testfolder + workingDir: ${PROJECTS_ROOT} + group: + kind: build + isDefault: false + - id: run + exec: + component: runtime + commandLine: npm start + workingDir: ${PROJECTS_ROOT} + group: + kind: run + isDefault: true + - id: buildandmkdir + composite: + label: Build and Mkdir + commands: + - fakecommand + - install + parallel: false + group: + kind: build + isDefault: true diff --git a/tests/examples/source/devfiles/nodejs/devfileCompositeRun.yaml b/tests/examples/source/devfiles/nodejs/devfileCompositeRun.yaml new file mode 100644 index 0000000..751720c --- /dev/null +++ b/tests/examples/source/devfiles/nodejs/devfileCompositeRun.yaml @@ -0,0 +1,53 @@ +schemaVersion: 2.0.0 +metadata: + name: nodejs + version: 1.0.0 +starterProjects: + - name: nodejs-starter + git: + remotes: + origin: "https://github.com/odo-devfiles/nodejs-ex.git" +components: + - name: runtime + container: + image: registry.access.redhat.com/ubi8/nodejs-12:1-36 + memoryLimit: 1024Mi + mountSources: true + endpoints: + - name: http-3000 + targetPort: 3000 +commands: + - id: install + exec: + component: runtime + commandLine: npm install + workingDir: ${PROJECTS_ROOT} + group: + kind: build + isDefault: false + - id: mkdir + exec: + component: runtime + commandLine: mkdir /projects/testfolder + workingDir: ${PROJECTS_ROOT} + group: + kind: build + isDefault: true + - id: run + exec: + component: runtime + commandLine: npm start + workingDir: ${PROJECTS_ROOT} + group: + kind: run + isDefault: false + - id: buildandmkdir + composite: + label: Build and Mkdir + commands: + - mkdir + - install + parallel: false + group: + kind: run + isDefault: true diff --git a/tests/examples/source/devfiles/nodejs/devfileIndirectNesting.yaml b/tests/examples/source/devfiles/nodejs/devfileIndirectNesting.yaml new file mode 100644 index 0000000..ce25a9d --- /dev/null +++ b/tests/examples/source/devfiles/nodejs/devfileIndirectNesting.yaml @@ -0,0 +1,67 @@ +schemaVersion: 2.0.0 +metadata: + name: test-devfile +starterProjects: + - name: nodejs-starter + git: + remotes: + origin: "https://github.com/odo-devfiles/nodejs-ex.git" +components: + - name: runtime + container: + image: registry.access.redhat.com/ubi8/nodejs-12:1-36 + memoryLimit: 1024Mi + mountSources: true +commands: + - id: install + exec: + component: runtime + commandLine: npm install + workingDir: ${PROJECTS_ROOT} + - id: echo1 + exec: + component: runtime + commandLine: echo hi + workingDir: ${PROJECTS_ROOT} + - id: run + exec: + component: runtime + commandLine: npm start + workingDir: ${PROJECTS_ROOT} + group: + kind: run + isDefault: true + - id: buildandecho + composite: + label: Build and Echo + commands: + - echo1 + - install + - nestedcommand + - echo3 + parallel: false + group: + kind: build + isDefault: true + - id: nestedcommand + composite: + label: Build and Echo + commands: + - buildandecho + - install + - echo2 + - echo3 + parallel: false + group: + kind: build + isDefault: false + - id: echo2 + exec: + component: runtime + commandLine: echo hello + workingDir: ${PROJECTS_ROOT} + - id: echo3 + exec: + component: runtime + commandLine: echo hellohii + workingDir: ${PROJECTS_ROOT} diff --git a/tests/examples/source/devfiles/nodejs/devfileNestedCompCommands.yaml b/tests/examples/source/devfiles/nodejs/devfileNestedCompCommands.yaml new file mode 100644 index 0000000..b087048 --- /dev/null +++ b/tests/examples/source/devfiles/nodejs/devfileNestedCompCommands.yaml @@ -0,0 +1,63 @@ +schemaVersion: 2.0.0 +metadata: + name: nodejs + version: 1.0.0 +starterProjects: + - name: nodejs-starter + git: + remotes: + origin: "https://github.com/odo-devfiles/nodejs-ex.git" +components: + - name: runtime + container: + image: registry.access.redhat.com/ubi8/nodejs-12:1-36 + memoryLimit: 1024Mi + mountSources: true + endpoints: + - name: http-3000 + targetPort: 3000 +commands: + - id: install + exec: + component: runtime + commandLine: npm install + workingDir: ${PROJECTS_ROOT} + group: + kind: build + isDefault: false + - id: mkdir + exec: + component: runtime + commandLine: mkdir /projects/testfolder + workingDir: ${PROJECTS_ROOT} + group: + kind: build + isDefault: false + - id: run + exec: + component: runtime + commandLine: npm start + workingDir: ${PROJECTS_ROOT} + group: + kind: run + isDefault: true + - id: nestedcommand + composite: + label: Build and Echo + commands: + - buildandmkdir + - install + parallel: true + group: + kind: build + isDefault: true + - id: buildandmkdir + composite: + label: Build and Mkdir + commands: + - mkdir + - install + parallel: false + group: + kind: build + isDefault: false diff --git a/tests/examples/source/devfiles/python/devfile-registry.yaml b/tests/examples/source/devfiles/python/devfile-registry.yaml new file mode 100644 index 0000000..5c1c57d --- /dev/null +++ b/tests/examples/source/devfiles/python/devfile-registry.yaml @@ -0,0 +1,49 @@ +commands: +- exec: + commandLine: pip install --user -r requirements.txt + component: py-web + group: + isDefault: true + kind: build + id: pip-install-requirements +- exec: + commandLine: python app.py + component: py-web + group: + isDefault: true + kind: run + workingDir: ${PROJECTS_ROOT} + id: run-app +- exec: + commandLine: pip install --user debugpy && python -m debugpy --listen 0.0.0.0:${DEBUG_PORT} + app.py + component: py-web + group: + kind: debug + workingDir: ${PROJECTS_ROOT} + id: debugpy +components: +- container: + endpoints: + - name: web + targetPort: 8080 + image: quay.io/eclipse/che-python-3.7:nightly + mountSources: true + name: py-web +metadata: + description: Python Stack with Python 3.7 + displayName: Python + icon: https://www.python.org/static/community_logos/python-logo-generic.svg + language: python + name: python-prj1-api-cajf + projectType: python + tags: + - Python + - pip + version: 1.0.0 +schemaVersion: 2.0.0 +starterProjects: +- git: + remotes: + origin: https://github.com/odo-devfiles/python-ex + name: python-example diff --git a/tests/examples/source/devfiles/springboot/devfile-with-metadataname-foobar.yaml b/tests/examples/source/devfiles/springboot/devfile-with-metadataname-foobar.yaml new file mode 100644 index 0000000..c938f72 --- /dev/null +++ b/tests/examples/source/devfiles/springboot/devfile-with-metadataname-foobar.yaml @@ -0,0 +1,50 @@ +--- +schemaVersion: 2.0.0 +metadata: + name: foobar- +starterProjects: + - name: springbootproject + git: + remotes: + origin: "https://github.com/odo-devfiles/springboot-ex.git" +components: + - name: tools + container: + image: quay.io/eclipse/che-java11-maven:next + memoryLimit: 768Mi + command: ['tail'] + args: [ '-f', '/dev/null'] + volumeMounts: + - name: springbootpvc + path: /data/cache/.m2 + mountSources: true + - name: runtime + container: + image: quay.io/eclipse/che-java11-maven:next + memoryLimit: 768Mi + endpoints: + - name: "8080-tcp" + targetPort: 8080 + volumeMounts: + - name: springbootpvc + path: /data/cache/.m2 + mountSources: false + - name: springbootpvc + volume: {} +commands: + - id: defaultbuild + exec: + component: tools + commandLine: "mvn clean -Dmaven.repo.local=/data/cache/.m2/repository package -Dmaven.test.skip=true" + workingDir: /projects + group: + kind: build + isDefault: true + - id: defaultrun + exec: + component: runtime + commandLine: "mvn -Dmaven.repo.local=/data/cache/.m2/repository spring-boot:run" + workingDir: /projects + group: + kind: run + isDefault: true diff --git a/tests/v2/devfiles/samples/Parent.yaml b/tests/v2/devfiles/samples/Parent.yaml new file mode 100644 index 0000000..5f315af --- /dev/null +++ b/tests/v2/devfiles/samples/Parent.yaml @@ -0,0 +1,82 @@ +schemaVersion: 2.1.0 +commands: +- apply: + component: testcontainerparent1 + group: + kind: test + isDefault: true + label: JXTVtfYNNsaiQcqFSwTavCaBlRGMaBOXaxXsgDRxFxsNxbuHfGQuQjBwJWJVmHd + id: testapplyparentcommand1 +- id: run + exec: + component: testcontainerparent1 + commandLine: npm start + workingDir: /project + group: + kind: run + isDefault: true + hotReloadCapable: true +- id: test + composite: + commands: [testapplyparentcommand1] + group: + kind: debug + label: testcompositeparent1 + parallel: true +components: + - container: + image: mKrpiOQnyGZ00003 + name: testcontainerparent1 + - kubernetes: + inlined: | + apiVersion: batch/v1 + kind: Job + metadata: + name: pi + spec: + template: + spec: + containers: + - name: job + image: myimage + command: ["some", "command"] + restartPolicy: Never + name: testkubeparent1 + - openshift: + uri: openshift.yaml + name: openshiftcomponent1 +projects: + - name: petclinic + git: + remotes: + origin: "https://github.com/spring-projects/spring-petclinic.git" + checkoutFrom: + remote: origin + revision: main + - name: petclinic-dev + zip: + location: https://github.com/spring-projects/spring-petclinic/archive/refs/heads/main.zip + attributes: + editorFree: true + user: default +starterProjects: + - name: user-app + git: + remotes: + origin: 'https://github.com/OpenLiberty/application-stack-starters.git' + description: An Open Liberty Starter project + subDir: /app + attributes: + workingDir: /home + - name: user-app2 + zip: + location: 'https://github.com/OpenLiberty/application-stack-starters.zip' +attributes: #only applicable to v2.1.0 + category: parentdevfile + title: This is a parent devfile +variables: #only applicable to v2.1.0 + version: 2.0.0 + tag: parent + lastUpdated: "2020" + + \ No newline at end of file diff --git a/tests/v2/devfiles/samples/Test_200.yaml b/tests/v2/devfiles/samples/Test_200.yaml new file mode 100644 index 0000000..0756a6a --- /dev/null +++ b/tests/v2/devfiles/samples/Test_200.yaml @@ -0,0 +1,78 @@ +commands: +- exec: + commandLine: npm install + component: runtime + group: + isDefault: true + kind: build + hotReloadCapable: false + workingDir: /project + id: install +- exec: + commandLine: npm start + component: runtime + group: + isDefault: true + kind: run + hotReloadCapable: false + workingDir: /project + id: run +- exec: + commandLine: npm run debug + component: runtime + group: + isDefault: true + kind: debug + hotReloadCapable: false + workingDir: /project + id: debug +- exec: + commandLine: npm test + component: runtime + group: + isDefault: true + kind: test + hotReloadCapable: false + workingDir: /project + id: test +components: +- container: + dedicatedPod: true + endpoints: + - name: http-3000 + secure: false + targetPort: 3000 + image: registry.access.redhat.com/ubi8/nodejs-14:latest + memoryLimit: 1024Mi + mountSources: true + sourceMapping: /project + volumeMounts: + - name: v1 + path: /v1 + - name: v2 + path: /v2 + name: runtime +- name: v1 + volume: + size: 1Gi +- name: v2 + volume: + size: 1Gi +metadata: + description: Stack with Node.js 14 + displayName: Node.js Runtime + icon: https://nodejs.org/static/images/logos/nodejs-new-pantone-black.svg + language: javascript + name: nodejs-defect-pcbx + projectType: nodejs + tags: + - NodeJS + - Express + - ubi8 + version: 1.0.1 +schemaVersion: 2.0.0 +starterProjects: +- git: + remotes: + origin: https://github.com/odo-devfiles/nodejs-ex.git + name: nodejs-starter diff --git a/tests/v2/devfiles/samples/Test_210.yaml b/tests/v2/devfiles/samples/Test_210.yaml new file mode 100644 index 0000000..8f37f6b --- /dev/null +++ b/tests/v2/devfiles/samples/Test_210.yaml @@ -0,0 +1,78 @@ +commands: +- exec: + commandLine: npm install + component: runtime + group: + isDefault: true + kind: build + hotReloadCapable: false + workingDir: /project + id: install +- exec: + commandLine: npm start + component: runtime + group: + isDefault: true + kind: run + hotReloadCapable: false + workingDir: /project + id: run +- exec: + commandLine: npm run debug + component: runtime + group: + isDefault: true + kind: debug + hotReloadCapable: false + workingDir: /project + id: debug +- exec: + commandLine: npm test + component: runtime + group: + isDefault: true + kind: test + hotReloadCapable: false + workingDir: /project + id: test +components: +- container: + dedicatedPod: true + endpoints: + - name: http-3000 + secure: false + targetPort: 3000 + image: registry.access.redhat.com/ubi8/nodejs-14:latest + memoryLimit: 1024Mi + mountSources: true + sourceMapping: /project + volumeMounts: + - name: v1 + path: /v1 + - name: v2 + path: /v2 + name: runtime +- name: v1 + volume: + size: 1Gi +- name: v2 + volume: + size: 1Gi +metadata: + description: Stack with Node.js 14 + displayName: Node.js Runtime + icon: https://nodejs.org/static/images/logos/nodejs-new-pantone-black.svg + language: javascript + name: nodejs-defect-pcbx + projectType: nodejs + tags: + - NodeJS + - Express + - ubi8 + version: 1.0.1 +schemaVersion: 2.1.0 +starterProjects: +- git: + remotes: + origin: https://github.com/odo-devfiles/nodejs-ex.git + name: nodejs-starter diff --git a/tests/v2/devfiles/samples/Test_220.yaml b/tests/v2/devfiles/samples/Test_220.yaml new file mode 100644 index 0000000..2c8f78f --- /dev/null +++ b/tests/v2/devfiles/samples/Test_220.yaml @@ -0,0 +1,83 @@ +commands: +- exec: + commandLine: npm install + component: runtime + group: + isDefault: true + kind: build + hotReloadCapable: false + workingDir: /project + id: install +- exec: + commandLine: npm start + component: runtime + group: + isDefault: true + kind: run + hotReloadCapable: false + workingDir: /project + id: run +- exec: + commandLine: npm run debug + component: runtime + group: + isDefault: true + kind: debug + hotReloadCapable: false + workingDir: /project + id: debug +- exec: + commandLine: npm test + component: runtime + group: + isDefault: true + kind: test + hotReloadCapable: false + workingDir: /project + id: test +components: +- name: outerloop-build + image: + imageName: nodejs-image:latest + dockerfile: + uri: Dockerfile +- container: + dedicatedPod: true + endpoints: + - name: http-3000 + secure: false + targetPort: 3000 + image: registry.access.redhat.com/ubi8/nodejs-14:latest + memoryLimit: 1024Mi + mountSources: true + sourceMapping: /project + volumeMounts: + - name: v1 + path: /v1 + - name: v2 + path: /v2 + name: runtime +- name: v1 + volume: + size: 1Gi +- name: v2 + volume: + size: 1Gi +metadata: + description: Stack with Node.js 14 + displayName: Node.js Runtime + icon: https://nodejs.org/static/images/logos/nodejs-new-pantone-black.svg + language: javascript + name: nodejs-defect-pcbx + projectType: nodejs + tags: + - NodeJS + - Express + - ubi8 + version: 1.0.1 +schemaVersion: 2.2.0 +starterProjects: +- git: + remotes: + origin: https://github.com/odo-devfiles/nodejs-ex.git + name: nodejs-starter diff --git a/tests/v2/devfiles/samples/Test_Parent_KubeCRD.yaml b/tests/v2/devfiles/samples/Test_Parent_KubeCRD.yaml new file mode 100644 index 0000000..4e13eba --- /dev/null +++ b/tests/v2/devfiles/samples/Test_Parent_KubeCRD.yaml @@ -0,0 +1,46 @@ +schemaVersion: 2.1.0 +parent: + kubernetes: + name: testkubeparent1 + namespace: default + commands: + - apply: + component: devbuild + group: + kind: build #override + isDefault: false #override + label: testcontainerparent1 #override + id: applycommand + components: + - container: + image: updatedimage #override + name: devbuild + projects: + - name: parentproject + git: + remotes: + neworigin: "https://github.com/spring-projects/spring-petclinic2.git" #override, should result in 2 remotes in flattened file + checkoutFrom: + remote: neworigin #override + revision: main #override + - name: parentproject2 + zip: + location: "https://github.com/spring-projects/spring-petclinic2.zip" #override + starterProjects: + - name: parentstarterproject + git: + remotes: + origin: "https://github.com/spring-projects/spring-petclinic2.git" #override + checkoutFrom: + remote: origin + revision: master #override + attributes: # only applicable to v2.1.0 + category: mainDevfile #override + title: This is a main devfile #override + variables: #only applicable to v2.1.0 + version: 2.1.0 #override + tag: main #override + + + + diff --git a/tests/v2/devfiles/samples/Test_Parent_LocalURI.yaml b/tests/v2/devfiles/samples/Test_Parent_LocalURI.yaml new file mode 100644 index 0000000..8e112b2 --- /dev/null +++ b/tests/v2/devfiles/samples/Test_Parent_LocalURI.yaml @@ -0,0 +1,95 @@ +schemaVersion: 2.1.0 +parent: + uri: "Parent.yaml" + commands: + - apply: + component: testcontainer1 #override, point to a container in the main devfile + group: + kind: test + isDefault: false #override + label: testcontainerparent #override + id: testapplyparentcommand1 + - id: run + exec: + component: testcontainerparent1 + commandLine: npm install #override + workingDir: /project2 #override + env: #addition, does not exist in parent + - name: PATH + value: /dir + - name: USER + value: user1 + group: + kind: build #override + isDefault: false #override + hotReloadCapable: false #override + - id: test + composite: + commands: [testapplyparentcommand1, run] #override + group: + kind: debug + label: testcompositeparent1 + parallel: false #override + components: + - kubernetes: + inlined: | #override + apiVersion: batch/v1 + kind: Pod + metadata: + name: pi + namespace: dev + spec: + template: + spec: + containers: + - name: newJob + image: myimage + command: ["some", "command"] + restartPolicy: Never + name: testkubeparent1 + - openshift: + uri: openshift2.yaml #override + name: openshiftcomponent1 + - container: + image: updatedimage #override + name: testcontainerparent1 + projects: + - name: petclinic + git: + remotes: + neworigin: "https://github.com/spring-projects/spring-petclinic2.git" #override, should result in 2 remotes in flattened file + checkoutFrom: + remote: neworigin #override + revision: master #override + - name: petclinic-dev + zip: + location: https://github.com/spring-projects/spring-petclinic/petclinic.zip #override + clonePath: /petclinic #overrides the default + attributes: + editorFree: false #override + user: user1 #override + starterProjects: + - name: user-app + git: + remotes: + origin: 'https://github.com/OpenLiberty/application-stack-starters-new.git' #override + description: An Open Liberty Starter project override #override + subDir: /newapp #override + attributes: #add additional attributes + env: test + user: user1 + attributes: # only applicable to v2.1.0 + category: mainDevfile #override + title: This is a main devfile #override + variables: #only applicable to v2.1.0 + version: 2.1.0 #override + tag: main #override + lastUpdated: "2021" #override +components: +- container: + image: mKrpiOQnyGZ00003 + name: testcontainer1 + + + + diff --git a/tests/v2/devfiles/samples/Test_Parent_RegistryURL.yaml b/tests/v2/devfiles/samples/Test_Parent_RegistryURL.yaml new file mode 100644 index 0000000..deea655 --- /dev/null +++ b/tests/v2/devfiles/samples/Test_Parent_RegistryURL.yaml @@ -0,0 +1,29 @@ +schemaVersion: 2.1.0 +parent: + id: nodejs + registryUrl: "https://registry.stage.devfile.io" + commands: + - id: run + exec: + component: runtime + commandLine: npm install #override + workingDir: /project2 #override + components: + - name: runtime + container: + image: registry.access.redhat.com/ubi8/nodejs-14:dev #override + memoryLimit: 2048Mi #override + mountSources: false #override + sourceMapping: /project2 #override + endpoints: + - name: http-9080 #this will result in a second endpoint + targetPort: 9080 + starterProjects: + - name: nodejs-starter + git: + remotes: + origin: "https://github.com/odo-devfiles/nodejs-ex2.git" #override + + + + diff --git a/tests/v2/devfiles/samples/testParent.yaml b/tests/v2/devfiles/samples/testParent.yaml new file mode 100644 index 0000000..2b8acf7 --- /dev/null +++ b/tests/v2/devfiles/samples/testParent.yaml @@ -0,0 +1,114 @@ +schemaVersion: "2.0.0" +commands: + - id: testexecparent1 + exec: + commandLine: 'echo "Hello ${GREETING} ${USER}"' + component: parserTest-tests + group: + isDefault: false + kind: run + hotReloadCapable: false + label: "Command Exec run" + env: + - name: "USER" + value: "Test Tester" + - name: "GREETING" + value: "Hello" + workingDir: This Directory + - id: testcompositeparent1 + attributes: + test: Composite Test test + scope: Api + composite: + label: Composite Test + commands: + - runTest1 + - runTest2 + parallel: false + group: + isDefault: true + kind: test +components: + - container: + args: [ Arg1,Arg2 ] + command: [ run1,run2 ] + dedicatedPod: true + image: "tester" + memoryLimit: "128M" + mountSources: false + endpoints: + - name: test-endpoint + attributes: + test: Apply Test + scope: Api + exposure: public + path: test-path + protocol: http + secure: false + targetPort: 1234 + volumeMounts: + - name: volume + path: mount + sourceMapping: sourceMapping + env: + - name: envName + value: envValue + name: "testcontainerparent1" + - name: "testopenshiftparent1" + openshift: + uri: test-uri + endpoints: + - name: test-endpoint + attributes: + test: Apply Test + scope: Api + exposure: public + path: test-path + protocol: http + secure: false + targetPort: 1234 +projects: + - name: testparentproject1 + git: + checkoutFrom: + remote: test-branch + remotes: + origin: test-origin + clonePath: /Users/test/projects + sparseCheckoutDirs: [thisDir, thatDir] + - name: testparentproject2 + github: + checkoutFrom: + remote: test-branch + remotes: + origin: test-origin + clonePath: /Users/test/projects + sparseCheckoutDirs: [thisDir, thatDir] + - name: testparentproject3 + zip: + location: git-repo.zip + clonePath: /Users/test/projects + sparseCheckoutDirs: [thisDir, thatDir] +starterProjects: +- name: testparentstarterproject1 + git: + checkoutFrom: + remote: test-branch + remotes: + origin: test-origin + description: Test starter project + subDir: test-subdir +- name: testparentstarterproject2 + github: + checkoutFrom: + remote: test-branch + remotes: + origin: test-origin + description: Test starter project + subDir: test-subdir +- name: testparentstarterproject3 + zip: + location: git-repo.zip + description: Test starter project + subDir: test-subdir + diff --git a/tests/v2/integrationTest/integration_test.go b/tests/v2/integrationTest/integration_test.go new file mode 100644 index 0000000..c993960 --- /dev/null +++ b/tests/v2/integrationTest/integration_test.go @@ -0,0 +1,360 @@ +package api + +import ( + "context" + "fmt" + "io/ioutil" + "regexp" + "testing" + + schema "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + "github.com/devfile/api/v2/pkg/attributes" + commonUtils "github.com/devfile/api/v2/test/v200/utils/common" + "github.com/devfile/library/pkg/devfile/parser" + "github.com/devfile/library/pkg/testingutil" + libraryUtils "github.com/devfile/library/tests/v2/utils/library" + kubev1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func getInvalidNodeJSDevfileList() []string { + return []string{ + "devfile-with-invalid-events.yaml", + "devfile-with-invalid-volmount.yaml", + "devfile-with-multiple-defaults.yaml", + "devfile-with-no-default.yaml", + "devfile-with-preStart.yaml", + "devfile-with-subDir.yaml", + "devfileCompositeInvalidComponent.yaml", + "devfileCompositeNonExistent.yaml", + "devfileIndirectNesting.yaml", + } +} + +func IsValidNodeJSDevFile(fileName string) bool { + + fileNames := getInvalidNodeJSDevfileList() + for _, item := range fileNames { + if item == fileName { + return false + } + } + return true +} + +func getValidNodeJSDevfileList(path string) ([]string, error) { + + fileList := make([]string, 0) + files, err := ioutil.ReadDir(path) + + if err != nil { + commonUtils.LogErrorMessage(fmt.Sprintf("Error in getting file list from the directory: %s : %v", path, err)) + } + + for _, file := range files { + if !file.IsDir() { + r, err := regexp.MatchString("^devfile.+yaml$", file.Name()) + + if err == nil && r { + if IsValidNodeJSDevFile(file.Name()) { + fileList = append(fileList, file.Name()) + } + } + } + } + return fileList, err +} + +func getValidDevfileList(path string) ([]string, error) { + + fileList := make([]string, 0) + files, err := ioutil.ReadDir(path) + + if err != nil { + commonUtils.LogErrorMessage(fmt.Sprintf("Error in getting file list from the directory: %s : %v", path, err)) + } + + for _, file := range files { + if !file.IsDir() { + r, err := regexp.MatchString("^devfile.+yaml$", file.Name()) + + if err == nil && r { + fileList = append(fileList, file.Name()) + } + } + } + return fileList, err +} + +func Test_Valid_NodeJS_Devfiles(t *testing.T) { + testContent := commonUtils.TestContent{} + testContent.AddParent = false + testContent.EditContent = false + + subDir := "nodejs" + srcDir := "../../examples/source/devfiles/" + subDir + "/" + + fileNames, _ := getValidNodeJSDevfileList(srcDir) + + libraryUtils.CopyTestDevfile(t, subDir, fileNames) + + for _, fileName := range fileNames { + testContent.FileName = fileName + libraryUtils.RunStaticTest(testContent, t) + } +} + +func Test_Invalid_NodeJS_Devfiles(t *testing.T) { + testContent := commonUtils.TestContent{} + testContent.AddParent = false + testContent.EditContent = false + + fileNames := getInvalidNodeJSDevfileList() + libraryUtils.CopyTestDevfile(t, "nodejs", fileNames) + + for _, fileName := range fileNames { + testContent.FileName = fileName + libraryUtils.RunStaticTestToFail(testContent, t) + } +} + +func Test_Valid_OpenLiberty_Devfiles(t *testing.T) { + testContent := commonUtils.TestContent{} + testContent.AddParent = false + testContent.EditContent = false + + subDir := "java-openliberty" + srcDir := "../../examples/source/devfiles/" + subDir + "/" + + fileNames, _ := getValidDevfileList(srcDir) + + libraryUtils.CopyTestDevfile(t, subDir, fileNames) + + for _, fileName := range fileNames { + testContent.FileName = fileName + libraryUtils.RunStaticTest(testContent, t) + } +} + +func Test_Valid_Python_Devfiles(t *testing.T) { + testContent := commonUtils.TestContent{} + testContent.AddParent = false + testContent.EditContent = false + + subDir := "python" + srcDir := "../../examples/source/devfiles/" + subDir + "/" + + fileNames, _ := getValidDevfileList(srcDir) + + libraryUtils.CopyTestDevfile(t, subDir, fileNames) + + for _, fileName := range fileNames { + testContent.FileName = fileName + libraryUtils.RunStaticTest(testContent, t) + } +} + +func Test_Valid_Springboot_Devfiles(t *testing.T) { + testContent := commonUtils.TestContent{} + testContent.AddParent = false + testContent.EditContent = false + + subDir := "springboot" + srcDir := "../../examples/source/devfiles/" + subDir + "/" + + fileNames, _ := getValidDevfileList(srcDir) + + libraryUtils.CopyTestDevfile(t, subDir, fileNames) + + for _, fileName := range fileNames { + testContent.FileName = fileName + libraryUtils.RunStaticTest(testContent, t) + } +} + +func Test_Parent_Local_URI(t *testing.T) { + testContent := commonUtils.TestContent{} + testContent.AddParent = true + testContent.EditContent = false + testContent.FileName = "Test_Parent_LocalURI.yaml" + //copy the parent and main devfile from devfiles/samples + libraryUtils.CopyDevfileSamples(t, []string{testContent.FileName, "Parent.yaml"}) + libraryUtils.RunStaticTest(testContent, t) + libraryUtils.RunMultiThreadedStaticTest(testContent, t) +} + +func Test_v200_Devfile(t *testing.T) { + testContent := commonUtils.TestContent{} + testContent.AddParent = false + testContent.EditContent = false + testContent.FileName = "Test_200.yaml" + libraryUtils.CopyDevfileSamples(t, []string{testContent.FileName}) + libraryUtils.RunStaticTest(testContent, t) + libraryUtils.RunMultiThreadedStaticTest(testContent, t) +} + +func Test_v210_Devfile(t *testing.T) { + testContent := commonUtils.TestContent{} + testContent.AddParent = false + testContent.EditContent = false + testContent.FileName = "Test_210.yaml" + libraryUtils.CopyDevfileSamples(t, []string{testContent.FileName}) + libraryUtils.RunStaticTest(testContent, t) + libraryUtils.RunMultiThreadedStaticTest(testContent, t) +} + +func Test_v220_Devfile(t *testing.T) { + testContent := commonUtils.TestContent{} + testContent.AddParent = false + testContent.EditContent = false + testContent.FileName = "Test_220.yaml" + libraryUtils.CopyDevfileSamples(t, []string{testContent.FileName}) + libraryUtils.RunStaticTest(testContent, t) + +} + +//Create kube client and context and set as ParserArgs for Parent Kubernetes reference test. Corresponding main devfile is ../devfile/samples/TestParent_KubeCRD.yaml +func setClientAndContextParserArgs() *parser.ParserArgs { + isTrue := true + name := "testkubeparent1" + parentSpec := schema.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: schema.DevWorkspaceTemplateSpecContent{ + Commands: []schema.Command{ + { + Id: "applycommand", + CommandUnion: schema.CommandUnion{ + Apply: &schema.ApplyCommand{ + Component: "devbuild", + LabeledCommand: schema.LabeledCommand{ + Label: "testcontainerparent", + BaseCommand: schema.BaseCommand{ + Group: &schema.CommandGroup{ + Kind: schema.TestCommandGroupKind, + IsDefault: &isTrue, + }, + }, + }, + }, + }, + }, + }, + Components: []schema.Component{ + { + Name: "devbuild", + ComponentUnion: schema.ComponentUnion{ + Container: &schema.ContainerComponent{ + Container: schema.Container{ + Image: "quay.io/nodejs-12", + }, + }, + }, + }, + }, + Projects: []schema.Project{ + { + Name: "parentproject", + ProjectSource: schema.ProjectSource{ + Git: &schema.GitProjectSource{ + GitLikeProjectSource: schema.GitLikeProjectSource{ + CheckoutFrom: &schema.CheckoutFrom{ + Revision: "master", + Remote: "origin", + }, + Remotes: map[string]string{"origin": "https://github.com/spring-projects/spring-petclinic.git"}, + }, + }, + }, + }, + { + Name: "parentproject2", + ProjectSource: schema.ProjectSource{ + Zip: &schema.ZipProjectSource{ + Location: "https://github.com/spring-projects/spring-petclinic.zip", + }, + }, + }, + }, + StarterProjects: []schema.StarterProject{ + { + Name: "parentstarterproject", + ProjectSource: schema.ProjectSource{ + Git: &schema.GitProjectSource{ + GitLikeProjectSource: schema.GitLikeProjectSource{ + CheckoutFrom: &schema.CheckoutFrom{ + Revision: "main", + Remote: "origin", + }, + Remotes: map[string]string{"origin": "https://github.com/spring-projects/spring-petclinic.git"}, + }, + }, + }, + }, + }, + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{"category": "parentDevfile", "title": "This is a parent devfile"}), + Variables: map[string]string{"version": "2.0.0", "tag": "parent"}, + }, + } + testK8sClient := &testingutil.FakeK8sClient{ + DevWorkspaceResources: map[string]schema.DevWorkspaceTemplate{ + name: { + TypeMeta: kubev1.TypeMeta{ + APIVersion: "2.1.0", + }, + Spec: parentSpec, + }, + }, + } + parserArgs := parser.ParserArgs{} + parserArgs.K8sClient = testK8sClient + parserArgs.Context = context.Background() + return &parserArgs +} + +func Test_Parent_KubeCRD(t *testing.T) { + testContent := commonUtils.TestContent{} + testContent.AddParent = true + testContent.EditContent = false + testContent.FileName = "Test_Parent_KubeCRD.yaml" + parserArgs := setClientAndContextParserArgs() + libraryUtils.CopyDevfileSamples(t, []string{testContent.FileName}) + libraryUtils.SetParserArgs(*parserArgs) + libraryUtils.RunStaticTest(testContent, t) + libraryUtils.RunMultiThreadedStaticTest(testContent, t) +} + +func Test_Parent_RegistryURL(t *testing.T) { + testContent := commonUtils.TestContent{} + testContent.AddParent = true + testContent.EditContent = false + testContent.FileName = "Test_Parent_RegistryURL.yaml" + libraryUtils.CopyDevfileSamples(t, []string{testContent.FileName}) + libraryUtils.RunStaticTest(testContent, t) + libraryUtils.RunMultiThreadedStaticTest(testContent, t) +} + +func Test_Everything(t *testing.T) { + testContent := commonUtils.TestContent{} + testContent.CommandTypes = commonUtils.CommandTypes + testContent.ComponentTypes = commonUtils.ComponentTypes + testContent.ProjectTypes = commonUtils.ProjectSourceTypes + testContent.StarterProjectTypes = commonUtils.ProjectSourceTypes + testContent.AddEvents = true + testContent.AddMetaData = true + testContent.EditContent = false + testContent.FileName = commonUtils.GetDevFileName() + libraryUtils.RunTest(testContent, t) + libraryUtils.RunMultiThreadTest(testContent, t) +} + +func Test_EverythingEdit(t *testing.T) { + testContent := commonUtils.TestContent{} + testContent.CommandTypes = commonUtils.CommandTypes + testContent.ComponentTypes = commonUtils.ComponentTypes + testContent.ProjectTypes = commonUtils.ProjectSourceTypes + testContent.StarterProjectTypes = commonUtils.ProjectSourceTypes + testContent.AddEvents = true + testContent.AddMetaData = true + testContent.EditContent = true + testContent.FileName = commonUtils.GetDevFileName() + libraryUtils.RunTest(testContent, t) + libraryUtils.RunMultiThreadTest(testContent, t) +} diff --git a/tests/v2/utils/library/command_test_utils.go b/tests/v2/utils/library/command_test_utils.go new file mode 100644 index 0000000..2a539b1 --- /dev/null +++ b/tests/v2/utils/library/command_test_utils.go @@ -0,0 +1,107 @@ +package utils + +import ( + "errors" + "fmt" + "io/ioutil" + + "github.com/google/go-cmp/cmp" + "sigs.k8s.io/yaml" + + schema "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + commonUtils "github.com/devfile/api/v2/test/v200/utils/common" +) + +// getSchemaCommand get a specified command from the devfile schema structure +func getSchemaCommand(commands []schema.Command, id string) (*schema.Command, bool) { + found := false + var schemaCommand schema.Command + for _, command := range commands { + if command.Id == id { + schemaCommand = command + found = true + break + } + } + return &schemaCommand, found +} + +// UpdateCommand randomly updates attribute values of a specified command in the devfile schema +func UpdateCommand(devfile *commonUtils.TestDevfile, commandId string) error { + + var err error + testCommand, found := getSchemaCommand(devfile.SchemaDevFile.Commands, commandId) + if found { + commonUtils.LogInfoMessage(fmt.Sprintf("Updating command id: %s", commandId)) + if testCommand.Exec != nil { + devfile.SetExecCommandValues(testCommand) + } else if testCommand.Composite != nil { + devfile.SetCompositeCommandValues(testCommand) + } else if testCommand.Apply != nil { + devfile.SetApplyCommandValues(testCommand) + } + } else { + err = errors.New(commonUtils.LogErrorMessage(fmt.Sprintf("Command not found in test : %s", commandId))) + } + return err +} + +// VerifyCommands verifies commands returned by the parser are the same as those saved in the devfile schema +func VerifyCommands(devfile *commonUtils.TestDevfile, parserCommands []schema.Command) error { + + commonUtils.LogInfoMessage("Enter VerifyCommands") + var errorString []string + + // Compare entire array of commands + if !cmp.Equal(parserCommands, devfile.SchemaDevFile.Commands) { + errorString = append(errorString, commonUtils.LogErrorMessage(fmt.Sprintf("Command array compare failed."))) + // Array compare failed. Narrow down by comparing indivdual commands + for _, command := range parserCommands { + if testCommand, found := getSchemaCommand(devfile.SchemaDevFile.Commands, command.Id); found { + if !cmp.Equal(command, *testCommand) { + parserFilename := commonUtils.AddSuffixToFileName(devfile.FileName, "_"+command.Id+"_Parser") + testFilename := commonUtils.AddSuffixToFileName(devfile.FileName, "_"+command.Id+"_Test") + commonUtils.LogInfoMessage(fmt.Sprintf(".......marshall and write devfile %s", devfile.FileName)) + c, err := yaml.Marshal(command) + if err != nil { + errorString = append(errorString, commonUtils.LogErrorMessage(fmt.Sprintf(".......marshall devfile %s", parserFilename))) + } else { + err = ioutil.WriteFile(parserFilename, c, 0644) + if err != nil { + errorString = append(errorString, commonUtils.LogErrorMessage(fmt.Sprintf(".......write devfile %s", parserFilename))) + } + } + commonUtils.LogInfoMessage(fmt.Sprintf(".......marshall and write devfile %s", testFilename)) + c, err = yaml.Marshal(testCommand) + if err != nil { + errorString = append(errorString, commonUtils.LogErrorMessage(fmt.Sprintf(".......marshall devfile %s", testFilename))) + } else { + err = ioutil.WriteFile(testFilename, c, 0644) + if err != nil { + errorString = append(errorString, commonUtils.LogErrorMessage(fmt.Sprintf(".......write devfile %s", testFilename))) + } + } + errorString = append(errorString, commonUtils.LogInfoMessage(fmt.Sprintf("Command %s did not match, see files : %s and %s", command.Id, parserFilename, testFilename))) + } else { + commonUtils.LogInfoMessage(fmt.Sprintf(" --> Command matched : %s", command.Id)) + } + } else { + errorString = append(errorString, commonUtils.LogErrorMessage(fmt.Sprintf("Command from parser not known to test - id : %s ", command.Id))) + } + + } + for _, command := range devfile.SchemaDevFile.Commands { + if _, found := getSchemaCommand(parserCommands, command.Id); !found { + errorString = append(errorString, commonUtils.LogErrorMessage(fmt.Sprintf("Command from test not returned by parser : %s ", command.Id))) + } + } + } else { + commonUtils.LogInfoMessage(fmt.Sprintf(" --> Command structures matched")) + } + + var err error + if len(errorString) > 0 { + err = errors.New(fmt.Sprint(errorString)) + } + return err +} diff --git a/tests/v2/utils/library/component_test_utils.go b/tests/v2/utils/library/component_test_utils.go new file mode 100644 index 0000000..d92f1a9 --- /dev/null +++ b/tests/v2/utils/library/component_test_utils.go @@ -0,0 +1,112 @@ +package utils + +import ( + "errors" + "fmt" + "io/ioutil" + + schema "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + commonUtils "github.com/devfile/api/v2/test/v200/utils/common" + "github.com/google/go-cmp/cmp" + "sigs.k8s.io/yaml" +) + +// getSchemaComponent returns a named component from an array of components +func getSchemaComponent(components []schema.Component, name string) (*schema.Component, bool) { + found := false + var schemaComponent schema.Component + for _, component := range components { + if component.Name == name { + schemaComponent = component + found = true + break + } + } + return &schemaComponent, found +} + +// UpdateComponent randomly updates the attribute values of a specified component +func UpdateComponent(devfile *commonUtils.TestDevfile, componentName string) error { + + var errorString []string + testComponent, found := getSchemaComponent(devfile.SchemaDevFile.Components, componentName) + if found { + commonUtils.LogInfoMessage(fmt.Sprintf("....... Updating component name: %s", componentName)) + if testComponent.Container != nil { + devfile.SetContainerComponentValues(testComponent) + } else if testComponent.Kubernetes != nil { + devfile.SetK8sComponentValues(testComponent) + } else if testComponent.Openshift != nil { + devfile.SetK8sComponentValues(testComponent) + } else if testComponent.Volume != nil { + devfile.SetVolumeComponentValues(testComponent) + } else { + errorString = append(errorString, commonUtils.LogInfoMessage(fmt.Sprintf("....... Component is not of expected type."))) + } + } else { + errorString = append(errorString, commonUtils.LogInfoMessage(fmt.Sprintf("....... Component not found in test : %s", componentName))) + } + var err error + if len(errorString) > 0 { + err = errors.New(fmt.Sprint(errorString)) + } + return err +} + +// VerifyComponents verifies components returned by the parser are the same as those saved in the devfile schema +func VerifyComponents(devfile *commonUtils.TestDevfile, parserComponents []schema.Component) error { + + commonUtils.LogInfoMessage("Enter VerifyComponents") + var errorString []string + + // Compare entire array of components + if !cmp.Equal(parserComponents, devfile.SchemaDevFile.Components) { + errorString = append(errorString, commonUtils.LogErrorMessage(fmt.Sprintf("Component array compare failed."))) + for _, component := range parserComponents { + if testComponent, found := getSchemaComponent(devfile.SchemaDevFile.Components, component.Name); found { + if !cmp.Equal(component, *testComponent) { + parserFilename := commonUtils.AddSuffixToFileName(devfile.FileName, "_"+component.Name+"_Parser") + testFilename := commonUtils.AddSuffixToFileName(devfile.FileName, "_"+component.Name+"_Test") + commonUtils.LogInfoMessage(fmt.Sprintf(".......marshall and write devfile %s", parserFilename)) + c, err := yaml.Marshal(component) + if err != nil { + errorString = append(errorString, commonUtils.LogErrorMessage(fmt.Sprintf(".......marshall devfile %s", parserFilename))) + } else { + err = ioutil.WriteFile(parserFilename, c, 0644) + if err != nil { + errorString = append(errorString, commonUtils.LogErrorMessage(fmt.Sprintf(".......write devfile %s", parserFilename))) + } + } + commonUtils.LogInfoMessage(fmt.Sprintf(".......marshall and write devfile %s", testFilename)) + c, err = yaml.Marshal(testComponent) + if err != nil { + errorString = append(errorString, commonUtils.LogErrorMessage(fmt.Sprintf(".......marshall devfile %s", testFilename))) + } else { + err = ioutil.WriteFile(testFilename, c, 0644) + if err != nil { + errorString = append(errorString, commonUtils.LogErrorMessage(fmt.Sprintf(".......write devfile %s", testFilename))) + } + } + errorString = append(errorString, commonUtils.LogErrorMessage(fmt.Sprintf("Component %s did not match, see files : %s and %s", component.Name, parserFilename, testFilename))) + } else { + commonUtils.LogInfoMessage(fmt.Sprintf(" --> Component matched : %s", component.Name)) + } + } else { + errorString = append(errorString, commonUtils.LogErrorMessage(fmt.Sprintf("Component from parser not known to test - id : %s ", component.Name))) + } + } + for _, component := range devfile.SchemaDevFile.Components { + if _, found := getSchemaComponent(parserComponents, component.Name); !found { + errorString = append(errorString, commonUtils.LogErrorMessage(fmt.Sprintf("Component from test not returned by parser : %s ", component.Name))) + } + } + } else { + commonUtils.LogInfoMessage(fmt.Sprintf("Component structures matched")) + } + + var err error + if len(errorString) > 0 { + err = errors.New(fmt.Sprint(errorString)) + } + return err +} diff --git a/tests/v2/utils/library/project_test_utils.go b/tests/v2/utils/library/project_test_utils.go new file mode 100644 index 0000000..799d751 --- /dev/null +++ b/tests/v2/utils/library/project_test_utils.go @@ -0,0 +1,192 @@ +package utils + +import ( + "errors" + "fmt" + "io/ioutil" + + "github.com/google/go-cmp/cmp" + "sigs.k8s.io/yaml" + + schema "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + commonUtils "github.com/devfile/api/v2/test/v200/utils/common" +) + +// getSchemaProject gets a named Project from the saved devfile schema structure +func getSchemaProject(projects []schema.Project, name string) (*schema.Project, bool) { + found := false + var schemaProject schema.Project + for _, project := range projects { + if project.Name == name { + schemaProject = project + found = true + break + } + } + return &schemaProject, found +} + +// getSchemaStarterProject gets a named Starter Project from the saved devfile schema structure +func getSchemaStarterProject(starterProjects []schema.StarterProject, name string) (*schema.StarterProject, bool) { + found := false + var schemaStarterProject schema.StarterProject + for _, starterProject := range starterProjects { + if starterProject.Name == name { + schemaStarterProject = starterProject + found = true + break + } + } + return &schemaStarterProject, found +} + +// UpdateProject randomly modifies an existing project +func UpdateProject(devfile *commonUtils.TestDevfile, projectName string) error { + + var err error + testProject, found := getSchemaProject(devfile.SchemaDevFile.Projects, projectName) + if found { + commonUtils.LogInfoMessage(fmt.Sprintf("Updating Project : %s", projectName)) + devfile.SetProjectValues(testProject) + } else { + err = errors.New(commonUtils.LogErrorMessage(fmt.Sprintf("Project not found in test : %s", projectName))) + } + return err + +} + +// UpdateStarterProject randomly modifies an existing starter project +func UpdateStarterProject(devfile *commonUtils.TestDevfile, projectName string) error { + + var err error + testStarterProject, found := getSchemaStarterProject(devfile.SchemaDevFile.StarterProjects, projectName) + if found { + commonUtils.LogInfoMessage(fmt.Sprintf("Updating Starter Project : %s", projectName)) + devfile.SetStarterProjectValues(testStarterProject) + } else { + err = errors.New(commonUtils.LogErrorMessage(fmt.Sprintf("Starter Project not found in test : %s", projectName))) + } + return err +} + +// VerifyProjects verifies projects returned by the parser are the same as those saved in the devfile schema +func VerifyProjects(devfile *commonUtils.TestDevfile, parserProjects []schema.Project) error { + + commonUtils.LogInfoMessage("Enter VerifyProjects") + var errorString []string + + // Compare entire array of projects + if !cmp.Equal(parserProjects, devfile.SchemaDevFile.Projects) { + // Compare failed so compare each project to find which one(s) don't compare + errorString = append(errorString, commonUtils.LogErrorMessage(fmt.Sprintf("Project array compare failed."))) + for _, project := range parserProjects { + if testProject, found := getSchemaProject(devfile.SchemaDevFile.Projects, project.Name); found { + if !cmp.Equal(project, *testProject) { + // Write out the failing project to a file, once as expected by the test, and a second as returned by the parser + parserFilename := commonUtils.AddSuffixToFileName(devfile.FileName, "_"+project.Name+"_Parser") + testFilename := commonUtils.AddSuffixToFileName(devfile.FileName, "_"+project.Name+"_Test") + commonUtils.LogInfoMessage(fmt.Sprintf(".......marshall and write devfile %s", parserFilename)) + c, err := yaml.Marshal(project) + if err != nil { + errorString = append(errorString, commonUtils.LogErrorMessage(fmt.Sprintf(".......marshall devfile %s", parserFilename))) + } else { + err = ioutil.WriteFile(parserFilename, c, 0644) + if err != nil { + errorString = append(errorString, commonUtils.LogErrorMessage(fmt.Sprintf(".......write devfile %s", parserFilename))) + } + } + commonUtils.LogInfoMessage(fmt.Sprintf(".......marshall and write devfile %s", testFilename)) + c, err = yaml.Marshal(testProject) + if err != nil { + errorString = append(errorString, commonUtils.LogErrorMessage(fmt.Sprintf(".......marshall devfile %s", testFilename))) + } else { + err = ioutil.WriteFile(testFilename, c, 0644) + if err != nil { + errorString = append(errorString, commonUtils.LogErrorMessage(fmt.Sprintf(".......write devfile %s", testFilename))) + } + } + errorString = append(errorString, commonUtils.LogErrorMessage(fmt.Sprintf("Project %s did not match, see files : %s and %s", project.Name, parserFilename, testFilename))) + } else { + commonUtils.LogInfoMessage(fmt.Sprintf(" --> Project matched : %s", project.Name)) + } + } else { + errorString = append(errorString, commonUtils.LogErrorMessage(fmt.Sprintf("Project from parser not known to test - id : %s ", project.Name))) + } + } + // Check test does not include projects which the parser did not return + for _, project := range devfile.SchemaDevFile.Projects { + if _, found := getSchemaProject(parserProjects, project.Name); !found { + errorString = append(errorString, commonUtils.LogErrorMessage(fmt.Sprintf("Project from test not returned by parser : %s ", project.Name))) + } + } + } else { + commonUtils.LogInfoMessage(fmt.Sprintf("Project structures matched")) + } + + var err error + if len(errorString) > 0 { + err = errors.New(fmt.Sprint(errorString)) + } + return err +} + +// VerifyStarterProjects verifies starter projects returned by the parser are the same as those saved in the devfile schema +func VerifyStarterProjects(devfile *commonUtils.TestDevfile, parserStarterProjects []schema.StarterProject) error { + + commonUtils.LogInfoMessage("Enter VerifyStarterProjects") + var errorString []string + + // Compare entire array of projects + if !cmp.Equal(parserStarterProjects, devfile.SchemaDevFile.StarterProjects) { + // Compare failed so compare each project to find which one(s) don't compare + errorString = append(errorString, commonUtils.LogErrorMessage(fmt.Sprintf("Starter Project array compare failed."))) + for _, starterProject := range parserStarterProjects { + if testStarterProject, found := getSchemaStarterProject(devfile.SchemaDevFile.StarterProjects, starterProject.Name); found { + if !cmp.Equal(starterProject, *testStarterProject) { + // Write out the failing starter project to a file, once as expected by the test, and a second as returned by the parser + parserFilename := commonUtils.AddSuffixToFileName(devfile.FileName, "_"+starterProject.Name+"_Parser") + testFilename := commonUtils.AddSuffixToFileName(devfile.FileName, "_"+starterProject.Name+"_Test") + commonUtils.LogInfoMessage(fmt.Sprintf(".......marshall and write devfile %s", parserFilename)) + c, err := yaml.Marshal(starterProject) + if err != nil { + errorString = append(errorString, commonUtils.LogErrorMessage(fmt.Sprintf(".......marshall devfile %s", parserFilename))) + } else { + err = ioutil.WriteFile(parserFilename, c, 0644) + if err != nil { + errorString = append(errorString, commonUtils.LogErrorMessage(fmt.Sprintf(".......write devfile %s", parserFilename))) + } + } + commonUtils.LogInfoMessage(fmt.Sprintf(".......marshall and write devfile %s", testFilename)) + c, err = yaml.Marshal(testStarterProject) + if err != nil { + errorString = append(errorString, commonUtils.LogErrorMessage(fmt.Sprintf(".......marshall devfile %s", testFilename))) + } else { + err = ioutil.WriteFile(testFilename, c, 0644) + if err != nil { + errorString = append(errorString, commonUtils.LogErrorMessage(fmt.Sprintf(".......write devfile %s", testFilename))) + } + } + errorString = append(errorString, commonUtils.LogErrorMessage(fmt.Sprintf("Starter Project %s did not match, see files : %s and %s", starterProject.Name, parserFilename, testFilename))) + } else { + commonUtils.LogInfoMessage(fmt.Sprintf(" --> Starter Project matched : %s", starterProject.Name)) + } + } else { + errorString = append(errorString, commonUtils.LogErrorMessage(fmt.Sprintf("Starter Project from parser not known to test - id : %s ", starterProject.Name))) + } + } + // Check test does not include projects which the parser did not return + for _, starterProject := range devfile.SchemaDevFile.StarterProjects { + if _, found := getSchemaStarterProject(parserStarterProjects, starterProject.Name); !found { + errorString = append(errorString, commonUtils.LogErrorMessage(fmt.Sprintf("Starter Project from test not returned by parser : %s ", starterProject.Name))) + } + } + } else { + commonUtils.LogInfoMessage(fmt.Sprintf("Starter Project structures matched")) + } + + var err error + if len(errorString) > 0 { + err = errors.New(fmt.Sprint(errorString)) + } + return err +} diff --git a/tests/v2/utils/library/test_utils.go b/tests/v2/utils/library/test_utils.go new file mode 100644 index 0000000..6136036 --- /dev/null +++ b/tests/v2/utils/library/test_utils.go @@ -0,0 +1,606 @@ +package utils + +import ( + "errors" + "fmt" + "os" + "strconv" + "strings" + "testing" + "time" + + schema "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + header "github.com/devfile/api/v2/pkg/devfile" + commonUtils "github.com/devfile/api/v2/test/v200/utils/common" + devfilepkg "github.com/devfile/library/pkg/devfile" + "github.com/devfile/library/pkg/devfile/parser" + devfileCtx "github.com/devfile/library/pkg/devfile/parser/context" + devfileData "github.com/devfile/library/pkg/devfile/parser/data" + "github.com/devfile/library/pkg/devfile/parser/data/v2/common" + "github.com/devfile/library/pkg/util" +) + +const ( + // numDevfiles : the number of devfiles to create for each test + numDevfiles = 5 + // numThreads : Number of threads used by multi-thread tests + numThreads = 5 + // schemaVersion: Latest schemaVersion + schemaVersion = "2.2.0" +) + +// DevfileValidator struct for DevfileValidator interface. +// The DevfileValidator interface is test/v200/utils/common/test_utils.go of the devfile/api repository. +type DevfileValidator struct{} + +var parserArgs = parser.ParserArgs{} + +//directory where test devfiles are generated and/or copied +const destDir = "tmp/library_test/" + +// WriteAndValidate implements DevfileValidator interface. +// writes to disk and validates the specified devfile +func (devfileValidator DevfileValidator) WriteAndValidate(devfile *commonUtils.TestDevfile) error { + err := writeDevfile(devfile) + if err != nil { + commonUtils.LogErrorMessage(fmt.Sprintf("Error writing file : %s : %v", devfile.FileName, err)) + } else { + err = validateDevfile(devfile) + if err != nil { + commonUtils.LogErrorMessage(fmt.Sprintf("Error vaidating file : %s : %v", devfile.FileName, err)) + } else { + err = verify(devfile) + } + } + return err +} + +// DevfileFollower struct for DevfileFollower interface. +// The DevfileFollower interface is defined in test/v200/utils/common/test_utils.go of the devfile/api repository +type DevfileFollower struct { + LibraryData devfileData.DevfileData +} + +// AddCommand adds the specified command to the library data +func (devfileFollower DevfileFollower) AddCommand(command schema.Command) error { + return devfileFollower.LibraryData.AddCommands([]schema.Command{command}) +} + +// UpdateCommand updates the specified command in the library data +func (devfileFollower DevfileFollower) UpdateCommand(command schema.Command) { + devfileFollower.LibraryData.UpdateCommand(command) +} + +// AddComponent adds the specified component to the library data +func (devfileFollower DevfileFollower) AddComponent(component schema.Component) error { + var components []schema.Component + components = append(components, component) + return devfileFollower.LibraryData.AddComponents(components) +} + +// UpdateComponent updates the specified component in the library data +func (devfileFollower DevfileFollower) UpdateComponent(component schema.Component) { + devfileFollower.LibraryData.UpdateComponent(component) +} + +// AddProject adds the specified project to the library data +func (devfileFollower DevfileFollower) AddProject(project schema.Project) error { + var projects []schema.Project + projects = append(projects, project) + return devfileFollower.LibraryData.AddProjects(projects) +} + +// UpdateProject updates the specified project in the library data +func (devfileFollower DevfileFollower) UpdateProject(project schema.Project) { + devfileFollower.LibraryData.UpdateProject(project) +} + +// AddStarterProject adds the specified starter project to the library data +func (devfileFollower DevfileFollower) AddStarterProject(starterProject schema.StarterProject) error { + var starterProjects []schema.StarterProject + starterProjects = append(starterProjects, starterProject) + return devfileFollower.LibraryData.AddStarterProjects(starterProjects) +} + +// UpdateStarterProject updates the specified starter project in the library data +func (devfileFollower DevfileFollower) UpdateStarterProject(starterProject schema.StarterProject) { + devfileFollower.LibraryData.UpdateStarterProject(starterProject) +} + +// AddEvent adds the specified event to the library data +func (devfileFollower DevfileFollower) AddEvent(event schema.Events) error { + return devfileFollower.LibraryData.AddEvents(event) +} + +// UpdateEvent updates the specified event in the library data +func (devfileFollower DevfileFollower) UpdateEvent(event schema.Events) { + devfileFollower.LibraryData.UpdateEvents(event.PostStart, event.PostStop, event.PreStart, event.PreStop) +} + +// SetParent sets the specified parent in the library data +func (devfileFollower DevfileFollower) SetParent(parent schema.Parent) error { + devfileFollower.LibraryData.SetParent(&parent) + return nil +} + +// UpdateParent updates the specified parent in the library data +func (devfileFollower DevfileFollower) UpdateParent(parent schema.Parent) { + devfileFollower.LibraryData.SetParent(&parent) +} + +// SetMetaData sets the specified metaData in the library data +func (devfileFollower DevfileFollower) SetMetaData(metaData header.DevfileMetadata) error { + devfileFollower.LibraryData.SetMetadata(metaData) + return nil +} + +// UpdateMetaData updates the specified UpdateMetaData in the library data +func (devfileFollower DevfileFollower) UpdateMetaData(updateMetaData header.DevfileMetadata) { + devfileFollower.LibraryData.SetMetadata(updateMetaData) +} + +// SetSchemaVersion sets the specified schemaVersion in the library data +func (devfileFollower DevfileFollower) SetSchemaVersion(schemaVersion string) { + devfileFollower.LibraryData.SetSchemaVersion(schemaVersion) +} + +// WriteDevfile uses the library to create a devfile on disk for use in a test. +func writeDevfile(devfile *commonUtils.TestDevfile) error { + var err error + + fileName := devfile.FileName + if !strings.HasSuffix(fileName, ".yaml") { + fileName += ".yaml" + } + + commonUtils.LogInfoMessage(fmt.Sprintf("Use Parser to write devfile %s", fileName)) + + ctx := devfileCtx.NewDevfileCtx(fileName) + + err = ctx.SetAbsPath() + if err != nil { + commonUtils.LogErrorMessage(fmt.Sprintf("Setting devfile path : %v", err)) + } else { + devObj := parser.DevfileObj{ + Ctx: ctx, + Data: devfile.Follower.(DevfileFollower).LibraryData, + } + err = devObj.WriteYamlDevfile() + if err != nil { + commonUtils.LogErrorMessage(fmt.Sprintf("Writing devfile : %v", err)) + } + } + + return err +} + +// validateDevfile uses the library to parse and validate a devfile on disk +func validateDevfile(devfile *commonUtils.TestDevfile) error { + var err error + + commonUtils.LogInfoMessage(fmt.Sprintf("Parse and Validate %s : ", devfile.FileName)) + + parserArgs.Path = devfile.FileName + libraryObj, warning, err := devfilepkg.ParseDevfileAndValidate(parserArgs) + + if len(warning.Commands) > 0 || len(warning.Components) > 0 || len(warning.Projects) > 0 || len(warning.StarterProjects) > 0 { + commonUtils.LogWarningMessage(fmt.Sprintf("top-level variables were not substituted successfully %+v\n", warning)) + } + + if err != nil { + commonUtils.LogErrorMessage(fmt.Sprintf("From ParseDevfileAndValidate %v : ", err)) + } else { + follower := devfile.Follower.(DevfileFollower) + follower.LibraryData = libraryObj.Data + } + + err = verifyEphemeralUnset(libraryObj) + + return err +} + +// validateDevfileToFail uses the library to parse and validate a devfile on disk and expect error or failure +func validateDevfileToFail(devfile *commonUtils.TestDevfile) error { + var err error + + commonUtils.LogInfoMessage(fmt.Sprintf("Parse and Validate %s and expects a failure: ", devfile.FileName)) + + parserArgs.Path = devfile.FileName + _, warning, err := devfilepkg.ParseDevfileAndValidate(parserArgs) + + if len(warning.Commands) > 0 || len(warning.Components) > 0 || len(warning.Projects) > 0 || len(warning.StarterProjects) > 0 { + commonUtils.LogWarningMessage(fmt.Sprintf("top-level variables were not substituted successfully %+v\n", warning)) + } + + if err == nil { + commonUtils.LogErrorMessage(fmt.Sprintf("From ParseDevfileAndValidate, expected error is not found.")) + } + + return err +} + +// verifyEphemeralUnset verifies volume.Ephemeral is not set on schema version 2.0.0 +func verifyEphemeralUnset(libraryObj parser.DevfileObj) error { + + if libraryObj.Data != nil { + version := libraryObj.Data.GetSchemaVersion() + + //verify volume.Ephemeral is not set on schema version 2.0.0 + if version == string(devfileData.APISchemaVersion200) { + volumes, err := libraryObj.Data.GetComponents(common.DevfileOptions{ + ComponentOptions: common.ComponentOptions{ + ComponentType: schema.VolumeComponentType, + }, + }) + + if err != nil { + return err + } + + for i := range volumes { + volume := volumes[i].Volume + if volume != nil && volume.Ephemeral != nil { + return errors.New("ephemeral is not supported on schema version 2.0.0") + } + } + } + } + + return nil +} + +// RunMultiThreadTest : Runs the same test on multiple threads, the test is based on the content of the specified TestContent +func RunMultiThreadTest(testContent commonUtils.TestContent, t *testing.T) { + + commonUtils.LogMessage(fmt.Sprintf("Start Threaded test for %s", testContent.FileName)) + + devfileName := testContent.FileName + for i := 1; i < numThreads; i++ { + testContent.FileName = commonUtils.AddSuffixToFileName(devfileName, "T"+strconv.Itoa(i)+"-") + go RunTest(testContent, t) + } + + commonUtils.LogMessage(fmt.Sprintf("Sleep 3 seconds to allow all threads to complete : %s", devfileName)) + time.Sleep(3 * time.Second) + commonUtils.LogMessage(fmt.Sprintf("Sleep complete : %s", devfileName)) + +} + +// RunMultiThreadedStaticTest : Runs the same test on multiple threads, the test is based on the content of the specified TestContent +func RunMultiThreadedStaticTest(testContent commonUtils.TestContent, t *testing.T) { + + commonUtils.LogMessage(fmt.Sprintf("Start Threaded static test for %s", testContent.FileName)) + devfileName := testContent.FileName + for i := 1; i < numThreads+1; i++ { + testContent.FileName = commonUtils.AddSuffixToFileName(devfileName, "T"+strconv.Itoa(i)+"-") + duplicateDevfileSample(t, devfileName, testContent.FileName) + go RunStaticTest(testContent, t) + } + + commonUtils.LogMessage(fmt.Sprintf("Sleep 3 seconds to allow all threads to complete : %s", devfileName)) + time.Sleep(3 * time.Second) + commonUtils.LogMessage(fmt.Sprintf("Sleep complete : %s", devfileName)) + +} + +// SetParserArgs : Used when parser args other than filename are set in the library tests +func SetParserArgs(args parser.ParserArgs) { + parserArgs = args +} + +// CopyDevfileSamples : Copies existing artifacts from the devfiles/samples directory to the tmp/library_test directory. Used in parent tests +func CopyDevfileSamples(t *testing.T, testDevfiles []string) { + + srcDir := "../devfiles/samples/" + dstDir := commonUtils.CreateTempDir("library_test") + + for i := range testDevfiles { + srcPath := srcDir + testDevfiles[i] + destPath := dstDir + testDevfiles[i] + + file, err := os.Stat(srcPath) + if err != nil { + t.Fatalf(commonUtils.LogErrorMessage(fmt.Sprintf("Error locating testDevfile %v ", err))) + } else { + commonUtils.LogMessage(fmt.Sprintf("copy file from %s to %s ", srcPath, destPath)) + util.CopyFile(srcPath, destPath, file) + } + } +} + +// CopyTestDevfile : Copies existing artifacts from the devfiles/samples directory to the tmp/library_test directory. Used in parent tests +func CopyTestDevfile(t *testing.T, subDir string, testDevfiles []string) { + + srcDir := "../../examples/source/devfiles/" + subDir + "/" + dstDir := commonUtils.CreateTempDir("library_test") + + for i := range testDevfiles { + srcPath := srcDir + testDevfiles[i] + destPath := dstDir + testDevfiles[i] + + file, err := os.Stat(srcPath) + if err != nil { + t.Fatalf(commonUtils.LogErrorMessage(fmt.Sprintf("Error locating testDevfile %v ", err))) + } else { + commonUtils.LogMessage(fmt.Sprintf("copy file from %s to %s ", srcPath, destPath)) + util.CopyFile(srcPath, destPath, file) + } + } +} + +//duplicateDevfileSample: Makes a copy of the parent devfile test artifact that is expected to exist in the tmp/library_test directory. +//This is used in the multi-threaded parent test scenarios +func duplicateDevfileSample(t *testing.T, src string, dst string) { + srcPath := destDir + src + destPath := destDir + dst + file, err := os.Stat(srcPath) + if err != nil { + t.Fatalf(commonUtils.LogErrorMessage(fmt.Sprintf("Error locating testDevfile %v ", err))) + } else { + commonUtils.LogMessage(fmt.Sprintf("duplicate file %s to %s ", srcPath, destPath)) + util.CopyFile(srcPath, destDir+dst, file) + } +} + +// RunTest : Runs a test to create and verify a devfile based on the content of the specified TestContent +func RunTest(testContent commonUtils.TestContent, t *testing.T) { + commonUtils.LogMessage(fmt.Sprintf("Start test for %s", testContent.FileName)) + devfileName := testContent.FileName + for i := 1; i <= numDevfiles; i++ { + + testContent.FileName = commonUtils.AddSuffixToFileName(devfileName, strconv.Itoa(i)) + commonUtils.LogMessage(fmt.Sprintf("Start test for %s", testContent.FileName)) + + validator := DevfileValidator{} + follower := DevfileFollower{} + libraryData, err := devfileData.NewDevfileData(schemaVersion) + if err != nil { + t.Fatalf(commonUtils.LogMessage(fmt.Sprintf("Error creating parser data : %v", err))) + } + libraryData.SetSchemaVersion(schemaVersion) + follower.LibraryData = libraryData + commonUtils.LogMessage(fmt.Sprintf("Parser data created with schema version : %s", follower.LibraryData.GetSchemaVersion())) + + testDevfile, err := commonUtils.GetDevfile(testContent.FileName, follower, validator) + if err != nil { + t.Fatalf(commonUtils.LogMessage(fmt.Sprintf("Error creating devfile : %v", err))) + } + + testDevfile.RunTest(testContent, t) + + if testContent.EditContent { + if len(testContent.CommandTypes) > 0 { + err = editCommands(&testDevfile) + if err != nil { + t.Fatalf(commonUtils.LogErrorMessage(fmt.Sprintf("ERROR editing commands : %s : %v", testContent.FileName, err))) + } + } + if len(testContent.ComponentTypes) > 0 { + err = editComponents(&testDevfile) + if err != nil { + t.Fatalf(commonUtils.LogErrorMessage(fmt.Sprintf("ERROR editing components : %s : %v", testContent.FileName, err))) + } + } + if len(testContent.ProjectTypes) > 0 { + err = editProjects(&testDevfile) + if err != nil { + t.Fatalf(commonUtils.LogErrorMessage(fmt.Sprintf("ERROR editing projects : %s : %v", testContent.FileName, err))) + } + } + if len(testContent.StarterProjectTypes) > 0 { + err = editStarterProjects(&testDevfile) + if err != nil { + t.Fatalf(commonUtils.LogErrorMessage(fmt.Sprintf("ERROR editing starter projects : %s : %v", testContent.FileName, err))) + } + } + + validator.WriteAndValidate(&testDevfile) + } + } +} + +//RunStaticTest : Runs fixed tests based on pre-existing artifacts +func RunStaticTest(testContent commonUtils.TestContent, t *testing.T) { + commonUtils.LogMessage(fmt.Sprintf("Start test for %s", testContent.FileName)) + follower := DevfileFollower{} + validator := DevfileValidator{} + testfileName := destDir + testContent.FileName + testDevfile, _ := commonUtils.GetDevfile(testfileName, follower, validator) + testDevfile.SchemaDevFile.Parent = &schema.Parent{} + err := validateDevfile(&testDevfile) + if err != nil { + t.Fatalf(commonUtils.LogErrorMessage(fmt.Sprintf("Error validating testDevfile %v ", err))) + } +} + +//RunStaticTestToFail : Runs fixed tests based on pre-existing artifacts and expects a failure +func RunStaticTestToFail(testContent commonUtils.TestContent, t *testing.T) { + commonUtils.LogMessage(fmt.Sprintf("Start test for %s", testContent.FileName)) + follower := DevfileFollower{} + validator := DevfileValidator{} + testfileName := destDir + testContent.FileName + testDevfile, _ := commonUtils.GetDevfile(testfileName, follower, validator) + testDevfile.SchemaDevFile.Parent = &schema.Parent{} + err := validateDevfileToFail(&testDevfile) + if err == nil { + t.Fatalf(commonUtils.LogErrorMessage(fmt.Sprintf("Error invalid testDevfile %s is wrongfully validated.", testContent.FileName))) + } else { + commonUtils.LogInfoMessage(fmt.Sprintf("Expected error was occurred in validating %s : %v", testContent.FileName, err)) + } +} + +// verify verifies the library contents of the specified devfile with the expected content +func verify(devfile *commonUtils.TestDevfile) error { + + commonUtils.LogInfoMessage(fmt.Sprintf("Verify %s : ", devfile.FileName)) + + var errorString []string + + libraryData := devfile.Follower.(DevfileFollower).LibraryData + commonUtils.LogInfoMessage(fmt.Sprintf("Get commands %s : ", devfile.FileName)) + commands, err := libraryData.GetCommands(common.DevfileOptions{}) + if err != nil { + errorString = append(errorString, commonUtils.LogErrorMessage(fmt.Sprintf("Getting Commands from library : %s : %v", devfile.FileName, err))) + } else { + if commands != nil && len(commands) > 0 { + err := VerifyCommands(devfile, commands) + if err != nil { + errorString = append(errorString, commonUtils.LogErrorMessage(fmt.Sprintf("Verify Commands %s : %v", devfile.FileName, err))) + } + } else { + commonUtils.LogInfoMessage(fmt.Sprintf("No commands found in %s : ", devfile.FileName)) + } + } + + commonUtils.LogInfoMessage(fmt.Sprintf("Get components %s : ", devfile.FileName)) + components, err := libraryData.GetComponents(common.DevfileOptions{}) + if err != nil { + errorString = append(errorString, commonUtils.LogErrorMessage(fmt.Sprintf("Getting Components from library : %s : %v", devfile.FileName, err))) + } else { + if components != nil && len(components) > 0 { + err := VerifyComponents(devfile, components) + if err != nil { + errorString = append(errorString, commonUtils.LogErrorMessage(fmt.Sprintf("Verify Components %s : %v", devfile.FileName, err))) + } + } else { + commonUtils.LogInfoMessage(fmt.Sprintf("No components found in %s : ", devfile.FileName)) + } + } + + commonUtils.LogInfoMessage(fmt.Sprintf("Get projects %s : ", devfile.FileName)) + projects, err := libraryData.GetProjects(common.DevfileOptions{}) + if err != nil { + errorString = append(errorString, commonUtils.LogErrorMessage(fmt.Sprintf("Getting Projects from library : %s : %v", devfile.FileName, err))) + } else { + if projects != nil && len(projects) > 0 { + err := VerifyProjects(devfile, projects) + if err != nil { + errorString = append(errorString, commonUtils.LogErrorMessage(fmt.Sprintf("Verify Projects %s : %v", devfile.FileName, err))) + } + } else { + commonUtils.LogInfoMessage(fmt.Sprintf("No projects found in %s : ", devfile.FileName)) + } + } + + commonUtils.LogInfoMessage(fmt.Sprintf("Get starter projects %s : ", devfile.FileName)) + starterProjects, err := libraryData.GetStarterProjects(common.DevfileOptions{}) + if err != nil { + errorString = append(errorString, commonUtils.LogErrorMessage(fmt.Sprintf("Getting Starter Projects from library : %s : %v", devfile.FileName, err))) + } else { + if starterProjects != nil && len(starterProjects) > 0 { + err := VerifyStarterProjects(devfile, starterProjects) + if err != nil { + errorString = append(errorString, commonUtils.LogErrorMessage(fmt.Sprintf("Verify Starter Projects %s : %v", devfile.FileName, err))) + } + } else { + commonUtils.LogInfoMessage(fmt.Sprintf("No starter projects found in %s : ", devfile.FileName)) + } + } + + var returnError error + if len(errorString) > 0 { + returnError = errors.New(fmt.Sprint(errorString)) + } + return returnError + +} + +// editCommands modifies random attributes for each of the commands in the devfile. +func editCommands(devfile *commonUtils.TestDevfile) error { + + var errorString []string + commonUtils.LogInfoMessage(fmt.Sprintf("Edit %s : ", devfile.FileName)) + + commonUtils.LogInfoMessage(fmt.Sprintf(" -> Get commands %s : ", devfile.FileName)) + commands, err := devfile.Follower.(DevfileFollower).LibraryData.GetCommands(common.DevfileOptions{}) + if err != nil { + errorString = append(errorString, commonUtils.LogErrorMessage(fmt.Sprintf("Getting commands from library : %s : %v", devfile.FileName, err))) + } else if len(commands) < 1 { + errorString = append(errorString, commonUtils.LogErrorMessage("Updating commands : No commands returned")) + } else { + for _, command := range commands { + err = UpdateCommand(devfile, command.Id) + if err != nil { + errorString = append(errorString, commonUtils.LogErrorMessage(fmt.Sprintf("Updating command : %s : %v", devfile.FileName, err))) + } + } + } + if len(errorString) > 0 { + err = errors.New(fmt.Sprint(errorString)) + } + return err +} + +// editComponents modifies random attributes for each of the components in the devfile. +func editComponents(devfile *commonUtils.TestDevfile) error { + + var errorString []string + commonUtils.LogInfoMessage(fmt.Sprintf("Edit %s : ", devfile.FileName)) + + commonUtils.LogInfoMessage(fmt.Sprintf(" -> Get components %s : ", devfile.FileName)) + components, err := devfile.Follower.(DevfileFollower).LibraryData.GetComponents(common.DevfileOptions{}) + if err != nil { + errorString = append(errorString, commonUtils.LogErrorMessage(fmt.Sprintf("Getting components from library : %s : %v", devfile.FileName, err))) + } else if len(components) < 1 { + errorString = append(errorString, commonUtils.LogErrorMessage("Updating components : No components returned")) + } else { + for _, component := range components { + err = UpdateComponent(devfile, component.Name) + if err != nil { + errorString = append(errorString, commonUtils.LogErrorMessage(fmt.Sprintf("Updating component : %s : %v", devfile.FileName, err))) + } + } + } + if len(errorString) > 0 { + err = errors.New(fmt.Sprint(errorString)) + } + return err +} + +// editProjects modifies random attributes for each of the projects in the devfile. +func editProjects(devfile *commonUtils.TestDevfile) error { + + var errorString []string + commonUtils.LogInfoMessage(fmt.Sprintf("Edit project %s : ", devfile.FileName)) + + commonUtils.LogInfoMessage(fmt.Sprintf(" -> Get projects %s : ", devfile.FileName)) + projects, err := devfile.Follower.(DevfileFollower).LibraryData.GetProjects(common.DevfileOptions{}) + if err != nil { + errorString = append(errorString, commonUtils.LogErrorMessage(fmt.Sprintf("Getting projects from library : %s : %v", devfile.FileName, err))) + } else if len(projects) < 1 { + errorString = append(errorString, commonUtils.LogErrorMessage("Updating projects : No projects returned")) + } else { + commonUtils.LogInfoMessage(fmt.Sprintf("Updating projects : %d projects found.", len(projects))) + for _, project := range projects { + err = UpdateProject(devfile, project.Name) + if err != nil { + errorString = append(errorString, commonUtils.LogErrorMessage(fmt.Sprintf("Updating project : %v", err))) + } + } + } + return err +} + +// editStarterProjects modifies random attributes for each of the starter projects in the devfile. +func editStarterProjects(devfile *commonUtils.TestDevfile) error { + + var errorString []string + commonUtils.LogInfoMessage(fmt.Sprintf("Edit starter project %s : ", devfile.FileName)) + + commonUtils.LogInfoMessage(fmt.Sprintf(" -> Get starter projects %s : ", devfile.FileName)) + starterProjects, err := devfile.Follower.(DevfileFollower).LibraryData.GetStarterProjects(common.DevfileOptions{}) + if err != nil { + errorString = append(errorString, commonUtils.LogErrorMessage(fmt.Sprintf("Getting starter projects from library : %s : %v", devfile.FileName, err))) + } else if len(starterProjects) < 1 { + errorString = append(errorString, commonUtils.LogErrorMessage("Updating starter projects : No starter projects returned")) + } else { + commonUtils.LogInfoMessage(fmt.Sprintf("Updating starter projects : %d starter projects found.", len(starterProjects))) + for _, starterProject := range starterProjects { + err = UpdateStarterProject(devfile, starterProject.Name) + if err != nil { + errorString = append(errorString, commonUtils.LogErrorMessage(fmt.Sprintf("Updating starter project : %v", err))) + } + } + } + return err +}