From 8452ca5932ab91bfbd90329658097087801fff10 Mon Sep 17 00:00:00 2001 From: Ricardo Maraschini Date: Fri, 8 Dec 2023 11:01:56 +0100 Subject: [PATCH] feat: add support for embedded cluster updates (#4201) * feat: add support for embedded cluster updates we now support embedded cluster upgrades together with the kots app upgrade. users can provide their own embedded cluster config object that is then applied to the cluster when it differs from the current active one. * trigger cluster upgrades on app version deploy not on poll * make embedded_cluster_status table * eventually exit cluster state update loop, only persist to DB on state change * trigger a single check of the cluster status at startup * don't prevent logging * only attempt to get embedded cluser installation state on embedded cluster systems * fix js missing property * unlock mutex, don't log about embedded-cluster on non-ec installs * when starting up, watch the embedded cluster state until it is Installed --------- Co-authored-by: Andrew Lavery --- go.mod | 17 +-- go.sum | 33 +++--- migrations/tables/app_version.yaml | 2 + migrations/tables/embeded_cluster_status.yaml | 17 +++ pkg/apiserver/server.go | 5 + pkg/embeddedcluster/monitor.go | 111 ++++++++++++++++++ pkg/embeddedcluster/util.go | 84 +++++++++++-- pkg/handlers/dashboard.go | 21 +++- pkg/kotsutil/kots.go | 33 ++++++ pkg/operator/operator.go | 22 ++++ pkg/store/kotsstore/embedded_cluster_store.go | 45 ++++++- pkg/store/kotsstore/version_store.go | 28 ++++- pkg/store/mock/mock.go | 58 +++++++++ pkg/store/store_interface.go | 2 + .../Dashboard/components/AppStatus.tsx | 32 ++++- .../Dashboard/components/Dashboard.tsx | 4 + web/src/types/index.ts | 1 + web/src/utilities/utilities.js | 19 +++ 18 files changed, 482 insertions(+), 52 deletions(-) create mode 100644 migrations/tables/embeded_cluster_status.yaml create mode 100644 pkg/embeddedcluster/monitor.go diff --git a/go.mod b/go.mod index 32a3a8cfac..256ab9b382 100644 --- a/go.mod +++ b/go.mod @@ -41,7 +41,7 @@ require ( github.com/mholt/archiver/v3 v3.5.1 github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a github.com/mitchellh/hashstructure v1.1.0 - github.com/onsi/ginkgo/v2 v2.13.1 + github.com/onsi/ginkgo/v2 v2.13.2 github.com/onsi/gomega v1.30.0 github.com/open-policy-agent/opa v0.58.0 github.com/ory/dockertest/v3 v3.10.0 @@ -49,7 +49,7 @@ require ( github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 github.com/pkg/errors v0.9.1 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 - github.com/replicatedhq/embedded-cluster-operator v0.4.1 + github.com/replicatedhq/embedded-cluster-operator v0.5.0 github.com/replicatedhq/kotskinds v0.0.0-20231004174055-e6676d808a82 github.com/replicatedhq/kurlkinds v1.3.6 github.com/replicatedhq/troubleshoot v0.76.4-0.20231102041618-a7bb9ea31e61 @@ -67,7 +67,7 @@ require ( github.com/tj/go-spin v1.1.0 github.com/vmware-tanzu/velero v1.10.1 go.uber.org/multierr v1.11.0 - go.uber.org/zap v1.25.0 + go.uber.org/zap v1.26.0 golang.org/x/crypto v0.14.0 golang.org/x/oauth2 v0.13.0 golang.org/x/sync v0.5.0 @@ -184,10 +184,11 @@ require ( github.com/go-ldap/ldap/v3 v3.4.4 // indirect github.com/go-logr/logr v1.3.0 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-logr/zapr v1.2.4 // indirect github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-openapi/analysis v0.21.4 // indirect github.com/go-openapi/errors v0.20.4 // indirect - github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/go-openapi/jsonpointer v0.20.0 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/loads v0.21.2 // indirect github.com/go-openapi/runtime v0.26.0 // indirect @@ -237,7 +238,7 @@ require ( github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/yamux v0.1.1 // indirect github.com/huandu/xstrings v1.4.0 // indirect - github.com/imdario/mergo v0.3.15 // indirect + github.com/imdario/mergo v0.3.16 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jackc/chunkreader/v2 v2.0.1 // indirect github.com/jackc/pgconn v1.10.1 // indirect @@ -389,9 +390,9 @@ require ( gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/square/go-jose.v2 v2.6.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect - k8s.io/apiextensions-apiserver v0.28.3 // indirect - k8s.io/apiserver v0.28.3 // indirect - k8s.io/component-base v0.28.3 // indirect + k8s.io/apiextensions-apiserver v0.28.4 // indirect + k8s.io/apiserver v0.28.4 // indirect + k8s.io/component-base v0.28.4 // indirect k8s.io/klog/v2 v2.100.1 // indirect k8s.io/kube-aggregator v0.19.12 // indirect k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect diff --git a/go.sum b/go.sum index 095ffbcdbc..f7de538725 100644 --- a/go.sum +++ b/go.sum @@ -356,8 +356,6 @@ github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs= github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= github.com/benbjohnson/clock v1.0.3/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= -github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= -github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -647,6 +645,7 @@ github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7 github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= @@ -1032,8 +1031,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1: github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM= -github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= +github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= @@ -1385,8 +1384,8 @@ github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108 github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= -github.com/onsi/ginkgo/v2 v2.13.1 h1:LNGfMbR2OVGBfXjvRZIZ2YCTQdGKtPLvuI1rMCCj3OU= -github.com/onsi/ginkgo/v2 v2.13.1/go.mod h1:XStQ8QcGwLyF4HdfcZB8SFOS/MWCgDuXMSBe6zrvLgM= +github.com/onsi/ginkgo/v2 v2.13.2 h1:Bi2gGVkfn6gQcjNjZJVO8Gf0FHzMPf2phUei9tejVMs= +github.com/onsi/ginkgo/v2 v2.13.2/go.mod h1:XStQ8QcGwLyF4HdfcZB8SFOS/MWCgDuXMSBe6zrvLgM= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.3.0/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= @@ -1530,8 +1529,8 @@ github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqn github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M= -github.com/replicatedhq/embedded-cluster-operator v0.4.1 h1:4LMbS5Z8adVe+nO4lFG0oI926RiQgSKOn3h2yjHXShI= -github.com/replicatedhq/embedded-cluster-operator v0.4.1/go.mod h1:Z9hN4T1105PiYVh2UcgkYLSLLQDhQiuP3aDB8KDBGZA= +github.com/replicatedhq/embedded-cluster-operator v0.5.0 h1:EihT/WoUU4uHF5F53Fh1K+jhtjhPTrLy/RdUGlHY4Hc= +github.com/replicatedhq/embedded-cluster-operator v0.5.0/go.mod h1:Ahieg2DIkZ3U4rfSdmR12M3ljpjS/lLnCLR92W7Oicw= github.com/replicatedhq/kotskinds v0.0.0-20231004174055-e6676d808a82 h1:QniKgIpcXu4wBMM4xIXGz+lkAU+hSIXFuVM+vxkNk0Y= github.com/replicatedhq/kotskinds v0.0.0-20231004174055-e6676d808a82/go.mod h1:QjhIUu3+OmHZ09u09j3FCoTt8F3BYtQglS+OLmftu9I= github.com/replicatedhq/kurlkinds v1.3.6 h1:/dhS32cSSZR4yS4vA8EquBvz+VgJCyTqBO9Xw+6eI4M= @@ -1856,6 +1855,7 @@ go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= go.uber.org/multierr v0.0.0-20180122172545-ddea229ff1df/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= @@ -1872,8 +1872,9 @@ go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= -go.uber.org/zap v1.25.0 h1:4Hvk6GtkucQ790dqmj7l1eEnRdKm3k3ZUrUMS2d5+5c= -go.uber.org/zap v1.25.0/go.mod h1:JIAUzQIH94IC4fOJQm7gMmBJP5k7wQfdcnYdPoEXJYk= +go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= +go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -2659,8 +2660,8 @@ k8s.io/api v0.28.4 h1:8ZBrLjwosLl/NYgv1P7EQLqoO8MGQApnbgH8tu3BMzY= k8s.io/api v0.28.4/go.mod h1:axWTGrY88s/5YE+JSt4uUi6NMM+gur1en2REMR7IRj0= k8s.io/apiextensions-apiserver v0.0.0-20190918161926-8f644eb6e783/go.mod h1:xvae1SZB3E17UpV59AWc271W/Ph25N+bjPyR63X6tPY= k8s.io/apiextensions-apiserver v0.17.0/go.mod h1:XiIFUakZywkUl54fVXa7QTEHcqQz9HG55nHd1DCoHj8= -k8s.io/apiextensions-apiserver v0.28.3 h1:Od7DEnhXHnHPZG+W9I97/fSQkVpVPQx2diy+2EtmY08= -k8s.io/apiextensions-apiserver v0.28.3/go.mod h1:NE1XJZ4On0hS11aWWJUTNkmVB03j9LM7gJSisbRt8Lc= +k8s.io/apiextensions-apiserver v0.28.4 h1:AZpKY/7wQ8n+ZYDtNHbAJBb+N4AXXJvyZx6ww6yAJvU= +k8s.io/apiextensions-apiserver v0.28.4/go.mod h1:pgQIZ1U8eJSMQcENew/0ShUTlePcSGFq6dxSxf2mwPM= k8s.io/apimachinery v0.0.0-20190913080033-27d36303b655/go.mod h1:nL6pwRT8NgfF8TT68DBI8uEePRt89cSvoXUVqbkWHq4= k8s.io/apimachinery v0.16.8/go.mod h1:Xk2vD2TRRpuWYLQNM6lT9R7DSFZUYG03SarNkbGrnKE= k8s.io/apimachinery v0.17.0/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZg= @@ -2672,8 +2673,8 @@ k8s.io/apimachinery v0.28.4/go.mod h1:wI37ncBvfAoswfq626yPTe6Bz1c22L7uaJ8dho83mg k8s.io/apiserver v0.0.0-20190918160949-bfa5e2e684ad/go.mod h1:XPCXEwhjaFN29a8NldXA901ElnKeKLrLtREO9ZhFyhg= k8s.io/apiserver v0.17.0/go.mod h1:ABM+9x/prjINN6iiffRVNCBR2Wk7uY4z+EtEGZD48cg= k8s.io/apiserver v0.19.12/go.mod h1:ldZAZTNIKfMMv/UUEhk6UyTXC0/34iRdNFHo+MJOPc4= -k8s.io/apiserver v0.28.3 h1:8Ov47O1cMyeDzTXz0rwcfIIGAP/dP7L8rWbEljRcg5w= -k8s.io/apiserver v0.28.3/go.mod h1:YIpM+9wngNAv8Ctt0rHG4vQuX/I5rvkEMtZtsxW2rNM= +k8s.io/apiserver v0.28.4 h1:BJXlaQbAU/RXYX2lRz+E1oPe3G3TKlozMMCZWu5GMgg= +k8s.io/apiserver v0.28.4/go.mod h1:Idq71oXugKZoVGUUL2wgBCTHbUR+FYTWa4rq9j4n23w= k8s.io/cli-runtime v0.28.2 h1:64meB2fDj10/ThIMEJLO29a1oujSm0GQmKzh1RtA/uk= k8s.io/cli-runtime v0.28.2/go.mod h1:bTpGOvpdsPtDKoyfG4EG041WIyFZLV9qq4rPlkyYfDA= k8s.io/client-go v0.0.0-20190918160344-1fbdaa4c8d90/go.mod h1:J69/JveO6XESwVgG53q3Uz5OSfgsv4uxpScmmyYOOlk= @@ -2695,8 +2696,8 @@ k8s.io/component-base v0.0.0-20190918160511-547f6c5d7090/go.mod h1:933PBGtQFJky3 k8s.io/component-base v0.17.0/go.mod h1:rKuRAokNMY2nn2A6LP/MiwpoaMRHpfRnrPaUJJj1Yoc= k8s.io/component-base v0.19.12/go.mod h1:tpwExE0sY3A7CwtlxGL7SnQOdQfUlnFybT6GmAD+z/s= k8s.io/component-base v0.23.6/go.mod h1:FGMPeMrjYu0UZBSAFcfloVDplj9IvU+uRMTOdE23Fj0= -k8s.io/component-base v0.28.3 h1:rDy68eHKxq/80RiMb2Ld/tbH8uAE75JdCqJyi6lXMzI= -k8s.io/component-base v0.28.3/go.mod h1:fDJ6vpVNSk6cRo5wmDa6eKIG7UlIQkaFmZN2fYgIUD8= +k8s.io/component-base v0.28.4 h1:c/iQLWPdUgI90O+T9TeECg8o7N3YJTiuz2sKxILYcYo= +k8s.io/component-base v0.28.4/go.mod h1:m9hR0uvqXDybiGL2nf/3Lf0MerAfQXzkfWhUY58JUbU= k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/gengo v0.0.0-20190822140433-26a664648505/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= diff --git a/migrations/tables/app_version.yaml b/migrations/tables/app_version.yaml index 7ee1a00947..f24b8116d2 100644 --- a/migrations/tables/app_version.yaml +++ b/migrations/tables/app_version.yaml @@ -71,3 +71,5 @@ spec: type: text - name: branding_archive type: text + - name: embeddedcluster_config + type: text diff --git a/migrations/tables/embeded_cluster_status.yaml b/migrations/tables/embeded_cluster_status.yaml new file mode 100644 index 0000000000..790f860ed1 --- /dev/null +++ b/migrations/tables/embeded_cluster_status.yaml @@ -0,0 +1,17 @@ +apiVersion: schemas.schemahero.io/v1alpha4 +kind: Table +metadata: + name: embedded-cluster-status +spec: + name: embedded_cluster_status + requires: [] + schema: + rqlite: + strict: true + primaryKey: + - updated_at + columns: + - name: updated_at + type: integer + - name: status + type: text diff --git a/pkg/apiserver/server.go b/pkg/apiserver/server.go index 8938f0986f..e13962e0d2 100644 --- a/pkg/apiserver/server.go +++ b/pkg/apiserver/server.go @@ -13,6 +13,7 @@ import ( "github.com/gorilla/mux" "github.com/replicatedhq/kots/pkg/automation" "github.com/replicatedhq/kots/pkg/binaries" + "github.com/replicatedhq/kots/pkg/embeddedcluster" "github.com/replicatedhq/kots/pkg/handlers" "github.com/replicatedhq/kots/pkg/helm" identitymigrate "github.com/replicatedhq/kots/pkg/identity/migrate" @@ -105,6 +106,10 @@ func Start(params *APIServerParams) { panic(err) } defer op.Shutdown() + + if err := embeddedcluster.InitClusterState(context.TODO(), k8sClientset, store); err != nil { + log.Println("Failed to initialize cluster state:", err) + } } if params.SharedPassword != "" { diff --git a/pkg/embeddedcluster/monitor.go b/pkg/embeddedcluster/monitor.go new file mode 100644 index 0000000000..4cb5d9f0ad --- /dev/null +++ b/pkg/embeddedcluster/monitor.go @@ -0,0 +1,111 @@ +package embeddedcluster + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/replicatedhq/embedded-cluster-operator/api/v1beta1" + "github.com/replicatedhq/kots/pkg/logger" + "github.com/replicatedhq/kots/pkg/store" + "k8s.io/client-go/kubernetes" +) + +var stateMut = sync.Mutex{} + +// MaybeStartClusterUpgrade checks if the embedded cluster is in a state that requires an upgrade. If so, +// it starts the upgrade process. We only start an upgrade if the following conditions are met: +// - The app has an embedded cluster configuration. +// - The app embedded cluster configuration differs from the current embedded cluster config. +func MaybeStartClusterUpgrade(ctx context.Context, client kubernetes.Interface, store store.Store, conf *v1beta1.Config) error { + if conf == nil { + return nil + } + + isEC, err := IsEmbeddedCluster(client) + if err != nil { + return fmt.Errorf("failed to check if embedded cluster is enabled: %w", err) + } + if !isEC { + return nil + } + + spec := conf.Spec + if upgrade, err := RequiresUpgrade(ctx, spec); err != nil { + return fmt.Errorf("failed to check if upgrade is required: %w", err) + } else if !upgrade { + return nil + } + if err := startClusterUpgrade(ctx, spec); err != nil { + return fmt.Errorf("failed to start cluster upgrade: %w", err) + } + + go watchClusterState(ctx, store) + + return nil +} + +// InitClusterState initializes the cluster state in the database. This should be called when the +// server launches. +func InitClusterState(ctx context.Context, client kubernetes.Interface, store store.Store) error { + isEC, err := IsEmbeddedCluster(client) + if err != nil { + return fmt.Errorf("failed to check if embedded cluster is enabled: %w", err) + } + if isEC { + go watchClusterState(ctx, store) + return nil + } + return nil +} + +// watchClusterState checks the status of the installation object and updates the cluster state +// after the cluster state has been 'installed' for 30 seconds, it will exit the loop. +// this function is blocking and should be run in a goroutine. +// if it is called multiple times, only one instance will run. +func watchClusterState(ctx context.Context, store store.Store) { + stateMut.Lock() + defer stateMut.Unlock() + numReady := 0 + lastState := "" + for numReady < 6 { + select { + case <-ctx.Done(): + return + case <-time.After(time.Second * 5): + } + state, err := updateClusterState(ctx, store, lastState) + if err != nil { + logger.Errorf("embeddedcluster monitor: fail updating state: %v", err) + } + + if state == v1beta1.InstallationStateInstalled { + numReady++ + } else { + numReady = 0 + } + lastState = state + } +} + +// updateClusterState updates the cluster state in the database. Gets the state from the cluster +// by reading the latest embedded cluster installation CRD. +// If the lastState is the same as the current state, it will not update the database. +func updateClusterState(ctx context.Context, store store.Store, lastState string) (string, error) { + installation, err := GetCurrentInstallation(ctx) + if err != nil { + return "", fmt.Errorf("failed to get current installation: %w", err) + } + state := v1beta1.InstallationStateUnknown + if installation.Status.State != "" { + state = installation.Status.State + } + // only update the state if it has changed + if state != lastState { + if err := store.SetEmbeddedClusterState(state); err != nil { + return "", fmt.Errorf("failed to update embedded cluster state: %w", err) + } + } + return state, nil +} diff --git a/pkg/embeddedcluster/util.go b/pkg/embeddedcluster/util.go index 1ed8576092..7119541e5c 100644 --- a/pkg/embeddedcluster/util.go +++ b/pkg/embeddedcluster/util.go @@ -1,9 +1,12 @@ package embeddedcluster import ( + "bytes" "context" + "encoding/json" "fmt" "sort" + "time" embeddedclusterv1beta1 "github.com/replicatedhq/embedded-cluster-operator/api/v1beta1" "github.com/replicatedhq/kots/pkg/k8sutil" @@ -58,34 +61,89 @@ func ClusterID(client kubernetes.Interface) (string, error) { return configMap.Data["embedded-cluster-id"], nil } -// ClusterConfig will get the list of installations, find the latest installation, and get that installation's config -func ClusterConfig(ctx context.Context) (*embeddedclusterv1beta1.ConfigSpec, error) { +// RequiresUpgrade returns true if the provided configuration differs from the latest active configuration. +func RequiresUpgrade(ctx context.Context, newcfg embeddedclusterv1beta1.ConfigSpec) (bool, error) { + curcfg, err := ClusterConfig(ctx) + if err != nil { + return false, fmt.Errorf("failed to get current cluster config: %w", err) + } + serializedCur, err := json.Marshal(curcfg) + if err != nil { + return false, err + } + serializedNew, err := json.Marshal(newcfg) + if err != nil { + return false, err + } + return !bytes.Equal(serializedCur, serializedNew), nil +} + +// GetCurrentInstallation returns the most recent installation object from the cluster. +func GetCurrentInstallation(ctx context.Context) (*embeddedclusterv1beta1.Installation, error) { clientConfig, err := k8sutil.GetClusterConfig() if err != nil { return nil, fmt.Errorf("failed to get cluster config: %w", err) } - scheme := runtime.NewScheme() embeddedclusterv1beta1.AddToScheme(scheme) - - kbClient, err := kbclient.New(clientConfig, kbclient.Options{ - Scheme: scheme, - }) + kbClient, err := kbclient.New(clientConfig, kbclient.Options{Scheme: scheme}) if err != nil { return nil, fmt.Errorf("failed to get kubebuilder client: %w", err) } - var installationList embeddedclusterv1beta1.InstallationList err = kbClient.List(ctx, &installationList, &kbclient.ListOptions{}) if err != nil { return nil, fmt.Errorf("failed to list installations: %w", err) } - - // determine which of these installations is the latest - sort.Slice(installationList.Items, func(i, j int) bool { - return installationList.Items[i].ObjectMeta.CreationTimestamp.After(installationList.Items[j].ObjectMeta.CreationTimestamp.Time) + if len(installationList.Items) == 0 { + return nil, fmt.Errorf("no installations found") + } + items := installationList.Items + sort.SliceStable(items, func(i, j int) bool { + return items[j].CreationTimestamp.Before(&items[i].CreationTimestamp) }) + return &installationList.Items[0], nil +} - latest := installationList.Items[0] +// ClusterConfig will extract the current cluster configuration from the latest installation +// object found in the cluster. +func ClusterConfig(ctx context.Context) (*embeddedclusterv1beta1.ConfigSpec, error) { + latest, err := GetCurrentInstallation(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get current installation: %w", err) + } return latest.Spec.Config, nil } + +// startClusterUpgrade will create a new installation with the provided config. +func startClusterUpgrade(ctx context.Context, newcfg embeddedclusterv1beta1.ConfigSpec) error { + clientConfig, err := k8sutil.GetClusterConfig() + if err != nil { + return fmt.Errorf("failed to get cluster config: %w", err) + } + scheme := runtime.NewScheme() + embeddedclusterv1beta1.AddToScheme(scheme) + kbClient, err := kbclient.New(clientConfig, kbclient.Options{Scheme: scheme}) + if err != nil { + return fmt.Errorf("failed to get kubebuilder client: %w", err) + } + current, err := GetCurrentInstallation(ctx) + if err != nil { + return fmt.Errorf("failed to get current installation: %w", err) + } + newins := embeddedclusterv1beta1.Installation{ + ObjectMeta: metav1.ObjectMeta{ + Name: time.Now().Format("20060102150405"), + }, + Spec: embeddedclusterv1beta1.InstallationSpec{ + ClusterID: current.Spec.ClusterID, + MetricsBaseURL: current.Spec.MetricsBaseURL, + AirGap: current.Spec.AirGap, + Config: &newcfg, + }, + } + if err := kbClient.Create(ctx, &newins); err != nil { + return fmt.Errorf("failed to create installation: %w", err) + } + return nil +} diff --git a/pkg/handlers/dashboard.go b/pkg/handlers/dashboard.go index ff41c8db39..744309595a 100644 --- a/pkg/handlers/dashboard.go +++ b/pkg/handlers/dashboard.go @@ -15,9 +15,10 @@ import ( ) type GetAppDashboardResponse struct { - AppStatus *appstatetypes.AppStatus `json:"appStatus"` - Metrics []version.MetricChart `json:"metrics"` - PrometheusAddress string `json:"prometheusAddress"` + AppStatus *appstatetypes.AppStatus `json:"appStatus"` + Metrics []version.MetricChart `json:"metrics"` + PrometheusAddress string `json:"prometheusAddress"` + EmbeddedClusterState string `json:"embeddedClusterState"` } func (h *Handler) GetAppDashboard(w http.ResponseWriter, r *http.Request) { @@ -62,6 +63,13 @@ func (h *Handler) GetAppDashboard(w http.ResponseWriter, r *http.Request) { return } + ecState, err := store.GetStore().GetEmbeddedClusterState() + if err != nil { + logger.Error(err) + w.WriteHeader(500) + return + } + parentSequence, err := store.GetStore().GetCurrentParentSequence(a.ID, clusterID) if err != nil { logger.Error(err) @@ -89,9 +97,10 @@ func (h *Handler) GetAppDashboard(w http.ResponseWriter, r *http.Request) { } getAppDashboardResponse := GetAppDashboardResponse{ - AppStatus: appStatus, - Metrics: metrics, - PrometheusAddress: prometheusAddress, + AppStatus: appStatus, + Metrics: metrics, + PrometheusAddress: prometheusAddress, + EmbeddedClusterState: ecState, } JSON(w, 200, getAppDashboardResponse) diff --git a/pkg/kotsutil/kots.go b/pkg/kotsutil/kots.go index 440a10ebba..3a25a7ff7f 100644 --- a/pkg/kotsutil/kots.go +++ b/pkg/kotsutil/kots.go @@ -17,6 +17,7 @@ import ( "github.com/blang/semver" "github.com/pkg/errors" + embeddedclusterv1beta1 "github.com/replicatedhq/embedded-cluster-operator/api/v1beta1" "github.com/replicatedhq/kots/pkg/archives" "github.com/replicatedhq/kots/pkg/binaries" "github.com/replicatedhq/kots/pkg/buildversion" @@ -51,6 +52,7 @@ func init() { velerov1.AddToScheme(scheme.Scheme) kurlscheme.AddToScheme(scheme.Scheme) applicationv1beta1.AddToScheme(scheme.Scheme) + embeddedclusterv1beta1.AddToScheme(scheme.Scheme) } var ( @@ -105,6 +107,8 @@ type KotsKinds struct { Installer *kurlv1beta1.Installer LintConfig *kotsv1beta1.LintConfig + + EmbeddedClusterConfig *embeddedclusterv1beta1.Config } func IsKotsKind(apiVersion string, kind string) bool { @@ -129,6 +133,10 @@ func IsKotsKind(apiVersion string, kind string) bool { if apiVersion == "kurl.sh/v1beta1" { return true } + // In addition to kotskinds, we exclude the embedded cluster configuration. + if apiVersion == "embeddedcluster.replicated.com/v1beta1" { + return true + } // In addition to kotskinds, we exclude the application crd for now if apiVersion == "app.k8s.io/v1beta1" { return true @@ -448,6 +456,17 @@ func (o KotsKinds) Marshal(g string, v string, k string) (string, error) { } } + if g == "embeddedcluster.replicated.com" && v == "v1beta1" && k == "Config" { + if o.EmbeddedClusterConfig == nil { + return "", nil + } + var b bytes.Buffer + if err := s.Encode(o.EmbeddedClusterConfig, &b); err != nil { + return "", errors.Wrap(err, "failed to encode embedded cluster config") + } + return string(b.Bytes()), nil + } + return "", errors.Errorf("unknown gvk %s/%s, Kind=%s", g, v, k) } @@ -528,6 +547,8 @@ func (k *KotsKinds) addKotsKinds(content []byte) error { k.Installer = decoded.(*kurlv1beta1.Installer) case "app.k8s.io/v1beta1, Kind=Application": k.Application = decoded.(*applicationv1beta1.Application) + case "embeddedcluster.replicated.com/v1beta1, Kind=Config": + k.EmbeddedClusterConfig = decoded.(*embeddedclusterv1beta1.Config) } } @@ -913,6 +934,18 @@ func LoadLicenseFromBytes(data []byte) (*kotsv1beta1.License, error) { return obj.(*kotsv1beta1.License), nil } +func LoadEmbeddedClusterConfigFromBytes(data []byte) (*embeddedclusterv1beta1.Config, error) { + decode := scheme.Codecs.UniversalDeserializer().Decode + obj, gvk, err := decode([]byte(data), nil, nil) + if err != nil { + return nil, errors.Wrap(err, "failed to decode embedded cluster config data") + } + if gvk.Group != "embeddedcluster.replicated.com" || gvk.Version != "v1beta1" || gvk.Kind != "Config" { + return nil, errors.Errorf("unexpected GVK: %s", gvk.String()) + } + return obj.(*embeddedclusterv1beta1.Config), nil +} + func LoadConfigValuesFromFile(configValuesFilePath string) (*kotsv1beta1.ConfigValues, error) { configValuesData, err := ioutil.ReadFile(configValuesFilePath) if err != nil { diff --git a/pkg/operator/operator.go b/pkg/operator/operator.go index eb0b698bc0..9409ca25d2 100644 --- a/pkg/operator/operator.go +++ b/pkg/operator/operator.go @@ -18,6 +18,7 @@ import ( apptypes "github.com/replicatedhq/kots/pkg/app/types" "github.com/replicatedhq/kots/pkg/apparchive" appstatetypes "github.com/replicatedhq/kots/pkg/appstate/types" + "github.com/replicatedhq/kots/pkg/embeddedcluster" identitydeploy "github.com/replicatedhq/kots/pkg/identity/deploy" identitytypes "github.com/replicatedhq/kots/pkg/identity/types" kotsadmobjects "github.com/replicatedhq/kots/pkg/kotsadm/objects" @@ -382,6 +383,11 @@ func (o *Operator) DeployApp(appID string, sequence int64) (deployed bool, deplo return false, errors.Wrap(err, "failed to apply status informers") } + isEmbeddedCluster, err := embeddedcluster.IsEmbeddedCluster(o.k8sClientset) + if err != nil { + return false, errors.Wrap(err, "failed to check if this is an embedded cluster installation") + } + o.client.ApplyNamespacesInformer(kotsKinds.KotsApplication.Spec.AdditionalNamespaces, imagePullSecrets) o.client.ApplyHooksInformer(kotsKinds.KotsApplication.Spec.AdditionalNamespaces) @@ -408,9 +414,25 @@ func (o *Operator) DeployApp(appID string, sequence int64) (deployed bool, deplo } deployed, err = o.client.DeployApp(deployArgs) if err != nil { + if isEmbeddedCluster { + go func() { + logger.Info("app deploy failed, starting cluster upgrade in the background") + err2 := embeddedcluster.MaybeStartClusterUpgrade(context.Background(), o.k8sClientset, o.store, kotsKinds.EmbeddedClusterConfig) + if err2 != nil { + logger.Error(errors.Wrap(err2, "failed to start cluster upgrade")) + } + logger.Info("cluster upgrade started") + }() + } + return false, errors.Wrap(err, "failed to deploy app") } + err = embeddedcluster.MaybeStartClusterUpgrade(context.TODO(), o.k8sClientset, o.store, kotsKinds.EmbeddedClusterConfig) + if err != nil { + return false, errors.Wrap(err, "failed to start cluster upgrade") + } + return deployed, nil } diff --git a/pkg/store/kotsstore/embedded_cluster_store.go b/pkg/store/kotsstore/embedded_cluster_store.go index 39cc5cda5b..5f99d93313 100644 --- a/pkg/store/kotsstore/embedded_cluster_store.go +++ b/pkg/store/kotsstore/embedded_cluster_store.go @@ -3,6 +3,8 @@ package kotsstore import ( "encoding/json" "fmt" + "time" + "github.com/rqlite/gorqlite" "github.com/replicatedhq/kots/pkg/persistence" @@ -20,7 +22,7 @@ func (s *KOTSStore) SetEmbeddedClusterInstallCommandRoles(roles []string) (strin Arguments: []interface{}{installID}, }) if err != nil { - return "", fmt.Errorf("delete embedded_cluster join token: %v: %v", err, wr.Err) + return "", fmt.Errorf("delete embedded_cluster join token: %w: %v", err, wr.Err) } jsonRoles, err := json.Marshal(roles) @@ -34,7 +36,7 @@ func (s *KOTSStore) SetEmbeddedClusterInstallCommandRoles(roles []string) (strin Arguments: []interface{}{installID, string(jsonRoles)}, }) if err != nil { - return "", fmt.Errorf("insert embedded_cluster join token: %v: %v", err, wr.Err) + return "", fmt.Errorf("insert embedded_cluster join token: %w: %v", err, wr.Err) } return installID, nil @@ -48,7 +50,7 @@ func (s *KOTSStore) GetEmbeddedClusterInstallCommandRoles(token string) ([]strin Arguments: []interface{}{token}, }) if err != nil { - return nil, fmt.Errorf("failed to query: %v: %v", err, rows.Err) + return nil, fmt.Errorf("failed to query: %w: %v", err, rows.Err) } if !rows.Next() { return nil, ErrNotFound @@ -67,3 +69,40 @@ func (s *KOTSStore) GetEmbeddedClusterInstallCommandRoles(token string) ([]strin return rolesArr, nil } + +func (s *KOTSStore) SetEmbeddedClusterState(state string) error { + db := persistence.MustGetDBSession() + query := ` +insert into embedded_cluster_status (updated_at, status) +values (?, ?) +on conflict (updated_at) do update set + status = EXCLUDED.status` + wr, err := db.WriteOneParameterized(gorqlite.ParameterizedStatement{ + Query: query, + Arguments: []interface{}{time.Now().Unix(), state}, + }) + if err != nil { + return fmt.Errorf("failed to write: %w: %v", err, wr.Err) + } + return nil +} + +func (s *KOTSStore) GetEmbeddedClusterState() (string, error) { + db := persistence.MustGetDBSession() + query := `select status from embedded_cluster_status ORDER BY updated_at DESC LIMIT 1` + rows, err := db.QueryOneParameterized(gorqlite.ParameterizedStatement{ + Query: query, + Arguments: []interface{}{}, + }) + if err != nil { + return "", fmt.Errorf("failed to query: %w: %v", err, rows.Err) + } + if !rows.Next() { + return "", nil + } + var state gorqlite.NullString + if err := rows.Scan(&state); err != nil { + return "", fmt.Errorf("failed to scan: %w", err) + } + return state.String, nil +} diff --git a/pkg/store/kotsstore/version_store.go b/pkg/store/kotsstore/version_store.go index 0f1ab3612b..27ed91aa4d 100644 --- a/pkg/store/kotsstore/version_store.go +++ b/pkg/store/kotsstore/version_store.go @@ -701,6 +701,11 @@ func (s *KOTSStore) upsertAppVersionRecordStatements(appID string, sequence int6 return nil, errors.Wrap(err, "failed to marshal configvalues spec") } + embeddedClusterConfig, err := kotsKinds.Marshal("embeddedcluster.replicated.com", "v1beta1", "Config") + if err != nil { + return nil, errors.Wrap(err, "failed to marshal configvalues spec") + } + var releasedAt *int64 if kotsKinds.Installation.Spec.ReleasedAt != nil { t := kotsKinds.Installation.Spec.ReleasedAt.Time.Unix() @@ -708,8 +713,8 @@ func (s *KOTSStore) upsertAppVersionRecordStatements(appID string, sequence int6 } query := `insert into app_version (app_id, sequence, created_at, version_label, is_required, release_notes, update_cursor, channel_id, channel_name, upstream_released_at, encryption_key, - supportbundle_spec, analyzer_spec, preflight_spec, app_spec, kots_app_spec, kots_installation_spec, kots_license, config_spec, config_values, backup_spec, identity_spec, branding_archive) - values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + supportbundle_spec, analyzer_spec, preflight_spec, app_spec, kots_app_spec, kots_installation_spec, kots_license, config_spec, config_values, backup_spec, identity_spec, branding_archive, embeddedcluster_config) + values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(app_id, sequence) DO UPDATE SET created_at = EXCLUDED.created_at, version_label = EXCLUDED.version_label, @@ -731,7 +736,8 @@ func (s *KOTSStore) upsertAppVersionRecordStatements(appID string, sequence int6 config_values = EXCLUDED.config_values, backup_spec = EXCLUDED.backup_spec, identity_spec = EXCLUDED.identity_spec, - branding_archive = EXCLUDED.branding_archive` + branding_archive = EXCLUDED.branding_archive, + embeddedcluster_config = EXCLUDED.embeddedcluster_config` statements = append(statements, gorqlite.ParameterizedStatement{ Query: query, @@ -759,6 +765,7 @@ func (s *KOTSStore) upsertAppVersionRecordStatements(appID string, sequence int6 backupSpec, identitySpec, base64.StdEncoding.EncodeToString(brandingArchive), + embeddedClusterConfig, }, }) @@ -811,7 +818,7 @@ func (s *KOTSStore) upsertAppDownstreamVersionStatements(appID string, clusterID func (s *KOTSStore) GetAppVersion(appID string, sequence int64) (*versiontypes.AppVersion, error) { db := persistence.MustGetDBSession() - query := `select app_id, sequence, update_cursor, channel_id, version_label, created_at, status, applied_at, kots_installation_spec, kots_app_spec, kots_license from app_version where app_id = ? and sequence = ?` + query := `select app_id, sequence, update_cursor, channel_id, version_label, created_at, status, applied_at, kots_installation_spec, kots_app_spec, kots_license, embeddedcluster_config from app_version where app_id = ? and sequence = ?` rows, err := db.QueryOneParameterized(gorqlite.ParameterizedStatement{ Query: query, Arguments: []interface{}{appID, sequence}, @@ -1086,8 +1093,9 @@ func (s *KOTSStore) appVersionFromRow(row gorqlite.QueryResult) (*versiontypes.A var updateCursor gorqlite.NullString var channelID gorqlite.NullString var versionLabel gorqlite.NullString + var embeddedClusterConfig gorqlite.NullString - if err := row.Scan(&v.AppID, &v.Sequence, &updateCursor, &channelID, &versionLabel, &createdAt, &status, &createdAt, &installationSpec, &kotsAppSpec, &licenseSpec); err != nil { + if err := row.Scan(&v.AppID, &v.Sequence, &updateCursor, &channelID, &versionLabel, &createdAt, &status, &createdAt, &installationSpec, &kotsAppSpec, &licenseSpec, &embeddedClusterConfig); err != nil { return nil, errors.Wrap(err, "failed to scan") } @@ -1127,6 +1135,16 @@ func (s *KOTSStore) appVersionFromRow(row gorqlite.QueryResult) (*versiontypes.A } } + if embeddedClusterConfig.Valid && embeddedClusterConfig.String != "" { + config, err := kotsutil.LoadEmbeddedClusterConfigFromBytes([]byte(embeddedClusterConfig.String)) + if err != nil { + return nil, errors.Wrap(err, "failed to read embedded cluster config") + } + if config != nil { + v.KOTSKinds.EmbeddedClusterConfig = config + } + } + v.CreatedOn = createdAt.Time if deployedAt.Valid { v.DeployedAt = &deployedAt.Time diff --git a/pkg/store/mock/mock.go b/pkg/store/mock/mock.go index 7a0eacccc2..a7479f0a75 100644 --- a/pkg/store/mock/mock.go +++ b/pkg/store/mock/mock.go @@ -772,6 +772,21 @@ func (mr *MockStoreMockRecorder) GetEmbeddedClusterInstallCommandRoles(token int return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEmbeddedClusterInstallCommandRoles", reflect.TypeOf((*MockStore)(nil).GetEmbeddedClusterInstallCommandRoles), token) } +// GetEmbeddedClusterState mocks base method. +func (m *MockStore) GetEmbeddedClusterState() (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetEmbeddedClusterState") + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetEmbeddedClusterState indicates an expected call of GetEmbeddedClusterState. +func (mr *MockStoreMockRecorder) GetEmbeddedClusterState(appID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEmbeddedClusterState", reflect.TypeOf((*MockStore)(nil).GetEmbeddedClusterState), appID) +} + // GetIgnoreRBACErrors mocks base method. func (m *MockStore) GetIgnoreRBACErrors(appID string, sequence int64) (bool, error) { m.ctrl.T.Helper() @@ -1645,6 +1660,20 @@ func (mr *MockStoreMockRecorder) SetEmbeddedClusterInstallCommandRoles(roles int return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetEmbeddedClusterInstallCommandRoles", reflect.TypeOf((*MockStore)(nil).SetEmbeddedClusterInstallCommandRoles), roles) } +// SetEmbeddedClusterState mocks base method. +func (m *MockStore) SetEmbeddedClusterState(state string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetEmbeddedClusterState", state) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetEmbeddedClusterState indicates an expected call of SetEmbeddedClusterState. +func (mr *MockStoreMockRecorder) SetEmbeddedClusterState(state interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetEmbeddedClusterState", reflect.TypeOf((*MockStore)(nil).SetEmbeddedClusterState), state) +} + // SetIgnorePreflightPermissionErrors mocks base method. func (m *MockStore) SetIgnorePreflightPermissionErrors(appID string, sequence int64) error { m.ctrl.T.Helper() @@ -4286,6 +4315,21 @@ func (mr *MockEmbeddedStoreMockRecorder) GetEmbeddedClusterAuthToken() *gomock.C return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEmbeddedClusterAuthToken", reflect.TypeOf((*MockEmbeddedStore)(nil).GetEmbeddedClusterAuthToken)) } +// GetEmbeddedClusterState mocks base method. +func (m *MockEmbeddedStore) GetEmbeddedClusterState(appID string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetEmbeddedClusterState", appID) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetEmbeddedClusterState indicates an expected call of GetEmbeddedClusterState. +func (mr *MockEmbeddedStoreMockRecorder) GetEmbeddedClusterState(appID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEmbeddedClusterState", reflect.TypeOf((*MockEmbeddedStore)(nil).GetEmbeddedClusterState), appID) +} + // SetEmbeddedClusterAuthToken mocks base method. func (m *MockEmbeddedStore) SetEmbeddedClusterAuthToken(token string) error { m.ctrl.T.Helper() @@ -4300,6 +4344,20 @@ func (mr *MockEmbeddedStoreMockRecorder) SetEmbeddedClusterAuthToken(token inter return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetEmbeddedClusterAuthToken", reflect.TypeOf((*MockEmbeddedStore)(nil).SetEmbeddedClusterAuthToken), token) } +// SetEmbeddedClusterState mocks base method. +func (m *MockEmbeddedStore) SetEmbeddedClusterState(state string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetEmbeddedClusterState", state) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetEmbeddedClusterState indicates an expected call of SetEmbeddedClusterState. +func (mr *MockEmbeddedStoreMockRecorder) SetEmbeddedClusterState(state interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetEmbeddedClusterState", reflect.TypeOf((*MockEmbeddedStore)(nil).SetEmbeddedClusterState), state) +} + // MockBrandingStore is a mock of BrandingStore interface. type MockBrandingStore struct { ctrl *gomock.Controller diff --git a/pkg/store/store_interface.go b/pkg/store/store_interface.go index 5038c1b6ad..529cb98b47 100644 --- a/pkg/store/store_interface.go +++ b/pkg/store/store_interface.go @@ -239,6 +239,8 @@ type KotsadmParamsStore interface { type EmbeddedStore interface { GetEmbeddedClusterAuthToken() (string, error) SetEmbeddedClusterAuthToken(token string) error + SetEmbeddedClusterState(state string) error + GetEmbeddedClusterState() (string, error) } type BrandingStore interface { diff --git a/web/src/features/Dashboard/components/AppStatus.tsx b/web/src/features/Dashboard/components/AppStatus.tsx index 03cc616495..db38a4f1e1 100644 --- a/web/src/features/Dashboard/components/AppStatus.tsx +++ b/web/src/features/Dashboard/components/AppStatus.tsx @@ -23,6 +23,7 @@ type Props = { links: PropLink[]; onViewAppStatusDetails: () => void; url: string | undefined; + embeddedClusterState: string; }; type State = { @@ -79,7 +80,7 @@ export default class AppStatus extends Component { }; render() { - const { appStatus, url, links, app } = this.props; + const { appStatus, url, links, app, embeddedClusterState } = this.props; const { dropdownOptions } = this.state; const defaultDisplayText = dropdownOptions.length > 0 ? dropdownOptions[0].displayText : ""; @@ -108,6 +109,35 @@ export default class AppStatus extends Component { > {Utilities.toTitleCase(appStatus)} + {!isEmpty(embeddedClusterState) && ( + <> + + Cluster State: + + + + {Utilities.clusterState(embeddedClusterState)} + + + )} {this.props.hasStatusInformers && ( { appStatus: null, metrics: [], prometheusAddress: "", + embeddedClusterState: "", }, currentVersion: null, displayErrorModal: false, @@ -526,6 +527,8 @@ const Dashboard = () => { setState({ dashboard: { appStatus: selectedAppClusterDashboardResponse.appStatus, + embeddedClusterState: + selectedAppClusterDashboardResponse.embeddedClusterState, prometheusAddress: selectedAppClusterDashboardResponse.prometheusAddress, metrics: selectedAppClusterDashboardResponse.metrics, @@ -658,6 +661,7 @@ const Dashboard = () => { links={links} app={app} hasStatusInformers={hasStatusInformers} + embeddedClusterState={state.dashboard.embeddedClusterState} /> diff --git a/web/src/types/index.ts b/web/src/types/index.ts index ab0b6912f9..50fcb26f4e 100644 --- a/web/src/types/index.ts +++ b/web/src/types/index.ts @@ -87,6 +87,7 @@ export type DashboardResponse = { appStatus: AppStatus | null; metrics: Chart[]; prometheusAddress: string; + embeddedClusterState: string; }; export type Downstream = { diff --git a/web/src/utilities/utilities.js b/web/src/utilities/utilities.js index 7c44e5a2aa..7ac80cb467 100644 --- a/web/src/utilities/utilities.js +++ b/web/src/utilities/utilities.js @@ -611,6 +611,25 @@ export const Utilities = { } }, + clusterState(state) { + switch (state) { + case "Waiting": + return "Waiting for a previous upgrade"; + case "Enqueued": + return "Upgrading"; + case "Installing": + return "Upgrading"; + case "Installed": + return "Up to date"; + case "Obsolete": + return "No active cluster upgrade found"; + case "Failed": + return "Failed"; + default: + return "Unknown"; + } + }, + // Converts string to titlecase i.e. 'hello' -> 'Hello' // @returns {String} toTitleCase(word) {