Add reconcile loop of Jenkins base configuration
This commit is contained in:
parent
14950293c5
commit
54aa39c502
|
|
@ -1,5 +1,6 @@
|
|||
.idea
|
||||
vendor
|
||||
deploy/namespace-init.yaml
|
||||
|
||||
# Temporary Build Files
|
||||
build/_output
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
20
Makefile
20
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
|
||||
|
|
|
|||
21
README.md
21
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
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
0.0.1
|
||||
v0.0.1
|
||||
|
|
|
|||
|
|
@ -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" ]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
---
|
||||
kind: RoleBinding
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
|
|
@ -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]))
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -70,21 +71,18 @@ type ReconcileJenkins struct {
|
|||
// that reads objects from the cache and writes to the apiserver
|
||||
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 {
|
||||
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)
|
||||
baseConfiguration := base.New(r.client, r.scheme, logger, jenkins, r.local, r.minikube)
|
||||
result, err := baseConfiguration.Reconcile()
|
||||
if err != nil {
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
|
||||
// Pod created successfully - don't requeue
|
||||
return reconcile.Result{}, nil
|
||||
} else if err != nil {
|
||||
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 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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue