diff --git a/.gitignore b/.gitignore index 05d2483f..caed9d11 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .idea vendor +deploy/namespace-init.yaml # Temporary Build Files build/_output diff --git a/Gopkg.lock b/Gopkg.lock index e7be5c67..7a221a99 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -2,245 +2,431 @@ [[projects]] + digest = "1:8713dd3229c46881bb56b24fa3b581a0faab01d12e2d973a830965c24061e449" name = "cloud.google.com/go" packages = ["compute/metadata"] - revision = "debcad1964693daf8ef4bc06292d7e828e075130" - version = "v0.31.0" + pruneopts = "NT" + revision = "1fd54cf41e6e0e178ffe3c52b0e2260281f603e3" + version = "v0.32.0" [[projects]] + digest = "1:d8ebbd207f3d3266d4423ce4860c9f3794956306ded6c7ba312ecc69cdfbf04c" name = "github.com/PuerkitoBio/purell" packages = ["."] + pruneopts = "NT" revision = "0bcb03f4b4d0a9428594752bd2a3b9aa0a9d4bd4" version = "v1.1.0" [[projects]] branch = "master" + digest = "1:8098cd40cd09879efbf12e33bcd51ead4a66006ac802cd563a66c4f3373b9727" name = "github.com/PuerkitoBio/urlesc" packages = ["."] + pruneopts = "NT" revision = "de5bf2ad457846296e2031421a34e2568e304e35" [[projects]] + branch = "master" + digest = "1:c819830f4f5ef85874a90ac3cbcc96cd322c715f5c96fbe4722eacd3dafbaa07" + name = "github.com/beorn7/perks" + packages = ["quantile"] + pruneopts = "NT" + revision = "3a771d992973f24aa725d07868b467d1ddfceafb" + +[[projects]] + digest = "1:494aa43d7f6741b0d03cbc5d39a2a656ea025ede36e935a8f7009ed5eec0069a" + name = "github.com/bndr/gojenkins" + packages = ["."] + pruneopts = "NT" + revision = "668c6a1cb16b659ece2e90506e643ea16abc712c" + version = "v1.0" + +[[projects]] + digest = "1:7335a115a551d88df60eb5664ecee74d59f29f9591d30bdadc4c63f20e04c003" + name = "github.com/coreos/prometheus-operator" + packages = ["pkg/client/monitoring/v1"] + pruneopts = "NT" + revision = "82a6ad2071ff653e38b3b4719ecb789d73f3ab05" + version = "v0.25.0" + +[[projects]] + digest = "1:4b8b5811da6970495e04d1f4e98bb89518cc3cfc3b3f456bdb876ed7b6c74049" name = "github.com/davecgh/go-spew" packages = ["spew"] + pruneopts = "NT" revision = "8991bc29aa16c548c550c7ff78260e27b9ab7c73" version = "v1.1.1" [[projects]] + digest = "1:707c9f4ee70cc3bb941cf5803d9b8e725bc02277a96301ac8e537510a712ec7c" + name = "github.com/docker/distribution" + packages = [ + "digest", + "reference", + ] + pruneopts = "NT" + revision = "48294d928ced5dd9b378f7fd7c6f5da3ff3f2c89" + version = "v2.6.2" + +[[projects]] + digest = "1:e6f888d4be8ec0f05c50e2aba83da4948b58045dee54d03be81fa74ea673302c" name = "github.com/emicklei/go-restful" packages = [ ".", - "log" + "log", ] + pruneopts = "NT" revision = "3eb9738c1697594ea6e71a7156a9bb32ed216cf0" version = "v2.8.0" [[projects]] + digest = "1:81466b4218bf6adddac2572a30ac733a9255919bc2f470b4827a317bd4ee1756" name = "github.com/ghodss/yaml" packages = ["."] + pruneopts = "NT" revision = "0ca9ea5df5451ffdf184b4428c902747c2c11cd7" version = "v1.0.0" [[projects]] branch = "master" + digest = "1:d421af4c4fe51d399667d573982d663fe1fa67020a88d3ae43466ebfe8e2b5c9" name = "github.com/go-logr/logr" packages = ["."] + pruneopts = "NT" revision = "9fb12b3b21c5415d16ac18dc5cd42c1cfdd40c4e" [[projects]] - branch = "master" + digest = "1:340497a512995aa69c0add901d79a2096b3449d35a44a6f1f1115091a9f8c687" name = "github.com/go-logr/zapr" packages = ["."] + pruneopts = "NT" revision = "7536572e8d55209135cd5e7ccf7fce43dca217ab" + version = "v0.1.0" [[projects]] + digest = "1:260f7ebefc63024c8dfe2c9f1a2935a89fa4213637a1f522f592f80c001cc441" name = "github.com/go-openapi/jsonpointer" packages = ["."] + pruneopts = "NT" revision = "ef5f0afec364d3b9396b7b77b43dbe26bf1f8004" - version = "v0.17.1" + version = "v0.17.2" [[projects]] + digest = "1:98abd61947ff5c7c6fcfec5473d02a4821ed3a2dd99a4fbfdb7925b0dd745546" name = "github.com/go-openapi/jsonreference" packages = ["."] + pruneopts = "NT" revision = "8483a886a90412cd6858df4ea3483dce9c8e35a3" - version = "v0.17.1" + version = "v0.17.2" [[projects]] + branch = "master" + digest = "1:dfab391de021809e0041f0ab5648da6b74dd16a685472a1b8c3dc06b3dca1ee2" name = "github.com/go-openapi/spec" packages = ["."] + pruneopts = "NT" revision = "5bae59e25b21498baea7f9d46e9c147ec106a42e" - version = "v0.17.1" [[projects]] + digest = "1:983f95b2fae6fe8fdd361738325ed6090f4f3bd15ce4db745e899fb5b0fdfc46" name = "github.com/go-openapi/swag" packages = ["."] + pruneopts = "NT" revision = "5899d5c5e619fda5fa86e14795a835f473ca284c" - version = "v0.17.1" + version = "v0.17.2" [[projects]] + digest = "1:2a9d5e367df8c95e780975ca1dd4010bef8e39a3777066d3880ce274b39d4b5a" name = "github.com/gogo/protobuf" packages = [ "proto", - "sortkeys" + "sortkeys", ] + pruneopts = "NT" revision = "636bf0302bc95575d69441b25a2603156ffdddf1" version = "v1.1.1" [[projects]] branch = "master" + digest = "1:e2b86e41f3d669fc36b50d31d32d22c8ac656c75aa5ea89717ce7177e134ff2a" name = "github.com/golang/glog" packages = ["."] + pruneopts = "NT" revision = "23def4e6c14b4da8ac2ed8007337bc5eb5007998" [[projects]] branch = "master" + digest = "1:aaedc94233e56ed57cdb04e3abfacc85c90c14082b62e3cdbe8ea72fc06ee035" name = "github.com/golang/groupcache" packages = ["lru"] - revision = "6f2cf27854a4a29e3811b0371547be335d411b8b" + pruneopts = "NT" + revision = "c65c006176ff7ff98bb916961c7abbc6b0afc0aa" [[projects]] + digest = "1:d7cb4458ea8782e6efacd8f4940796ec559c90833509c436f40c4085b98156dd" name = "github.com/golang/protobuf" packages = [ "proto", "ptypes", "ptypes/any", "ptypes/duration", - "ptypes/timestamp" + "ptypes/timestamp", ] + pruneopts = "NT" revision = "aa810b61a9c79d51363740d207bb46cf8e620ed5" version = "v1.2.0" [[projects]] branch = "master" + digest = "1:05f95ffdfcf651bdb0f05b40b69e7f5663047f8da75c72d58728acb59b5cc107" name = "github.com/google/btree" packages = ["."] + pruneopts = "NT" revision = "4030bb1f1f0c35b30ca7009e9ebd06849dd45306" [[projects]] branch = "master" + digest = "1:52c5834e2bebac9030c97cc0798ac11c3aa8a39f098aeb419f142533da6cd3cc" name = "github.com/google/gofuzz" packages = ["."] + pruneopts = "NT" revision = "24818f796faf91cd76ec7bddd72458fbced7a6c1" [[projects]] + digest = "1:a1578f7323eca2b88021fdc9a79a99833d40b12c32a5ea4f284e2fad19ea2657" name = "github.com/google/uuid" packages = ["."] + pruneopts = "NT" revision = "d460ce9f8df2e77fb1ba55ca87fafed96c607494" version = "v1.0.0" [[projects]] + digest = "1:289332c13b80edfefc88397cce5266c16845dcf204fa2f6ac7e464ee4c7f6e96" name = "github.com/googleapis/gnostic" packages = [ "OpenAPIv2", "compiler", - "extensions" + "extensions", ] + pruneopts = "NT" revision = "7c663266750e7d82587642f65e60bc4083f1f84e" version = "v0.2.0" [[projects]] branch = "master" + digest = "1:2a2caa63899dae26ed3e4510b806549fd416d94db24ad68279daa62881b26488" name = "github.com/gregjones/httpcache" packages = [ ".", - "diskcache" + "diskcache", ] + pruneopts = "NT" revision = "9cad4c3443a7200dd6400aef47183728de563a38" [[projects]] + digest = "1:b42cde0e1f3c816dd57f57f7bbcf05ca40263ad96f168714c130c611fc0856a6" name = "github.com/hashicorp/golang-lru" packages = [ ".", - "simplelru" + "simplelru", ] + pruneopts = "NT" revision = "20f1fb78b0740ba8c3cb143a61e86ba5c8669768" version = "v0.5.0" [[projects]] + digest = "1:9a52adf44086cead3b384e5d0dbf7a1c1cce65e67552ee3383a8561c42a18cd3" name = "github.com/imdario/mergo" packages = ["."] + pruneopts = "NT" revision = "9f23e2d6bd2a77f959b2bf6acdbefd708a83a4a4" version = "v0.3.6" [[projects]] + digest = "1:1d39c063244ad17c4b18e8da1551163b6ffb52bd1640a49a8ec5c3b7bf4dbd5d" name = "github.com/json-iterator/go" packages = ["."] + pruneopts = "NT" revision = "1624edc4454b8682399def8740d46db5e4362ba4" version = "v1.1.5" +[[projects]] + digest = "1:4059c14e87a2de3a434430340521b5feece186c1469eff0834c29a63870de3ed" + name = "github.com/konsorten/go-windows-terminal-sequences" + packages = ["."] + pruneopts = "NT" + revision = "5c8c8bd35d3832f5d134ae1e1e375b69a4d25242" + version = "v1.0.1" + [[projects]] branch = "master" + digest = "1:7d9fcac7f1228470c4ea0ee31cdfb662a758c44df691e39b3e76c11d3e12ba8f" name = "github.com/mailru/easyjson" packages = [ "buffer", "jlexer", - "jwriter" + "jwriter", ] + pruneopts = "NT" revision = "60711f1a8329503b04e1c88535f419d0bb440bff" [[projects]] branch = "master" + digest = "1:0e9bfc47ab9941ecc3344e580baca5deb4091177e84dd9773b48b38ec26b93d5" name = "github.com/mattbaird/jsonpatch" packages = ["."] + pruneopts = "NT" revision = "81af80346b1a01caae0cbc27fd3c1ba5b11e189f" [[projects]] + digest = "1:ea1db000388d88b31db7531c83016bef0d6db0d908a07794bfc36aca16fbf935" + name = "github.com/matttproud/golang_protobuf_extensions" + packages = ["pbutil"] + pruneopts = "NT" + revision = "c12348ce28de40eed0136aa2b644d0ee0650e56c" + version = "v1.0.1" + +[[projects]] + digest = "1:2f42fa12d6911c7b7659738758631bec870b7e9b4c6be5444f963cdcfccc191f" name = "github.com/modern-go/concurrent" packages = ["."] + pruneopts = "NT" revision = "bacd9c7ef1dd9b15be4a9909b8ac7a4e313eec94" version = "1.0.3" [[projects]] + digest = "1:c6aca19413b13dc59c220ad7430329e2ec454cc310bc6d8de2c7e2b93c18a0f6" name = "github.com/modern-go/reflect2" packages = ["."] + pruneopts = "NT" revision = "4b7aa43c6742a2c18fdef89dd197aaae7dac7ccd" version = "1.0.1" [[projects]] - branch = "master" + digest = "1:ee01ef8a6bac37c704936dfd61a59421a1a5e26b3f9f2c669c5eeb41dde9c5ca" name = "github.com/operator-framework/operator-sdk" packages = [ - "pkg/util/k8sutil", - "version" + "pkg/k8sutil", + "pkg/sdk", + "pkg/test", + "pkg/test/e2eutil", + "version", ] - revision = "d4a6b28b3249a1f07068406cc48679f525d07cfc" + pruneopts = "NT" + revision = "ec5387ceebfe7a33b53fd49711d294d93c0fb264" + version = "v0.1.0" [[projects]] + digest = "1:93b1d84c5fa6d1ea52f4114c37714cddd84d5b78f151b62bb101128dd51399bf" name = "github.com/pborman/uuid" packages = ["."] + pruneopts = "NT" revision = "adf5a7427709b9deb95d29d3fa8a2bf9cfd388f1" version = "v1.2" [[projects]] branch = "master" + digest = "1:bf2ac97824a7221eb16b096aecc1c390d4c8a4e49524386aaa2e2dd215cbfb31" name = "github.com/petar/GoLLRB" packages = ["llrb"] + pruneopts = "NT" revision = "53be0d36a84c2a886ca057d34b6aa4468df9ccb4" [[projects]] + digest = "1:e4e9e026b8e4c5630205cd0208efb491b40ad40552e57f7a646bb8a46896077b" name = "github.com/peterbourgon/diskv" packages = ["."] + pruneopts = "NT" revision = "5f041e8faa004a95c88a202771f4cc3e991971e6" version = "v2.0.1" [[projects]] + digest = "1:5cf3f025cbee5951a4ee961de067c8a89fc95a5adabead774f82822efabab121" + name = "github.com/pkg/errors" + packages = ["."] + pruneopts = "NT" + revision = "645ef00459ed84a119197bfb8d8205042c6df63d" + version = "v0.8.0" + +[[projects]] + digest = "1:bb1dbe98a0b4bf193608772ae3d3c4ec64f64bc3f11b1845f11b603b91146fbc" + name = "github.com/prometheus/client_golang" + packages = [ + "prometheus", + "prometheus/internal", + "prometheus/promhttp", + ] + pruneopts = "NT" + revision = "1cafe34db7fdec6022e17e00e1c1ea501022f3e4" + version = "v0.9.0" + +[[projects]] + branch = "master" + digest = "1:c2cc5049e927e2749c0d5163c9f8d924880d83e84befa732b9aad0b6be227bed" + name = "github.com/prometheus/client_model" + packages = ["go"] + pruneopts = "NT" + revision = "5c3871d89910bfb32f5fcab2aa4b9ec68e65a99f" + +[[projects]] + branch = "master" + digest = "1:7351e64118be635099e3911e6fff7908a257816bad9f159016a9d11849669489" + name = "github.com/prometheus/common" + packages = [ + "expfmt", + "internal/bitbucket.org/ww/goautoneg", + "model", + ] + pruneopts = "NT" + revision = "7e9e6cabbd393fc208072eedef99188d0ce788b6" + +[[projects]] + branch = "master" + digest = "1:523adcc0953fdf00dab08f45cad651f74682fb489bd2d672aa9f96e568e2f11f" + name = "github.com/prometheus/procfs" + packages = [ + ".", + "internal/util", + "nfs", + "xfs", + ] + pruneopts = "NT" + revision = "185b4288413d2a0dd0806f78c90dde719829e5ae" + +[[projects]] + digest = "1:cd2f2cba5b7ffafd0412fb647ff4bcff170292de57270f05fbbf391e3eb9566b" + name = "github.com/sirupsen/logrus" + packages = ["."] + pruneopts = "NT" + revision = "bcd833dfe83d3cebad139e4a29ed79cb2318bf95" + version = "v1.2.0" + +[[projects]] + digest = "1:9d8420bbf131d1618bde6530af37c3799340d3762cc47210c1d9532a4c3a2779" name = "github.com/spf13/pflag" packages = ["."] + pruneopts = "NT" revision = "298182f68c66c05229eb03ac171abe6e309ee79a" version = "v1.0.3" [[projects]] + digest = "1:22f696cee54865fb8e9ff91df7b633f6b8f22037a8015253c6b6a71ca82219c7" name = "go.uber.org/atomic" packages = ["."] + pruneopts = "NT" revision = "1ea20fb1cbb1cc08cbd0d913a96dead89aa18289" version = "v1.3.2" [[projects]] + digest = "1:58ca93bdf81bac106ded02226b5395a0595d5346cdc4caa8d9c1f3a5f8f9976e" name = "go.uber.org/multierr" packages = ["."] + pruneopts = "NT" revision = "3c4937480c32f4c13a875a1829af76c98ca3d40a" version = "v1.1.0" [[projects]] + digest = "1:572fa4496563920f3e3107a2294cf2621d6cc4ffd03403fb6397b1bab9fa082a" name = "go.uber.org/zap" packages = [ ".", @@ -248,19 +434,23 @@ "internal/bufferpool", "internal/color", "internal/exit", - "zapcore" + "zapcore", ] + pruneopts = "NT" revision = "ff33455a0e382e8a81d14dd7c922020b6b5e7982" version = "v1.9.1" [[projects]] branch = "master" + digest = "1:47924c7ab4b3a18145d150e535525f582a07511e5452c4e1a5b79d883c2a429f" name = "golang.org/x/crypto" packages = ["ssh/terminal"] - revision = "0c41d7ab0a0ee717d4590a44bcb987dfd9e183eb" + pruneopts = "NT" + revision = "4d3f4d9ffa16a13f451c3b2999e9c49e9750bf06" [[projects]] branch = "master" + digest = "1:a7fcf4f3e5247a06ad4c28108f0bc1d4ab980a1a0567e7790260cf2d3d77f37d" name = "golang.org/x/net" packages = [ "context", @@ -268,32 +458,38 @@ "http/httpguts", "http2", "http2/hpack", - "idna" + "idna", ] - revision = "9b4f9f5ad5197c79fd623a3638e70d8b26cef344" + pruneopts = "NT" + revision = "c10e9556a7bc0e7c942242b606f0acf024ad5d6a" [[projects]] branch = "master" + digest = "1:b17011812136abe011d81b40b30470808df923687e831760511d878408d208df" name = "golang.org/x/oauth2" packages = [ ".", "google", "internal", "jws", - "jwt" + "jwt", ] - revision = "9dcd33a902f40452422c2367fefcb95b54f9f8f8" + pruneopts = "NT" + revision = "e0f2c55a7fc7d04742e0eef7918aa2389b0e1919" [[projects]] branch = "master" + digest = "1:04b92b5bc6c1031cc9083fbc2fdeda90f3a69b7c1bf5eed6bbf9a3563d946c6e" name = "golang.org/x/sys" packages = [ "unix", - "windows" + "windows", ] - revision = "44b849a8bc13eb42e95e6c6c5e360481b73ec710" + pruneopts = "NT" + revision = "9b800f95dbbc54abff0acf7ee32d88ba4e328c89" [[projects]] + digest = "1:8c74f97396ed63cc2ef04ebb5fc37bb032871b8fd890a25991ed40974b00cd2a" name = "golang.org/x/text" packages = [ "collate", @@ -310,29 +506,35 @@ "unicode/cldr", "unicode/norm", "unicode/rangetable", - "width" + "width", ] + pruneopts = "NT" revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0" version = "v0.3.0" [[projects]] branch = "master" + digest = "1:c9e7a4b4d47c0ed205d257648b0e5b0440880cb728506e318f8ac7cd36270bc4" name = "golang.org/x/time" packages = ["rate"] + pruneopts = "NT" revision = "fbb02b2291d28baffd63558aa44b4b56f178d650" [[projects]] branch = "master" + digest = "1:d9646d2cb3517e532bcc75614352e589c5f26e6d1b8ae2587eaef0c7c60cea3a" name = "golang.org/x/tools" packages = [ "go/ast/astutil", "imports", "internal/fastwalk", - "internal/gopathwalk" + "internal/gopathwalk", ] - revision = "40a48ad93fbe707101afb2099b738471f70594ec" + pruneopts = "NT" + revision = "92b943e6bff73e0dfe9e975d94043d8f31067b06" [[projects]] + digest = "1:2a4972ee51c3b9dfafbb3451fa0552e7a198d9d12c721bfc492050fe2f72e0f6" name = "google.golang.org/appengine" packages = [ ".", @@ -344,24 +546,30 @@ "internal/modules", "internal/remote_api", "internal/urlfetch", - "urlfetch" + "urlfetch", ] - revision = "ae0ab99deb4dc413a2b4bd6c8bdd0eb67f1e4d06" - version = "v1.2.0" + pruneopts = "NT" + revision = "4a4468ece617fc8205e99368fa2200e9d1fad421" + version = "v1.3.0" [[projects]] + digest = "1:2d1fbdc6777e5408cabeb02bf336305e724b925ff4546ded0fa8715a7267922a" name = "gopkg.in/inf.v0" packages = ["."] + pruneopts = "NT" revision = "d2d2541c53f18d2a059457998ce2876cc8e67cbf" version = "v0.9.1" [[projects]] + digest = "1:7c95b35057a0ff2e19f707173cc1a947fa43a6eb5c4d300d196ece0334046082" name = "gopkg.in/yaml.v2" packages = ["."] + pruneopts = "NT" revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183" version = "v2.2.1" [[projects]] + digest = "1:f11e5753e619f411a51a49d60d39b2bc4da6766f5f0af2e2291daa6a3d9385d5" name = "k8s.io/api" packages = [ "admission/v1beta1", @@ -393,11 +601,24 @@ "settings/v1alpha1", "storage/v1", "storage/v1alpha1", - "storage/v1beta1" + "storage/v1beta1", ] + pruneopts = "NT" revision = "2d6f90ab1293a1fb871cf149423ebb72aa7423aa" [[projects]] + digest = "1:117a407949aaaad9f7fbe3da8d6c2055f2c223ac0cbebd39f47ff71899622a91" + name = "k8s.io/apiextensions-apiserver" + packages = [ + "pkg/apis/apiextensions", + "pkg/apis/apiextensions/v1beta1", + "pkg/client/clientset/clientset/scheme", + ] + pruneopts = "NT" + revision = "408db4a50408e2149acbd657bceb2480c13cb0a4" + +[[projects]] + digest = "1:b07bf863262aae765494d60f0d524483f211b29f9bb27d445a79c13af8676bf2" name = "k8s.io/apimachinery" packages = [ "pkg/api/errors", @@ -441,14 +662,18 @@ "pkg/version", "pkg/watch", "third_party/forked/golang/json", - "third_party/forked/golang/reflect" + "third_party/forked/golang/reflect", ] + pruneopts = "NT" revision = "103fd098999dc9c0c88536f5c9ad2e5da39373ae" [[projects]] + digest = "1:1689a49a3ebc6e379849181f1e0899fccf143cab47586078721818bdcdb712bc" name = "k8s.io/client-go" packages = [ + "deprecated-dynamic", "discovery", + "discovery/cached", "dynamic", "kubernetes", "kubernetes/scheme", @@ -511,11 +736,14 @@ "util/homedir", "util/integer", "util/jsonpath", - "util/retry" + "util/retry", + "util/workqueue", ] + pruneopts = "NT" revision = "1f13a808da65775f22cbf47862c4e5898d8f4ca1" [[projects]] + digest = "1:8ab487a323486c8bbbaa3b689850487fdccc6cbea8690620e083b2d230a4447e" name = "k8s.io/code-generator" packages = [ "cmd/client-gen", @@ -541,12 +769,14 @@ "cmd/lister-gen/generators", "cmd/openapi-gen", "cmd/openapi-gen/args", - "pkg/util" + "pkg/util", ] + pruneopts = "T" revision = "6702109cc68eb6fe6350b83e14407c8d7309fd1a" [[projects]] branch = "master" + digest = "1:c84b5ef38d786290246a9cc791a48b6ed890cd32468179a51a91492161ec6d65" name = "k8s.io/gengo" packages = [ "args", @@ -556,12 +786,13 @@ "generator", "namer", "parser", - "types" + "types", ] + pruneopts = "NT" revision = "7338e4bfd6915369a1375890db1bbda0158c9863" [[projects]] - branch = "master" + digest = "1:c48a795cd7048bb1888273bc604b6e69b22f9b8089c3df65f77cc527757b515c" name = "k8s.io/kube-openapi" packages = [ "cmd/openapi-gen/args", @@ -569,11 +800,13 @@ "pkg/generators", "pkg/generators/rules", "pkg/util/proto", - "pkg/util/sets" + "pkg/util/sets", ] - revision = "96e8bb74ecdddb93a882ef95d2b8ec49e93168ee" + pruneopts = "NT" + revision = "0cf8f7e6ed1d2e3d47d02e3b6e559369af24d803" [[projects]] + digest = "1:d1b7a6ed45c957e6308759f31fdbff8063741ecb08b7c3b6d67f0c9f4357b2ae" name = "sigs.k8s.io/controller-runtime" packages = [ "pkg/cache", @@ -581,24 +814,76 @@ "pkg/client", "pkg/client/apiutil", "pkg/client/config", + "pkg/controller", + "pkg/controller/controllerutil", + "pkg/event", + "pkg/handler", + "pkg/internal/controller", "pkg/internal/recorder", "pkg/leaderelection", "pkg/manager", "pkg/patch", + "pkg/predicate", + "pkg/reconcile", "pkg/recorder", "pkg/runtime/inject", "pkg/runtime/log", + "pkg/runtime/scheme", "pkg/runtime/signals", + "pkg/source", + "pkg/source/internal", "pkg/webhook/admission", "pkg/webhook/admission/types", - "pkg/webhook/types" + "pkg/webhook/types", ] - revision = "53fc44b56078cd095b11bd44cfa0288ee4cf718f" - version = "v0.1.4" + pruneopts = "NT" + revision = "5fd1e9e9fac5261e9ad9d47c375afc014fc31d21" + version = "v0.1.7" [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "9770b0fb0242b365a1b827dd7a2b2d675d9f86e7d80565012a2df243730476a2" + input-imports = [ + "github.com/bndr/gojenkins", + "github.com/docker/distribution/reference", + "github.com/go-logr/logr", + "github.com/go-logr/zapr", + "github.com/operator-framework/operator-sdk/pkg/k8sutil", + "github.com/operator-framework/operator-sdk/pkg/sdk", + "github.com/operator-framework/operator-sdk/pkg/test", + "github.com/operator-framework/operator-sdk/pkg/test/e2eutil", + "github.com/operator-framework/operator-sdk/version", + "go.uber.org/zap", + "k8s.io/api/core/v1", + "k8s.io/apimachinery/pkg/api/errors", + "k8s.io/apimachinery/pkg/api/resource", + "k8s.io/apimachinery/pkg/apis/meta/v1", + "k8s.io/apimachinery/pkg/labels", + "k8s.io/apimachinery/pkg/runtime", + "k8s.io/apimachinery/pkg/runtime/schema", + "k8s.io/apimachinery/pkg/types", + "k8s.io/apimachinery/pkg/util/intstr", + "k8s.io/apimachinery/pkg/util/wait", + "k8s.io/client-go/plugin/pkg/client/auth/gcp", + "k8s.io/code-generator/cmd/client-gen", + "k8s.io/code-generator/cmd/conversion-gen", + "k8s.io/code-generator/cmd/deepcopy-gen", + "k8s.io/code-generator/cmd/defaulter-gen", + "k8s.io/code-generator/cmd/informer-gen", + "k8s.io/code-generator/cmd/lister-gen", + "k8s.io/code-generator/cmd/openapi-gen", + "k8s.io/gengo/args", + "sigs.k8s.io/controller-runtime/pkg/client", + "sigs.k8s.io/controller-runtime/pkg/client/config", + "sigs.k8s.io/controller-runtime/pkg/controller", + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil", + "sigs.k8s.io/controller-runtime/pkg/handler", + "sigs.k8s.io/controller-runtime/pkg/manager", + "sigs.k8s.io/controller-runtime/pkg/reconcile", + "sigs.k8s.io/controller-runtime/pkg/runtime/log", + "sigs.k8s.io/controller-runtime/pkg/runtime/scheme", + "sigs.k8s.io/controller-runtime/pkg/runtime/signals", + "sigs.k8s.io/controller-runtime/pkg/source", + ] solver-name = "gps-cdcl" solver-version = 1 diff --git a/Makefile b/Makefile index 42f815ed..f3c4c5b4 100644 --- a/Makefile +++ b/Makefile @@ -103,7 +103,7 @@ build: $(NAME) ## Builds a dynamic executable or package .PHONY: $(NAME) $(NAME): $(wildcard *.go) $(wildcard */*.go) VERSION.txt @echo "+ $@" - go build -tags "$(BUILDTAGS)" ${GO_LDFLAGS} -o build/_output/bin/$(NAME) $(BUILD_PATH) + CGO_ENABLED=0 go build -tags "$(BUILDTAGS)" ${GO_LDFLAGS} -o build/_output/bin/$(NAME) $(BUILD_PATH) .PHONY: static static: ## Builds a static executable @@ -144,6 +144,24 @@ test: ## Runs the go tests CURRENT_DIRECTORY := $(shell pwd) e2e: ## Runs e2e tests @echo "+ $@" + @echo "E2E_IMAGE: $(E2E_IMAGE)" +ifeq ($(E2E_IMAGE),) + $(error You must provide an image to e2e tests) +endif + cp deploy/service_account.yaml deploy/namespace-init.yaml + cat deploy/role.yaml >> deploy/namespace-init.yaml + cat deploy/role_binding.yaml >> deploy/namespace-init.yaml + cat deploy/operator.yaml >> deploy/namespace-init.yaml + sed -i 's|REPLACE_IMAGE|$(E2E_IMAGE)|g' deploy/namespace-init.yaml +ifeq ($(ENVIRONMENT),minikube) + sed -i 's|imagePullPolicy: IfNotPresent|imagePullPolicy: Never|g' deploy/namespace-init.yaml + sed -i 's|REPLACE_ARGS|args: ["--minikube"]|g' deploy/namespace-init.yaml +else + sed -i 's|REPLACE_ARGS||g' deploy/namespace-init.yaml +endif + + @RUNNING_TESTS=1 go test -parallel=2 "./test/e2e/" -tags "$(BUILDTAGS) cgo" -v \ + -root=$(CURRENT_DIRECTORY) -kubeconfig=$(HOME)/.kube/config -globalMan deploy/crds/virtuslab_v1alpha1_jenkins_crd.yaml -namespacedMan deploy/namespace-init.yaml .PHONY: vet vet: ## Verifies `go vet` passes diff --git a/README.md b/README.md index 47bc4d1f..c17bb9a2 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,23 @@ # jenkins-operator + Kubernetes native Jenkins operator + +## Developer guide + +## TODO + +- send Kubernetes events + +Base configuration: +- install configuration as a code Jenkins plugin +- restart Jenkins when scripts config map or base configuration config map have changed +- install and configure Kubernetes plugin +- disable insecure options + +User configuration: +- AWS s3 restore backup job +- AWS s3 backup job +- create and run seed jobs +- apply custom configuration by configuration as a code Jenkins plugin +- trigger backup job before pod deletion +- verify Jenkins configuration events diff --git a/VERSION.txt b/VERSION.txt index 8acdd82b..45c7a584 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -0.0.1 +v0.0.1 diff --git a/build/Dockerfile b/build/Dockerfile index ad1d7e32..8eb53a47 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -1,5 +1,7 @@ -FROM alpine:3.6 +FROM alpine:3.8 USER nobody ADD build/_output/bin/jenkins-operator /usr/local/bin/jenkins-operator + +CMD [ "/usr/local/bin/jenkins-operator" ] diff --git a/cmd/manager/main.go b/cmd/manager/main.go index 5ddb2353..0fa59327 100644 --- a/cmd/manager/main.go +++ b/cmd/manager/main.go @@ -11,8 +11,8 @@ import ( "github.com/VirtusLab/jenkins-operator/pkg/log" "github.com/VirtusLab/jenkins-operator/version" - "github.com/operator-framework/operator-sdk/pkg/sdk" "github.com/operator-framework/operator-sdk/pkg/k8sutil" + "github.com/operator-framework/operator-sdk/pkg/sdk" sdkVersion "github.com/operator-framework/operator-sdk/version" _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" "sigs.k8s.io/controller-runtime/pkg/client/config" @@ -20,30 +20,29 @@ import ( "sigs.k8s.io/controller-runtime/pkg/runtime/signals" ) -func printInfo(namespace string) { +func printInfo() { log.Log.Info(fmt.Sprintf("Version: %s", version.Version)) log.Log.Info(fmt.Sprintf("Git commit: %s", version.GitCommit)) log.Log.Info(fmt.Sprintf("Go Version: %s", runtime.Version())) log.Log.Info(fmt.Sprintf("Go OS/Arch: %s/%s", runtime.GOOS, runtime.GOARCH)) log.Log.Info(fmt.Sprintf("operator-sdk Version: %v", sdkVersion.Version)) - log.Log.Info(fmt.Sprintf("watch namespace: %v", namespace)) } func main() { + minikube := flag.Bool("minikube", false, "Use minikube as a Kubernetes platform") + local := flag.Bool("local", false, "Run operator locally") debug := flag.Bool("debug", false, "Set log level to debug") flag.Parse() - if err := log.SetupLogger(debug); err != nil { - log.Log.Error(err, "unable to construct the logger") - os.Exit(-1) - } + log.SetupLogger(debug) + printInfo() namespace, err := k8sutil.GetWatchNamespace() if err != nil { log.Log.Error(err, "failed to get watch namespace") os.Exit(-1) } - printInfo(namespace) + log.Log.Info(fmt.Sprintf("watch namespace: %v", namespace)) sdk.ExposeMetricsPort() @@ -70,7 +69,7 @@ func main() { } // Setup all Controllers - if err := controller.AddToManager(mgr); err != nil { + if err := controller.AddToManager(mgr, *local, *minikube); err != nil { log.Log.Error(err, "failed to setup controllers") os.Exit(-1) } diff --git a/deploy/crds/virtuslab_v1alpha1_jenkins_cr.yaml b/deploy/crds/virtuslab_v1alpha1_jenkins_cr.yaml index c5b319a4..a32ae90e 100644 --- a/deploy/crds/virtuslab_v1alpha1_jenkins_cr.yaml +++ b/deploy/crds/virtuslab_v1alpha1_jenkins_cr.yaml @@ -1,7 +1,7 @@ apiVersion: virtuslab.com/v1alpha1 kind: Jenkins metadata: - name: example-jenkins + name: example spec: - # Add fields here - size: 3 + master: + image: jenkins/jenkins diff --git a/deploy/operator.yaml b/deploy/operator.yaml index b2c53a68..99be636f 100644 --- a/deploy/operator.yaml +++ b/deploy/operator.yaml @@ -1,3 +1,4 @@ +--- apiVersion: apps/v1 kind: Deployment metadata: @@ -22,7 +23,8 @@ spec: name: metrics command: - jenkins-operator - imagePullPolicy: Always + REPLACE_ARGS + imagePullPolicy: IfNotPresent env: - name: WATCH_NAMESPACE valueFrom: diff --git a/deploy/role.yaml b/deploy/role.yaml index f345ebe6..550c2878 100644 --- a/deploy/role.yaml +++ b/deploy/role.yaml @@ -1,3 +1,4 @@ +--- apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: diff --git a/deploy/role_binding.yaml b/deploy/role_binding.yaml index 775f0a7a..8224b7c1 100644 --- a/deploy/role_binding.yaml +++ b/deploy/role_binding.yaml @@ -1,3 +1,4 @@ +--- kind: RoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: diff --git a/deploy/service_account.yaml b/deploy/service_account.yaml index ce3f76f1..21b293ce 100644 --- a/deploy/service_account.yaml +++ b/deploy/service_account.yaml @@ -1,3 +1,4 @@ +--- apiVersion: v1 kind: ServiceAccount metadata: diff --git a/pkg/apis/virtuslab/v1alpha1/jenkins_types.go b/pkg/apis/virtuslab/v1alpha1/jenkins_types.go index 2c6828a4..f5406851 100644 --- a/pkg/apis/virtuslab/v1alpha1/jenkins_types.go +++ b/pkg/apis/virtuslab/v1alpha1/jenkins_types.go @@ -1,6 +1,7 @@ package v1alpha1 import ( + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -11,12 +12,21 @@ import ( type JenkinsSpec struct { // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster // Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file + Master JenkinsMaster `json:"master,omitempty"` +} + +// JenkinsMaster defines the Jenkins master pod attributes +type JenkinsMaster struct { + Image string `json:"image,omitempty"` + Annotations map[string]string `json:"masterAnnotations,omitempty"` + Resources corev1.ResourceRequirements `json:"resources,omitempty"` } // JenkinsStatus defines the observed state of Jenkins type JenkinsStatus struct { // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster // Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file + BaseConfigurationCompletedTime *metav1.Time `json:"baseConfigurationCompletedTime,omitempty"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/pkg/apis/virtuslab/v1alpha1/register.go b/pkg/apis/virtuslab/v1alpha1/register.go index 2469c2e0..92948670 100644 --- a/pkg/apis/virtuslab/v1alpha1/register.go +++ b/pkg/apis/virtuslab/v1alpha1/register.go @@ -10,6 +10,11 @@ import ( "sigs.k8s.io/controller-runtime/pkg/runtime/scheme" ) +const ( + // Kind defines Jenkins CRD kind name + Kind = "Jenkins" +) + var ( // SchemeGroupVersion is group version used to register these objects SchemeGroupVersion = schema.GroupVersion{Group: "virtuslab.com", Version: "v1alpha1"} diff --git a/pkg/apis/virtuslab/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/virtuslab/v1alpha1/zz_generated.deepcopy.go index fd1aeed6..f7f25b8d 100644 --- a/pkg/apis/virtuslab/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/virtuslab/v1alpha1/zz_generated.deepcopy.go @@ -29,8 +29,8 @@ func (in *Jenkins) DeepCopyInto(out *Jenkins) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec - out.Status = in.Status + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) return } @@ -85,9 +85,34 @@ func (in *JenkinsList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *JenkinsMaster) DeepCopyInto(out *JenkinsMaster) { + *out = *in + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + in.Resources.DeepCopyInto(&out.Resources) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JenkinsMaster. +func (in *JenkinsMaster) DeepCopy() *JenkinsMaster { + if in == nil { + return nil + } + out := new(JenkinsMaster) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *JenkinsSpec) DeepCopyInto(out *JenkinsSpec) { *out = *in + in.Master.DeepCopyInto(&out.Master) return } @@ -104,6 +129,10 @@ func (in *JenkinsSpec) DeepCopy() *JenkinsSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *JenkinsStatus) DeepCopyInto(out *JenkinsStatus) { *out = *in + if in.BaseConfigurationCompletedTime != nil { + in, out := &in.BaseConfigurationCompletedTime, &out.BaseConfigurationCompletedTime + *out = (*in).DeepCopy() + } return } diff --git a/pkg/controller/controller.go b/pkg/controller/controller.go index 7c069f3e..fa1f2a88 100644 --- a/pkg/controller/controller.go +++ b/pkg/controller/controller.go @@ -5,12 +5,12 @@ import ( ) // AddToManagerFuncs is a list of functions to add all Controllers to the Manager -var AddToManagerFuncs []func(manager.Manager) error +var AddToManagerFuncs []func(manager manager.Manager, local, minikube bool) error // AddToManager adds all Controllers to the Manager -func AddToManager(m manager.Manager) error { +func AddToManager(m manager.Manager, local, minikube bool) error { for _, f := range AddToManagerFuncs { - if err := f(m); err != nil { + if err := f(m, local, minikube); err != nil { return err } } diff --git a/pkg/controller/jenkins/client/jenkins.go b/pkg/controller/jenkins/client/jenkins.go new file mode 100644 index 00000000..1dbd010d --- /dev/null +++ b/pkg/controller/jenkins/client/jenkins.go @@ -0,0 +1,100 @@ +package client + +import ( + "bytes" + "fmt" + "net/http" + "os/exec" + "strings" + + "github.com/bndr/gojenkins" +) + +// Jenkins defines Jenkins API +type Jenkins interface { + GenerateToken(userName, tokenName string) (*UserToken, error) + //Info() (*gojenkins.executorResponse, error) + CreateNode(name string, numExecutors int, description string, remoteFS string, options ...interface{}) (*gojenkins.Node, error) + CreateJob(config string, options ...interface{}) (*gojenkins.Job, error) + RenameJob(job string, name string) *gojenkins.Job + CopyJob(copyFrom string, newName string) (*gojenkins.Job, error) + DeleteJob(name string) (bool, error) + BuildJob(name string, options ...interface{}) (bool, error) + GetNode(name string) (*gojenkins.Node, error) + GetBuild(jobName string, number int64) (*gojenkins.Build, error) + GetJob(id string) (*gojenkins.Job, error) + GetAllNodes() ([]*gojenkins.Node, error) + //GetAllBuildIds(job string) ([]jobBuild, error) + //GetAllJobNames() ([]job, error) + GetAllJobs() ([]*gojenkins.Job, error) + GetQueue() (*gojenkins.Queue, error) + GetQueueUrl() string + //GetArtifactData(id string) (*fingerPrintResponse, error) + GetPlugins(depth int) (*gojenkins.Plugins, error) + HasPlugin(name string) (*gojenkins.Plugin, error) + ValidateFingerPrint(id string) (bool, error) + GetView(name string) (*gojenkins.View, error) + GetAllViews() ([]*gojenkins.View, error) + CreateView(name string, viewType string) (*gojenkins.View, error) +} + +type jenkins struct { + gojenkins.Jenkins +} + +// BuildJenkinsAPIUrl returns Jenkins API URL +func BuildJenkinsAPIUrl(namespace, serviceName string, portNumber int, local, minikube bool) (string, error) { + // Get Jenkins URL from minikube command + if local && minikube { + cmd := exec.Command("minikube", "service", "--url", "-n", namespace, serviceName) + var out bytes.Buffer + cmd.Stdout = &out + err := cmd.Run() + if err != nil { + return "", err + } + lines := strings.Split(out.String(), "\n") + // First is for http, the second one is for Jenkins slaves communication + // see pkg/controller/jenkins/configuration/base/resources/service.go + url := lines[0] + return url, nil + } + + if local { + // When run locally make port-forward to jenkins pod ('kubectl -n default port-forward jenkins-operator-example 8080') + return fmt.Sprintf("http://localhost:%d", portNumber), nil + } + + // Connect through Kubernetes service, operator has to be run inside cluster + return fmt.Sprintf("http://%s:%d", serviceName, portNumber), nil +} + +// New creates Jenkins API client +func New(url, user, passwordOrToken string) (Jenkins, error) { + if strings.HasSuffix(url, "/") { + url = url[:len(url)-1] + } + + jenkinsClient := &jenkins{} + jenkinsClient.Server = url + jenkinsClient.Requester = &gojenkins.Requester{ + Base: url, + SslVerify: true, + Client: http.DefaultClient, + BasicAuth: &gojenkins.BasicAuth{Username: user, Password: passwordOrToken}, + Headers: http.Header{}, + } + if _, err := jenkinsClient.Init(); err != nil { + return nil, err + } + + status, err := jenkinsClient.Poll() + if err != nil { + return nil, err + } + if status != http.StatusOK { + return nil, fmt.Errorf("Invalid status code returned: %d", status) + } + + return jenkinsClient, nil +} diff --git a/pkg/controller/jenkins/client/token.go b/pkg/controller/jenkins/client/token.go new file mode 100644 index 00000000..c095e3bb --- /dev/null +++ b/pkg/controller/jenkins/client/token.go @@ -0,0 +1,52 @@ +package client + +import ( + "errors" + "fmt" + "net/http" + "strconv" +) + +type userTokenResponseData struct { + Name string `json:"tokenName"` + UUID string `json:"tokenUuid"` + Value string `json:"tokenValue"` +} + +type userTokenResponse struct { + Status string `json:"status"` + Data userTokenResponseData `json:"data"` +} + +// UserToken defines user token for Jenkins API communication +type UserToken struct { + raw *userTokenResponse + base string +} + +// GetToken returns user token +func (token *UserToken) GetToken() string { + return token.raw.Data.Value +} + +func (jenkins *jenkins) GenerateToken(userName, tokenName string) (*UserToken, error) { + token := &UserToken{raw: new(userTokenResponse), + base: fmt.Sprintf("/user/%s/descriptorByName/jenkins.security.ApiTokenProperty/generateNewToken", userName)} + endpoint := token.base + data := map[string]string{"newTokenName": tokenName} + r, err := jenkins.Requester.Post(endpoint, nil, token.raw, data) + + if err != nil { + return nil, err + } + + if r.StatusCode == http.StatusOK { + if token.raw.Status == "ok" { + return token, nil + } + + return nil, errors.New(token.raw.Status) + } + + return nil, errors.New(strconv.Itoa(r.StatusCode)) +} diff --git a/pkg/controller/jenkins/configuration/base/reconcile.go b/pkg/controller/jenkins/configuration/base/reconcile.go new file mode 100644 index 00000000..89026e12 --- /dev/null +++ b/pkg/controller/jenkins/configuration/base/reconcile.go @@ -0,0 +1,302 @@ +package base + +import ( + "context" + "fmt" + "reflect" + "time" + + virtuslabv1alpha1 "github.com/VirtusLab/jenkins-operator/pkg/apis/virtuslab/v1alpha1" + jenkinsclient "github.com/VirtusLab/jenkins-operator/pkg/controller/jenkins/client" + "github.com/VirtusLab/jenkins-operator/pkg/controller/jenkins/configuration/base/resources" + "github.com/VirtusLab/jenkins-operator/pkg/log" + + "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +// ReconcileJenkinsBaseConfiguration defines values required for Jenkins base configuration +type ReconcileJenkinsBaseConfiguration struct { + client client.Client + scheme *runtime.Scheme + logger logr.Logger + jenkins *virtuslabv1alpha1.Jenkins + local, minikube bool +} + +// New create structure which takes care of base configuration +func New(client client.Client, scheme *runtime.Scheme, logger logr.Logger, + jenkins *virtuslabv1alpha1.Jenkins, local, minikube bool) *ReconcileJenkinsBaseConfiguration { + return &ReconcileJenkinsBaseConfiguration{ + client: client, + scheme: scheme, + logger: logger, + jenkins: jenkins, + local: local, + minikube: minikube, + } +} + +// Reconcile takes care of base configuration +func (r *ReconcileJenkinsBaseConfiguration) Reconcile() (*reconcile.Result, error) { + if !r.validate(r.jenkins) { + r.logger.V(log.VWarn).Info("Please correct Jenkins CR") + return &reconcile.Result{}, nil + } + + metaObject := resources.NewResourceObjectMeta(r.jenkins) + + if err := r.createOperatorCredentialsSecret(metaObject); err != nil { + return &reconcile.Result{}, err + } + r.logger.V(log.VDebug).Info("Operator credentials secret is present") + + if err := r.createScriptsConfigMap(metaObject); err != nil { + return &reconcile.Result{}, err + } + r.logger.V(log.VDebug).Info("Scripts config map is present") + + if err := r.createBaseConfigurationConfigMap(metaObject); err != nil { + return &reconcile.Result{}, err + } + r.logger.V(log.VDebug).Info("Base configuration config map is present") + + if err := r.createService(metaObject); err != nil { + return &reconcile.Result{}, err + } + r.logger.V(log.VDebug).Info("Service is present") + + result, err := r.createJenkinsMasterPod(metaObject) + if err != nil { + return &reconcile.Result{}, err + } + if result != nil { + return result, nil + } + r.logger.V(log.VDebug).Info("Jenkins master pod is present") + + result, err = r.waitForJenkins(metaObject) + if err != nil { + return &reconcile.Result{}, err + } + if result != nil { + return result, nil + } + r.logger.V(log.VDebug).Info("Jenkins master pod is ready") + + _, err = r.getJenkinsClient(metaObject) + if err != nil { + return &reconcile.Result{}, err + } + r.logger.V(log.VDebug).Info("Jenkins API client set") + + return nil, nil +} + +func (r *ReconcileJenkinsBaseConfiguration) createOperatorCredentialsSecret(meta metav1.ObjectMeta) error { + found := &corev1.Secret{} + err := r.client.Get(context.TODO(), types.NamespacedName{Name: resources.GetOperatorCredentialsSecretName(r.jenkins), Namespace: r.jenkins.ObjectMeta.Namespace}, found) + + if err != nil && apierrors.IsNotFound(err) { + return r.createResource(resources.NewOperatorCredentialsSecret(meta, r.jenkins)) + } else if err != nil && !apierrors.IsNotFound(err) { + return err + } + + if found.Data[resources.OperatorCredentialsSecretUserNameKey] != nil && + found.Data[resources.OperatorCredentialsSecretPasswordKey] != nil { + return nil + } + + return r.updateResource(resources.NewOperatorCredentialsSecret(meta, r.jenkins)) +} + +func (r *ReconcileJenkinsBaseConfiguration) createScriptsConfigMap(meta metav1.ObjectMeta) error { + scripts, err := resources.NewScriptsConfigMap(meta, r.jenkins) + if err != nil { + return err + } + return r.createOrUpdateResource(scripts) +} + +func (r *ReconcileJenkinsBaseConfiguration) createBaseConfigurationConfigMap(meta metav1.ObjectMeta) error { + scripts, err := resources.NewBaseConfigurationConfigMap(meta, r.jenkins) + if err != nil { + return err + } + return r.createOrUpdateResource(scripts) +} + +func (r *ReconcileJenkinsBaseConfiguration) createService(meta metav1.ObjectMeta) error { + err := r.createResource(resources.NewService(&meta, r.minikube)) + if err != nil && !apierrors.IsAlreadyExists(err) { + return err + } + + return nil +} + +func (r *ReconcileJenkinsBaseConfiguration) getJenkinsMasterPod(meta metav1.ObjectMeta) (*corev1.Pod, error) { + jenkinsMasterPod := resources.NewJenkinsMasterPod(meta, r.jenkins) + currentJenkinsMasterPod := &corev1.Pod{} + err := r.client.Get(context.TODO(), types.NamespacedName{Name: jenkinsMasterPod.Name, Namespace: jenkinsMasterPod.Namespace}, currentJenkinsMasterPod) + if err != nil { + return nil, err + } + return currentJenkinsMasterPod, nil +} + +func (r *ReconcileJenkinsBaseConfiguration) createJenkinsMasterPod(meta metav1.ObjectMeta) (*reconcile.Result, error) { + // Check if this Pod already exists + currentJenkinsMasterPod, err := r.getJenkinsMasterPod(meta) + if err != nil && errors.IsNotFound(err) { + jenkinsMasterPod := resources.NewJenkinsMasterPod(meta, r.jenkins) + r.logger.Info(fmt.Sprintf("Creating a new Jenkins Master Pod %s/%s", jenkinsMasterPod.Namespace, jenkinsMasterPod.Name)) + err = r.createResource(jenkinsMasterPod) + if err != nil { + return nil, err + } + if r.jenkins.Status.BaseConfigurationCompletedTime != nil { + r.jenkins.Status.BaseConfigurationCompletedTime = nil + err = r.updateResource(r.jenkins) + if err != nil { + return nil, err + } + } + return nil, nil + } else if err != nil && !errors.IsNotFound(err) { + return nil, err + } + + // Recreate pod + recreatePod := false + if currentJenkinsMasterPod != nil && + (currentJenkinsMasterPod.Status.Phase == corev1.PodFailed || + currentJenkinsMasterPod.Status.Phase == corev1.PodSucceeded || + currentJenkinsMasterPod.Status.Phase == corev1.PodUnknown) { + r.logger.Info(fmt.Sprintf("Invalid Jenkins pod phase %v, recreating pod", currentJenkinsMasterPod.Status.Phase)) + recreatePod = true + } + + if currentJenkinsMasterPod != nil && + r.jenkins.Spec.Master.Image != currentJenkinsMasterPod.Spec.Containers[0].Image { + r.logger.Info(fmt.Sprintf("Jenkins image has changed to %v, recreating pod", r.jenkins.Spec.Master.Image)) + recreatePod = true + } + + if currentJenkinsMasterPod != nil && len(r.jenkins.Spec.Master.Annotations) > 0 && + !reflect.DeepEqual(r.jenkins.Spec.Master.Annotations, currentJenkinsMasterPod.ObjectMeta.Annotations) { + r.logger.Info(fmt.Sprintf("Jenkins pod annotations have changed to %v, recreating pod", r.jenkins.Spec.Master.Annotations)) + recreatePod = true + } + + if currentJenkinsMasterPod != nil && + !reflect.DeepEqual(r.jenkins.Spec.Master.Resources, currentJenkinsMasterPod.Spec.Containers[0].Resources) { + r.logger.Info(fmt.Sprintf("Jenkins pod resources have changed to %v, recreating pod", r.jenkins.Spec.Master.Resources)) + recreatePod = true + } + + if currentJenkinsMasterPod != nil && recreatePod && currentJenkinsMasterPod.ObjectMeta.DeletionTimestamp == nil { + r.logger.Info(fmt.Sprintf("Terminating Jenkins Master Pod %s/%s", currentJenkinsMasterPod.Namespace, currentJenkinsMasterPod.Name)) + if err := r.client.Delete(context.TODO(), currentJenkinsMasterPod); err != nil { + return nil, err + } + return &reconcile.Result{Requeue: true}, nil + } + + return nil, nil +} + +func (r *ReconcileJenkinsBaseConfiguration) waitForJenkins(meta metav1.ObjectMeta) (*reconcile.Result, error) { + jenkinsMasterPodStatus, err := r.getJenkinsMasterPod(meta) + if err != nil { + return nil, err + } + + if jenkinsMasterPodStatus.ObjectMeta.DeletionTimestamp != nil { + r.logger.Info("Jenkins master pod is terminating") + return &reconcile.Result{Requeue: true, RequeueAfter: time.Second * 5}, nil + } + + if jenkinsMasterPodStatus.Status.Phase != corev1.PodRunning { + r.logger.Info("Jenkins master pod not ready") + return &reconcile.Result{Requeue: true, RequeueAfter: time.Second * 5}, nil + } + + for _, containerStatus := range jenkinsMasterPodStatus.Status.ContainerStatuses { + if !containerStatus.Ready { + r.logger.Info("Jenkins master pod not ready, readiness probe failed") + return &reconcile.Result{Requeue: true, RequeueAfter: time.Second * 5}, nil + } + } + + return nil, nil +} + +func (r *ReconcileJenkinsBaseConfiguration) getJenkinsClient(meta metav1.ObjectMeta) (jenkinsclient.Jenkins, error) { + jenkinsURL, err := jenkinsclient.BuildJenkinsAPIUrl( + r.jenkins.ObjectMeta.Namespace, meta.Name, resources.HTTPPortInt, r.local, r.minikube) + if err != nil { + return nil, err + } + r.logger.V(log.VDebug).Info(fmt.Sprintf("Jenkins API URL %s", jenkinsURL)) + + credentialsSecret := &corev1.Secret{} + err = r.client.Get(context.TODO(), types.NamespacedName{Name: resources.GetOperatorCredentialsSecretName(r.jenkins), Namespace: r.jenkins.ObjectMeta.Namespace}, credentialsSecret) + if err != nil { + return nil, err + } + currentJenkinsMasterPod, err := r.getJenkinsMasterPod(meta) + if err != nil { + return nil, err + } + + var tokenCreationTime *time.Time + tokenCreationTimeBytes := credentialsSecret.Data[resources.OperatorCredentialsSecretTokenCreationKey] + if tokenCreationTimeBytes != nil { + tokenCreationTime = &time.Time{} + err = tokenCreationTime.UnmarshalText(tokenCreationTimeBytes) + if err != nil { + tokenCreationTime = nil + } + + } + if credentialsSecret.Data[resources.OperatorCredentialsSecretTokenKey] == nil || + tokenCreationTimeBytes == nil || tokenCreationTime == nil || + currentJenkinsMasterPod.ObjectMeta.CreationTimestamp.Time.UTC().After(tokenCreationTime.UTC()) { + r.logger.Info("Generating Jenkins API token for operator") + userName := string(credentialsSecret.Data[resources.OperatorCredentialsSecretUserNameKey]) + jenkinsClient, err := jenkinsclient.New( + jenkinsURL, + userName, + string(credentialsSecret.Data[resources.OperatorCredentialsSecretPasswordKey])) + if err != nil { + return nil, err + } + + token, err := jenkinsClient.GenerateToken(userName, "token") + if err != nil { + return nil, err + } + + credentialsSecret.Data[resources.OperatorCredentialsSecretTokenKey] = []byte(token.GetToken()) + now, _ := time.Now().UTC().MarshalText() + credentialsSecret.Data[resources.OperatorCredentialsSecretTokenCreationKey] = now + err = r.updateResource(credentialsSecret) + if err != nil { + return nil, err + } + } + + return jenkinsclient.New( + jenkinsURL, + string(credentialsSecret.Data[resources.OperatorCredentialsSecretUserNameKey]), + string(credentialsSecret.Data[resources.OperatorCredentialsSecretTokenKey])) +} diff --git a/pkg/controller/jenkins/configuration/base/resources.go b/pkg/controller/jenkins/configuration/base/resources.go new file mode 100644 index 00000000..2d157f18 --- /dev/null +++ b/pkg/controller/jenkins/configuration/base/resources.go @@ -0,0 +1,56 @@ +package base + +import ( + "context" + "fmt" + + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +func (r *ReconcileJenkinsBaseConfiguration) createResource(obj metav1.Object) error { + runtimeObj, ok := obj.(runtime.Object) + if !ok { + return fmt.Errorf("is not a %T a runtime.Object", obj) + } + + // Set Jenkins instance as the owner and controller + if err := controllerutil.SetControllerReference(r.jenkins, obj, r.scheme); err != nil { + return err + } + + return r.client.Create(context.TODO(), runtimeObj) +} + +func (r *ReconcileJenkinsBaseConfiguration) updateResource(obj metav1.Object) error { + runtimeObj, ok := obj.(runtime.Object) + if !ok { + return fmt.Errorf("is not a %T a runtime.Object", obj) + } + + // Set Jenkins instance as the owner and controller, don't check error(can be already set) + _ = controllerutil.SetControllerReference(r.jenkins, obj, r.scheme) + + return r.client.Update(context.TODO(), runtimeObj) +} + +func (r *ReconcileJenkinsBaseConfiguration) createOrUpdateResource(obj metav1.Object) error { + runtimeObj, ok := obj.(runtime.Object) + if !ok { + return fmt.Errorf("is not a %T a runtime.Object", obj) + } + + // Set Jenkins instance as the owner and controller, don't check error(can be already set) + _ = controllerutil.SetControllerReference(r.jenkins, obj, r.scheme) + + err := r.client.Create(context.TODO(), runtimeObj) + if err != nil && errors.IsAlreadyExists(err) { + return r.updateResource(obj) + } else if err != nil && !errors.IsAlreadyExists(err) { + return err + } + + return nil +} diff --git a/pkg/controller/jenkins/configuration/base/resources/base_configuration_configmap.go b/pkg/controller/jenkins/configuration/base/resources/base_configuration_configmap.go new file mode 100644 index 00000000..db268edf --- /dev/null +++ b/pkg/controller/jenkins/configuration/base/resources/base_configuration_configmap.go @@ -0,0 +1,72 @@ +package resources + +import ( + "fmt" + "text/template" + + virtuslabv1alpha1 "github.com/VirtusLab/jenkins-operator/pkg/apis/virtuslab/v1alpha1" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const createOperatorUserFileName = "createOperatorUser.groovy" + +var createOperatorUserGroovyFmtTemplate = template.Must(template.New(createOperatorUserFileName).Parse(` +import hudson.security.* + +def jenkins = jenkins.model.Jenkins.getInstance() + +def hudsonRealm = new HudsonPrivateSecurityRealm(false) +hudsonRealm.createAccount( + new File('{{ .OperatorCredentialsPath }}/{{ .OperatorUserNameFile }}').text, + new File('{{ .OperatorCredentialsPath }}/{{ .OperatorPasswordFile }}').text) +jenkins.setSecurityRealm(hudsonRealm) + +def strategy = new FullControlOnceLoggedInAuthorizationStrategy() +strategy.setAllowAnonymousRead(false) +jenkins.setAuthorizationStrategy(strategy) +jenkins.save() +`)) + +func buildCreateJenkinsOperatorUserGroovyScript() (*string, error) { + data := struct { + OperatorCredentialsPath string + OperatorUserNameFile string + OperatorPasswordFile string + }{ + OperatorCredentialsPath: jenkinsOperatorCredentialsVolumePath, + OperatorUserNameFile: OperatorCredentialsSecretUserNameKey, + OperatorPasswordFile: OperatorCredentialsSecretPasswordKey, + } + + output, err := renderTemplate(createOperatorUserGroovyFmtTemplate, data) + if err != nil { + return nil, err + } + + return &output, nil +} + +// GetBaseConfigurationConfigMapName returns name of Kubernetes config map used to base configuration +func GetBaseConfigurationConfigMapName(jenkins *virtuslabv1alpha1.Jenkins) string { + return fmt.Sprintf("jenkins-operator-base-configuration-%s", jenkins.ObjectMeta.Name) +} + +// NewBaseConfigurationConfigMap builds Kubernetes config map used to base configuration +func NewBaseConfigurationConfigMap(meta metav1.ObjectMeta, jenkins *virtuslabv1alpha1.Jenkins) (*corev1.ConfigMap, error) { + meta.Name = GetBaseConfigurationConfigMapName(jenkins) + + createJenkinsOperatorUserGroovy, err := buildCreateJenkinsOperatorUserGroovyScript() + if err != nil { + return nil, err + } + + return &corev1.ConfigMap{ + TypeMeta: buildConfigMapTypeMeta(), + ObjectMeta: meta, + Data: map[string]string{ + createOperatorUserFileName: *createJenkinsOperatorUserGroovy, + }, + }, nil +} diff --git a/pkg/controller/jenkins/resources/doc.go b/pkg/controller/jenkins/configuration/base/resources/doc.go similarity index 100% rename from pkg/controller/jenkins/resources/doc.go rename to pkg/controller/jenkins/configuration/base/resources/doc.go diff --git a/pkg/controller/jenkins/configuration/base/resources/meta.go b/pkg/controller/jenkins/configuration/base/resources/meta.go new file mode 100644 index 00000000..8a8281b4 --- /dev/null +++ b/pkg/controller/jenkins/configuration/base/resources/meta.go @@ -0,0 +1,31 @@ +package resources + +import ( + "fmt" + + virtuslabv1alpha1 "github.com/VirtusLab/jenkins-operator/pkg/apis/virtuslab/v1alpha1" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// NewResourceObjectMeta builds ObjectMeta for all Kubernetes resources created by operator +func NewResourceObjectMeta(jenkins *virtuslabv1alpha1.Jenkins) metav1.ObjectMeta { + return metav1.ObjectMeta{ + Name: GetResourceName(jenkins), + Namespace: jenkins.ObjectMeta.Namespace, + Labels: BuildResourceLabels(jenkins), + } +} + +// BuildResourceLabels returns labels for all Kubernetes resources created by operator +func BuildResourceLabels(jenkins *virtuslabv1alpha1.Jenkins) map[string]string { + return map[string]string{ + "app": "jenkins-master", + "jenkins-cr": jenkins.Name, + } +} + +// GetResourceName returns name of Kubernetes resource base on Jenkins CR +func GetResourceName(jenkins *virtuslabv1alpha1.Jenkins) string { + return fmt.Sprintf("jenkins-operator-%s", jenkins.ObjectMeta.Name) +} diff --git a/pkg/controller/jenkins/configuration/base/resources/pod.go b/pkg/controller/jenkins/configuration/base/resources/pod.go new file mode 100644 index 00000000..bbbbe9a7 --- /dev/null +++ b/pkg/controller/jenkins/configuration/base/resources/pod.go @@ -0,0 +1,176 @@ +package resources + +import ( + "fmt" + + virtuslabv1alpha1 "github.com/VirtusLab/jenkins-operator/pkg/apis/virtuslab/v1alpha1" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" +) + +const ( + jenkinsHomeVolumeName = "home" + jenkinsHomePath = "/var/jenkins/home" + + jenkinsScriptsVolumeName = "scripts" + jenkinsScriptsVolumePath = "/var/jenkins/scripts" + initScriptName = "init.sh" + + jenkinsOperatorCredentialsVolumeName = "operator-credentials" + jenkinsOperatorCredentialsVolumePath = "/var/jenkins/operator-credentials" + + jenkinsBaseConfigurationVolumeName = "base-configuration" + jenkinsBaseConfigurationVolumePath = "/var/jenkins/base-configuration" + + httpPortName = "http" + slavePortName = "slavelistener" + // HTTPPortInt defines Jenkins master HTTP port + HTTPPortInt = 8080 + slavePortInt = 50000 + httpPortInt32 = int32(8080) + slavePortInt32 = int32(50000) + + jenkinsUserUID = int64(1000) // build in Docker image jenkins user UID +) + +func buildPodTypeMeta() metav1.TypeMeta { + return metav1.TypeMeta{ + Kind: "Pod", + APIVersion: "v1", + } +} + +// NewJenkinsMasterPod builds Jenkins Master Kubernetes Pod resource +func NewJenkinsMasterPod(objectMeta metav1.ObjectMeta, jenkins *virtuslabv1alpha1.Jenkins) *corev1.Pod { + initialDelaySeconds := int32(30) + timeoutSeconds := int32(5) + failureThreshold := int32(12) + runAsUser := jenkinsUserUID + + objectMeta.Annotations = jenkins.Spec.Master.Annotations + + return &corev1.Pod{ + TypeMeta: buildPodTypeMeta(), + ObjectMeta: objectMeta, + Spec: corev1.PodSpec{ + RestartPolicy: corev1.RestartPolicyNever, + SecurityContext: &corev1.PodSecurityContext{ + RunAsUser: &runAsUser, + RunAsGroup: &runAsUser, + }, + Containers: []corev1.Container{ + { + Name: "jenkins-master", + Image: jenkins.Spec.Master.Image, + Command: []string{ + "bash", + fmt.Sprintf("%s/%s", jenkinsScriptsVolumePath, initScriptName), + }, + LivenessProbe: &corev1.Probe{ + Handler: corev1.Handler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: "/login", + Port: intstr.FromString(httpPortName), + Scheme: corev1.URISchemeHTTP, + }, + }, + InitialDelaySeconds: initialDelaySeconds, + TimeoutSeconds: timeoutSeconds, + FailureThreshold: failureThreshold, + }, + ReadinessProbe: &corev1.Probe{ + Handler: corev1.Handler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: "/login", + Port: intstr.FromString(httpPortName), + Scheme: corev1.URISchemeHTTP, + }, + }, + InitialDelaySeconds: initialDelaySeconds, + }, + Ports: []corev1.ContainerPort{ + { + Name: slavePortName, + ContainerPort: slavePortInt32, + }, + { + Name: httpPortName, + ContainerPort: httpPortInt32, + }, + }, + Env: []corev1.EnvVar{ + { + Name: "JENKINS_HOME", + Value: jenkinsHomePath, + }, + { + Name: "JAVA_OPTS", + Value: "-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XX:MaxRAMFraction=1 -Djenkins.install.runSetupWizard=false -Djava.awt.headless=true", + }, + }, + Resources: jenkins.Spec.Master.Resources, + VolumeMounts: []corev1.VolumeMount{ + { + Name: jenkinsHomeVolumeName, + MountPath: jenkinsHomePath, + ReadOnly: false, + }, + { + Name: jenkinsScriptsVolumeName, + MountPath: jenkinsScriptsVolumePath, + ReadOnly: true, + }, + { + Name: jenkinsBaseConfigurationVolumeName, + MountPath: jenkinsBaseConfigurationVolumePath, + ReadOnly: true, + }, + { + Name: jenkinsOperatorCredentialsVolumeName, + MountPath: jenkinsOperatorCredentialsVolumePath, + ReadOnly: true, + }, + }, + }, + }, + Volumes: []corev1.Volume{ + { + Name: jenkinsHomeVolumeName, + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, + { + Name: jenkinsScriptsVolumeName, + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: getScriptsConfigMapName(jenkins), + }, + }, + }, + }, + { + Name: jenkinsBaseConfigurationVolumeName, + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: GetBaseConfigurationConfigMapName(jenkins), + }, + }, + }, + }, + { + Name: jenkinsOperatorCredentialsVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: GetOperatorCredentialsSecretName(jenkins), + }, + }, + }, + }, + }, + } +} diff --git a/pkg/controller/jenkins/configuration/base/resources/random.go b/pkg/controller/jenkins/configuration/base/resources/random.go new file mode 100644 index 00000000..00ec62b2 --- /dev/null +++ b/pkg/controller/jenkins/configuration/base/resources/random.go @@ -0,0 +1,20 @@ +package resources + +import ( + "math/rand" + "time" +) + +var randomCharset = []rune("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + +func randomString(n int) string { + b := make([]rune, n) + for i := range b { + b[i] = randomCharset[rand.Intn(len(randomCharset))] + } + return string(b) +} + +func init() { + rand.Seed(time.Now().UnixNano()) +} diff --git a/pkg/controller/jenkins/configuration/base/resources/scripts_configmap.go b/pkg/controller/jenkins/configuration/base/resources/scripts_configmap.go new file mode 100644 index 00000000..08d6efff --- /dev/null +++ b/pkg/controller/jenkins/configuration/base/resources/scripts_configmap.go @@ -0,0 +1,68 @@ +package resources + +import ( + "fmt" + "text/template" + + virtuslabv1alpha1 "github.com/VirtusLab/jenkins-operator/pkg/apis/virtuslab/v1alpha1" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var initBashTemplate = template.Must(template.New(initScriptName).Parse(`#!/usr/bin/env bash +set -e +set -x + +# https://wiki.jenkins.io/display/JENKINS/Post-initialization+script +mkdir -p {{ .JenkinsHomePath }}/init.groovy.d +cp -n {{ .BaseConfigurationPath }}/*.groovy {{ .JenkinsHomePath }}/init.groovy.d + +/sbin/tini -s -- /usr/local/bin/jenkins.sh +`)) + +func buildConfigMapTypeMeta() metav1.TypeMeta { + return metav1.TypeMeta{ + Kind: "ConfigMap", + APIVersion: "v1", + } +} + +func buildInitBashScript() (*string, error) { + data := struct { + JenkinsHomePath string + BaseConfigurationPath string + }{ + JenkinsHomePath: jenkinsHomePath, + BaseConfigurationPath: jenkinsBaseConfigurationVolumePath, + } + + output, err := renderTemplate(initBashTemplate, data) + if err != nil { + return nil, err + } + + return &output, nil +} + +func getScriptsConfigMapName(jenkins *virtuslabv1alpha1.Jenkins) string { + return fmt.Sprintf("jenkins-operator-scripts-%s", jenkins.ObjectMeta.Name) +} + +// NewScriptsConfigMap builds Kubernetes config map used to store scripts +func NewScriptsConfigMap(meta metav1.ObjectMeta, jenkins *virtuslabv1alpha1.Jenkins) (*corev1.ConfigMap, error) { + meta.Name = getScriptsConfigMapName(jenkins) + + initBashScript, err := buildInitBashScript() + if err != nil { + return nil, err + } + + return &corev1.ConfigMap{ + TypeMeta: buildConfigMapTypeMeta(), + ObjectMeta: meta, + Data: map[string]string{ + initScriptName: *initBashScript, + }, + }, nil +} diff --git a/pkg/controller/jenkins/configuration/base/resources/secret.go b/pkg/controller/jenkins/configuration/base/resources/secret.go new file mode 100644 index 00000000..4c247922 --- /dev/null +++ b/pkg/controller/jenkins/configuration/base/resources/secret.go @@ -0,0 +1,50 @@ +package resources + +import ( + "fmt" + + virtuslabv1alpha1 "github.com/VirtusLab/jenkins-operator/pkg/apis/virtuslab/v1alpha1" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + // OperatorUserName defines username for Jenkins API calls + OperatorUserName = "jenkins-operator" + // OperatorCredentialsSecretUserNameKey defines key of username in operator credentials secret + OperatorCredentialsSecretUserNameKey = "user" + // OperatorCredentialsSecretPasswordKey defines key of password in operator credentials secret + OperatorCredentialsSecretPasswordKey = "password" + // OperatorCredentialsSecretTokenKey defines key of token in operator credentials secret + OperatorCredentialsSecretTokenKey = "token" + // OperatorCredentialsSecretTokenCreationKey defines key of token creation time in operator credentials secret + OperatorCredentialsSecretTokenCreationKey = "tokenCreationTime" +) + +func buildSecretTypeMeta() metav1.TypeMeta { + return metav1.TypeMeta{ + Kind: "Secret", + APIVersion: "v1", + } +} + +// GetOperatorCredentialsSecretName returns name of Kubernetes secret used to store jenkins operator credentials +// to allow calls to Jenkins API +func GetOperatorCredentialsSecretName(jenkins *virtuslabv1alpha1.Jenkins) string { + return fmt.Sprintf("jenkins-operator-credentials-%s", jenkins.Name) +} + +// NewOperatorCredentialsSecret builds the Kubernetes secret used to store jenkins operator credentials +// to allow calls to Jenkins API +func NewOperatorCredentialsSecret(meta metav1.ObjectMeta, jenkins *virtuslabv1alpha1.Jenkins) *corev1.Secret { + meta.Name = GetOperatorCredentialsSecretName(jenkins) + return &corev1.Secret{ + TypeMeta: buildSecretTypeMeta(), + ObjectMeta: meta, + Data: map[string][]byte{ + OperatorCredentialsSecretUserNameKey: []byte(OperatorUserName), + OperatorCredentialsSecretPasswordKey: []byte(randomString(20)), + }, + } +} diff --git a/pkg/controller/jenkins/configuration/base/resources/service.go b/pkg/controller/jenkins/configuration/base/resources/service.go new file mode 100644 index 00000000..88d44a17 --- /dev/null +++ b/pkg/controller/jenkins/configuration/base/resources/service.go @@ -0,0 +1,49 @@ +package resources + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" +) + +func buildServiceTypeMeta() metav1.TypeMeta { + return metav1.TypeMeta{ + Kind: "Service", + APIVersion: "v1", + } +} + +// NewService builds the Kubernetes service resource +func NewService(meta *metav1.ObjectMeta, minikube bool) *corev1.Service { + service := &corev1.Service{ + TypeMeta: buildServiceTypeMeta(), + ObjectMeta: *meta, + Spec: corev1.ServiceSpec{ + Selector: meta.Labels, + // The first port have to be Jenkins http port because when run with minikube + // command 'minikube service' returns endpoints in the same sequence + Ports: []corev1.ServicePort{ + { + Name: httpPortName, + Port: httpPortInt32, + TargetPort: intstr.FromInt(HTTPPortInt), + }, + { + Name: slavePortName, + Port: slavePortInt32, + TargetPort: intstr.FromInt(slavePortInt), + }, + }, + }, + } + + if minikube { + // When running locally with minikube cluster Jenkins Service have to be exposed via node port + // to allow communication operator -> Jenkins API + service.Spec.Type = corev1.ServiceTypeNodePort + } else { + service.Spec.Type = corev1.ServiceTypeClusterIP + } + + return service +} diff --git a/pkg/controller/jenkins/configuration/base/resources/template.go b/pkg/controller/jenkins/configuration/base/resources/template.go new file mode 100644 index 00000000..a0f7c588 --- /dev/null +++ b/pkg/controller/jenkins/configuration/base/resources/template.go @@ -0,0 +1,15 @@ +package resources + +import ( + "bytes" + "text/template" +) + +func renderTemplate(template *template.Template, data interface{}) (string, error) { + var buffer bytes.Buffer + if err := template.Execute(&buffer, data); err != nil { + return "", err + } + + return buffer.String(), nil +} diff --git a/pkg/controller/jenkins/configuration/base/validate.go b/pkg/controller/jenkins/configuration/base/validate.go new file mode 100644 index 00000000..529463d2 --- /dev/null +++ b/pkg/controller/jenkins/configuration/base/validate.go @@ -0,0 +1,27 @@ +package base + +import ( + "regexp" + + virtuslabv1alpha1 "github.com/VirtusLab/jenkins-operator/pkg/apis/virtuslab/v1alpha1" + + docker "github.com/docker/distribution/reference" +) + +var ( + dockerImageRegexp = regexp.MustCompile(`^` + docker.TagRegexp.String() + `$`) +) + +func (r *ReconcileJenkinsBaseConfiguration) validate(jenkins *virtuslabv1alpha1.Jenkins) bool { + if jenkins.Spec.Master.Image == "" { + r.logger.V(0).Info("Image not set") + return false + } + + if !dockerImageRegexp.MatchString(jenkins.Spec.Master.Image) && !docker.ReferenceRegexp.MatchString(jenkins.Spec.Master.Image) { + r.logger.V(0).Info("Invalid image") + return false + } + + return true +} diff --git a/pkg/controller/jenkins/jenkins_controller.go b/pkg/controller/jenkins/jenkins_controller.go index 0bdbd0b8..7a99bebb 100644 --- a/pkg/controller/jenkins/jenkins_controller.go +++ b/pkg/controller/jenkins/jenkins_controller.go @@ -2,37 +2,38 @@ package jenkins import ( "context" - "log" virtuslabv1alpha1 "github.com/VirtusLab/jenkins-operator/pkg/apis/virtuslab/v1alpha1" + "github.com/VirtusLab/jenkins-operator/pkg/controller/jenkins/configuration/base" + "github.com/VirtusLab/jenkins-operator/pkg/log" + + "github.com/go-logr/logr" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/manager" "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/source" ) -/** -* USER ACTION REQUIRED: This is a scaffold file intended for the user to modify with their own Controller -* business logic. Delete these comments after modifying this file.* - */ - // Add creates a new Jenkins Controller and adds it to the Manager. The Manager will set fields on the Controller // and Start it when the Manager is Started. -func Add(mgr manager.Manager) error { - return add(mgr, newReconciler(mgr)) +func Add(mgr manager.Manager, local, minikube bool) error { + return add(mgr, newReconciler(mgr, local, minikube)) } // newReconciler returns a new reconcile.Reconciler -func newReconciler(mgr manager.Manager) reconcile.Reconciler { - return &ReconcileJenkins{client: mgr.GetClient(), scheme: mgr.GetScheme()} +func newReconciler(mgr manager.Manager, local, minikube bool) reconcile.Reconciler { + return &ReconcileJenkins{ + client: mgr.GetClient(), + scheme: mgr.GetScheme(), + local: local, + minikube: minikube, + } } // add adds a new Controller to mgr with r as the reconcile.Reconciler @@ -68,23 +69,20 @@ var _ reconcile.Reconciler = &ReconcileJenkins{} type ReconcileJenkins struct { // This client, initialized using mgr.Client() above, is a split client // that reads objects from the cache and writes to the apiserver - client client.Client - scheme *runtime.Scheme + client client.Client + scheme *runtime.Scheme + local, minikube bool } // Reconcile reads that state of the cluster for a Jenkins object and makes changes based on the state read // and what is in the Jenkins.Spec -// TODO(user): Modify this Reconcile function to implement your Controller logic. This example creates -// a Pod as an example -// Note: -// The Controller will requeue the Request to be processed again if the returned error is non-nil or -// Result.Requeue is true, otherwise upon completion it will remove the work from the queue. func (r *ReconcileJenkins) Reconcile(request reconcile.Request) (reconcile.Result, error) { - log.Printf("Reconciling Jenkins %s/%s\n", request.Namespace, request.Name) + logger := r.buildLogger(request.Name) + logger.Info("Reconciling Jenkins") // Fetch the Jenkins instance - instance := &virtuslabv1alpha1.Jenkins{} - err := r.client.Get(context.TODO(), request.NamespacedName, instance) + jenkins := &virtuslabv1alpha1.Jenkins{} + err := r.client.Get(context.TODO(), request.NamespacedName, jenkins) if err != nil { if errors.IsNotFound(err) { // Request object not found, could have been deleted after reconcile request. @@ -96,54 +94,26 @@ func (r *ReconcileJenkins) Reconcile(request reconcile.Request) (reconcile.Resul return reconcile.Result{}, err } - // Define a new Pod object - pod := newPodForCR(instance) - - // Set Jenkins instance as the owner and controller - if err := controllerutil.SetControllerReference(instance, pod, r.scheme); err != nil { + baseConfiguration := base.New(r.client, r.scheme, logger, jenkins, r.local, r.minikube) + result, err := baseConfiguration.Reconcile() + if err != nil { return reconcile.Result{}, err } - - // Check if this Pod already exists - found := &corev1.Pod{} - err = r.client.Get(context.TODO(), types.NamespacedName{Name: pod.Name, Namespace: pod.Namespace}, found) - if err != nil && errors.IsNotFound(err) { - log.Printf("Creating a new Pod %s/%s\n", pod.Namespace, pod.Name) - err = r.client.Create(context.TODO(), pod) + if result != nil { + return *result, nil + } + if err == nil && result == nil && jenkins.Status.BaseConfigurationCompletedTime == nil { + now := metav1.Now() + jenkins.Status.BaseConfigurationCompletedTime = &now + err = r.client.Update(context.TODO(), jenkins) if err != nil { return reconcile.Result{}, err } - - // Pod created successfully - don't requeue - return reconcile.Result{}, nil - } else if err != nil { - return reconcile.Result{}, err } - // Pod already exists - don't requeue - log.Printf("Skip reconcile: Pod %s/%s already exists", found.Namespace, found.Name) return reconcile.Result{}, nil } -// newPodForCR returns a busybox pod with the same name/namespace as the cr -func newPodForCR(cr *virtuslabv1alpha1.Jenkins) *corev1.Pod { - labels := map[string]string{ - "app": cr.Name, - } - return &corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Name: cr.Name + "-pod", - Namespace: cr.Namespace, - Labels: labels, - }, - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - { - Name: "busybox", - Image: "busybox", - Command: []string{"sleep", "3600"}, - }, - }, - }, - } +func (r *ReconcileJenkins) buildLogger(jenkinsName string) logr.Logger { + return log.Log.WithValues("cr", jenkinsName) } diff --git a/pkg/log/log.go b/pkg/log/log.go index a57b2eb4..50395ba1 100644 --- a/pkg/log/log.go +++ b/pkg/log/log.go @@ -1,19 +1,28 @@ -// TODO delete after resolve issue https://github.com/operator-framework/operator-sdk/issues/503 package log +// FIXME delete after bump to v0.2.0 version + import ( + "log" + "github.com/go-logr/logr" "github.com/go-logr/zapr" "go.uber.org/zap" + runtimelog "sigs.k8s.io/controller-runtime/pkg/runtime/log" ) +// Log represents global logger var Log logr.Logger -// ZapLogger is a Logger implementation. -// If development is true, a Zap development config will be used -// (stacktraces on warnings, no sampling), otherwise a Zap production -// config will be used (stacktraces on errors, sampling). -func SetupLogger(development *bool) error { +const ( + // VWarn defines warning log level + VWarn = -1 + // VDebug defines debug log level + VDebug = 1 +) + +// SetupLogger setups global logger +func SetupLogger(development *bool) { var zapLog *zap.Logger var err error @@ -25,9 +34,10 @@ func SetupLogger(development *bool) error { zapLog, err = zapLogCfg.Build(zap.AddCallerSkip(1)) } if err != nil { - return err + log.Fatal(err) } - Log = zapr.NewLogger(zapLog) - return nil + Log = zapr.NewLogger(zapLog).WithName("jenkins-operator") + // Enable logging in controller-runtime, without this you won't get logs when reconcile loop return an error + runtimelog.SetLogger(Log) } diff --git a/test/e2e/base_configuration_test.go b/test/e2e/base_configuration_test.go new file mode 100644 index 00000000..e8e64143 --- /dev/null +++ b/test/e2e/base_configuration_test.go @@ -0,0 +1,84 @@ +package e2e + +import ( + "context" + "reflect" + "testing" + + virtuslabv1alpha1 "github.com/VirtusLab/jenkins-operator/pkg/apis/virtuslab/v1alpha1" + + "github.com/bndr/gojenkins" + framework "github.com/operator-framework/operator-sdk/pkg/test" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestBaseConfiguration(t *testing.T) { + t.Parallel() + namespace, ctx := setupTest(t) + // Deletes test namespace + defer ctx.Cleanup() + + jenkins := &virtuslabv1alpha1.Jenkins{ + ObjectMeta: metav1.ObjectMeta{ + Name: "e2e", + Namespace: namespace, + }, + Spec: virtuslabv1alpha1.JenkinsSpec{ + Master: virtuslabv1alpha1.JenkinsMaster{ + Image: "jenkins/jenkins", + Annotations: map[string]string{"test": "label"}, + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("1"), + corev1.ResourceMemory: resource.MustParse("1Gi"), + }, + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("2"), + corev1.ResourceMemory: resource.MustParse("2Gi"), + }, + }, + }, + }, + } + t.Logf("Jenkins CR %+v", *jenkins) + if err := framework.Global.Client.Create(context.TODO(), jenkins, nil); err != nil { + t.Fatal(err) + } + + waitForJenkinsBaseConfigurationToComplete(t, jenkins) + + verifyJenkinsMasterPodAttributes(t, jenkins) + verifyJenkinsAPIConnection(t, jenkins) +} + +func verifyJenkinsAPIConnection(t *testing.T, jenkins *virtuslabv1alpha1.Jenkins) *gojenkins.Jenkins { + client, err := createJenkinsAPIClient(jenkins) + if err != nil { + t.Fatal(err) + } + + t.Log("I can establish connection to Jenkins API") + return client +} + +func verifyJenkinsMasterPodAttributes(t *testing.T, jenkins *virtuslabv1alpha1.Jenkins) { + jenkinsPod := getJenkinsMasterPod(t, jenkins) + + for key, value := range jenkins.Spec.Master.Annotations { + if jenkinsPod.ObjectMeta.Annotations[key] != value { + t.Fatalf("Invalid Jenkins pod annotation expected '%+v', actual '%+v'", jenkins.Spec.Master.Annotations, jenkinsPod.ObjectMeta.Annotations) + } + } + + if jenkinsPod.Spec.Containers[0].Image != jenkins.Spec.Master.Image { + t.Fatalf("Invalid jenkins pod image expected '%s', actual '%s'", jenkins.Spec.Master.Image, jenkinsPod.Spec.Containers[0].Image) + } + + if !reflect.DeepEqual(jenkinsPod.Spec.Containers[0].Resources, jenkins.Spec.Master.Resources) { + t.Fatalf("Invalid jenkins pod continer resources expected '%+v', actual '%+v'", jenkins.Spec.Master.Resources, jenkinsPod.Spec.Containers[0].Resources) + } + + t.Log("Jenkins pod attributes are valid") +} diff --git a/test/e2e/jenkins.go b/test/e2e/jenkins.go new file mode 100644 index 00000000..2cdc0fe8 --- /dev/null +++ b/test/e2e/jenkins.go @@ -0,0 +1,65 @@ +package e2e + +import ( + "context" + "fmt" + "net/http" + "testing" + + virtuslabv1alpha1 "github.com/VirtusLab/jenkins-operator/pkg/apis/virtuslab/v1alpha1" + jenkinsclient "github.com/VirtusLab/jenkins-operator/pkg/controller/jenkins/client" + "github.com/VirtusLab/jenkins-operator/pkg/controller/jenkins/configuration/base/resources" + + "github.com/bndr/gojenkins" + framework "github.com/operator-framework/operator-sdk/pkg/test" + "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/types" +) + +func getJenkinsMasterPod(t *testing.T, jenkins *virtuslabv1alpha1.Jenkins) *v1.Pod { + lo := metav1.ListOptions{ + LabelSelector: labels.SelectorFromSet(resources.BuildResourceLabels(jenkins)).String(), + } + podList, err := framework.Global.KubeClient.CoreV1().Pods(jenkins.ObjectMeta.Namespace).List(lo) + if err != nil { + t.Fatal(err) + } + if len(podList.Items) != 1 { + t.Fatalf("Jenkins pod not found, pod list: %+v", podList) + } + return &podList.Items[0] +} + +func createJenkinsAPIClient(jenkins *virtuslabv1alpha1.Jenkins) (*gojenkins.Jenkins, error) { + adminSecret := &v1.Secret{} + namespacedName := types.NamespacedName{Namespace: jenkins.Namespace, Name: resources.GetOperatorCredentialsSecretName(jenkins)} + if err := framework.Global.Client.Get(context.TODO(), namespacedName, adminSecret); err != nil { + return nil, err + } + + jenkinsAPIURL, err := jenkinsclient.BuildJenkinsAPIUrl(jenkins.ObjectMeta.Namespace, resources.GetResourceName(jenkins), resources.HTTPPortInt, true, true) + if err != nil { + return nil, err + } + + jenkinsClient := gojenkins.CreateJenkins( + jenkinsAPIURL, + string(adminSecret.Data[resources.OperatorCredentialsSecretUserNameKey]), + string(adminSecret.Data[resources.OperatorCredentialsSecretTokenKey]), + ) + if _, err := jenkinsClient.Init(); err != nil { + return nil, err + } + + status, err := jenkinsClient.Poll() + if err != nil { + return nil, err + } + if status != http.StatusOK { + return nil, fmt.Errorf("invalid status code returned: %d", status) + } + + return jenkinsClient, nil +} diff --git a/test/e2e/main_test.go b/test/e2e/main_test.go new file mode 100644 index 00000000..c0e4ac2e --- /dev/null +++ b/test/e2e/main_test.go @@ -0,0 +1,56 @@ +package e2e + +import ( + "testing" + + "github.com/VirtusLab/jenkins-operator/pkg/apis" + virtuslabv1alpha1 "github.com/VirtusLab/jenkins-operator/pkg/apis/virtuslab/v1alpha1" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + framework "github.com/operator-framework/operator-sdk/pkg/test" + "github.com/operator-framework/operator-sdk/pkg/test/e2eutil" + + f "github.com/operator-framework/operator-sdk/pkg/test" +) + +const ( + jenkinsOperatorDeploymentName = "jenkins-operator" +) + +func TestMain(m *testing.M) { + f.MainEntry(m) +} + +func setupTest(t *testing.T) (string, *framework.TestCtx) { + ctx := framework.NewTestCtx(t) + err := ctx.InitializeClusterResources(nil) + if err != nil { + t.Fatalf("could not initialize cluster resources: %v", err) + } + + jenkinsServiceList := &virtuslabv1alpha1.JenkinsList{ + TypeMeta: metav1.TypeMeta{ + Kind: virtuslabv1alpha1.Kind, + APIVersion: virtuslabv1alpha1.SchemeGroupVersion.String(), + }, + } + err = framework.AddToFrameworkScheme(apis.AddToScheme, jenkinsServiceList) + if err != nil { + t.Fatalf("could not add scheme to framework scheme: %v", err) + } + + namespace, err := ctx.GetNamespace() + if err != nil { + t.Fatalf("could not get namespace: %v", err) + } + t.Logf("Test namespace '%s'", namespace) + + // wait for jenkins-operator to be ready + err = e2eutil.WaitForDeployment(t, framework.Global.KubeClient, namespace, jenkinsOperatorDeploymentName, 1, retryInterval, timeout) + if err != nil { + t.Fatal(err) + } + + return namespace, ctx +} diff --git a/test/e2e/wait.go b/test/e2e/wait.go new file mode 100644 index 00000000..092e4a2d --- /dev/null +++ b/test/e2e/wait.go @@ -0,0 +1,51 @@ +package e2e + +import ( + goctx "context" + "fmt" + "testing" + "time" + + virtuslabv1alpha1 "github.com/VirtusLab/jenkins-operator/pkg/apis/virtuslab/v1alpha1" + + framework "github.com/operator-framework/operator-sdk/pkg/test" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/wait" +) + +var ( + retryInterval = time.Second * 5 + timeout = time.Second * 30 +) + +// checkConditionFunc is used to check if a condition for the jenkins CR is true +type checkConditionFunc func(*virtuslabv1alpha1.Jenkins) bool + +func waitForJenkinsBaseConfigurationToComplete(t *testing.T, jenkins *virtuslabv1alpha1.Jenkins) { + t.Log("Waiting for Jenkins base configuration to complete") + _, err := WaitUntilJenkinsConditionTrue(retryInterval, 30, jenkins, func(jenkins *virtuslabv1alpha1.Jenkins) bool { + t.Logf("Current Jenkins status '%+v'", jenkins.Status) + return jenkins.Status.BaseConfigurationCompletedTime != nil + }) + if err != nil { + t.Fatal(err) + } + t.Log("Jenkins pod is running") +} + +// WaitUntilJenkinsConditionTrue retries until the specified condition check becomes true for the jenkins CR +func WaitUntilJenkinsConditionTrue(retryInterval time.Duration, retries int, jenkins *virtuslabv1alpha1.Jenkins, checkCondition checkConditionFunc) (*virtuslabv1alpha1.Jenkins, error) { + jenkinsStatus := &virtuslabv1alpha1.Jenkins{} + err := wait.Poll(retryInterval, time.Duration(retries)*retryInterval, func() (bool, error) { + namespacedName := types.NamespacedName{Namespace: jenkins.Namespace, Name: jenkins.Name} + err := framework.Global.Client.Get(goctx.TODO(), namespacedName, jenkinsStatus) + if err != nil { + return false, fmt.Errorf("failed to get CR: %v", err) + } + return checkCondition(jenkinsStatus), nil + }) + if err != nil { + return nil, err + } + return jenkinsStatus, nil +} diff --git a/version/version.go b/version/version.go index 078ccf3a..7222dc9c 100644 --- a/version/version.go +++ b/version/version.go @@ -1,4 +1,7 @@ package version +// Version indicates which version of the binary is running. var Version string + +// GitCommit indicates which git hash the binary was built off of var GitCommit string