diff --git a/cmd/kots/cli/install.go b/cmd/kots/cli/install.go index 73d26ca746..84170c7dbc 100644 --- a/cmd/kots/cli/install.go +++ b/cmd/kots/cli/install.go @@ -6,7 +6,6 @@ import ( "encoding/json" "fmt" "io" - "io/ioutil" "mime/multipart" "net/http" "net/url" @@ -33,6 +32,7 @@ import ( "github.com/replicatedhq/kots/pkg/kurl" "github.com/replicatedhq/kots/pkg/logger" "github.com/replicatedhq/kots/pkg/metrics" + "github.com/replicatedhq/kots/pkg/print" "github.com/replicatedhq/kots/pkg/pull" "github.com/replicatedhq/kots/pkg/replicatedapp" "github.com/replicatedhq/kots/pkg/store/kotsstore" @@ -47,6 +47,10 @@ import ( "k8s.io/client-go/kubernetes" ) +var client = &http.Client{ + Timeout: time.Second * 30, +} + func InstallCmd() *cobra.Command { cmd := &cobra.Command{ Use: "install [upstream uri]", @@ -321,7 +325,7 @@ func InstallCmd() *cobra.Command { log.ActionWithoutSpinner("Extracting airgap bundle") - airgapRootDir, err := ioutil.TempDir("", "kotsadm-airgap") + airgapRootDir, err := os.MkdirTemp("", "kotsadm-airgap") if err != nil { return errors.Wrap(err, "failed to create temp dir") } @@ -426,6 +430,7 @@ func InstallCmd() *cobra.Command { case err := <-errChan: if err != nil { log.Error(err) + // TODO: Why is this a negative exit code? os.Exit(-1) } case <-stopCh: @@ -445,8 +450,16 @@ func InstallCmd() *cobra.Command { case storetypes.VersionPendingPreflight: log.ActionWithSpinner("Waiting for preflight checks to complete") if err := ValidatePreflightStatus(deployOptions, authSlug, apiEndpoint); err != nil { - log.FinishSpinnerWithError() - return errors.Wrap(err, "failed to validate preflight results") + perr := preflightError{} + if errors.As(err, &perr) { + log.FinishSpinner() // We succeeded waiting for the results. Don't finish with an error + log.Errorf(perr.Msg) + print.PreflightErrors(log, perr.Results) + cmd.SilenceErrors = true // Stop Cobra from printing the error, we format the message ourselves + } else { + log.FinishSpinnerWithError() + } + return err } log.FinishSpinner() case storetypes.VersionPendingConfig: @@ -692,7 +705,7 @@ func getIngressConfig(v *viper.Viper) (*kotsv1beta1.IngressConfig, error) { ingressConfig := kotsv1beta1.IngressConfig{} if ingressConfigPath != "" { - content, err := ioutil.ReadFile(ingressConfigPath) + content, err := os.ReadFile(ingressConfigPath) if err != nil { return nil, errors.Wrap(err, "failed to read ingress service config file") } @@ -719,7 +732,7 @@ func getIdentityConfig(v *viper.Viper) (*kotsv1beta1.IdentityConfig, error) { identityConfig := kotsv1beta1.IdentityConfig{} if identityConfigPath != "" { - content, err := ioutil.ReadFile(identityConfigPath) + content, err := os.ReadFile(identityConfigPath) if err != nil { return nil, errors.Wrap(err, "failed to read identity service config file") } @@ -910,7 +923,7 @@ func getAutomatedInstallStatus(url string, authSlug string) (*kotsstore.TaskStat return nil, errors.Errorf("unexpected status code %d", resp.StatusCode) } - b, err := ioutil.ReadAll(resp.Body) + b, err := io.ReadAll(resp.Body) if err != nil { return nil, errors.Wrap(err, "failed to read response body") } @@ -964,7 +977,7 @@ func getPreflightResponse(url string, authSlug string) (*handlers.GetPreflightRe newReq.Header.Add("Content-Type", "application/json") newReq.Header.Add("Authorization", authSlug) - resp, err := http.DefaultClient.Do(newReq) + resp, err := client.Do(newReq) if err != nil { return nil, errors.Wrap(err, "failed to execute request") } @@ -974,7 +987,7 @@ func getPreflightResponse(url string, authSlug string) (*handlers.GetPreflightRe return nil, errors.Errorf("unexpected status code %d", resp.StatusCode) } - b, err := ioutil.ReadAll(resp.Body) + b, err := io.ReadAll(resp.Body) if err != nil { return nil, errors.Wrap(err, "failed to read response body") } @@ -1005,6 +1018,17 @@ func checkPreflightsComplete(response *handlers.GetPreflightResultResponse) (boo return true, nil } +type preflightError struct { + Msg string + Results []*preflight.UploadPreflightResult +} + +func (e preflightError) Error() string { + return e.Msg +} + +func (e preflightError) Unwrap() error { return fmt.Errorf(e.Msg) } + func checkPreflightResults(response *handlers.GetPreflightResultResponse) (bool, error) { if response.PreflightResult.Result == "" { return false, nil @@ -1027,14 +1051,23 @@ func checkPreflightResults(response *handlers.GetPreflightResultResponse) (bool, } if isWarn && isFail { - return false, errors.New("There are preflight check failures and warnings for the application. The app was not deployed.") + return false, preflightError{ + Msg: "There are preflight check failures and warnings for the application. The app was not deployed.", + Results: results.Results, + } } if isWarn { - return false, errors.New("There are preflight check warnings for the application. The app was not deployed.") + return false, preflightError{ + Msg: "There are preflight check warnings for the application. The app was not deployed.", + Results: results.Results, + } } if isFail { - return false, errors.New("There are preflight check failures for the application. The app was not deployed.") + return false, preflightError{ + Msg: "There are preflight check failures for the application. The app was not deployed.", + Results: results.Results, + } } return true, nil diff --git a/cmd/kots/cli/install_test.go b/cmd/kots/cli/install_test.go index e44e43a1e5..7ff8e06adc 100644 --- a/cmd/kots/cli/install_test.go +++ b/cmd/kots/cli/install_test.go @@ -223,7 +223,7 @@ var _ = Describe("Install", func() { }) It("returns an error if the preflight collection results cannot be parsed", func() { - invalidPreflightCollection, err := json.Marshal(handlers.GetPreflightResultResponse{ + invalidPreflightCollection, _ := json.Marshal(handlers.GetPreflightResultResponse{ PreflightProgress: `{invalid: json}`, PreflightResult: preflighttypes.PreflightResult{}, }) @@ -239,13 +239,13 @@ var _ = Describe("Install", func() { ), ) - err = ValidatePreflightStatus(validDeployOptions, authSlug, server.URL()) + err := ValidatePreflightStatus(validDeployOptions, authSlug, server.URL()) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("failed to unmarshal collect progress for preflights")) }) It("returns an error if the upload preflight results are invalid", func() { - invalidUploadPreflightResponse, err := json.Marshal(handlers.GetPreflightResultResponse{ + invalidUploadPreflightResponse, _ := json.Marshal(handlers.GetPreflightResultResponse{ PreflightProgress: "", PreflightResult: preflighttypes.PreflightResult{ Result: "{invalid: json}", @@ -262,7 +262,7 @@ var _ = Describe("Install", func() { ), ) - err = ValidatePreflightStatus(validDeployOptions, authSlug, server.URL()) + err := ValidatePreflightStatus(validDeployOptions, authSlug, server.URL()) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("failed to unmarshal upload preflight results")) }) diff --git a/cmd/kots/cli/set-config.go b/cmd/kots/cli/set-config.go index c07b7c738c..800d7931bd 100644 --- a/cmd/kots/cli/set-config.go +++ b/cmd/kots/cli/set-config.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" "fmt" + "io" "io/ioutil" "net/http" "net/url" @@ -127,7 +128,7 @@ func SetConfigCmd() *cobra.Command { } defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { log.FinishSpinnerWithError() return errors.Wrap(err, "failed to read server response") diff --git a/go.mod b/go.mod index 8f421fca21..ab58fe1255 100644 --- a/go.mod +++ b/go.mod @@ -47,9 +47,9 @@ require ( github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 github.com/pkg/errors v0.9.1 github.com/pmezard/go-difflib v1.0.0 - github.com/replicatedhq/kotskinds v0.0.0-20230724164735-f83482cc9cfe + github.com/replicatedhq/kotskinds v0.0.0-20231004174055-e6676d808a82 github.com/replicatedhq/kurlkinds v1.3.6 - github.com/replicatedhq/troubleshoot v0.72.1 + github.com/replicatedhq/troubleshoot v0.74.0 github.com/replicatedhq/yaml/v3 v3.0.0-beta5-replicatedhq github.com/robfig/cron v1.2.0 github.com/robfig/cron/v3 v3.0.1 @@ -74,16 +74,16 @@ require ( gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 helm.sh/helm/v3 v3.12.3 - k8s.io/api v0.28.1 - k8s.io/apimachinery v0.28.1 - k8s.io/cli-runtime v0.28.1 - k8s.io/client-go v0.28.1 + k8s.io/api v0.28.2 + k8s.io/apimachinery v0.28.2 + k8s.io/cli-runtime v0.28.2 + k8s.io/client-go v0.28.2 k8s.io/cluster-bootstrap v0.23.6 k8s.io/helm v2.14.3+incompatible k8s.io/kubelet v0.23.6 k8s.io/utils v0.0.0-20230505201702-9f6742963106 sigs.k8s.io/application v0.8.3 - sigs.k8s.io/controller-runtime v0.16.1 + sigs.k8s.io/controller-runtime v0.16.2 sigs.k8s.io/kustomize/api v0.14.0 sigs.k8s.io/kustomize/kyaml v0.14.3 sigs.k8s.io/yaml v1.3.0 @@ -320,7 +320,7 @@ require ( github.com/rubenv/sql-migrate v1.3.1 // indirect github.com/russellhaering/goxmldsig v1.3.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/shirou/gopsutil/v3 v3.23.8 // indirect + github.com/shirou/gopsutil/v3 v3.23.9 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/sigstore/fulcio v1.2.0 // indirect github.com/sigstore/rekor v1.2.0 // indirect @@ -355,15 +355,15 @@ require ( go.mongodb.org/mongo-driver v1.11.3 // indirect go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect go.opencensus.io v0.24.0 // indirect - go.opentelemetry.io/otel v1.17.0 // indirect - go.opentelemetry.io/otel/metric v1.17.0 // indirect - go.opentelemetry.io/otel/sdk v1.17.0 // indirect - go.opentelemetry.io/otel/trace v1.17.0 // indirect + go.opentelemetry.io/otel v1.18.0 // indirect + go.opentelemetry.io/otel/metric v1.18.0 // indirect + go.opentelemetry.io/otel/sdk v1.18.0 // indirect + go.opentelemetry.io/otel/trace v1.18.0 // indirect go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect golang.org/x/exp v0.0.0-20230321023759-10a507213a29 // indirect golang.org/x/mod v0.10.0 // indirect golang.org/x/net v0.14.0 // indirect - golang.org/x/sys v0.11.0 // indirect + golang.org/x/sys v0.12.0 // indirect golang.org/x/term v0.11.0 // indirect golang.org/x/text v0.13.0 // indirect golang.org/x/time v0.3.0 // indirect @@ -387,7 +387,7 @@ require ( k8s.io/kube-aggregator v0.19.12 // indirect k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect k8s.io/kubectl v0.28.1 // indirect - k8s.io/metrics v0.28.1 // indirect + k8s.io/metrics v0.28.2 // indirect oras.land/oras-go v1.2.3 // indirect periph.io/x/host/v3 v3.8.2 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect diff --git a/go.sum b/go.sum index 85f0a2a2de..ae32d56b3f 100644 --- a/go.sum +++ b/go.sum @@ -1525,14 +1525,14 @@ 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/kotskinds v0.0.0-20230724164735-f83482cc9cfe h1:3AJInd06UxzqHmgy8+24CPsT2tYSE0zToJZyuX9q+MA= -github.com/replicatedhq/kotskinds v0.0.0-20230724164735-f83482cc9cfe/go.mod h1:QjhIUu3+OmHZ09u09j3FCoTt8F3BYtQglS+OLmftu9I= +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= github.com/replicatedhq/kurlkinds v1.3.6/go.mod h1:c5+hoAkuARgftB2Ft3RCyWRZZPhL0clHEaw7XoGDAg4= github.com/replicatedhq/termui/v3 v3.1.1-0.20200811145416-f40076d26851 h1:eRlNDHxGfVkPCRXbA4BfQJvt5DHjFiTtWy3R/t4djyY= github.com/replicatedhq/termui/v3 v3.1.1-0.20200811145416-f40076d26851/go.mod h1:JDxG6+uubnk9/BZ2yUsyAJJwlptjrnmB2MPF5d2Xe/8= -github.com/replicatedhq/troubleshoot v0.72.1 h1:PA1kOU6N1PFbxFkizdLEP/FbvhmV1plrHgG2JoxAZw4= -github.com/replicatedhq/troubleshoot v0.72.1/go.mod h1:5lp7iXnOUaKY4EPemnwrLiu3HTQhq070epZCJLxoPiE= +github.com/replicatedhq/troubleshoot v0.74.0 h1:K/QQjLmJ7sAUoy9u7ozfjFQsWrI0L/tz2KAhhyn7KPY= +github.com/replicatedhq/troubleshoot v0.74.0/go.mod h1:9SqMtBOtkSuc4Jgeqt87Sook4DumKn8n1vkMdN7eiK8= github.com/replicatedhq/yaml/v3 v3.0.0-beta5-replicatedhq h1:PwPggruelq2336c1Ayg5STFqgbn/QB1tWLQwrVlU7ZQ= github.com/replicatedhq/yaml/v3 v3.0.0-beta5-replicatedhq/go.mod h1:Txa7LopbYCU8aRgmNe0n+y/EPMz50NbCPcVVJBquwag= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -1580,8 +1580,8 @@ github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shirou/gopsutil v0.0.0-20180427012116-c95755e4bcd7/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= -github.com/shirou/gopsutil/v3 v3.23.8 h1:xnATPiybo6GgdRoC4YoGnxXZFRc3dqQTGi73oLvvBrE= -github.com/shirou/gopsutil/v3 v3.23.8/go.mod h1:7hmCaBn+2ZwaZOr6jmPBZDfawwMGuo1id3C6aM8EDqQ= +github.com/shirou/gopsutil/v3 v3.23.9 h1:ZI5bWVeu2ep4/DIxB4U9okeYJ7zp/QLTO4auRb/ty/E= +github.com/shirou/gopsutil/v3 v3.23.9/go.mod h1:x/NWSb71eMcjFIO0vhyGW5nZ7oSIgVjrCnADckb85GA= github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= @@ -1815,21 +1815,21 @@ go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/contrib v0.20.0/go.mod h1:G/EtFaa6qaN7+LxqfIAT3GiZa7Wv5DTBUzl5H4LY0Kc= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.20.0/go.mod h1:2AboqHi0CiIZU0qwhtUfCYD1GeUzvvIXWNkhDt7ZMG4= go.opentelemetry.io/otel v0.20.0/go.mod h1:Y3ugLH2oa81t5QO+Lty+zXf8zC9L26ax4Nzoxm/dooo= -go.opentelemetry.io/otel v1.17.0 h1:MW+phZ6WZ5/uk2nd93ANk/6yJ+dVrvNWUjGhnnFU5jM= -go.opentelemetry.io/otel v1.17.0/go.mod h1:I2vmBGtFaODIVMBSTPVDlJSzBDNf93k60E6Ft0nyjo0= +go.opentelemetry.io/otel v1.18.0 h1:TgVozPGZ01nHyDZxK5WGPFB9QexeTMXEH7+tIClWfzs= +go.opentelemetry.io/otel v1.18.0/go.mod h1:9lWqYO0Db579XzVuCKFNPDl4s73Voa+zEck3wHaAYQI= go.opentelemetry.io/otel/exporters/otlp v0.20.0/go.mod h1:YIieizyaN77rtLJra0buKiNBOm9XQfkPEKBeuhoMwAM= go.opentelemetry.io/otel/metric v0.20.0/go.mod h1:598I5tYlH1vzBjn+BTuhzTCSb/9debfNp6R3s7Pr1eU= -go.opentelemetry.io/otel/metric v1.17.0 h1:iG6LGVz5Gh+IuO0jmgvpTB6YVrCGngi8QGm+pMd8Pdc= -go.opentelemetry.io/otel/metric v1.17.0/go.mod h1:h4skoxdZI17AxwITdmdZjjYJQH5nzijUUjm+wtPph5o= +go.opentelemetry.io/otel/metric v1.18.0 h1:JwVzw94UYmbx3ej++CwLUQZxEODDj/pOuTCvzhtRrSQ= +go.opentelemetry.io/otel/metric v1.18.0/go.mod h1:nNSpsVDjWGfb7chbRLUNW+PBNdcSTHD4Uu5pfFMOI0k= go.opentelemetry.io/otel/oteltest v0.20.0/go.mod h1:L7bgKf9ZB7qCwT9Up7i9/pn0PWIa9FqQ2IQ8LoxiGnw= go.opentelemetry.io/otel/sdk v0.20.0/go.mod h1:g/IcepuwNsoiX5Byy2nNV0ySUF1em498m7hBWC279Yc= -go.opentelemetry.io/otel/sdk v1.17.0 h1:FLN2X66Ke/k5Sg3V623Q7h7nt3cHXaW1FOvKKrW0IpE= -go.opentelemetry.io/otel/sdk v1.17.0/go.mod h1:U87sE0f5vQB7hwUoW98pW5Rz4ZDuCFBZFNUBlSgmDFQ= +go.opentelemetry.io/otel/sdk v1.18.0 h1:e3bAB0wB3MljH38sHzpV/qWrOTCFrdZF2ct9F8rBkcY= +go.opentelemetry.io/otel/sdk v1.18.0/go.mod h1:1RCygWV7plY2KmdskZEDDBs4tJeHG92MdHZIluiYs/M= go.opentelemetry.io/otel/sdk/export/metric v0.20.0/go.mod h1:h7RBNMsDJ5pmI1zExLi+bJK+Dr8NQCh0qGhm1KDnNlE= go.opentelemetry.io/otel/sdk/metric v0.20.0/go.mod h1:knxiS8Xd4E/N+ZqKmUPf3gTTZ4/0TjTXukfxjzSTpHE= go.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16gUEi0Nf1iBdgw= -go.opentelemetry.io/otel/trace v1.17.0 h1:/SWhSRHmDPOImIAetP1QAeMnZYiQXrTy4fMMYOdSKWQ= -go.opentelemetry.io/otel/trace v1.17.0/go.mod h1:I/4vKTgFclIsXRVucpH25X0mpFSczM7aHeaz0ZBLWjY= +go.opentelemetry.io/otel/trace v1.18.0 h1:NY+czwbHbmndxojTEKiSMHkG2ClNH2PwmcHrdo0JY10= +go.opentelemetry.io/otel/trace v1.18.0/go.mod h1:T2+SGJGuYZY3bjj5rgh/hN7KIrlpWC5nS8Mjvzckz+0= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.starlark.net v0.0.0-20230525235612-a134d8f9ddca h1:VdD38733bfYv5tUZwEIskMM93VanwNIi5bIKnDrJdEY= go.starlark.net v0.0.0-20230525235612-a134d8f9ddca/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds= @@ -2203,8 +2203,9 @@ golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -2661,8 +2662,8 @@ k8s.io/api v0.17.0/go.mod h1:npsyOePkeP0CPwyGfXDHxvypiYMJxBWAMpQxCaJ4ZxI= k8s.io/api v0.19.0/go.mod h1:I1K45XlvTrDjmj5LoM5LuP/KYrhWbjUKT/SoPG0qTjw= k8s.io/api v0.19.12/go.mod h1:EK+KvSq2urA6+CjVdZyAHEphXoLq2K2eW6lxOzTKSaY= k8s.io/api v0.23.6/go.mod h1:1kFaYxGCFHYp3qd6a85DAj/yW8aVD6XLZMqJclkoi9g= -k8s.io/api v0.28.1 h1:i+0O8k2NPBCPYaMB+uCkseEbawEt/eFaiRqUx8aB108= -k8s.io/api v0.28.1/go.mod h1:uBYwID+66wiL28Kn2tBjBYQdEU0Xk0z5qF8bIBqk/Dg= +k8s.io/api v0.28.2 h1:9mpl5mOb6vXZvqbQmankOfPIGiudghwCoLl1EYfUZbw= +k8s.io/api v0.28.2/go.mod h1:RVnJBsjU8tcMq7C3iaRSGMeaKt2TWEUXcpIt/90fjEg= 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.1 h1:l2ThkBRjrWpw4f24uq0Da2HaEgqJZ7pcgiEUTKSmQZw= @@ -2673,23 +2674,23 @@ k8s.io/apimachinery v0.17.0/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZ k8s.io/apimachinery v0.19.0/go.mod h1:DnPGDnARWFvYa3pMHgSxtbZb7gpzzAZ1pTfaUNDVlmA= k8s.io/apimachinery v0.19.12/go.mod h1:9eb44nUQSsz9QZiilFRuMj3ZbTmoWolU8S2gnXoRMjo= k8s.io/apimachinery v0.23.6/go.mod h1:BEuFMMBaIbcOqVIJqNZJXGFTP4W6AycEpb5+m/97hrM= -k8s.io/apimachinery v0.28.1 h1:EJD40og3GizBSV3mkIoXQBsws32okPOy+MkRyzh6nPY= -k8s.io/apimachinery v0.28.1/go.mod h1:X0xh/chESs2hP9koe+SdIAcXWcQ+RM5hy0ZynB+yEvw= +k8s.io/apimachinery v0.28.2 h1:KCOJLrc6gu+wV1BYgwik4AF4vXOlVJPdiqn0yAWWwXQ= +k8s.io/apimachinery v0.28.2/go.mod h1:RdzF87y/ngqk9H4z3EL2Rppv5jj95vGS/HaFXrLDApU= 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.1 h1:dw2/NKauDZCnOUAzIo2hFhtBRUo6gQK832NV8kuDbGM= k8s.io/apiserver v0.28.1/go.mod h1:d8aizlSRB6yRgJ6PKfDkdwCy2DXt/d1FDR6iJN9kY1w= -k8s.io/cli-runtime v0.28.1 h1:7Njc4eD5kaO4tYdSYVJJEs54koYD/vT6gxOq8dEVf9g= -k8s.io/cli-runtime v0.28.1/go.mod h1:yIThSWkAVLqeRs74CMkq6lNFW42GyJmvMtcNn01SZho= +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= k8s.io/client-go v0.16.8/go.mod h1:WmPuN0yJTKHXoklExKxzo3jSXmr3EnN+65uaTb5VuNs= k8s.io/client-go v0.17.0/go.mod h1:TYgR6EUHs6k45hb6KWjVD6jFZvJV4gHDikv/It0xz+k= k8s.io/client-go v0.19.0/go.mod h1:H9E/VT95blcFQnlyShFgnFT9ZnJOAceiUHM3MlRC+mU= k8s.io/client-go v0.19.12/go.mod h1:BAGKQraZ6fDmXhT46pGXWZQQqN7P4E0BJux0+9O6Gt0= k8s.io/client-go v0.23.6/go.mod h1:Umt5icFOMLV/+qbtZ3PR0D+JA6lvvb3syzodv4irpK4= -k8s.io/client-go v0.28.1 h1:pRhMzB8HyLfVwpngWKE8hDcXRqifh1ga2Z/PU9SXVK8= -k8s.io/client-go v0.28.1/go.mod h1:pEZA3FqOsVkCc07pFVzK076R+P/eXqsgx5zuuRWukNE= +k8s.io/client-go v0.28.2 h1:DNoYI1vGq0slMBN/SWKMZMw0Rq+0EQW6/AK4v9+3VeY= +k8s.io/client-go v0.28.2/go.mod h1:sMkApowspLuc7omj1FOSUxSoqjr+d5Q0Yc0LOFnYFJY= k8s.io/cluster-bootstrap v0.23.6 h1:ynacd9d9qq8nvmAsWmIQ5HXnOMKHI4WgbQB37FKnUsA= k8s.io/cluster-bootstrap v0.23.6/go.mod h1:JbFrtiOjW1YDujFuUzPbuOSCnSUIkXUjEeci5BxB8ug= k8s.io/code-generator v0.0.0-20190912054826-cd179ad6a269/go.mod h1:V5BD6M4CyaN5m+VthcclXWsVcT1Hu+glwa1bi3MIsyE= @@ -2731,8 +2732,8 @@ k8s.io/kubectl v0.28.1 h1:jAq4yKEqQL+fwkWcEsUWxhJ7uIRcOYQraJxx4SyAMTY= k8s.io/kubectl v0.28.1/go.mod h1:a0nk/lMMeKBulp0lMTJAKbkjZg1ykqfLfz/d6dnv1ak= k8s.io/kubelet v0.23.6 h1:tuscMqYCt9cxWursmTU9OJ2tPLv66Ji+AGbuV1Z/lug= k8s.io/kubelet v0.23.6/go.mod h1:ROttmKIUkB9in4NyX/SfnAoXGfW/Dju3VCGFP34F5ac= -k8s.io/metrics v0.28.1 h1:Q0AsAEZKlAzhqrvfoGyHjz2qAFlef0SqfGJ1YWJ+ITU= -k8s.io/metrics v0.28.1/go.mod h1:8lKkAajigcZWu0o9XCEBr++YVCzT48q1ck+f9CEBhZY= +k8s.io/metrics v0.28.2 h1:Z/oMk5SmiT/Ji1SaWOPfW2l9W831BLO9/XxDq9iS3ak= +k8s.io/metrics v0.28.2/go.mod h1:QTIIdjMrq+KodO+rmp6R9Pr1LZO8kTArNtkWoQXw0sw= k8s.io/utils v0.0.0-20190801114015-581e00157fb1/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= k8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= k8s.io/utils v0.0.0-20200729134348-d5654de09c73/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= @@ -2759,8 +2760,8 @@ sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.15/go.mod h1:LEScyz sigs.k8s.io/application v0.8.3 h1:5UETobiVhxTkKn3pIESImXiMNmSg3VkM5+JvmYGDPko= sigs.k8s.io/application v0.8.3/go.mod h1:Mv+ht9RE/QNtITYCzRbt3XTIN6t6so6cInmiyg6wOIg= sigs.k8s.io/controller-runtime v0.4.0/go.mod h1:ApC79lpY3PHW9xj/w9pj+lYkLgwAAUZwfXkME1Lajns= -sigs.k8s.io/controller-runtime v0.16.1 h1:+15lzrmHsE0s2kNl0Dl8cTchI5Cs8qofo5PGcPrV9z0= -sigs.k8s.io/controller-runtime v0.16.1/go.mod h1:vpMu3LpI5sYWtujJOa2uPK61nB5rbwlN7BAB8aSLvGU= +sigs.k8s.io/controller-runtime v0.16.2 h1:mwXAVuEk3EQf478PQwQ48zGOXvW27UJc8NHktQVuIPU= +sigs.k8s.io/controller-runtime v0.16.2/go.mod h1:vpMu3LpI5sYWtujJOa2uPK61nB5rbwlN7BAB8aSLvGU= sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6/go.mod h1:p4QtZmO4uMYipTQNzagwnNoseA6OxSUutVw05NhYDRs= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= diff --git a/pkg/airgap/airgap.go b/pkg/airgap/airgap.go index 80dee5a4ff..083baa5217 100644 --- a/pkg/airgap/airgap.go +++ b/pkg/airgap/airgap.go @@ -227,6 +227,7 @@ func CreateAppFromAirgap(opts CreateAirgapAppOpts) (finalError error) { Password: opts.RegistryPassword, IsReadOnly: opts.RegistryIsReadOnly, }, + AppID: opts.PendingApp.ID, AppSlug: opts.PendingApp.Slug, AppSequence: 0, AppVersionLabel: instParams.AppVersionLabel, diff --git a/pkg/airgap/update.go b/pkg/airgap/update.go index f9e1b80e2a..945dd9cac7 100644 --- a/pkg/airgap/update.go +++ b/pkg/airgap/update.go @@ -173,6 +173,7 @@ func UpdateAppFromPath(a *apptypes.App, airgapRoot string, airgapBundlePath stri Silent: true, RewriteImages: true, RewriteImageOptions: registrySettings, + AppID: a.ID, AppSlug: a.Slug, AppSequence: appSequence, SkipCompatibilityCheck: skipCompatibilityCheck, diff --git a/pkg/apiserver/server.go b/pkg/apiserver/server.go index 8938f0986f..acea21be6c 100644 --- a/pkg/apiserver/server.go +++ b/pkg/apiserver/server.go @@ -187,6 +187,12 @@ func Start(params *APIServerParams) { handlers.RegisterTokenAuthRoutes(handler, debugRouter, loggingRouter) + /********************************************************************** + * License ID auth routes + **********************************************************************/ + + handlers.RegisterLicenseIDAuthRoutes(r.PathPrefix("").Subrouter(), kotsStore, handler) + /********************************************************************** * Session auth routes **********************************************************************/ diff --git a/pkg/base/replicated.go b/pkg/base/replicated.go index 580cf1dac2..3e194ccda9 100644 --- a/pkg/base/replicated.go +++ b/pkg/base/replicated.go @@ -23,6 +23,7 @@ import ( kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" kotsv1beta2 "github.com/replicatedhq/kotskinds/apis/kots/v1beta2" kotsscheme "github.com/replicatedhq/kotskinds/client/kotsclientset/scheme" + "github.com/replicatedhq/kotskinds/pkg/helmchart" troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" troubleshootscheme "github.com/replicatedhq/troubleshoot/pkg/client/troubleshootclientset/scheme" velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" @@ -502,7 +503,7 @@ func getKotsKinds(u *upstreamtypes.Upstream) (*kotsutil.KotsKinds, error) { // FindHelmChartArchiveInRelease iterates through all files in the release (upstreamFiles), looking for a helm chart archive // that matches the chart name and version specified in the kotsHelmChart parameter -func FindHelmChartArchiveInRelease(upstreamFiles []upstreamtypes.UpstreamFile, kotsHelmChart kotsutil.HelmChartInterface) ([]byte, error) { +func FindHelmChartArchiveInRelease(upstreamFiles []upstreamtypes.UpstreamFile, kotsHelmChart helmchart.HelmChartInterface) ([]byte, error) { for _, upstreamFile := range upstreamFiles { if !isHelmChart(upstreamFile.Content) { continue diff --git a/pkg/handlers/handlers.go b/pkg/handlers/handlers.go index 11b2c708c2..aa711b7b89 100644 --- a/pkg/handlers/handlers.go +++ b/pkg/handlers/handlers.go @@ -259,22 +259,35 @@ func RegisterSessionAuthRoutes(r *mux.Router, kotsStore store.Store, handler KOT HandlerFunc(middleware.EnforceAccess(policy.BackupRead, handler.GetVeleroStatus)) // KURL - r.Name("Kurl").Path("/api/v1/kurl").HandlerFunc(NotImplemented) // I'm not sure why this is here - r.Name("GenerateNodeJoinCommandWorker").Path("/api/v1/kurl/generate-node-join-command-worker").Methods("POST"). - HandlerFunc(middleware.EnforceAccess(policy.ClusterWrite, handler.GenerateNodeJoinCommandWorker)) - r.Name("GenerateNodeJoinCommandMaster").Path("/api/v1/kurl/generate-node-join-command-master").Methods("POST"). - HandlerFunc(middleware.EnforceAccess(policy.ClusterWrite, handler.GenerateNodeJoinCommandMaster)) - r.Name("GenerateNodeJoinCommandSecondary").Path("/api/v1/kurl/generate-node-join-command-secondary").Methods("POST"). - HandlerFunc(middleware.EnforceAccess(policy.ClusterWrite, handler.GenerateNodeJoinCommandSecondary)) - r.Name("GenerateNodeJoinCommandPrimary").Path("/api/v1/kurl/generate-node-join-command-primary").Methods("POST"). - HandlerFunc(middleware.EnforceAccess(policy.ClusterWrite, handler.GenerateNodeJoinCommandPrimary)) - r.Name("DrainNode").Path("/api/v1/kurl/nodes/{nodeName}/drain").Methods("POST"). - HandlerFunc(middleware.EnforceAccess(policy.ClusterWrite, handler.DrainNode)) - r.Name("DeleteNode").Path("/api/v1/kurl/nodes/{nodeName}").Methods("DELETE"). - HandlerFunc(middleware.EnforceAccess(policy.ClusterWrite, handler.DeleteNode)) + r.Name("Kurl").Path("/api/v1/kurl").HandlerFunc(NotImplemented) + r.Name("GenerateKurlNodeJoinCommandWorker").Path("/api/v1/kurl/generate-node-join-command-worker").Methods("POST"). + HandlerFunc(middleware.EnforceAccess(policy.ClusterWrite, handler.GenerateKurlNodeJoinCommandWorker)) + r.Name("GenerateKurlNodeJoinCommandMaster").Path("/api/v1/kurl/generate-node-join-command-master").Methods("POST"). + HandlerFunc(middleware.EnforceAccess(policy.ClusterWrite, handler.GenerateKurlNodeJoinCommandMaster)) + r.Name("GenerateKurlNodeJoinCommandSecondary").Path("/api/v1/kurl/generate-node-join-command-secondary").Methods("POST"). + HandlerFunc(middleware.EnforceAccess(policy.ClusterWrite, handler.GenerateKurlNodeJoinCommandSecondary)) + r.Name("GenerateKurlNodeJoinCommandPrimary").Path("/api/v1/kurl/generate-node-join-command-primary").Methods("POST"). + HandlerFunc(middleware.EnforceAccess(policy.ClusterWrite, handler.GenerateKurlNodeJoinCommandPrimary)) + r.Name("DrainKurlNode").Path("/api/v1/kurl/nodes/{nodeName}/drain").Methods("POST"). + HandlerFunc(middleware.EnforceAccess(policy.ClusterWrite, handler.DrainKurlNode)) + r.Name("DeleteKurlNode").Path("/api/v1/kurl/nodes/{nodeName}").Methods("DELETE"). + HandlerFunc(middleware.EnforceAccess(policy.ClusterWrite, handler.DeleteKurlNode)) r.Name("GetKurlNodes").Path("/api/v1/kurl/nodes").Methods("GET"). HandlerFunc(middleware.EnforceAccess(policy.ClusterRead, handler.GetKurlNodes)) + // HelmVM + r.Name("HelmVM").Path("/api/v1/helmvm").HandlerFunc(NotImplemented) + r.Name("GenerateHelmVMNodeJoinCommandSecondary").Path("/api/v1/helmvm/generate-node-join-command-secondary").Methods("POST"). + HandlerFunc(middleware.EnforceAccess(policy.ClusterWrite, handler.GenerateHelmVMNodeJoinCommandSecondary)) + r.Name("GenerateHelmVMNodeJoinCommandPrimary").Path("/api/v1/helmvm/generate-node-join-command-primary").Methods("POST"). + HandlerFunc(middleware.EnforceAccess(policy.ClusterWrite, handler.GenerateHelmVMNodeJoinCommandPrimary)) + r.Name("DrainHelmVMNode").Path("/api/v1/helmvm/nodes/{nodeName}/drain").Methods("POST"). + HandlerFunc(middleware.EnforceAccess(policy.ClusterWrite, handler.DrainHelmVMNode)) + r.Name("DeleteHelmVMNode").Path("/api/v1/helmvm/nodes/{nodeName}").Methods("DELETE"). + HandlerFunc(middleware.EnforceAccess(policy.ClusterWrite, handler.DeleteHelmVMNode)) + r.Name("GetHelmVMNodes").Path("/api/v1/helmvm/nodes").Methods("GET"). + HandlerFunc(middleware.EnforceAccess(policy.ClusterRead, handler.GetHelmVMNodes)) + // Prometheus r.Name("SetPrometheusAddress").Path("/api/v1/prometheus").Methods("POST"). HandlerFunc(middleware.EnforceAccess(policy.PrometheussettingsWrite, handler.SetPrometheusAddress)) @@ -341,7 +354,13 @@ func RegisterUnauthenticatedRoutes(handler *Handler, kotsStore store.Store, debu // These handlers should be called by the application only. loggingRouter.Path("/license/v1/license").Methods("GET").HandlerFunc(handler.GetPlatformLicenseCompatibility) - loggingRouter.Path("/api/v1/app/custom-metrics").Methods("POST").HandlerFunc(handler.GetSendCustomApplicationMetricsHandler(kotsStore)) + loggingRouter.Path("/api/v1/app/custom-metrics").Methods("POST").HandlerFunc(handler.GetSendCustomAppMetricsHandler(kotsStore)) +} + +func RegisterLicenseIDAuthRoutes(r *mux.Router, kotsStore store.Store, handler KOTSHandler) { + r.Use(LoggingMiddleware, RequireValidLicenseMiddleware(kotsStore)) + + r.Name("GetAppMetrics").Path("/api/v1/app/metrics").Methods("GET").HandlerFunc(handler.GetAppMetrics) } func StreamJSON(c *websocket.Conn, payload interface{}) { diff --git a/pkg/handlers/handlers_test.go b/pkg/handlers/handlers_test.go index 7b15466cc9..91bd7c0731 100644 --- a/pkg/handlers/handlers_test.go +++ b/pkg/handlers/handlers_test.go @@ -1136,64 +1136,64 @@ var HandlerPolicyTests = map[string][]HandlerPolicyTest{ }, "Kurl": {}, // Not implemented - "GenerateNodeJoinCommandWorker": { + "GenerateKurlNodeJoinCommandWorker": { { Roles: []rbactypes.Role{rbac.ClusterAdminRole}, SessionRoles: []string{rbac.ClusterAdminRoleID}, Calls: func(storeRecorder *mock_store.MockStoreMockRecorder, handlerRecorder *mock_handlers.MockKOTSHandlerMockRecorder) { - handlerRecorder.GenerateNodeJoinCommandWorker(gomock.Any(), gomock.Any()) + handlerRecorder.GenerateKurlNodeJoinCommandWorker(gomock.Any(), gomock.Any()) }, ExpectStatus: http.StatusOK, }, }, - "GenerateNodeJoinCommandMaster": { + "GenerateKurlNodeJoinCommandMaster": { { Roles: []rbactypes.Role{rbac.ClusterAdminRole}, SessionRoles: []string{rbac.ClusterAdminRoleID}, Calls: func(storeRecorder *mock_store.MockStoreMockRecorder, handlerRecorder *mock_handlers.MockKOTSHandlerMockRecorder) { - handlerRecorder.GenerateNodeJoinCommandMaster(gomock.Any(), gomock.Any()) + handlerRecorder.GenerateKurlNodeJoinCommandMaster(gomock.Any(), gomock.Any()) }, ExpectStatus: http.StatusOK, }, }, - "GenerateNodeJoinCommandSecondary": { + "GenerateKurlNodeJoinCommandSecondary": { { Roles: []rbactypes.Role{rbac.ClusterAdminRole}, SessionRoles: []string{rbac.ClusterAdminRoleID}, Calls: func(storeRecorder *mock_store.MockStoreMockRecorder, handlerRecorder *mock_handlers.MockKOTSHandlerMockRecorder) { - handlerRecorder.GenerateNodeJoinCommandSecondary(gomock.Any(), gomock.Any()) + handlerRecorder.GenerateKurlNodeJoinCommandSecondary(gomock.Any(), gomock.Any()) }, ExpectStatus: http.StatusOK, }, }, - "GenerateNodeJoinCommandPrimary": { + "GenerateKurlNodeJoinCommandPrimary": { { Roles: []rbactypes.Role{rbac.ClusterAdminRole}, SessionRoles: []string{rbac.ClusterAdminRoleID}, Calls: func(storeRecorder *mock_store.MockStoreMockRecorder, handlerRecorder *mock_handlers.MockKOTSHandlerMockRecorder) { - handlerRecorder.GenerateNodeJoinCommandPrimary(gomock.Any(), gomock.Any()) + handlerRecorder.GenerateKurlNodeJoinCommandPrimary(gomock.Any(), gomock.Any()) }, ExpectStatus: http.StatusOK, }, }, - "DrainNode": { + "DrainKurlNode": { { Vars: map[string]string{"nodeName": "node-name"}, Roles: []rbactypes.Role{rbac.ClusterAdminRole}, SessionRoles: []string{rbac.ClusterAdminRoleID}, Calls: func(storeRecorder *mock_store.MockStoreMockRecorder, handlerRecorder *mock_handlers.MockKOTSHandlerMockRecorder) { - handlerRecorder.DrainNode(gomock.Any(), gomock.Any()) + handlerRecorder.DrainKurlNode(gomock.Any(), gomock.Any()) }, ExpectStatus: http.StatusOK, }, }, - "DeleteNode": { + "DeleteKurlNode": { { Vars: map[string]string{"nodeName": "node-name"}, Roles: []rbactypes.Role{rbac.ClusterAdminRole}, SessionRoles: []string{rbac.ClusterAdminRoleID}, Calls: func(storeRecorder *mock_store.MockStoreMockRecorder, handlerRecorder *mock_handlers.MockKOTSHandlerMockRecorder) { - handlerRecorder.DeleteNode(gomock.Any(), gomock.Any()) + handlerRecorder.DeleteKurlNode(gomock.Any(), gomock.Any()) }, ExpectStatus: http.StatusOK, }, @@ -1209,6 +1209,60 @@ var HandlerPolicyTests = map[string][]HandlerPolicyTest{ }, }, + "HelmVM": {}, // Not implemented + "GenerateHelmVMNodeJoinCommandSecondary": { + { + Roles: []rbactypes.Role{rbac.ClusterAdminRole}, + SessionRoles: []string{rbac.ClusterAdminRoleID}, + Calls: func(storeRecorder *mock_store.MockStoreMockRecorder, handlerRecorder *mock_handlers.MockKOTSHandlerMockRecorder) { + handlerRecorder.GenerateHelmVMNodeJoinCommandSecondary(gomock.Any(), gomock.Any()) + }, + ExpectStatus: http.StatusOK, + }, + }, + "GenerateHelmVMNodeJoinCommandPrimary": { + { + Roles: []rbactypes.Role{rbac.ClusterAdminRole}, + SessionRoles: []string{rbac.ClusterAdminRoleID}, + Calls: func(storeRecorder *mock_store.MockStoreMockRecorder, handlerRecorder *mock_handlers.MockKOTSHandlerMockRecorder) { + handlerRecorder.GenerateHelmVMNodeJoinCommandPrimary(gomock.Any(), gomock.Any()) + }, + ExpectStatus: http.StatusOK, + }, + }, + "DrainHelmVMNode": { + { + Vars: map[string]string{"nodeName": "node-name"}, + Roles: []rbactypes.Role{rbac.ClusterAdminRole}, + SessionRoles: []string{rbac.ClusterAdminRoleID}, + Calls: func(storeRecorder *mock_store.MockStoreMockRecorder, handlerRecorder *mock_handlers.MockKOTSHandlerMockRecorder) { + handlerRecorder.DrainHelmVMNode(gomock.Any(), gomock.Any()) + }, + ExpectStatus: http.StatusOK, + }, + }, + "DeleteHelmVMNode": { + { + Vars: map[string]string{"nodeName": "node-name"}, + Roles: []rbactypes.Role{rbac.ClusterAdminRole}, + SessionRoles: []string{rbac.ClusterAdminRoleID}, + Calls: func(storeRecorder *mock_store.MockStoreMockRecorder, handlerRecorder *mock_handlers.MockKOTSHandlerMockRecorder) { + handlerRecorder.DeleteHelmVMNode(gomock.Any(), gomock.Any()) + }, + ExpectStatus: http.StatusOK, + }, + }, + "GetHelmVMNodes": { + { + Roles: []rbactypes.Role{rbac.ClusterAdminRole}, + SessionRoles: []string{rbac.ClusterAdminRoleID}, + Calls: func(storeRecorder *mock_store.MockStoreMockRecorder, handlerRecorder *mock_handlers.MockKOTSHandlerMockRecorder) { + handlerRecorder.GetHelmVMNodes(gomock.Any(), gomock.Any()) + }, + ExpectStatus: http.StatusOK, + }, + }, + // Prometheus "SetPrometheusAddress": { { diff --git a/pkg/handlers/helmvm_delete_node.go b/pkg/handlers/helmvm_delete_node.go new file mode 100644 index 0000000000..1b732ab07f --- /dev/null +++ b/pkg/handlers/helmvm_delete_node.go @@ -0,0 +1,50 @@ +package handlers + +import ( + "context" + "net/http" + + "github.com/gorilla/mux" + "github.com/replicatedhq/kots/pkg/helmvm" + "github.com/replicatedhq/kots/pkg/k8sutil" + "github.com/replicatedhq/kots/pkg/logger" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func (h *Handler) DeleteHelmVMNode(w http.ResponseWriter, r *http.Request) { + client, err := k8sutil.GetClientset() + if err != nil { + logger.Error(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + restconfig, err := k8sutil.GetClusterConfig() + if err != nil { + logger.Error(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + ctx := context.Background() + nodeName := mux.Vars(r)["nodeName"] + node, err := client.CoreV1().Nodes().Get(ctx, nodeName, metav1.GetOptions{}) + if err != nil { + if errors.IsNotFound(err) { + logger.Errorf("Failed to delete node %s: not found", nodeName) + w.WriteHeader(http.StatusNotFound) + return + } + logger.Error(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + if err := helmvm.DeleteNode(ctx, client, restconfig, node); err != nil { + logger.Error(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + logger.Infof("Node %s successfully deleted", node.Name) +} diff --git a/pkg/handlers/helmvm_drain_node.go b/pkg/handlers/helmvm_drain_node.go new file mode 100644 index 0000000000..ae0a337f6f --- /dev/null +++ b/pkg/handlers/helmvm_drain_node.go @@ -0,0 +1,45 @@ +package handlers + +import ( + "context" + "net/http" + + "github.com/gorilla/mux" + "github.com/replicatedhq/kots/pkg/helmvm" + "github.com/replicatedhq/kots/pkg/k8sutil" + "github.com/replicatedhq/kots/pkg/logger" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func (h *Handler) DrainHelmVMNode(w http.ResponseWriter, r *http.Request) { + client, err := k8sutil.GetClientset() + if err != nil { + logger.Error(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + ctx := context.Background() + nodeName := mux.Vars(r)["nodeName"] + node, err := client.CoreV1().Nodes().Get(ctx, nodeName, metav1.GetOptions{}) + if err != nil { + if errors.IsNotFound(err) { + logger.Errorf("Failed to drain node %s: not found", nodeName) + w.WriteHeader(http.StatusNotFound) + return + } + logger.Error(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + // This pod may get evicted and not be able to respond to the request + go func() { + if err := helmvm.DrainNode(ctx, client, node); err != nil { + logger.Error(err) + return + } + logger.Infof("Node %s successfully drained", node.Name) + }() +} diff --git a/pkg/handlers/helmvm_get.go b/pkg/handlers/helmvm_get.go new file mode 100644 index 0000000000..cd440d116f --- /dev/null +++ b/pkg/handlers/helmvm_get.go @@ -0,0 +1,26 @@ +package handlers + +import ( + "net/http" + + "github.com/replicatedhq/kots/pkg/helmvm" + "github.com/replicatedhq/kots/pkg/k8sutil" + "github.com/replicatedhq/kots/pkg/logger" +) + +func (h *Handler) GetHelmVMNodes(w http.ResponseWriter, r *http.Request) { + client, err := k8sutil.GetClientset() + if err != nil { + logger.Error(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + nodes, err := helmvm.GetNodes(client) + if err != nil { + logger.Error(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + JSON(w, http.StatusOK, nodes) +} diff --git a/pkg/handlers/helmvm_node_join_command.go b/pkg/handlers/helmvm_node_join_command.go new file mode 100644 index 0000000000..6604b659d9 --- /dev/null +++ b/pkg/handlers/helmvm_node_join_command.go @@ -0,0 +1,55 @@ +package handlers + +import ( + "net/http" + "time" + + "github.com/replicatedhq/kots/pkg/helmvm" + "github.com/replicatedhq/kots/pkg/k8sutil" + "github.com/replicatedhq/kots/pkg/logger" +) + +type GenerateHelmVMNodeJoinCommandResponse struct { + Command []string `json:"command"` + Expiry string `json:"expiry"` +} + +func (h *Handler) GenerateHelmVMNodeJoinCommandSecondary(w http.ResponseWriter, r *http.Request) { + client, err := k8sutil.GetClientset() + if err != nil { + logger.Error(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + command, expiry, err := helmvm.GenerateAddNodeCommand(client, false) + if err != nil { + logger.Error(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + JSON(w, http.StatusOK, GenerateHelmVMNodeJoinCommandResponse{ + Command: command, + Expiry: expiry.Format(time.RFC3339), + }) +} + +func (h *Handler) GenerateHelmVMNodeJoinCommandPrimary(w http.ResponseWriter, r *http.Request) { + client, err := k8sutil.GetClientset() + if err != nil { + logger.Error(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + command, expiry, err := helmvm.GenerateAddNodeCommand(client, true) + if err != nil { + logger.Error(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + JSON(w, http.StatusOK, GenerateHelmVMNodeJoinCommandResponse{ + Command: command, + Expiry: expiry.Format(time.RFC3339), + }) +} diff --git a/pkg/handlers/interface.go b/pkg/handlers/interface.go index 40d7f22bdf..c6cb2a00db 100644 --- a/pkg/handlers/interface.go +++ b/pkg/handlers/interface.go @@ -130,14 +130,21 @@ type KOTSHandler interface { GetVeleroStatus(w http.ResponseWriter, r *http.Request) // KURL - GenerateNodeJoinCommandWorker(w http.ResponseWriter, r *http.Request) - GenerateNodeJoinCommandMaster(w http.ResponseWriter, r *http.Request) - GenerateNodeJoinCommandSecondary(w http.ResponseWriter, r *http.Request) - GenerateNodeJoinCommandPrimary(w http.ResponseWriter, r *http.Request) - DrainNode(w http.ResponseWriter, r *http.Request) - DeleteNode(w http.ResponseWriter, r *http.Request) + GenerateKurlNodeJoinCommandWorker(w http.ResponseWriter, r *http.Request) + GenerateKurlNodeJoinCommandMaster(w http.ResponseWriter, r *http.Request) + GenerateKurlNodeJoinCommandSecondary(w http.ResponseWriter, r *http.Request) + GenerateKurlNodeJoinCommandPrimary(w http.ResponseWriter, r *http.Request) + DrainKurlNode(w http.ResponseWriter, r *http.Request) + DeleteKurlNode(w http.ResponseWriter, r *http.Request) GetKurlNodes(w http.ResponseWriter, r *http.Request) + // HelmVM + GenerateHelmVMNodeJoinCommandSecondary(w http.ResponseWriter, r *http.Request) + GenerateHelmVMNodeJoinCommandPrimary(w http.ResponseWriter, r *http.Request) + DrainHelmVMNode(w http.ResponseWriter, r *http.Request) + DeleteHelmVMNode(w http.ResponseWriter, r *http.Request) + GetHelmVMNodes(w http.ResponseWriter, r *http.Request) + // Prometheus SetPrometheusAddress(w http.ResponseWriter, r *http.Request) @@ -155,4 +162,7 @@ type KOTSHandler interface { // Helm IsHelmManaged(w http.ResponseWriter, r *http.Request) GetAppValuesFile(w http.ResponseWriter, r *http.Request) + + // APIs available to applications (except legacy /license/v1/license) + GetAppMetrics(w http.ResponseWriter, r *http.Request) } diff --git a/pkg/handlers/kurl_delete_node.go b/pkg/handlers/kurl_delete_node.go index 02f075d88f..1edf8ac46f 100644 --- a/pkg/handlers/kurl_delete_node.go +++ b/pkg/handlers/kurl_delete_node.go @@ -13,7 +13,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -func (h *Handler) DeleteNode(w http.ResponseWriter, r *http.Request) { +func (h *Handler) DeleteKurlNode(w http.ResponseWriter, r *http.Request) { client, err := k8sutil.GetClientset() if err != nil { logger.Error(err) diff --git a/pkg/handlers/kurl_drain_node.go b/pkg/handlers/kurl_drain_node.go index 98809a9e7a..80e58b05d1 100644 --- a/pkg/handlers/kurl_drain_node.go +++ b/pkg/handlers/kurl_drain_node.go @@ -12,7 +12,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -func (h *Handler) DrainNode(w http.ResponseWriter, r *http.Request) { +func (h *Handler) DrainKurlNode(w http.ResponseWriter, r *http.Request) { client, err := k8sutil.GetClientset() if err != nil { logger.Error(err) diff --git a/pkg/handlers/kurl_node_join_command.go b/pkg/handlers/kurl_node_join_command.go index 6d1bdfa1b9..1f0ed4a438 100644 --- a/pkg/handlers/kurl_node_join_command.go +++ b/pkg/handlers/kurl_node_join_command.go @@ -9,12 +9,12 @@ import ( "github.com/replicatedhq/kots/pkg/logger" ) -type GenerateNodeJoinCommandResponse struct { +type GenerateKurlNodeJoinCommandResponse struct { Command []string `json:"command"` Expiry string `json:"expiry"` } -func (h *Handler) GenerateNodeJoinCommandWorker(w http.ResponseWriter, r *http.Request) { +func (h *Handler) GenerateKurlNodeJoinCommandWorker(w http.ResponseWriter, r *http.Request) { client, err := k8sutil.GetClientset() if err != nil { logger.Error(err) @@ -28,13 +28,13 @@ func (h *Handler) GenerateNodeJoinCommandWorker(w http.ResponseWriter, r *http.R w.WriteHeader(http.StatusInternalServerError) return } - JSON(w, http.StatusOK, GenerateNodeJoinCommandResponse{ + JSON(w, http.StatusOK, GenerateKurlNodeJoinCommandResponse{ Command: command, Expiry: expiry.Format(time.RFC3339), }) } -func (h *Handler) GenerateNodeJoinCommandMaster(w http.ResponseWriter, r *http.Request) { +func (h *Handler) GenerateKurlNodeJoinCommandMaster(w http.ResponseWriter, r *http.Request) { client, err := k8sutil.GetClientset() if err != nil { logger.Error(err) @@ -48,13 +48,13 @@ func (h *Handler) GenerateNodeJoinCommandMaster(w http.ResponseWriter, r *http.R w.WriteHeader(http.StatusInternalServerError) return } - JSON(w, http.StatusOK, GenerateNodeJoinCommandResponse{ + JSON(w, http.StatusOK, GenerateKurlNodeJoinCommandResponse{ Command: command, Expiry: expiry.Format(time.RFC3339), }) } -func (h *Handler) GenerateNodeJoinCommandSecondary(w http.ResponseWriter, r *http.Request) { +func (h *Handler) GenerateKurlNodeJoinCommandSecondary(w http.ResponseWriter, r *http.Request) { client, err := k8sutil.GetClientset() if err != nil { logger.Error(err) @@ -68,13 +68,13 @@ func (h *Handler) GenerateNodeJoinCommandSecondary(w http.ResponseWriter, r *htt w.WriteHeader(http.StatusInternalServerError) return } - JSON(w, http.StatusOK, GenerateNodeJoinCommandResponse{ + JSON(w, http.StatusOK, GenerateKurlNodeJoinCommandResponse{ Command: command, Expiry: expiry.Format(time.RFC3339), }) } -func (h *Handler) GenerateNodeJoinCommandPrimary(w http.ResponseWriter, r *http.Request) { +func (h *Handler) GenerateKurlNodeJoinCommandPrimary(w http.ResponseWriter, r *http.Request) { client, err := k8sutil.GetClientset() if err != nil { logger.Error(err) @@ -88,7 +88,7 @@ func (h *Handler) GenerateNodeJoinCommandPrimary(w http.ResponseWriter, r *http. w.WriteHeader(http.StatusInternalServerError) return } - JSON(w, http.StatusOK, GenerateNodeJoinCommandResponse{ + JSON(w, http.StatusOK, GenerateKurlNodeJoinCommandResponse{ Command: command, Expiry: expiry.Format(time.RFC3339), }) diff --git a/pkg/handlers/metadata.go b/pkg/handlers/metadata.go index 37952c971a..81903b26dd 100644 --- a/pkg/handlers/metadata.go +++ b/pkg/handlers/metadata.go @@ -52,6 +52,7 @@ type MetadataResponseBranding struct { type AdminConsoleMetadata struct { IsAirgap bool `json:"isAirgap"` IsKurl bool `json:"isKurl"` + IsHelmVM bool `json:"isHelmVM"` } // GetMetadataHandler helper function that returns a http handler func that returns metadata. It takes a function that @@ -72,6 +73,7 @@ func GetMetadataHandler(getK8sInfoFn MetadataK8sFn, kotsStore store.Store) http. if kuberneteserrors.IsNotFound(err) { metadataResponse.AdminConsoleMetadata.IsAirgap = kotsadmMetadata.IsAirgap metadataResponse.AdminConsoleMetadata.IsKurl = kotsadmMetadata.IsKurl + metadataResponse.AdminConsoleMetadata.IsHelmVM = kotsadmMetadata.IsHelmVM logger.Info(fmt.Sprintf("config map %q not found", metadataConfigMapName)) JSON(w, http.StatusOK, &metadataResponse) @@ -114,6 +116,7 @@ func GetMetadataHandler(getK8sInfoFn MetadataK8sFn, kotsStore store.Store) http. metadataResponse.AdminConsoleMetadata = AdminConsoleMetadata{ IsAirgap: kotsadmMetadata.IsAirgap, IsKurl: kotsadmMetadata.IsKurl, + IsHelmVM: kotsadmMetadata.IsHelmVM, } JSON(w, http.StatusOK, metadataResponse) diff --git a/pkg/handlers/custom_metrics.go b/pkg/handlers/metrics.go similarity index 68% rename from pkg/handlers/custom_metrics.go rename to pkg/handlers/metrics.go index 4f658ae7a2..38de662f2b 100644 --- a/pkg/handlers/custom_metrics.go +++ b/pkg/handlers/metrics.go @@ -10,16 +10,26 @@ import ( "github.com/replicatedhq/kots/pkg/kotsutil" "github.com/replicatedhq/kots/pkg/logger" "github.com/replicatedhq/kots/pkg/replicatedapp" + "github.com/replicatedhq/kots/pkg/reporting" + "github.com/replicatedhq/kots/pkg/session" "github.com/replicatedhq/kots/pkg/store" ) -type SendCustomApplicationMetricsRequest struct { - Data ApplicationMetricsData `json:"data"` +func (h *Handler) GetAppMetrics(w http.ResponseWriter, r *http.Request) { + app := session.ContextGetApp(r) + reportingInfo := reporting.GetReportingInfo(app.ID) + headers := reporting.GetReportingInfoHeaders(reportingInfo) + + JSON(w, http.StatusOK, headers) +} + +type SendCustomAppMetricsRequest struct { + Data CustomAppMetricsData `json:"data"` } -type ApplicationMetricsData map[string]interface{} +type CustomAppMetricsData map[string]interface{} -func (h *Handler) GetSendCustomApplicationMetricsHandler(kotsStore store.Store) http.HandlerFunc { +func (h *Handler) GetSendCustomAppMetricsHandler(kotsStore store.Store) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if kotsadm.IsAirgap() { w.WriteHeader(http.StatusForbidden) @@ -48,20 +58,20 @@ func (h *Handler) GetSendCustomApplicationMetricsHandler(kotsStore store.Store) return } - request := SendCustomApplicationMetricsRequest{} + request := SendCustomAppMetricsRequest{} if err := json.NewDecoder(r.Body).Decode(&request); err != nil { logger.Error(errors.Wrap(err, "decode request")) w.WriteHeader(http.StatusBadRequest) return } - if err := validateCustomMetricsData(request.Data); err != nil { + if err := validateCustomAppMetricsData(request.Data); err != nil { w.WriteHeader(http.StatusBadRequest) w.Write([]byte(err.Error())) return } - err = replicatedapp.SendApplicationMetricsData(license, app, request.Data) + err = replicatedapp.SendCustomAppMetricsData(license, app, request.Data) if err != nil { logger.Error(errors.Wrap(err, "set application data")) w.WriteHeader(http.StatusBadRequest) @@ -72,13 +82,17 @@ func (h *Handler) GetSendCustomApplicationMetricsHandler(kotsStore store.Store) } } -func validateCustomMetricsData(data ApplicationMetricsData) error { +func validateCustomAppMetricsData(data CustomAppMetricsData) error { if len(data) == 0 { return errors.New("no data provided") } for key, val := range data { valType := reflect.TypeOf(val) + if valType == nil { + return errors.Errorf("%s value is nil, only scalar values are allowed", key) + } + switch valType.Kind() { case reflect.Slice: return errors.Errorf("%s value is an array, only scalar values are allowed", key) diff --git a/pkg/handlers/custom_metrics_test.go b/pkg/handlers/metrics_test.go similarity index 81% rename from pkg/handlers/custom_metrics_test.go rename to pkg/handlers/metrics_test.go index 8e0b3bda1b..79ae39b3bb 100644 --- a/pkg/handlers/custom_metrics_test.go +++ b/pkg/handlers/metrics_test.go @@ -16,15 +16,15 @@ import ( "github.com/stretchr/testify/require" ) -func Test_validateCustomMetricsData(t *testing.T) { +func Test_validateCustomAppMetricsData(t *testing.T) { tests := []struct { name string - data ApplicationMetricsData + data CustomAppMetricsData wantErr bool }{ { name: "all values are valid", - data: ApplicationMetricsData{ + data: CustomAppMetricsData{ "key1": "val1", "key2": 6, "key3": 6.6, @@ -34,12 +34,12 @@ func Test_validateCustomMetricsData(t *testing.T) { }, { name: "no data", - data: ApplicationMetricsData{}, + data: CustomAppMetricsData{}, wantErr: true, }, { name: "array value", - data: ApplicationMetricsData{ + data: CustomAppMetricsData{ "key1": 10, "key2": []string{"val1", "val2"}, }, @@ -47,17 +47,25 @@ func Test_validateCustomMetricsData(t *testing.T) { }, { name: "map value", - data: ApplicationMetricsData{ + data: CustomAppMetricsData{ "key1": 10, "key2": map[string]string{"key1": "val1"}, }, wantErr: true, }, + { + name: "nil value", + data: CustomAppMetricsData{ + "key1": nil, + "key2": 4, + }, + wantErr: true, + }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - err := validateCustomMetricsData(test.data) + err := validateCustomAppMetricsData(test.data) if test.wantErr { require.Error(t, err) } else { @@ -67,7 +75,7 @@ func Test_validateCustomMetricsData(t *testing.T) { } } -func Test_SendCustomApplicationMetrics(t *testing.T) { +func Test_SendCustomAppMetrics(t *testing.T) { req := require.New(t) customMetricsData := []byte(`{"data":{"key1_string":"val1","key2_int":5,"key3_float":1.5,"key4_numeric_string":"1.6"}}`) appID := "app-id-123" @@ -114,7 +122,7 @@ spec: // Validate - handler.GetSendCustomApplicationMetricsHandler(mockStore)(clientWriter, clientRequest) + handler.GetSendCustomAppMetricsHandler(mockStore)(clientWriter, clientRequest) req.Equal(http.StatusOK, clientWriter.Code) } diff --git a/pkg/handlers/middleware.go b/pkg/handlers/middleware.go index ea328f8a89..82ca484693 100644 --- a/pkg/handlers/middleware.go +++ b/pkg/handlers/middleware.go @@ -107,3 +107,21 @@ func RequireValidSessionQuietMiddleware(kotsStore store.Store) mux.MiddlewareFun }) } } + +func RequireValidLicenseMiddleware(kotsStore store.Store) mux.MiddlewareFunc { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + license, app, err := requireValidLicense(kotsStore, w, r) + if err != nil { + if !kotsStore.IsNotFound(err) { + logger.Error(errors.Wrapf(err, "request %q", r.RequestURI)) + } + return + } + + r = session.ContextSetLicense(r, license) + r = session.ContextSetApp(r, app) + next.ServeHTTP(w, r) + }) + } +} diff --git a/pkg/handlers/mock/mock.go b/pkg/handlers/mock/mock.go index 45f4d1c242..cf9fe09ede 100644 --- a/pkg/handlers/mock/mock.go +++ b/pkg/handlers/mock/mock.go @@ -262,16 +262,28 @@ func (mr *MockKOTSHandlerMockRecorder) DeleteBackup(w, r interface{}) *gomock.Ca return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBackup", reflect.TypeOf((*MockKOTSHandler)(nil).DeleteBackup), w, r) } -// DeleteNode mocks base method. -func (m *MockKOTSHandler) DeleteNode(w http.ResponseWriter, r *http.Request) { +// DeleteHelmVMNode mocks base method. +func (m *MockKOTSHandler) DeleteHelmVMNode(w http.ResponseWriter, r *http.Request) { m.ctrl.T.Helper() - m.ctrl.Call(m, "DeleteNode", w, r) + m.ctrl.Call(m, "DeleteHelmVMNode", w, r) } -// DeleteNode indicates an expected call of DeleteNode. -func (mr *MockKOTSHandlerMockRecorder) DeleteNode(w, r interface{}) *gomock.Call { +// DeleteHelmVMNode indicates an expected call of DeleteHelmVMNode. +func (mr *MockKOTSHandlerMockRecorder) DeleteHelmVMNode(w, r interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteNode", reflect.TypeOf((*MockKOTSHandler)(nil).DeleteNode), w, r) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteHelmVMNode", reflect.TypeOf((*MockKOTSHandler)(nil).DeleteHelmVMNode), w, r) +} + +// DeleteKurlNode mocks base method. +func (m *MockKOTSHandler) DeleteKurlNode(w http.ResponseWriter, r *http.Request) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "DeleteKurlNode", w, r) +} + +// DeleteKurlNode indicates an expected call of DeleteKurlNode. +func (mr *MockKOTSHandlerMockRecorder) DeleteKurlNode(w, r interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteKurlNode", reflect.TypeOf((*MockKOTSHandler)(nil).DeleteKurlNode), w, r) } // DeleteRedact mocks base method. @@ -382,16 +394,28 @@ func (mr *MockKOTSHandlerMockRecorder) DownloadSupportBundle(w, r interface{}) * return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DownloadSupportBundle", reflect.TypeOf((*MockKOTSHandler)(nil).DownloadSupportBundle), w, r) } -// DrainNode mocks base method. -func (m *MockKOTSHandler) DrainNode(w http.ResponseWriter, r *http.Request) { +// DrainHelmVMNode mocks base method. +func (m *MockKOTSHandler) DrainHelmVMNode(w http.ResponseWriter, r *http.Request) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "DrainHelmVMNode", w, r) +} + +// DrainHelmVMNode indicates an expected call of DrainHelmVMNode. +func (mr *MockKOTSHandlerMockRecorder) DrainHelmVMNode(w, r interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DrainHelmVMNode", reflect.TypeOf((*MockKOTSHandler)(nil).DrainHelmVMNode), w, r) +} + +// DrainKurlNode mocks base method. +func (m *MockKOTSHandler) DrainKurlNode(w http.ResponseWriter, r *http.Request) { m.ctrl.T.Helper() - m.ctrl.Call(m, "DrainNode", w, r) + m.ctrl.Call(m, "DrainKurlNode", w, r) } -// DrainNode indicates an expected call of DrainNode. -func (mr *MockKOTSHandlerMockRecorder) DrainNode(w, r interface{}) *gomock.Call { +// DrainKurlNode indicates an expected call of DrainKurlNode. +func (mr *MockKOTSHandlerMockRecorder) DrainKurlNode(w, r interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DrainNode", reflect.TypeOf((*MockKOTSHandler)(nil).DrainNode), w, r) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DrainKurlNode", reflect.TypeOf((*MockKOTSHandler)(nil).DrainKurlNode), w, r) } // ExchangePlatformLicense mocks base method. @@ -418,52 +442,76 @@ func (mr *MockKOTSHandlerMockRecorder) GarbageCollectImages(w, r interface{}) *g return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GarbageCollectImages", reflect.TypeOf((*MockKOTSHandler)(nil).GarbageCollectImages), w, r) } -// GenerateNodeJoinCommandMaster mocks base method. -func (m *MockKOTSHandler) GenerateNodeJoinCommandMaster(w http.ResponseWriter, r *http.Request) { +// GenerateHelmVMNodeJoinCommandPrimary mocks base method. +func (m *MockKOTSHandler) GenerateHelmVMNodeJoinCommandPrimary(w http.ResponseWriter, r *http.Request) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "GenerateHelmVMNodeJoinCommandPrimary", w, r) +} + +// GenerateHelmVMNodeJoinCommandPrimary indicates an expected call of GenerateHelmVMNodeJoinCommandPrimary. +func (mr *MockKOTSHandlerMockRecorder) GenerateHelmVMNodeJoinCommandPrimary(w, r interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GenerateHelmVMNodeJoinCommandPrimary", reflect.TypeOf((*MockKOTSHandler)(nil).GenerateHelmVMNodeJoinCommandPrimary), w, r) +} + +// GenerateHelmVMNodeJoinCommandSecondary mocks base method. +func (m *MockKOTSHandler) GenerateHelmVMNodeJoinCommandSecondary(w http.ResponseWriter, r *http.Request) { m.ctrl.T.Helper() - m.ctrl.Call(m, "GenerateNodeJoinCommandMaster", w, r) + m.ctrl.Call(m, "GenerateHelmVMNodeJoinCommandSecondary", w, r) } -// GenerateNodeJoinCommandMaster indicates an expected call of GenerateNodeJoinCommandMaster. -func (mr *MockKOTSHandlerMockRecorder) GenerateNodeJoinCommandMaster(w, r interface{}) *gomock.Call { +// GenerateHelmVMNodeJoinCommandSecondary indicates an expected call of GenerateHelmVMNodeJoinCommandSecondary. +func (mr *MockKOTSHandlerMockRecorder) GenerateHelmVMNodeJoinCommandSecondary(w, r interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GenerateNodeJoinCommandMaster", reflect.TypeOf((*MockKOTSHandler)(nil).GenerateNodeJoinCommandMaster), w, r) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GenerateHelmVMNodeJoinCommandSecondary", reflect.TypeOf((*MockKOTSHandler)(nil).GenerateHelmVMNodeJoinCommandSecondary), w, r) } -// GenerateNodeJoinCommandPrimary mocks base method. -func (m *MockKOTSHandler) GenerateNodeJoinCommandPrimary(w http.ResponseWriter, r *http.Request) { +// GenerateKurlNodeJoinCommandMaster mocks base method. +func (m *MockKOTSHandler) GenerateKurlNodeJoinCommandMaster(w http.ResponseWriter, r *http.Request) { m.ctrl.T.Helper() - m.ctrl.Call(m, "GenerateNodeJoinCommandPrimary", w, r) + m.ctrl.Call(m, "GenerateKurlNodeJoinCommandMaster", w, r) } -// GenerateNodeJoinCommandPrimary indicates an expected call of GenerateNodeJoinCommandPrimary. -func (mr *MockKOTSHandlerMockRecorder) GenerateNodeJoinCommandPrimary(w, r interface{}) *gomock.Call { +// GenerateKurlNodeJoinCommandMaster indicates an expected call of GenerateKurlNodeJoinCommandMaster. +func (mr *MockKOTSHandlerMockRecorder) GenerateKurlNodeJoinCommandMaster(w, r interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GenerateNodeJoinCommandPrimary", reflect.TypeOf((*MockKOTSHandler)(nil).GenerateNodeJoinCommandPrimary), w, r) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GenerateKurlNodeJoinCommandMaster", reflect.TypeOf((*MockKOTSHandler)(nil).GenerateKurlNodeJoinCommandMaster), w, r) } -// GenerateNodeJoinCommandSecondary mocks base method. -func (m *MockKOTSHandler) GenerateNodeJoinCommandSecondary(w http.ResponseWriter, r *http.Request) { +// GenerateKurlNodeJoinCommandPrimary mocks base method. +func (m *MockKOTSHandler) GenerateKurlNodeJoinCommandPrimary(w http.ResponseWriter, r *http.Request) { m.ctrl.T.Helper() - m.ctrl.Call(m, "GenerateNodeJoinCommandSecondary", w, r) + m.ctrl.Call(m, "GenerateKurlNodeJoinCommandPrimary", w, r) } -// GenerateNodeJoinCommandSecondary indicates an expected call of GenerateNodeJoinCommandSecondary. -func (mr *MockKOTSHandlerMockRecorder) GenerateNodeJoinCommandSecondary(w, r interface{}) *gomock.Call { +// GenerateKurlNodeJoinCommandPrimary indicates an expected call of GenerateKurlNodeJoinCommandPrimary. +func (mr *MockKOTSHandlerMockRecorder) GenerateKurlNodeJoinCommandPrimary(w, r interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GenerateNodeJoinCommandSecondary", reflect.TypeOf((*MockKOTSHandler)(nil).GenerateNodeJoinCommandSecondary), w, r) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GenerateKurlNodeJoinCommandPrimary", reflect.TypeOf((*MockKOTSHandler)(nil).GenerateKurlNodeJoinCommandPrimary), w, r) } -// GenerateNodeJoinCommandWorker mocks base method. -func (m *MockKOTSHandler) GenerateNodeJoinCommandWorker(w http.ResponseWriter, r *http.Request) { +// GenerateKurlNodeJoinCommandSecondary mocks base method. +func (m *MockKOTSHandler) GenerateKurlNodeJoinCommandSecondary(w http.ResponseWriter, r *http.Request) { m.ctrl.T.Helper() - m.ctrl.Call(m, "GenerateNodeJoinCommandWorker", w, r) + m.ctrl.Call(m, "GenerateKurlNodeJoinCommandSecondary", w, r) } -// GenerateNodeJoinCommandWorker indicates an expected call of GenerateNodeJoinCommandWorker. -func (mr *MockKOTSHandlerMockRecorder) GenerateNodeJoinCommandWorker(w, r interface{}) *gomock.Call { +// GenerateKurlNodeJoinCommandSecondary indicates an expected call of GenerateKurlNodeJoinCommandSecondary. +func (mr *MockKOTSHandlerMockRecorder) GenerateKurlNodeJoinCommandSecondary(w, r interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GenerateNodeJoinCommandWorker", reflect.TypeOf((*MockKOTSHandler)(nil).GenerateNodeJoinCommandWorker), w, r) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GenerateKurlNodeJoinCommandSecondary", reflect.TypeOf((*MockKOTSHandler)(nil).GenerateKurlNodeJoinCommandSecondary), w, r) +} + +// GenerateKurlNodeJoinCommandWorker mocks base method. +func (m *MockKOTSHandler) GenerateKurlNodeJoinCommandWorker(w http.ResponseWriter, r *http.Request) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "GenerateKurlNodeJoinCommandWorker", w, r) +} + +// GenerateKurlNodeJoinCommandWorker indicates an expected call of GenerateKurlNodeJoinCommandWorker. +func (mr *MockKOTSHandlerMockRecorder) GenerateKurlNodeJoinCommandWorker(w, r interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GenerateKurlNodeJoinCommandWorker", reflect.TypeOf((*MockKOTSHandler)(nil).GenerateKurlNodeJoinCommandWorker), w, r) } // GetAdminConsoleUpdateStatus mocks base method. @@ -550,6 +598,18 @@ func (mr *MockKOTSHandlerMockRecorder) GetAppIdentityServiceConfig(w, r interfac return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAppIdentityServiceConfig", reflect.TypeOf((*MockKOTSHandler)(nil).GetAppIdentityServiceConfig), w, r) } +// GetAppMetrics mocks base method. +func (m *MockKOTSHandler) GetAppMetrics(w http.ResponseWriter, r *http.Request) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "GetAppMetrics", w, r) +} + +// GetAppMetrics indicates an expected call of GetAppMetrics. +func (mr *MockKOTSHandlerMockRecorder) GetAppMetrics(w, r interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAppMetrics", reflect.TypeOf((*MockKOTSHandler)(nil).GetAppMetrics), w, r) +} + // GetAppRegistry mocks base method. func (m *MockKOTSHandler) GetAppRegistry(w http.ResponseWriter, r *http.Request) { m.ctrl.T.Helper() @@ -706,6 +766,18 @@ func (mr *MockKOTSHandlerMockRecorder) GetGlobalSnapshotSettings(w, r interface{ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGlobalSnapshotSettings", reflect.TypeOf((*MockKOTSHandler)(nil).GetGlobalSnapshotSettings), w, r) } +// GetHelmVMNodes mocks base method. +func (m *MockKOTSHandler) GetHelmVMNodes(w http.ResponseWriter, r *http.Request) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "GetHelmVMNodes", w, r) +} + +// GetHelmVMNodes indicates an expected call of GetHelmVMNodes. +func (mr *MockKOTSHandlerMockRecorder) GetHelmVMNodes(w, r interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetHelmVMNodes", reflect.TypeOf((*MockKOTSHandler)(nil).GetHelmVMNodes), w, r) +} + // GetIdentityServiceConfig mocks base method. func (m *MockKOTSHandler) GetIdentityServiceConfig(w http.ResponseWriter, r *http.Request) { m.ctrl.T.Helper() diff --git a/pkg/handlers/preflight.go b/pkg/handlers/preflight.go index fc9cd17c98..5138d6b8af 100644 --- a/pkg/handlers/preflight.go +++ b/pkg/handlers/preflight.go @@ -201,7 +201,7 @@ func (h *Handler) StartPreflightChecks(w http.ResponseWriter, r *http.Request) { return } - archiveDir, err := ioutil.TempDir("", "kotsadm") + archiveDir, err := os.MkdirTemp("", "kotsadm") if err != nil { logger.Error(errors.Wrap(err, "failed to create temp dir")) w.WriteHeader(http.StatusInternalServerError) diff --git a/pkg/handlers/session.go b/pkg/handlers/session.go index 2179f996ed..819e41db1c 100644 --- a/pkg/handlers/session.go +++ b/pkg/handlers/session.go @@ -8,13 +8,16 @@ import ( "time" "github.com/pkg/errors" + apptypes "github.com/replicatedhq/kots/pkg/app/types" "github.com/replicatedhq/kots/pkg/handlers/types" "github.com/replicatedhq/kots/pkg/k8sutil" + "github.com/replicatedhq/kots/pkg/kotsutil" "github.com/replicatedhq/kots/pkg/logger" "github.com/replicatedhq/kots/pkg/session" sessiontypes "github.com/replicatedhq/kots/pkg/session/types" "github.com/replicatedhq/kots/pkg/store" "github.com/replicatedhq/kots/pkg/util" + kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" kuberneteserrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -155,3 +158,47 @@ func requireValidKOTSToken(w http.ResponseWriter, r *http.Request) error { return errors.New("invalid auth") } + +func requireValidLicense(kotsStore store.Store, w http.ResponseWriter, r *http.Request) (*kotsv1beta1.License, *apptypes.App, error) { + if r.Method == "OPTIONS" { + return nil, nil, nil + } + + licenseID := r.Header.Get("authorization") + if licenseID == "" { + err := errors.New("missing authorization header") + response := types.ErrorResponse{Error: util.StrPointer(err.Error())} + JSON(w, http.StatusUnauthorized, response) + return nil, nil, err + } + + apps, err := kotsStore.ListInstalledApps() + if err != nil { + return nil, nil, errors.Wrap(err, "get all apps") + } + + var license *kotsv1beta1.License + var app *apptypes.App + + for _, a := range apps { + l, err := kotsutil.LoadLicenseFromBytes([]byte(a.License)) + if err != nil { + return nil, nil, errors.Wrap(err, "load license") + } + + if l.Spec.LicenseID == licenseID { + license = l + app = a + break + } + } + + if license == nil { + err := errors.New("license ID is not valid") + response := types.ErrorResponse{Error: util.StrPointer(err.Error())} + JSON(w, http.StatusUnauthorized, response) + return nil, nil, err + } + + return license, app, nil +} diff --git a/pkg/helmvm/delete_node.go b/pkg/helmvm/delete_node.go new file mode 100644 index 0000000000..24d7e5e46d --- /dev/null +++ b/pkg/helmvm/delete_node.go @@ -0,0 +1,13 @@ +package helmvm + +import ( + "context" + + corev1 "k8s.io/api/core/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" +) + +func DeleteNode(ctx context.Context, client kubernetes.Interface, restconfig *rest.Config, node *corev1.Node) error { + return nil +} diff --git a/pkg/helmvm/drain_node.go b/pkg/helmvm/drain_node.go new file mode 100644 index 0000000000..b8fa55afbb --- /dev/null +++ b/pkg/helmvm/drain_node.go @@ -0,0 +1,12 @@ +package helmvm + +import ( + "context" + + corev1 "k8s.io/api/core/v1" + "k8s.io/client-go/kubernetes" +) + +func DrainNode(ctx context.Context, client kubernetes.Interface, node *corev1.Node) error { + return nil +} diff --git a/pkg/helmvm/exec.go b/pkg/helmvm/exec.go new file mode 100644 index 0000000000..04f94635de --- /dev/null +++ b/pkg/helmvm/exec.go @@ -0,0 +1,11 @@ +package helmvm + +import ( + corev1client "k8s.io/client-go/kubernetes/typed/core/v1" + "k8s.io/client-go/rest" +) + +// SyncExec returns exitcode, stdout, stderr. A non-zero exit code from the command is not considered an error. +func SyncExec(coreClient corev1client.CoreV1Interface, clientConfig *rest.Config, ns, pod, container string, command ...string) (int, string, string, error) { + return 0, "", "", nil +} diff --git a/pkg/helmvm/helmvm_nodes.go b/pkg/helmvm/helmvm_nodes.go new file mode 100644 index 0000000000..e00dca2108 --- /dev/null +++ b/pkg/helmvm/helmvm_nodes.go @@ -0,0 +1,212 @@ +package helmvm + +import ( + "context" + "crypto/tls" + "encoding/json" + "fmt" + "io" + "math" + "net/http" + "os" + "strconv" + "time" + + "github.com/pkg/errors" + "github.com/replicatedhq/kots/pkg/helmvm/types" + "github.com/replicatedhq/kots/pkg/logger" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + statsv1alpha1 "k8s.io/kubelet/pkg/apis/stats/v1alpha1" +) + +// GetNodes will get a list of nodes with stats +func GetNodes(client kubernetes.Interface) (*types.HelmVMNodes, error) { + nodes, err := client.CoreV1().Nodes().List(context.TODO(), metav1.ListOptions{}) + if err != nil { + return nil, errors.Wrap(err, "list nodes") + } + + toReturn := types.HelmVMNodes{} + + for _, node := range nodes.Items { + cpuCapacity := types.CapacityAvailable{} + memoryCapacity := types.CapacityAvailable{} + podCapacity := types.CapacityAvailable{} + + memoryCapacity.Capacity = float64(node.Status.Capacity.Memory().Value()) / math.Pow(2, 30) // capacity in GB + + cpuCapacity.Capacity, err = strconv.ParseFloat(node.Status.Capacity.Cpu().String(), 64) + if err != nil { + return nil, errors.Wrapf(err, "parse CPU capacity %q for node %s", node.Status.Capacity.Cpu().String(), node.Name) + } + + podCapacity.Capacity = float64(node.Status.Capacity.Pods().Value()) + + nodeIP := "" + for _, address := range node.Status.Addresses { + if address.Type == corev1.NodeInternalIP { + nodeIP = address.Address + } + } + + if nodeIP == "" { + logger.Infof("Did not find address for node %s, %+v", node.Name, node.Status.Addresses) + } else { + nodeMetrics, err := getNodeMetrics(nodeIP) + if err != nil { + logger.Infof("Got error retrieving stats for node %q: %v", node.Name, err) + } else { + if nodeMetrics.Node.Memory != nil && nodeMetrics.Node.Memory.AvailableBytes != nil { + memoryCapacity.Available = float64(*nodeMetrics.Node.Memory.AvailableBytes) / math.Pow(2, 30) + } + + if nodeMetrics.Node.CPU != nil && nodeMetrics.Node.CPU.UsageNanoCores != nil { + cpuCapacity.Available = cpuCapacity.Capacity - (float64(*nodeMetrics.Node.CPU.UsageNanoCores) / math.Pow(10, 9)) + } + + podCapacity.Available = podCapacity.Capacity - float64(len(nodeMetrics.Pods)) + } + } + + nodeLabelArray := []string{} + for k, v := range node.Labels { + nodeLabelArray = append(nodeLabelArray, fmt.Sprintf("%s:%s", k, v)) + } + + toReturn.Nodes = append(toReturn.Nodes, types.Node{ + Name: node.Name, + IsConnected: isConnected(node), + IsReady: isReady(node), + IsPrimaryNode: isPrimary(node), + CanDelete: node.Spec.Unschedulable && !isConnected(node), + KubeletVersion: node.Status.NodeInfo.KubeletVersion, + CPU: cpuCapacity, + Memory: memoryCapacity, + Pods: podCapacity, + Labels: nodeLabelArray, + Conditions: findNodeConditions(node.Status.Conditions), + }) + } + + isHelmVM, err := IsHelmVM(client) + if err != nil { + return nil, errors.Wrap(err, "is helmvm") + } + toReturn.IsHelmVMEnabled = isHelmVM + + isHA, err := IsHA(client) + if err != nil { + return nil, errors.Wrap(err, "is ha") + } + toReturn.HA = isHA + + return &toReturn, nil +} + +func findNodeConditions(conditions []corev1.NodeCondition) types.NodeConditions { + discoveredConditions := types.NodeConditions{} + for _, condition := range conditions { + if condition.Type == "MemoryPressure" { + discoveredConditions.MemoryPressure = condition.Status == corev1.ConditionTrue + } + if condition.Type == "DiskPressure" { + discoveredConditions.DiskPressure = condition.Status == corev1.ConditionTrue + } + if condition.Type == "PIDPressure" { + discoveredConditions.PidPressure = condition.Status == corev1.ConditionTrue + } + if condition.Type == "Ready" { + discoveredConditions.Ready = condition.Status == corev1.ConditionTrue + } + } + return discoveredConditions +} + +// get kubelet PKI info from /etc/kubernetes/pki/kubelet, use it to hit metrics server at `http://${nodeIP}:10255/stats/summary` +func getNodeMetrics(nodeIP string) (*statsv1alpha1.Summary, error) { + client := http.Client{ + Timeout: time.Second, + } + port := 10255 + + // only use mutual TLS if client cert exists + _, err := os.ReadFile("/etc/kubernetes/pki/kubelet/client.crt") + if err == nil { + cert, err := tls.LoadX509KeyPair("/etc/kubernetes/pki/kubelet/client.crt", "/etc/kubernetes/pki/kubelet/client.key") + if err != nil { + return nil, errors.Wrap(err, "get client keypair") + } + + // this will leak memory + client.Transport = &http.Transport{ + TLSClientConfig: &tls.Config{ + Certificates: []tls.Certificate{cert}, + InsecureSkipVerify: true, + }, + } + port = 10250 + } + + r, err := client.Get(fmt.Sprintf("https://%s:%d/stats/summary", nodeIP, port)) + if err != nil { + return nil, errors.Wrapf(err, "get node %s stats", nodeIP) + } + defer r.Body.Close() + + body, err := io.ReadAll(r.Body) + if err != nil { + return nil, errors.Wrapf(err, "read node %s stats response", nodeIP) + } + + summary := statsv1alpha1.Summary{} + err = json.Unmarshal(body, &summary) + if err != nil { + return nil, errors.Wrapf(err, "parse node %s stats response", nodeIP) + } + + return &summary, nil +} + +func isConnected(node corev1.Node) bool { + for _, taint := range node.Spec.Taints { + if taint.Key == "node.kubernetes.io/unreachable" { + return false + } + } + + return true +} + +func isReady(node corev1.Node) bool { + for _, condition := range node.Status.Conditions { + if condition.Type == "Ready" { + return condition.Status == corev1.ConditionTrue + } + } + + return false +} + +func isPrimary(node corev1.Node) bool { + for label := range node.ObjectMeta.Labels { + if label == "node-role.kubernetes.io/master" { + return true + } + if label == "node-role.kubernetes.io/control-plane" { + return true + } + } + + return false +} + +func internalIP(node corev1.Node) string { + for _, address := range node.Status.Addresses { + if address.Type == corev1.NodeInternalIP { + return address.Address + } + } + return "" +} diff --git a/pkg/helmvm/node_join.go b/pkg/helmvm/node_join.go new file mode 100644 index 0000000000..6aad6255a9 --- /dev/null +++ b/pkg/helmvm/node_join.go @@ -0,0 +1,12 @@ +package helmvm + +import ( + "time" + + "k8s.io/client-go/kubernetes" +) + +// GenerateAddNodeCommand will generate the HelmVM node add command for a primary or secondary node +func GenerateAddNodeCommand(client kubernetes.Interface, primary bool) ([]string, *time.Time, error) { + return nil, nil, nil +} diff --git a/pkg/helmvm/types/types.go b/pkg/helmvm/types/types.go new file mode 100644 index 0000000000..c298dfbd93 --- /dev/null +++ b/pkg/helmvm/types/types.go @@ -0,0 +1,33 @@ +package types + +type HelmVMNodes struct { + Nodes []Node `json:"nodes"` + HA bool `json:"ha"` + IsHelmVMEnabled bool `json:"isHelmVMEnabled"` +} + +type Node struct { + Name string `json:"name"` + IsConnected bool `json:"isConnected"` + IsReady bool `json:"isReady"` + IsPrimaryNode bool `json:"isPrimaryNode"` + CanDelete bool `json:"canDelete"` + KubeletVersion string `json:"kubeletVersion"` + CPU CapacityAvailable `json:"cpu"` + Memory CapacityAvailable `json:"memory"` + Pods CapacityAvailable `json:"pods"` + Labels []string `json:"labels"` + Conditions NodeConditions `json:"conditions"` +} + +type CapacityAvailable struct { + Capacity float64 `json:"capacity"` + Available float64 `json:"available"` +} + +type NodeConditions struct { + MemoryPressure bool `json:"memoryPressure"` + DiskPressure bool `json:"diskPressure"` + PidPressure bool `json:"pidPressure"` + Ready bool `json:"ready"` +} diff --git a/pkg/helmvm/util.go b/pkg/helmvm/util.go new file mode 100644 index 0000000000..7d2817f93e --- /dev/null +++ b/pkg/helmvm/util.go @@ -0,0 +1,13 @@ +package helmvm + +import ( + "k8s.io/client-go/kubernetes" +) + +func IsHelmVM(clientset kubernetes.Interface) (bool, error) { + return false, nil +} + +func IsHA(clientset kubernetes.Interface) (bool, error) { + return false, nil +} diff --git a/pkg/k8sutil/kotsadm.go b/pkg/k8sutil/kotsadm.go index bc101e168b..a3b625bf50 100644 --- a/pkg/k8sutil/kotsadm.go +++ b/pkg/k8sutil/kotsadm.go @@ -9,6 +9,7 @@ import ( types "github.com/replicatedhq/kots/pkg/k8sutil/types" kotsadmtypes "github.com/replicatedhq/kots/pkg/kotsadm/types" "github.com/replicatedhq/kots/pkg/util" + "github.com/segmentio/ksuid" corev1 "k8s.io/api/core/v1" kuberneteserrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -81,11 +82,23 @@ func IsKotsadmClusterScoped(ctx context.Context, clientset kubernetes.Interface, return false } -func GetKotsadmIDConfigMap() (*corev1.ConfigMap, error) { - clientset, err := GetClientset() - if err != nil { - return nil, errors.Wrap(err, "failed to get clientset") +func GetKotsadmID(clientset kubernetes.Interface) string { + var clusterID string + configMap, err := GetKotsadmIDConfigMap(clientset) + // if configmap is not found, generate a new guid and create a new configmap, if configmap is found, use the existing guid, otherwise generate + if err != nil && !kuberneteserrors.IsNotFound(err) { + clusterID = ksuid.New().String() + } else if configMap != nil { + clusterID = configMap.Data["id"] + } else { + // configmap is missing for some reason, recreate with new guid, this will appear as a new instance in the report + clusterID = ksuid.New().String() + CreateKotsadmIDConfigMap(clientset, clusterID) } + return clusterID +} + +func GetKotsadmIDConfigMap(clientset kubernetes.Interface) (*corev1.ConfigMap, error) { namespace := util.PodNamespace existingConfigmap, err := clientset.CoreV1().ConfigMaps(namespace).Get(context.TODO(), KotsadmIDConfigMapName, metav1.GetOptions{}) if err != nil && !kuberneteserrors.IsNotFound(err) { @@ -96,12 +109,8 @@ func GetKotsadmIDConfigMap() (*corev1.ConfigMap, error) { return existingConfigmap, nil } -func CreateKotsadmIDConfigMap(kotsadmID string) error { +func CreateKotsadmIDConfigMap(clientset kubernetes.Interface, kotsadmID string) error { var err error = nil - clientset, err := GetClientset() - if err != nil { - return err - } configmap := corev1.ConfigMap{ TypeMeta: metav1.TypeMeta{ APIVersion: "v1", @@ -136,11 +145,7 @@ func IsKotsadmIDConfigMapPresent() (bool, error) { return true, nil } -func UpdateKotsadmIDConfigMap(kotsadmID string) error { - clientset, err := GetClientset() - if err != nil { - return errors.Wrap(err, "failed to get clientset") - } +func UpdateKotsadmIDConfigMap(clientset kubernetes.Interface, kotsadmID string) error { namespace := util.PodNamespace existingConfigMap, err := clientset.CoreV1().ConfigMaps(namespace).Get(context.TODO(), KotsadmIDConfigMapName, metav1.GetOptions{}) if err != nil && !kuberneteserrors.IsNotFound(err) { diff --git a/pkg/k8sutil/kotsadm_test.go b/pkg/k8sutil/kotsadm_test.go new file mode 100644 index 0000000000..455bb34fa4 --- /dev/null +++ b/pkg/k8sutil/kotsadm_test.go @@ -0,0 +1,62 @@ +package k8sutil + +import ( + "context" + "testing" + + "gopkg.in/go-playground/assert.v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/fake" +) + +func TestGetKotsadmID(t *testing.T) { + + type args struct { + clientset kubernetes.Interface + } + tests := []struct { + name string + args args + want string + shouldCreateConfigMap bool + }{ + { + name: "configmap exists", + args: args{ + clientset: fake.NewSimpleClientset(&corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: KotsadmIDConfigMapName}, + Data: map[string]string{"id": "cluster-id"}, + }), + }, + want: "cluster-id", + shouldCreateConfigMap: false, + }, + { + name: "configmap does not exist, should create", + args: args{ + clientset: fake.NewSimpleClientset(), + }, + want: "", + shouldCreateConfigMap: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := GetKotsadmID(tt.args.clientset) + if tt.want != "" { + assert.Equal(t, tt.want, got) + } else { + // a random uuid is generated + assert.NotEqual(t, "", got) + } + + if tt.shouldCreateConfigMap { + // should have created the configmap if it didn't exist + _, err := tt.args.clientset.CoreV1().ConfigMaps("").Get(context.TODO(), KotsadmIDConfigMapName, metav1.GetOptions{}) + assert.Equal(t, nil, err) + } + }) + } +} diff --git a/pkg/kotsadm/metadata.go b/pkg/kotsadm/metadata.go index 0e93bf2c38..9c9b045cb0 100644 --- a/pkg/kotsadm/metadata.go +++ b/pkg/kotsadm/metadata.go @@ -1,6 +1,7 @@ package kotsadm import ( + "github.com/replicatedhq/kots/pkg/helmvm" "github.com/replicatedhq/kots/pkg/kotsadm/types" "github.com/replicatedhq/kots/pkg/kurl" "k8s.io/client-go/kubernetes" @@ -8,9 +9,12 @@ import ( func GetMetadata(clientset kubernetes.Interface) types.Metadata { isKurl, _ := kurl.IsKurl(clientset) + isHelmVM, _ := helmvm.IsHelmVM(clientset) + metadata := types.Metadata{ IsAirgap: IsAirgap(), IsKurl: isKurl, + IsHelmVM: isHelmVM, } return metadata diff --git a/pkg/kotsadm/types/metadata.go b/pkg/kotsadm/types/metadata.go index 9e2586ec15..79e8b142a6 100644 --- a/pkg/kotsadm/types/metadata.go +++ b/pkg/kotsadm/types/metadata.go @@ -3,4 +3,5 @@ package types type Metadata struct { IsAirgap bool IsKurl bool + IsHelmVM bool } diff --git a/pkg/kotsadmupstream/upstream.go b/pkg/kotsadmupstream/upstream.go index 7d2f6eb4f4..1e2e77e5fd 100644 --- a/pkg/kotsadmupstream/upstream.go +++ b/pkg/kotsadmupstream/upstream.go @@ -222,6 +222,7 @@ func DownloadUpdate(appID string, update types.Update, skipPreflights bool, skip ExcludeAdminConsole: true, CreateAppDir: false, ReportWriter: pipeWriter, + AppID: a.ID, AppSlug: a.Slug, AppSequence: appSequence, IsGitOps: a.IsGitOps, diff --git a/pkg/kotsutil/kots.go b/pkg/kotsutil/kots.go index a26490eaf8..440a10ebba 100644 --- a/pkg/kotsutil/kots.go +++ b/pkg/kotsutil/kots.go @@ -75,25 +75,6 @@ type OverlySimpleMetadata struct { Namespace string `yaml:"namespace"` } -// HelmChartInterface represents any kots.io HelmChart (v1beta1 or v1beta2) -type HelmChartInterface interface { - GetAPIVersion() string - GetChartName() string - GetChartVersion() string - GetReleaseName() string - GetDirName() string - GetNamespace() string - GetUpgradeFlags() []string - GetWeight() int64 - GetHelmVersion() string - GetBuilderValues() (map[string]interface{}, error) - SetChartNamespace(namespace string) -} - -// v1beta1 and v1beta2 HelmChart structs must implement HelmChartInterface -var _ HelmChartInterface = (*kotsv1beta1.HelmChart)(nil) -var _ HelmChartInterface = (*kotsv1beta2.HelmChart)(nil) - // KotsKinds are all of the special "client-side" kinds that are packaged in // an application. These should be pointers because they are all optional. // But a few are still expected in the code later, so we make them not pointers, diff --git a/pkg/kotsutil/troubleshoot.go b/pkg/kotsutil/troubleshoot.go index 9f93a4d8a8..67a4d08fc8 100644 --- a/pkg/kotsutil/troubleshoot.go +++ b/pkg/kotsutil/troubleshoot.go @@ -2,7 +2,6 @@ package kotsutil import ( "context" - "io/ioutil" "os" "path/filepath" @@ -22,7 +21,7 @@ func LoadTSKindsFromPath(dir string) (*troubleshootloader.TroubleshootKinds, err return nil } - contents, err := ioutil.ReadFile(path) + contents, err := os.ReadFile(path) if err != nil { return errors.Wrap(err, "failed to read file") } diff --git a/pkg/kotsutil/yaml.go b/pkg/kotsutil/yaml.go index fb31c73d5f..d613deedd0 100644 --- a/pkg/kotsutil/yaml.go +++ b/pkg/kotsutil/yaml.go @@ -2,10 +2,13 @@ package kotsutil import ( "bytes" + "fmt" + "strings" "github.com/pkg/errors" "github.com/replicatedhq/kots/pkg/util" yaml "github.com/replicatedhq/yaml/v3" + goyaml "gopkg.in/yaml.v3" k8syaml "sigs.k8s.io/yaml" ) @@ -85,3 +88,159 @@ func removeNilFieldsFromMap(input map[string]interface{}) bool { return removedItems } + +func MergeYAMLNodes(targetNodes []*goyaml.Node, overrideNodes []*goyaml.Node) []*goyaml.Node { + // Since inputs are arrays and not maps, we need to: + // 1. Copy all keys in targetNodes, overriding the ones that match from overrideNodes + // 2. Add all keys from overrideNodes that don't exist in targetNodes + + if len(overrideNodes) == 0 { + return targetNodes + } + + if len(targetNodes) == 0 { + return overrideNodes + } + + // Special case where top level node is either a mapping node or an array + if len(targetNodes) == 1 && len(overrideNodes) == 1 { + if targetNodes[0].Kind == goyaml.MappingNode && overrideNodes[0].Kind == goyaml.MappingNode { + return []*goyaml.Node{ + { + Kind: goyaml.MappingNode, + Content: MergeYAMLNodes(targetNodes[0].Content, overrideNodes[0].Content), + }, + } + } + + if targetNodes[0].Value == overrideNodes[0].Value { + return overrideNodes + } + + return append(targetNodes, overrideNodes...) + } + + // 1. Copy all keys in targetNodes, overriding the ones that match from overrideNodes + newNodes := make([]*goyaml.Node, 0) + for i := 0; i < len(targetNodes)-1; i += 2 { + var additionalNode *goyaml.Node + for j := 0; j < len(overrideNodes)-1; j += 2 { + nodeNameI := targetNodes[i] + nodeValueI := targetNodes[i+1] + + nodeNameJ := overrideNodes[j] + nodeValueJ := overrideNodes[j+1] + + if nodeNameI.Value != nodeNameJ.Value { + continue + } + + additionalNode = &goyaml.Node{ + Kind: nodeValueJ.Kind, + Tag: nodeValueJ.Tag, + Line: nodeValueJ.Line, + Style: nodeValueJ.Style, + Anchor: nodeValueJ.Anchor, + Value: nodeValueJ.Value, + Alias: nodeValueJ.Alias, + HeadComment: nodeValueJ.HeadComment, + LineComment: nodeValueJ.LineComment, + FootComment: nodeValueJ.FootComment, + Column: nodeValueJ.Column, + } + + if nodeValueI.Kind == goyaml.MappingNode && nodeValueJ.Kind == goyaml.MappingNode { + additionalNode.Content = MergeYAMLNodes(nodeValueI.Content, nodeValueJ.Content) + } else { + additionalNode.Content = nodeValueJ.Content + } + + break + } + + if additionalNode != nil { + newNodes = append(newNodes, targetNodes[i], additionalNode) + } else { + newNodes = append(newNodes, targetNodes[i], targetNodes[i+1]) + } + } + + // 2. Add all keys from overrideNodes that don't exist in targetNodes + for j := 0; j < len(overrideNodes)-1; j += 2 { + isFound := false + for i := 0; i < len(newNodes)-1; i += 2 { + nodeNameI := newNodes[i] + nodeValueI := newNodes[i+1] + + additionalNodeName := overrideNodes[j] + additionalNodeValue := overrideNodes[j+1] + + if nodeNameI.Value != additionalNodeName.Value { + continue + } + + if nodeValueI.Kind == goyaml.MappingNode && additionalNodeValue.Kind == goyaml.MappingNode { + nodeValueI.Content = MergeYAMLNodes(nodeValueI.Content, additionalNodeValue.Content) + } + + isFound = true + break + } + + if !isFound { + newNodes = append(newNodes, overrideNodes[j], overrideNodes[j+1]) + } + } + + return newNodes +} + +func ContentToDocNode(doc *goyaml.Node, nodes []*goyaml.Node) *goyaml.Node { + if doc == nil { + return &goyaml.Node{ + Kind: goyaml.DocumentNode, + Content: nodes, + } + } + return &goyaml.Node{ + Kind: doc.Kind, + Tag: doc.Tag, + Line: doc.Line, + Style: doc.Style, + Anchor: doc.Anchor, + Value: doc.Value, + Alias: doc.Alias, + HeadComment: doc.HeadComment, + LineComment: doc.LineComment, + FootComment: doc.FootComment, + Column: doc.Column, + Content: nodes, + } +} + +func NodeToYAML(node *goyaml.Node) ([]byte, error) { + var renderedContents bytes.Buffer + yamlEncoder := goyaml.NewEncoder(&renderedContents) + yamlEncoder.SetIndent(2) // this may change indentations of the original values.yaml, but this matches out tests + err := yamlEncoder.Encode(node) + if err != nil { + return nil, errors.Wrap(err, "marshal") + } + + return renderedContents.Bytes(), nil +} + +// Handy functions for printing YAML nodes +func PrintNodes(nodes []*goyaml.Node, i int) { + for _, n := range nodes { + PrintNode(n, i) + } +} +func PrintNode(n *goyaml.Node, i int) { + if n == nil { + return + } + indent := strings.Repeat(" ", i*2) + fmt.Printf("%stag:%v, style:%v, kind:%v, value:%v\n", indent, n.Tag, n.Style, n.Kind, n.Value) + PrintNodes(n.Content, i+1) +} diff --git a/pkg/kurl/kurl_nodes.go b/pkg/kurl/kurl_nodes.go index bf85770c19..b70158e29d 100644 --- a/pkg/kurl/kurl_nodes.go +++ b/pkg/kurl/kurl_nodes.go @@ -8,6 +8,7 @@ import ( "io/ioutil" "math" "net/http" + "os" "strconv" "time" @@ -140,7 +141,7 @@ func getNodeMetrics(nodeIP string) (*statsv1alpha1.Summary, error) { port := 10255 // only use mutual TLS if client cert exists - _, err := ioutil.ReadFile("/etc/kubernetes/pki/kubelet/client.crt") + _, err := os.ReadFile("/etc/kubernetes/pki/kubelet/client.crt") if err == nil { cert, err := tls.LoadX509KeyPair("/etc/kubernetes/pki/kubelet/client.crt", "/etc/kubernetes/pki/kubelet/client.key") if err != nil { diff --git a/pkg/online/online.go b/pkg/online/online.go index a7435fb1a9..2d91e585e4 100644 --- a/pkg/online/online.go +++ b/pkg/online/online.go @@ -151,6 +151,7 @@ func CreateAppFromOnline(opts CreateOnlineAppOpts) (_ *kotsutil.KotsKinds, final ConfigFile: configFile, IdentityConfigFile: identityConfigFile, ReportWriter: pipeWriter, + AppID: opts.PendingApp.ID, AppSlug: opts.PendingApp.Slug, AppSequence: 0, AppVersionLabel: opts.PendingApp.VersionLabel, diff --git a/pkg/operator/client/client.go b/pkg/operator/client/client.go index 59dacf3c70..dfa64abdb5 100644 --- a/pkg/operator/client/client.go +++ b/pkg/operator/client/client.go @@ -22,7 +22,6 @@ import ( appstatetypes "github.com/replicatedhq/kots/pkg/appstate/types" "github.com/replicatedhq/kots/pkg/binaries" "github.com/replicatedhq/kots/pkg/k8sutil" - "github.com/replicatedhq/kots/pkg/kotsutil" "github.com/replicatedhq/kots/pkg/logger" "github.com/replicatedhq/kots/pkg/operator/applier" operatortypes "github.com/replicatedhq/kots/pkg/operator/types" @@ -32,6 +31,7 @@ import ( "github.com/replicatedhq/kots/pkg/supportbundle" supportbundletypes "github.com/replicatedhq/kots/pkg/supportbundle/types" "github.com/replicatedhq/kots/pkg/util" + "github.com/replicatedhq/kotskinds/pkg/helmchart" "go.uber.org/zap" ) @@ -285,7 +285,7 @@ func (c *Client) deployHelmCharts(deployArgs operatortypes.DeployAppArgs) (*comm defer os.RemoveAll(curV1Beta2HelmDir) // find removed charts - prevKotsV1Beta1Charts := []kotsutil.HelmChartInterface{} + prevKotsV1Beta1Charts := []helmchart.HelmChartInterface{} if deployArgs.PreviousKotsKinds != nil && deployArgs.PreviousKotsKinds.V1Beta1HelmCharts != nil { for _, kotsChart := range deployArgs.PreviousKotsKinds.V1Beta1HelmCharts.Items { kc := kotsChart @@ -293,7 +293,7 @@ func (c *Client) deployHelmCharts(deployArgs operatortypes.DeployAppArgs) (*comm } } - curV1Beta1KotsCharts := []kotsutil.HelmChartInterface{} + curV1Beta1KotsCharts := []helmchart.HelmChartInterface{} if deployArgs.KotsKinds != nil && deployArgs.KotsKinds.V1Beta1HelmCharts != nil { for _, kotsChart := range deployArgs.KotsKinds.V1Beta1HelmCharts.Items { kc := kotsChart @@ -301,7 +301,7 @@ func (c *Client) deployHelmCharts(deployArgs operatortypes.DeployAppArgs) (*comm } } - prevKotsV1Beta2Charts := []kotsutil.HelmChartInterface{} + prevKotsV1Beta2Charts := []helmchart.HelmChartInterface{} if deployArgs.PreviousKotsKinds != nil && deployArgs.PreviousKotsKinds.V1Beta2HelmCharts != nil { for _, kotsChart := range deployArgs.PreviousKotsKinds.V1Beta2HelmCharts.Items { kc := kotsChart @@ -309,7 +309,7 @@ func (c *Client) deployHelmCharts(deployArgs operatortypes.DeployAppArgs) (*comm } } - curV1Beta2KotsCharts := []kotsutil.HelmChartInterface{} + curV1Beta2KotsCharts := []helmchart.HelmChartInterface{} if deployArgs.KotsKinds != nil && deployArgs.KotsKinds.V1Beta2HelmCharts != nil { for _, kotsChart := range deployArgs.KotsKinds.V1Beta2HelmCharts.Items { kc := kotsChart @@ -446,7 +446,7 @@ func (c *Client) undeployManifests(undeployArgs operatortypes.UndeployAppArgs) e } func (c *Client) undeployHelmCharts(undeployArgs operatortypes.UndeployAppArgs) error { - kotsCharts := []kotsutil.HelmChartInterface{} + kotsCharts := []helmchart.HelmChartInterface{} if undeployArgs.KotsKinds != nil { if undeployArgs.KotsKinds.V1Beta1HelmCharts != nil { for _, v1Beta1Chart := range undeployArgs.KotsKinds.V1Beta1HelmCharts.Items { diff --git a/pkg/operator/client/deploy.go b/pkg/operator/client/deploy.go index ac43bb28f9..4d2f881e3a 100644 --- a/pkg/operator/client/deploy.go +++ b/pkg/operator/client/deploy.go @@ -18,11 +18,11 @@ import ( "github.com/replicatedhq/kots/pkg/archives" "github.com/replicatedhq/kots/pkg/helm" "github.com/replicatedhq/kots/pkg/k8sutil" - "github.com/replicatedhq/kots/pkg/kotsutil" "github.com/replicatedhq/kots/pkg/logger" "github.com/replicatedhq/kots/pkg/operator/applier" operatortypes "github.com/replicatedhq/kots/pkg/operator/types" "github.com/replicatedhq/kots/pkg/util" + "github.com/replicatedhq/kotskinds/pkg/helmchart" "github.com/replicatedhq/yaml/v3" corev1 "k8s.io/api/core/v1" kuberneteserrors "k8s.io/apimachinery/pkg/api/errors" @@ -250,7 +250,7 @@ func (c *Client) ensureResourcesPresent(deployArgs operatortypes.DeployAppArgs) return &deployRes, nil } -func (c *Client) installWithHelm(v1Beta1ChartsDir, v1beta2ChartsDir string, kotsCharts []kotsutil.HelmChartInterface) (*commandResult, error) { +func (c *Client) installWithHelm(v1Beta1ChartsDir, v1beta2ChartsDir string, kotsCharts []helmchart.HelmChartInterface) (*commandResult, error) { orderedDirs, err := getSortedCharts(v1Beta1ChartsDir, v1beta2ChartsDir, kotsCharts, c.TargetNamespace, false) if err != nil { return nil, errors.Wrap(err, "failed to get sorted charts") @@ -331,7 +331,7 @@ type orderedDir struct { APIVersion string } -func getSortedCharts(v1Beta1ChartsDir string, v1Beta2ChartsDir string, kotsCharts []kotsutil.HelmChartInterface, targetNamespace string, isUninstall bool) ([]orderedDir, error) { +func getSortedCharts(v1Beta1ChartsDir string, v1Beta2ChartsDir string, kotsCharts []helmchart.HelmChartInterface, targetNamespace string, isUninstall bool) ([]orderedDir, error) { // get a list of the chart directories foundDirs := []orderedDir{} @@ -505,7 +505,7 @@ func findChartNameAndVersionInArchive(archivePath string) (string, string, error return findChartNameAndVersion(tmpDir) } -func (c *Client) uninstallWithHelm(v1Beta1ChartsDir, v1Beta2ChartsDir string, kotsCharts []kotsutil.HelmChartInterface) error { +func (c *Client) uninstallWithHelm(v1Beta1ChartsDir, v1Beta2ChartsDir string, kotsCharts []helmchart.HelmChartInterface) error { orderedDirs, err := getSortedCharts(v1Beta1ChartsDir, v1Beta2ChartsDir, kotsCharts, c.TargetNamespace, true) if err != nil { return errors.Wrap(err, "failed to get sorted charts") @@ -540,17 +540,17 @@ func (c *Client) uninstallWithHelm(v1Beta1ChartsDir, v1Beta2ChartsDir string, ko type getRemovedChartsOptions struct { prevV1Beta1Dir string curV1Beta1Dir string - previousV1Beta1KotsCharts []kotsutil.HelmChartInterface - currentV1Beta1KotsCharts []kotsutil.HelmChartInterface + previousV1Beta1KotsCharts []helmchart.HelmChartInterface + currentV1Beta1KotsCharts []helmchart.HelmChartInterface prevV1Beta2Dir string curV1Beta2Dir string - previousV1Beta2KotsCharts []kotsutil.HelmChartInterface - currentV1Beta2KotsCharts []kotsutil.HelmChartInterface + previousV1Beta2KotsCharts []helmchart.HelmChartInterface + currentV1Beta2KotsCharts []helmchart.HelmChartInterface } // getRemovedCharts returns a list of helm release names that were removed in the current version -func getRemovedCharts(opts getRemovedChartsOptions) ([]kotsutil.HelmChartInterface, error) { - prevCharts := []kotsutil.HelmChartInterface{} +func getRemovedCharts(opts getRemovedChartsOptions) ([]helmchart.HelmChartInterface, error) { + prevCharts := []helmchart.HelmChartInterface{} if opts.prevV1Beta1Dir != "" { prevV1Beta1ChartsDir := filepath.Join(opts.prevV1Beta1Dir, "charts") @@ -570,7 +570,7 @@ func getRemovedCharts(opts getRemovedChartsOptions) ([]kotsutil.HelmChartInterfa prevCharts = append(prevCharts, matching...) } - curCharts := []kotsutil.HelmChartInterface{} + curCharts := []helmchart.HelmChartInterface{} if opts.curV1Beta1Dir != "" { curV1Beta1ChartsDir := filepath.Join(opts.curV1Beta1Dir, "charts") @@ -590,7 +590,7 @@ func getRemovedCharts(opts getRemovedChartsOptions) ([]kotsutil.HelmChartInterfa curCharts = append(curCharts, matching...) } - removedCharts := []kotsutil.HelmChartInterface{} + removedCharts := []helmchart.HelmChartInterface{} for _, prevChart := range prevCharts { found := false for _, curChart := range curCharts { @@ -612,13 +612,13 @@ func getRemovedCharts(opts getRemovedChartsOptions) ([]kotsutil.HelmChartInterfa return removedCharts, nil } -func findMatchingHelmCharts(chartsDir string, kotsCharts []kotsutil.HelmChartInterface) ([]kotsutil.HelmChartInterface, error) { +func findMatchingHelmCharts(chartsDir string, kotsCharts []helmchart.HelmChartInterface) ([]helmchart.HelmChartInterface, error) { dirContent, err := ioutil.ReadDir(chartsDir) if err != nil { return nil, errors.Wrapf(err, "failed to list chart dir %s", chartsDir) } - matching := []kotsutil.HelmChartInterface{} + matching := []helmchart.HelmChartInterface{} for _, kotsChart := range kotsCharts { for _, f := range dirContent { diff --git a/pkg/operator/client/deploy_test.go b/pkg/operator/client/deploy_test.go index 15c2b168a4..98a3535b87 100644 --- a/pkg/operator/client/deploy_test.go +++ b/pkg/operator/client/deploy_test.go @@ -12,9 +12,9 @@ import ( "github.com/mholt/archiver/v3" "github.com/pmezard/go-difflib/difflib" - "github.com/replicatedhq/kots/pkg/kotsutil" "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" "github.com/replicatedhq/kotskinds/apis/kots/v1beta2" + "github.com/replicatedhq/kotskinds/pkg/helmchart" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -28,7 +28,7 @@ func Test_getSortedCharts(t *testing.T) { name string v1Beta1Files []file v1Beta2Files []file - kotsCharts []kotsutil.HelmChartInterface + kotsCharts []helmchart.HelmChartInterface targetNamespace string isUninstall bool want []orderedDir @@ -79,7 +79,7 @@ version: "v1" `, }, }, - kotsCharts: []kotsutil.HelmChartInterface{ + kotsCharts: []helmchart.HelmChartInterface{ &v1beta1.HelmChart{ TypeMeta: metav1.TypeMeta{ APIVersion: "kots.io/v1beta1", @@ -214,7 +214,7 @@ version: "v1" `, }, }, - kotsCharts: []kotsutil.HelmChartInterface{ + kotsCharts: []helmchart.HelmChartInterface{ &v1beta1.HelmChart{ TypeMeta: metav1.TypeMeta{ APIVersion: "kots.io/v1beta1", @@ -332,7 +332,7 @@ version: ver1 `, }, }, - kotsCharts: []kotsutil.HelmChartInterface{ + kotsCharts: []helmchart.HelmChartInterface{ &v1beta1.HelmChart{ TypeMeta: metav1.TypeMeta{ APIVersion: "kots.io/v1beta1", @@ -401,7 +401,7 @@ version: ver2 `, }, }, - kotsCharts: []kotsutil.HelmChartInterface{ + kotsCharts: []helmchart.HelmChartInterface{ &v1beta1.HelmChart{ TypeMeta: metav1.TypeMeta{ APIVersion: "kots.io/v1beta1", @@ -472,7 +472,7 @@ version: ver2 `, }, }, - kotsCharts: []kotsutil.HelmChartInterface{ + kotsCharts: []helmchart.HelmChartInterface{ &v1beta1.HelmChart{ TypeMeta: metav1.TypeMeta{ APIVersion: "kots.io/v1beta1", @@ -526,7 +526,7 @@ version: ver2 `, }, }, - kotsCharts: []kotsutil.HelmChartInterface{ + kotsCharts: []helmchart.HelmChartInterface{ &v1beta1.HelmChart{ TypeMeta: metav1.TypeMeta{ APIVersion: "kots.io/v1beta1", @@ -599,7 +599,7 @@ version: ver2 `, }, }, - kotsCharts: []kotsutil.HelmChartInterface{ + kotsCharts: []helmchart.HelmChartInterface{ &v1beta1.HelmChart{ TypeMeta: metav1.TypeMeta{ APIVersion: "kots.io/v1beta1", @@ -668,7 +668,7 @@ version: ver2 contents: "H4sIFAAAAAAA/ykAK2FIUjBjSE02THk5NWIzVjBkUzVpWlM5Nk9WVjZNV2xqYW5keVRRbz1IZWxtAOzSsQoCMQwG4M59ij5B/dvECrf6Du4ZDixcq/TOA99eEF3O0YII+ZZ/yJA/kJJrLjLtjmdpi79LmUx3AJCYnwlgm8CeTWCO6UBEiQxCTBSMQ/8qn27zIs3g613b4/6EXPNpbHO+1MGt0VYp4+BeT2HX9wQePthfd1VKKdXPIwAA//8d5AfYAAgAAA==", }, }, - kotsCharts: []kotsutil.HelmChartInterface{ + kotsCharts: []helmchart.HelmChartInterface{ &v1beta2.HelmChart{ TypeMeta: metav1.TypeMeta{ APIVersion: "kots.io/v1beta2", @@ -716,7 +716,7 @@ version: ver2 contents: "H4sIFAAAAAAA/ykAK2FIUjBjSE02THk5NWIzVjBkUzVpWlM5Nk9WVjZNV2xqYW5keVRRbz1IZWxtAOzSsQoCMQwG4M59ij5B/dvECrf6Du4ZDixcq/TOA99eEF3O0YII+ZZ/yJA/kJJrLjLtjmdpi79LmUx3AJCYnwlgm8CeTWCO6UBEiQxCTBSMQ/8qn27zIs3g613b4/6EXPNpbHO+1MGt0VYp4+BeT2HX9wQePthfd1VKKdXPIwAA//8d5AfYAAgAAA==", }, }, - kotsCharts: []kotsutil.HelmChartInterface{ + kotsCharts: []helmchart.HelmChartInterface{ &v1beta2.HelmChart{ TypeMeta: metav1.TypeMeta{ APIVersion: "kots.io/v1beta2", @@ -799,7 +799,7 @@ version: ver2 contents: "H4sIFAAAAAAA/ykAK2FIUjBjSE02THk5NWIzVjBkUzVpWlM5Nk9WVjZNV2xqYW5keVRRbz1IZWxtAOzSsQoCMQwG4M59ij5B/dvECrf6Du4ZDixcq/TOA99eEF3O0YII+ZZ/yJA/kJJrLjLtjmdpi79LmUx3AJCYnwlgm8CeTWCO6UBEiQxCTBSMQ/8qn27zIs3g613b4/6EXPNpbHO+1MGt0VYp4+BeT2HX9wQePthfd1VKKdXPIwAA//8d5AfYAAgAAA==", }, }, - kotsCharts: []kotsutil.HelmChartInterface{ + kotsCharts: []helmchart.HelmChartInterface{ &v1beta1.HelmChart{ TypeMeta: metav1.TypeMeta{ APIVersion: "kots.io/v1beta1", @@ -916,7 +916,7 @@ version: ver2 contents: "H4sIFAAAAAAA/ykAK2FIUjBjSE02THk5NWIzVjBkUzVpWlM5Nk9WVjZNV2xqYW5keVRRbz1IZWxtAOzSsQoCMQwG4M59ij5B/dvECrf6Du4ZDixcq/TOA99eEF3O0YII+ZZ/yJA/kJJrLjLtjmdpi79LmUx3AJCYnwlgm8CeTWCO6UBEiQxCTBSMQ/8qn27zIs3g613b4/6EXPNpbHO+1MGt0VYp4+BeT2HX9wQePthfd1VKKdXPIwAA//8d5AfYAAgAAA==", }, }, - kotsCharts: []kotsutil.HelmChartInterface{ + kotsCharts: []helmchart.HelmChartInterface{ &v1beta1.HelmChart{ TypeMeta: metav1.TypeMeta{ APIVersion: "kots.io/v1beta1", @@ -1052,11 +1052,11 @@ func Test_getRemovedCharts(t *testing.T) { curV1Beta1Charts []chart prevV1Beta2Charts []chart curV1Beta2Charts []chart - previousV1Beta1KotsCharts []kotsutil.HelmChartInterface - currentV1Beta1KotsCharts []kotsutil.HelmChartInterface - previousV1Beta2KotsCharts []kotsutil.HelmChartInterface - currentV1Beta2KotsCharts []kotsutil.HelmChartInterface - want []kotsutil.HelmChartInterface + previousV1Beta1KotsCharts []helmchart.HelmChartInterface + currentV1Beta1KotsCharts []helmchart.HelmChartInterface + previousV1Beta2KotsCharts []helmchart.HelmChartInterface + currentV1Beta2KotsCharts []helmchart.HelmChartInterface + want []helmchart.HelmChartInterface }{ // ---- V1BETA1 ---- // { @@ -1085,7 +1085,7 @@ func Test_getRemovedCharts(t *testing.T) { dirName: "chart2-release", }, }, - previousV1Beta1KotsCharts: []kotsutil.HelmChartInterface{ + previousV1Beta1KotsCharts: []helmchart.HelmChartInterface{ &v1beta1.HelmChart{ TypeMeta: metav1.TypeMeta{ APIVersion: "kots.io/v1beta1", @@ -1119,7 +1119,7 @@ func Test_getRemovedCharts(t *testing.T) { }, }, }, - currentV1Beta1KotsCharts: []kotsutil.HelmChartInterface{ + currentV1Beta1KotsCharts: []helmchart.HelmChartInterface{ &v1beta1.HelmChart{ TypeMeta: metav1.TypeMeta{ APIVersion: "kots.io/v1beta1", @@ -1153,7 +1153,7 @@ func Test_getRemovedCharts(t *testing.T) { }, }, }, - want: []kotsutil.HelmChartInterface{}, + want: []helmchart.HelmChartInterface{}, }, { name: "v1beta1 -- chart1 removed", @@ -1176,7 +1176,7 @@ func Test_getRemovedCharts(t *testing.T) { dirName: "chart2-release", }, }, - previousV1Beta1KotsCharts: []kotsutil.HelmChartInterface{ + previousV1Beta1KotsCharts: []helmchart.HelmChartInterface{ &v1beta1.HelmChart{ TypeMeta: metav1.TypeMeta{ APIVersion: "kots.io/v1beta1", @@ -1210,7 +1210,7 @@ func Test_getRemovedCharts(t *testing.T) { }, }, }, - currentV1Beta1KotsCharts: []kotsutil.HelmChartInterface{ + currentV1Beta1KotsCharts: []helmchart.HelmChartInterface{ &v1beta1.HelmChart{ TypeMeta: metav1.TypeMeta{ APIVersion: "kots.io/v1beta1", @@ -1228,7 +1228,7 @@ func Test_getRemovedCharts(t *testing.T) { }, }, }, - want: []kotsutil.HelmChartInterface{ + want: []helmchart.HelmChartInterface{ &v1beta1.HelmChart{ TypeMeta: metav1.TypeMeta{ APIVersion: "kots.io/v1beta1", @@ -1273,7 +1273,7 @@ func Test_getRemovedCharts(t *testing.T) { dirName: "chart2-release", }, }, - previousV1Beta1KotsCharts: []kotsutil.HelmChartInterface{ + previousV1Beta1KotsCharts: []helmchart.HelmChartInterface{ &v1beta1.HelmChart{ TypeMeta: metav1.TypeMeta{ APIVersion: "kots.io/v1beta1", @@ -1307,7 +1307,7 @@ func Test_getRemovedCharts(t *testing.T) { }, }, }, - currentV1Beta1KotsCharts: []kotsutil.HelmChartInterface{ + currentV1Beta1KotsCharts: []helmchart.HelmChartInterface{ &v1beta1.HelmChart{ TypeMeta: metav1.TypeMeta{ APIVersion: "kots.io/v1beta1", @@ -1341,7 +1341,7 @@ func Test_getRemovedCharts(t *testing.T) { }, }, }, - want: []kotsutil.HelmChartInterface{}, + want: []helmchart.HelmChartInterface{}, }, { name: "v1beta1 -- chart2 old release removed because release name changed", @@ -1369,7 +1369,7 @@ func Test_getRemovedCharts(t *testing.T) { dirName: "chart2-new-release", }, }, - previousV1Beta1KotsCharts: []kotsutil.HelmChartInterface{ + previousV1Beta1KotsCharts: []helmchart.HelmChartInterface{ &v1beta1.HelmChart{ TypeMeta: metav1.TypeMeta{ APIVersion: "kots.io/v1beta1", @@ -1403,7 +1403,7 @@ func Test_getRemovedCharts(t *testing.T) { }, }, }, - currentV1Beta1KotsCharts: []kotsutil.HelmChartInterface{ + currentV1Beta1KotsCharts: []helmchart.HelmChartInterface{ &v1beta1.HelmChart{ TypeMeta: metav1.TypeMeta{ APIVersion: "kots.io/v1beta1", @@ -1437,7 +1437,7 @@ func Test_getRemovedCharts(t *testing.T) { }, }, }, - want: []kotsutil.HelmChartInterface{ + want: []helmchart.HelmChartInterface{ &v1beta1.HelmChart{ TypeMeta: metav1.TypeMeta{ APIVersion: "kots.io/v1beta1", @@ -1482,7 +1482,7 @@ func Test_getRemovedCharts(t *testing.T) { dirName: "chart2-release", }, }, - previousV1Beta1KotsCharts: []kotsutil.HelmChartInterface{ + previousV1Beta1KotsCharts: []helmchart.HelmChartInterface{ &v1beta1.HelmChart{ TypeMeta: metav1.TypeMeta{ APIVersion: "kots.io/v1beta1", @@ -1517,7 +1517,7 @@ func Test_getRemovedCharts(t *testing.T) { }, }, }, - currentV1Beta1KotsCharts: []kotsutil.HelmChartInterface{ + currentV1Beta1KotsCharts: []helmchart.HelmChartInterface{ &v1beta1.HelmChart{ TypeMeta: metav1.TypeMeta{ APIVersion: "kots.io/v1beta1", @@ -1552,7 +1552,7 @@ func Test_getRemovedCharts(t *testing.T) { }, }, }, - want: []kotsutil.HelmChartInterface{}, + want: []helmchart.HelmChartInterface{}, }, { name: "v1beta1 -- chart1 old namespace removed because namespace changed", @@ -1580,7 +1580,7 @@ func Test_getRemovedCharts(t *testing.T) { dirName: "chart2-release", }, }, - previousV1Beta1KotsCharts: []kotsutil.HelmChartInterface{ + previousV1Beta1KotsCharts: []helmchart.HelmChartInterface{ &v1beta1.HelmChart{ TypeMeta: metav1.TypeMeta{ APIVersion: "kots.io/v1beta1", @@ -1615,7 +1615,7 @@ func Test_getRemovedCharts(t *testing.T) { }, }, }, - currentV1Beta1KotsCharts: []kotsutil.HelmChartInterface{ + currentV1Beta1KotsCharts: []helmchart.HelmChartInterface{ &v1beta1.HelmChart{ TypeMeta: metav1.TypeMeta{ APIVersion: "kots.io/v1beta1", @@ -1650,7 +1650,7 @@ func Test_getRemovedCharts(t *testing.T) { }, }, }, - want: []kotsutil.HelmChartInterface{ + want: []helmchart.HelmChartInterface{ &v1beta1.HelmChart{ TypeMeta: metav1.TypeMeta{ APIVersion: "kots.io/v1beta1", @@ -1697,7 +1697,7 @@ func Test_getRemovedCharts(t *testing.T) { dirName: "chart2-release", }, }, - previousV1Beta2KotsCharts: []kotsutil.HelmChartInterface{ + previousV1Beta2KotsCharts: []helmchart.HelmChartInterface{ &v1beta2.HelmChart{ TypeMeta: metav1.TypeMeta{ APIVersion: "kots.io/v1beta2", @@ -1731,7 +1731,7 @@ func Test_getRemovedCharts(t *testing.T) { }, }, }, - currentV1Beta2KotsCharts: []kotsutil.HelmChartInterface{ + currentV1Beta2KotsCharts: []helmchart.HelmChartInterface{ &v1beta2.HelmChart{ TypeMeta: metav1.TypeMeta{ APIVersion: "kots.io/v1beta2", @@ -1765,7 +1765,7 @@ func Test_getRemovedCharts(t *testing.T) { }, }, }, - want: []kotsutil.HelmChartInterface{}, + want: []helmchart.HelmChartInterface{}, }, { name: "v1beta2 -- chart1 removed", @@ -1788,7 +1788,7 @@ func Test_getRemovedCharts(t *testing.T) { dirName: "chart2-release", }, }, - previousV1Beta2KotsCharts: []kotsutil.HelmChartInterface{ + previousV1Beta2KotsCharts: []helmchart.HelmChartInterface{ &v1beta2.HelmChart{ TypeMeta: metav1.TypeMeta{ APIVersion: "kots.io/v1beta2", @@ -1822,7 +1822,7 @@ func Test_getRemovedCharts(t *testing.T) { }, }, }, - currentV1Beta2KotsCharts: []kotsutil.HelmChartInterface{ + currentV1Beta2KotsCharts: []helmchart.HelmChartInterface{ &v1beta2.HelmChart{ TypeMeta: metav1.TypeMeta{ APIVersion: "kots.io/v1beta2", @@ -1840,7 +1840,7 @@ func Test_getRemovedCharts(t *testing.T) { }, }, }, - want: []kotsutil.HelmChartInterface{ + want: []helmchart.HelmChartInterface{ &v1beta2.HelmChart{ TypeMeta: metav1.TypeMeta{ APIVersion: "kots.io/v1beta2", @@ -1885,7 +1885,7 @@ func Test_getRemovedCharts(t *testing.T) { dirName: "chart2-release", }, }, - previousV1Beta2KotsCharts: []kotsutil.HelmChartInterface{ + previousV1Beta2KotsCharts: []helmchart.HelmChartInterface{ &v1beta2.HelmChart{ TypeMeta: metav1.TypeMeta{ APIVersion: "kots.io/v1beta2", @@ -1919,7 +1919,7 @@ func Test_getRemovedCharts(t *testing.T) { }, }, }, - currentV1Beta2KotsCharts: []kotsutil.HelmChartInterface{ + currentV1Beta2KotsCharts: []helmchart.HelmChartInterface{ &v1beta2.HelmChart{ TypeMeta: metav1.TypeMeta{ APIVersion: "kots.io/v1beta2", @@ -1953,7 +1953,7 @@ func Test_getRemovedCharts(t *testing.T) { }, }, }, - want: []kotsutil.HelmChartInterface{}, + want: []helmchart.HelmChartInterface{}, }, { name: "v1beta2 -- chart2 old release removed because release name changed", @@ -1981,7 +1981,7 @@ func Test_getRemovedCharts(t *testing.T) { dirName: "chart2-new-release", }, }, - previousV1Beta2KotsCharts: []kotsutil.HelmChartInterface{ + previousV1Beta2KotsCharts: []helmchart.HelmChartInterface{ &v1beta2.HelmChart{ TypeMeta: metav1.TypeMeta{ APIVersion: "kots.io/v1beta2", @@ -2015,7 +2015,7 @@ func Test_getRemovedCharts(t *testing.T) { }, }, }, - currentV1Beta2KotsCharts: []kotsutil.HelmChartInterface{ + currentV1Beta2KotsCharts: []helmchart.HelmChartInterface{ &v1beta2.HelmChart{ TypeMeta: metav1.TypeMeta{ APIVersion: "kots.io/v1beta2", @@ -2049,7 +2049,7 @@ func Test_getRemovedCharts(t *testing.T) { }, }, }, - want: []kotsutil.HelmChartInterface{ + want: []helmchart.HelmChartInterface{ &v1beta2.HelmChart{ TypeMeta: metav1.TypeMeta{ APIVersion: "kots.io/v1beta2", @@ -2094,7 +2094,7 @@ func Test_getRemovedCharts(t *testing.T) { dirName: "chart2-release", }, }, - previousV1Beta2KotsCharts: []kotsutil.HelmChartInterface{ + previousV1Beta2KotsCharts: []helmchart.HelmChartInterface{ &v1beta2.HelmChart{ TypeMeta: metav1.TypeMeta{ APIVersion: "kots.io/v1beta2", @@ -2129,7 +2129,7 @@ func Test_getRemovedCharts(t *testing.T) { }, }, }, - currentV1Beta2KotsCharts: []kotsutil.HelmChartInterface{ + currentV1Beta2KotsCharts: []helmchart.HelmChartInterface{ &v1beta2.HelmChart{ TypeMeta: metav1.TypeMeta{ APIVersion: "kots.io/v1beta2", @@ -2164,7 +2164,7 @@ func Test_getRemovedCharts(t *testing.T) { }, }, }, - want: []kotsutil.HelmChartInterface{}, + want: []helmchart.HelmChartInterface{}, }, { name: "v1beta2 -- chart1 old namespace removed because namespace changed", @@ -2192,7 +2192,7 @@ func Test_getRemovedCharts(t *testing.T) { dirName: "chart2-release", }, }, - previousV1Beta2KotsCharts: []kotsutil.HelmChartInterface{ + previousV1Beta2KotsCharts: []helmchart.HelmChartInterface{ &v1beta2.HelmChart{ TypeMeta: metav1.TypeMeta{ APIVersion: "kots.io/v1beta2", @@ -2227,7 +2227,7 @@ func Test_getRemovedCharts(t *testing.T) { }, }, }, - currentV1Beta2KotsCharts: []kotsutil.HelmChartInterface{ + currentV1Beta2KotsCharts: []helmchart.HelmChartInterface{ &v1beta2.HelmChart{ TypeMeta: metav1.TypeMeta{ APIVersion: "kots.io/v1beta2", @@ -2262,7 +2262,7 @@ func Test_getRemovedCharts(t *testing.T) { }, }, }, - want: []kotsutil.HelmChartInterface{ + want: []helmchart.HelmChartInterface{ &v1beta2.HelmChart{ TypeMeta: metav1.TypeMeta{ APIVersion: "kots.io/v1beta2", @@ -2299,7 +2299,7 @@ func Test_getRemovedCharts(t *testing.T) { dirName: "chart1-release", }, }, - previousV1Beta1KotsCharts: []kotsutil.HelmChartInterface{ + previousV1Beta1KotsCharts: []helmchart.HelmChartInterface{ &v1beta1.HelmChart{ TypeMeta: metav1.TypeMeta{ APIVersion: "kots.io/v1beta1", @@ -2317,7 +2317,7 @@ func Test_getRemovedCharts(t *testing.T) { }, }, }, - currentV1Beta1KotsCharts: []kotsutil.HelmChartInterface{ + currentV1Beta1KotsCharts: []helmchart.HelmChartInterface{ &v1beta1.HelmChart{ TypeMeta: metav1.TypeMeta{ APIVersion: "kots.io/v1beta1", @@ -2349,7 +2349,7 @@ func Test_getRemovedCharts(t *testing.T) { dirName: "chart2-release", }, }, - previousV1Beta2KotsCharts: []kotsutil.HelmChartInterface{ + previousV1Beta2KotsCharts: []helmchart.HelmChartInterface{ &v1beta2.HelmChart{ TypeMeta: metav1.TypeMeta{ APIVersion: "kots.io/v1beta2", @@ -2367,7 +2367,7 @@ func Test_getRemovedCharts(t *testing.T) { }, }, }, - currentV1Beta2KotsCharts: []kotsutil.HelmChartInterface{ + currentV1Beta2KotsCharts: []helmchart.HelmChartInterface{ &v1beta2.HelmChart{ TypeMeta: metav1.TypeMeta{ APIVersion: "kots.io/v1beta2", @@ -2385,7 +2385,7 @@ func Test_getRemovedCharts(t *testing.T) { }, }, }, - want: []kotsutil.HelmChartInterface{}, + want: []helmchart.HelmChartInterface{}, }, { name: "mix of v1beta1 and v1beta2 -- chart2 removed", @@ -2403,7 +2403,7 @@ func Test_getRemovedCharts(t *testing.T) { dirName: "chart1-release", }, }, - previousV1Beta1KotsCharts: []kotsutil.HelmChartInterface{ + previousV1Beta1KotsCharts: []helmchart.HelmChartInterface{ &v1beta1.HelmChart{ TypeMeta: metav1.TypeMeta{ APIVersion: "kots.io/v1beta1", @@ -2421,7 +2421,7 @@ func Test_getRemovedCharts(t *testing.T) { }, }, }, - currentV1Beta1KotsCharts: []kotsutil.HelmChartInterface{ + currentV1Beta1KotsCharts: []helmchart.HelmChartInterface{ &v1beta1.HelmChart{ TypeMeta: metav1.TypeMeta{ APIVersion: "kots.io/v1beta1", @@ -2447,7 +2447,7 @@ func Test_getRemovedCharts(t *testing.T) { }, }, curV1Beta2Charts: []chart{}, - previousV1Beta2KotsCharts: []kotsutil.HelmChartInterface{ + previousV1Beta2KotsCharts: []helmchart.HelmChartInterface{ &v1beta2.HelmChart{ TypeMeta: metav1.TypeMeta{ APIVersion: "kots.io/v1beta2", @@ -2465,8 +2465,8 @@ func Test_getRemovedCharts(t *testing.T) { }, }, }, - currentV1Beta2KotsCharts: []kotsutil.HelmChartInterface{}, - want: []kotsutil.HelmChartInterface{ + currentV1Beta2KotsCharts: []helmchart.HelmChartInterface{}, + want: []helmchart.HelmChartInterface{ &v1beta2.HelmChart{ TypeMeta: metav1.TypeMeta{ APIVersion: "kots.io/v1beta2", diff --git a/pkg/policy/middleware.go b/pkg/policy/middleware.go index 452bf2925f..3e3504880d 100644 --- a/pkg/policy/middleware.go +++ b/pkg/policy/middleware.go @@ -75,21 +75,6 @@ func (m *Middleware) EnforceAccess(p *Policy, handler http.HandlerFunc) http.Han } } -func (m *Middleware) EnforceLicense(handler http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - license := session.ContextGetLicense(r) - if license == nil { - err := errors.New("no valid license for request") - logger.Error(err) - w.WriteHeader(http.StatusForbidden) - w.Write([]byte(err.Error())) - return - } - - handler(w, r) - } -} - // TODO: move everything below here to a shared package type ErrorResponse struct { diff --git a/pkg/preflight/execute.go b/pkg/preflight/execute.go index 0d1a9b9472..4b73d70648 100644 --- a/pkg/preflight/execute.go +++ b/pkg/preflight/execute.go @@ -45,7 +45,7 @@ func setPreflightResult(appID string, sequence int64, preflightResults *types.Pr // execute will execute the preflights using spec in preflightSpec. // This spec should be rendered, no template functions remaining func execute(appID string, sequence int64, preflightSpec *troubleshootv1beta2.Preflight, ignorePermissionErrors bool) (*types.PreflightResults, error) { - logger.Debug("executing preflight checks", + logger.Info("executing preflight checks", zap.String("appID", appID), zap.Int64("sequence", sequence)) @@ -65,7 +65,12 @@ func execute(appID string, sequence int64, preflightSpec *troubleshootv1beta2.Pr if err, ok := msg.(error); ok { logger.Errorf("error while running preflights: %v", err) } else { - logger.Infof("preflight progress: %v", msg) + switch m := msg.(type) { + case preflight.CollectProgress: + logger.Infof("preflight progress: %s", m.String()) + default: + logger.Infof("preflight progress: %+v", msg) + } } progress, ok := msg.(preflight.CollectProgress) @@ -121,7 +126,7 @@ func execute(appID string, sequence int64, preflightSpec *troubleshootv1beta2.Pr KubernetesRestConfig: restConfig, } - logger.Debug("preflight collect phase") + logger.Info("preflight collect phase") collectResults, err := troubleshootpreflight.Collect(collectOpts, preflightSpec) if err != nil && !isPermissionsError(err) { preflightRunError = err @@ -147,7 +152,7 @@ func execute(appID string, sequence int64, preflightSpec *troubleshootv1beta2.Pr } uploadPreflightResults.Errors = rbacErrors } else { - logger.Debug("preflight analyze phase") + logger.Info("preflight analyze phase") analyzeResults := collectResults.Analyze() // the typescript api added some flair to this result diff --git a/pkg/preflight/preflight.go b/pkg/preflight/preflight.go index fddf9ea7df..edcfea6e4e 100644 --- a/pkg/preflight/preflight.go +++ b/pkg/preflight/preflight.go @@ -160,13 +160,26 @@ func Run(appID string, appSlug string, sequence int64, isAirgap bool, archiveDir preflight.Spec.Collectors = collectors go func() { - logger.Debug("preflight checks beginning") + logger.Info("preflight checks beginning") uploadPreflightResults, err := execute(appID, sequence, preflight, ignoreRBAC) if err != nil { logger.Error(errors.Wrap(err, "failed to run preflight checks")) return } - logger.Debug("preflight checks completed") + + // Log the preflight results if there are any warnings or errors + // The app may not get installed so we need to see this info for debugging + if GetPreflightState(uploadPreflightResults) != "pass" { + logger.Warnf("Preflight checks completed with warnings or errors. The application will not get deployed") + for _, result := range uploadPreflightResults.Results { + if result == nil { + continue + } + logger.Infof("preflight state=%s title=%q message=%q", GetPreflightCheckState(result), result.Title, result.Message) + } + } else { + logger.Info("preflight checks completed") + } go func() { err := reporting.GetReporter().SubmitAppInfo(appID) // send app and preflight info when preflights finish @@ -216,6 +229,26 @@ func Run(appID string, appSlug string, sequence int64, isAirgap bool, archiveDir return nil } +// GetPreflightCheckState returns the state of a single preflight check result +func GetPreflightCheckState(p *troubleshootpreflight.UploadPreflightResult) string { + if p == nil { + return "unknown" + } + + if p.IsFail { + return "fail" + } + + if p.IsWarn { + return "warn" + } + + if p.IsPass { + return "pass" + } + return "unknown" +} + // maybeDeployFirstVersion will deploy the first version if preflight checks pass func maybeDeployFirstVersion(appID string, sequence int64, preflightResults *types.PreflightResults) (bool, error) { if sequence != 0 { @@ -247,6 +280,10 @@ func maybeDeployFirstVersion(appID string, sequence int64, preflightResults *typ return true, nil } +// GetPreflightState returns a single state based on checking all +// preflight checks results. If there are any errors, the state is fail. +// If there are no errors and any warnings, the state is warn. +// Otherwise, the state is pass. func GetPreflightState(preflightResults *types.PreflightResults) string { if len(preflightResults.Errors) > 0 { return "fail" diff --git a/pkg/print/config.go b/pkg/print/config.go index adadbbe4cf..3507dcc75a 100644 --- a/pkg/print/config.go +++ b/pkg/print/config.go @@ -6,6 +6,8 @@ import ( configtypes "github.com/replicatedhq/kots/pkg/kotsadmconfig/types" "github.com/replicatedhq/kots/pkg/logger" + "github.com/replicatedhq/kots/pkg/preflight" + tsPreflight "github.com/replicatedhq/troubleshoot/pkg/preflight" ) func ConfigValidationErrors(log *logger.CLILogger, groupValidationErrors []configtypes.ConfigGroupValidationError) { @@ -26,3 +28,16 @@ func ConfigValidationErrors(log *logger.CLILogger, groupValidationErrors []confi log.FinishSpinnerWithError() log.Errorf(sb.String()) } + +func PreflightErrors(log *logger.CLILogger, results []*tsPreflight.UploadPreflightResult) { + w := NewTabWriter() + defer w.Flush() + + fmt.Fprintf(w, "\n") + fmtColumns := "%s\t%s\t%s\n" + fmt.Fprintf(w, fmtColumns, "STATE", "TITLE", "MESSAGE") + for _, result := range results { + fmt.Fprintf(w, fmtColumns, strings.ToUpper(preflight.GetPreflightCheckState(result)), result.Title, result.Message) + } + fmt.Fprintf(w, "\n") +} diff --git a/pkg/pull/pull.go b/pkg/pull/pull.go index c9df6ef167..24ef5f4436 100644 --- a/pkg/pull/pull.go +++ b/pkg/pull/pull.go @@ -58,6 +58,7 @@ type PullOptions struct { RewriteImageOptions registrytypes.RegistrySettings SkipHelmChartCheck bool ReportWriter io.Writer + AppID string AppSlug string AppSequence int64 AppVersionLabel string @@ -286,6 +287,9 @@ func Pull(upstreamURI string, pullOptions PullOptions) (string, error) { IsOpenShift: k8sutil.IsOpenShift(clientset), IsGKEAutopilot: k8sutil.IsGKEAutopilot(clientset), IncludeMinio: pullOptions.IncludeMinio, + IsAirgap: pullOptions.AirgapRoot != "", + KotsadmID: k8sutil.GetKotsadmID(clientset), + AppID: pullOptions.AppID, } if err := upstream.WriteUpstream(u, writeUpstreamOptions); err != nil { log.FinishSpinnerWithError() diff --git a/pkg/replicatedapp/api.go b/pkg/replicatedapp/api.go index d4e14c59be..710e4de1d5 100644 --- a/pkg/replicatedapp/api.go +++ b/pkg/replicatedapp/api.go @@ -172,7 +172,7 @@ func getApplicationMetadataFromHost(host string, endpoint string, upstream *url. return respBody, nil } -func SendApplicationMetricsData(license *kotsv1beta1.License, app *apptypes.App, data map[string]interface{}) error { +func SendCustomAppMetricsData(license *kotsv1beta1.License, app *apptypes.App, data map[string]interface{}) error { url := fmt.Sprintf("%s/application/custom-metrics", license.Spec.Endpoint) payload := struct { diff --git a/pkg/reporting/app.go b/pkg/reporting/app.go index a7942bbdb7..dcb02e12c4 100644 --- a/pkg/reporting/app.go +++ b/pkg/reporting/app.go @@ -138,13 +138,18 @@ func initFromDownstream() error { return errors.Wrap(err, "failed to check configmap") } + clientset, err := k8sutil.GetClientset() + if err != nil { + return errors.Wrap(err, "failed to get clientset") + } + if isKotsadmIDGenerated && !cmpExists { kotsadmID := ksuid.New().String() - err = k8sutil.CreateKotsadmIDConfigMap(kotsadmID) + err = k8sutil.CreateKotsadmIDConfigMap(clientset, kotsadmID) } else if !isKotsadmIDGenerated && !cmpExists { - err = k8sutil.CreateKotsadmIDConfigMap(clusterID) + err = k8sutil.CreateKotsadmIDConfigMap(clientset, clusterID) } else if !isKotsadmIDGenerated && cmpExists { - err = k8sutil.UpdateKotsadmIDConfigMap(clusterID) + err = k8sutil.UpdateKotsadmIDConfigMap(clientset, clusterID) } else { // id exists and so as configmap, noop } @@ -181,16 +186,7 @@ func GetReportingInfo(appID string) *types.ReportingInfo { if util.IsHelmManaged() { r.ClusterID = clusterID } else { - configMap, err := k8sutil.GetKotsadmIDConfigMap() - if err != nil { - r.ClusterID = ksuid.New().String() - } else if configMap != nil { - r.ClusterID = configMap.Data["id"] - } else { - // configmap is missing for some reason, recreate with new guid, this will appear as a new instance in the report - r.ClusterID = ksuid.New().String() - k8sutil.CreateKotsadmIDConfigMap(r.ClusterID) - } + r.ClusterID = k8sutil.GetKotsadmID(clientset) di, err := getDownstreamInfo(appID) if err != nil { diff --git a/pkg/reporting/distribution.go b/pkg/reporting/distribution.go index eb91f41c25..8d2d8c6124 100644 --- a/pkg/reporting/distribution.go +++ b/pkg/reporting/distribution.go @@ -53,7 +53,12 @@ func distributionFromServerGroupAndResources(clientset kubernetes.Interface) Dis func distributionFromProviderId(clientset kubernetes.Interface) Distribution { nodes, err := clientset.CoreV1().Nodes().List(context.TODO(), metav1.ListOptions{}) - logger.Infof("Found %d nodes", len(nodes.Items)) + nodeCount := len(nodes.Items) + if nodeCount > 1 { + logger.Infof("Found %d nodes", nodeCount) + } else { + logger.Infof("Found %d node", nodeCount) + } if err != nil { logger.Infof("got error listing node: %v", err.Error()) } diff --git a/pkg/reporting/util.go b/pkg/reporting/util.go index b7b22e90a9..e9da1ba89f 100644 --- a/pkg/reporting/util.go +++ b/pkg/reporting/util.go @@ -10,57 +10,69 @@ import ( ) func InjectReportingInfoHeaders(req *http.Request, reportingInfo *types.ReportingInfo) { + headers := GetReportingInfoHeaders(reportingInfo) + + for key, value := range headers { + req.Header.Set(key, value) + } +} + +func GetReportingInfoHeaders(reportingInfo *types.ReportingInfo) map[string]string { + headers := make(map[string]string) + if reportingInfo == nil { - return + return headers } - req.Header.Set("X-Replicated-K8sVersion", reportingInfo.K8sVersion) - req.Header.Set("X-Replicated-IsKurl", strconv.FormatBool(reportingInfo.IsKurl)) - req.Header.Set("X-Replicated-AppStatus", reportingInfo.AppStatus) - req.Header.Set("X-Replicated-ClusterID", reportingInfo.ClusterID) - req.Header.Set("X-Replicated-InstanceID", reportingInfo.InstanceID) - req.Header.Set("X-Replicated-ReplHelmInstalls", strconv.Itoa(reportingInfo.Downstream.ReplHelmInstalls)) - req.Header.Set("X-Replicated-NativeHelmInstalls", strconv.Itoa(reportingInfo.Downstream.NativeHelmInstalls)) + headers["X-Replicated-K8sVersion"] = reportingInfo.K8sVersion + headers["X-Replicated-IsKurl"] = strconv.FormatBool(reportingInfo.IsKurl) + headers["X-Replicated-AppStatus"] = reportingInfo.AppStatus + headers["X-Replicated-ClusterID"] = reportingInfo.ClusterID + headers["X-Replicated-InstanceID"] = reportingInfo.InstanceID + headers["X-Replicated-ReplHelmInstalls"] = strconv.Itoa(reportingInfo.Downstream.ReplHelmInstalls) + headers["X-Replicated-NativeHelmInstalls"] = strconv.Itoa(reportingInfo.Downstream.NativeHelmInstalls) if reportingInfo.Downstream.Cursor != "" { - req.Header.Set("X-Replicated-DownstreamChannelSequence", reportingInfo.Downstream.Cursor) + headers["X-Replicated-DownstreamChannelSequence"] = reportingInfo.Downstream.Cursor } if reportingInfo.Downstream.ChannelID != "" { - req.Header.Set("X-Replicated-DownstreamChannelID", reportingInfo.Downstream.ChannelID) + headers["X-Replicated-DownstreamChannelID"] = reportingInfo.Downstream.ChannelID } else if reportingInfo.Downstream.ChannelName != "" { - req.Header.Set("X-Replicated-DownstreamChannelName", reportingInfo.Downstream.ChannelName) + headers["X-Replicated-DownstreamChannelName"] = reportingInfo.Downstream.ChannelName } if reportingInfo.Downstream.Status != "" { - req.Header.Set("X-Replicated-InstallStatus", reportingInfo.Downstream.Status) + headers["X-Replicated-InstallStatus"] = reportingInfo.Downstream.Status } if reportingInfo.Downstream.PreflightState != "" { - req.Header.Set("X-Replicated-PreflightStatus", reportingInfo.Downstream.PreflightState) + headers["X-Replicated-PreflightStatus"] = reportingInfo.Downstream.PreflightState } if reportingInfo.Downstream.Sequence != nil { - req.Header.Set("X-Replicated-DownstreamSequence", strconv.FormatInt(*reportingInfo.Downstream.Sequence, 10)) + headers["X-Replicated-DownstreamSequence"] = strconv.FormatInt(*reportingInfo.Downstream.Sequence, 10) } if reportingInfo.Downstream.Source != "" { - req.Header.Set("X-Replicated-DownstreamSource", reportingInfo.Downstream.Source) + headers["X-Replicated-DownstreamSource"] = reportingInfo.Downstream.Source } - req.Header.Set("X-Replicated-SkipPreflights", strconv.FormatBool(reportingInfo.Downstream.SkipPreflights)) + headers["X-Replicated-SkipPreflights"] = strconv.FormatBool(reportingInfo.Downstream.SkipPreflights) if reportingInfo.KOTSInstallID != "" { - req.Header.Set("X-Replicated-KotsInstallID", reportingInfo.KOTSInstallID) + headers["X-Replicated-KotsInstallID"] = reportingInfo.KOTSInstallID } if reportingInfo.KURLInstallID != "" { - req.Header.Set("X-Replicated-KurlInstallID", reportingInfo.KURLInstallID) + headers["X-Replicated-KurlInstallID"] = reportingInfo.KURLInstallID } - req.Header.Set("X-Replicated-KurlNodeCountTotal", strconv.Itoa(reportingInfo.KurlNodeCountTotal)) - req.Header.Set("X-Replicated-KurlNodeCountReady", strconv.Itoa(reportingInfo.KurlNodeCountReady)) + headers["X-Replicated-KurlNodeCountTotal"] = strconv.Itoa(reportingInfo.KurlNodeCountTotal) + headers["X-Replicated-KurlNodeCountReady"] = strconv.Itoa(reportingInfo.KurlNodeCountReady) - req.Header.Set("X-Replicated-IsGitOpsEnabled", strconv.FormatBool(reportingInfo.IsGitOpsEnabled)) - req.Header.Set("X-Replicated-GitOpsProvider", reportingInfo.GitOpsProvider) + headers["X-Replicated-IsGitOpsEnabled"] = strconv.FormatBool(reportingInfo.IsGitOpsEnabled) + headers["X-Replicated-GitOpsProvider"] = reportingInfo.GitOpsProvider if reportingInfo.K8sDistribution != "" { - req.Header.Set("X-Replicated-K8sDistribution", reportingInfo.K8sDistribution) + headers["X-Replicated-K8sDistribution"] = reportingInfo.K8sDistribution } + + return headers } func canReport(endpoint string) bool { diff --git a/pkg/rewrite/rewrite.go b/pkg/rewrite/rewrite.go index cca1f3e5a5..3b1ba5e4ec 100644 --- a/pkg/rewrite/rewrite.go +++ b/pkg/rewrite/rewrite.go @@ -74,6 +74,7 @@ func Rewrite(rewriteOptions RewriteOptions) error { CurrentVersionIsRequired: rewriteOptions.Installation.Spec.IsRequired, CurrentReplicatedRegistryDomain: rewriteOptions.Installation.Spec.ReplicatedRegistryDomain, CurrentReplicatedProxyDomain: rewriteOptions.Installation.Spec.ReplicatedProxyDomain, + CurrentReplicatedChartNames: rewriteOptions.Installation.Spec.ReplicatedChartNames, EncryptionKey: rewriteOptions.Installation.Spec.EncryptionKey, License: rewriteOptions.License, AppSequence: rewriteOptions.AppSequence, @@ -103,6 +104,9 @@ func Rewrite(rewriteOptions RewriteOptions) error { PreserveInstallation: true, IsOpenShift: k8sutil.IsOpenShift(clientset), IsGKEAutopilot: k8sutil.IsGKEAutopilot(clientset), + IsAirgap: rewriteOptions.IsAirgap, + KotsadmID: k8sutil.GetKotsadmID(clientset), + AppID: rewriteOptions.AppID, } if err = upstream.WriteUpstream(u, writeUpstreamOptions); err != nil { log.FinishSpinnerWithError() diff --git a/pkg/upstream/fetch.go b/pkg/upstream/fetch.go index 3f325d4fe0..b7663280f8 100644 --- a/pkg/upstream/fetch.go +++ b/pkg/upstream/fetch.go @@ -49,6 +49,7 @@ func downloadUpstream(upstreamURI string, fetchOptions *types.FetchOptions) (*ty pickVersionIsRequired(fetchOptions), pickReplicatedRegistryDomain(fetchOptions), pickReplicatedProxyDomain(fetchOptions), + pickReplicatedChartNames(fetchOptions), fetchOptions.AppSlug, fetchOptions.AppSequence, fetchOptions.Airgap != nil, @@ -110,3 +111,10 @@ func pickCursor(fetchOptions *types.FetchOptions) replicatedapp.ReplicatedCursor Cursor: fetchOptions.CurrentCursor, } } + +func pickReplicatedChartNames(fetchOptions *types.FetchOptions) []string { + if fetchOptions.Airgap != nil { + return fetchOptions.Airgap.Spec.ReplicatedChartNames + } + return fetchOptions.CurrentReplicatedChartNames +} diff --git a/pkg/upstream/helm.go b/pkg/upstream/helm.go new file mode 100644 index 0000000000..287a723aff --- /dev/null +++ b/pkg/upstream/helm.go @@ -0,0 +1,461 @@ +package upstream + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "io/fs" + "os" + "path/filepath" + "strings" + + "github.com/pkg/errors" + "github.com/replicatedhq/kots/pkg/buildversion" + "github.com/replicatedhq/kots/pkg/kotsutil" + "github.com/replicatedhq/kots/pkg/upstream/types" + "gopkg.in/yaml.v3" +) + +// configureChart will configure the chart archive (values.yaml), +// repackage it, and return the updated content of the chart +func configureChart(chartContent []byte, u *types.Upstream, options types.WriteOptions) ([]byte, error) { + replicatedChartName, isSubchart, err := findReplicatedChart(bytes.NewReader(chartContent), u.ReplicatedChartNames) + if err != nil { + return nil, errors.Wrap(err, "find replicated chart") + } + if replicatedChartName == "" { + return chartContent, nil + } + + chartValues, pathInArchive, extractedArchiveRoot, err := findTopLevelChartValues(bytes.NewReader(chartContent)) + if err != nil { + return nil, errors.Wrap(err, "find top level chart values") + } + defer os.RemoveAll(extractedArchiveRoot) + + updatedValues, err := configureChartValues(chartValues, replicatedChartName, isSubchart, u, options) + if err != nil { + return nil, errors.Wrap(err, "configure values yaml") + } + + if err := os.WriteFile(filepath.Join(extractedArchiveRoot, pathInArchive), updatedValues, 0644); err != nil { + return nil, errors.Wrap(err, "write configured values.yaml") + } + + updatedArchive, err := packageChartArchive(extractedArchiveRoot) + if err != nil { + return nil, errors.Wrap(err, "package chart archive") + } + defer os.RemoveAll(updatedArchive) + + updatedContents, err := os.ReadFile(updatedArchive) + if err != nil { + return nil, errors.Wrap(err, "read updated archive") + } + + return updatedContents, nil +} + +// findReplicatedChart will look for the replicated chart in the archive +// and return the name of the replicated chart and whether it is the parent chart or a subchart +func findReplicatedChart(chartArchive io.Reader, replicatedChartNames []string) (string, bool, error) { + gzReader, err := gzip.NewReader(chartArchive) + if err != nil { + return "", false, errors.Wrap(err, "failed to create gzip reader") + } + defer gzReader.Close() + + tarReader := tar.NewReader(gzReader) + for { + header, err := tarReader.Next() + if err == io.EOF { + break + } + if err != nil { + return "", false, errors.Wrap(err, "failed to read header from tar") + } + + switch header.Typeflag { + case tar.TypeReg: + if filepath.Base(header.Name) != "Chart.yaml" { + continue + } + + // we only care about the root Chart.yaml file or the Chart.yaml file of direct subcharts (not subsubcharts) + parts := strings.Split(header.Name, string(os.PathSeparator)) // e.g. replicated/Chart.yaml or nginx/charts/replicated/Chart.yaml + if len(parts) != 2 && len(parts) != 4 { + continue + } + + content, err := io.ReadAll(tarReader) + if err != nil { + return "", false, errors.Wrapf(err, "failed to read file %s", header.Name) + } + + chartInfo := struct { + ChartName string `json:"name" yaml:"name"` + }{} + if err := yaml.Unmarshal(content, &chartInfo); err != nil { + return "", false, errors.Wrapf(err, "failed to unmarshal %s", header.Name) + } + + for _, replicatedChartName := range replicatedChartNames { + if chartInfo.ChartName == replicatedChartName { + return replicatedChartName, len(parts) == 4, nil + } + } + } + } + + return "", false, nil +} + +func findTopLevelChartValues(r io.Reader) (valuesYaml []byte, pathInArchive string, workspace string, finalErr error) { + workspace, err := os.MkdirTemp("", "extracted-chart-") + if err != nil { + finalErr = errors.Wrap(err, "failed to create temp directory") + return + } + + defer func() { + if finalErr != nil { + os.RemoveAll(workspace) + workspace = "" + } + }() + + gzReader, err := gzip.NewReader(r) + if err != nil { + finalErr = errors.Wrap(err, "failed to create gzip reader") + return + } + + tarReader := tar.NewReader(gzReader) + for { + header, err := tarReader.Next() + if err == io.EOF { + break + } + if err != nil { + finalErr = errors.Wrap(err, "failed to read header from tar") + return + } + + switch header.Typeflag { + case tar.TypeDir: + if err := os.Mkdir(filepath.Join(workspace, header.Name), fs.FileMode(header.Mode)); err != nil { + finalErr = errors.Wrap(err, "failed to create directory from archive") + return + } + case tar.TypeReg: + content, err := io.ReadAll(tarReader) + if err != nil { + finalErr = errors.Wrap(err, "failed to read file") + return + } + + if filepath.Base(header.Name) == "values.yaml" { + // only get the values.yaml from the top level chart + p := filepath.Dir(header.Name) + if !strings.Contains(p, string(os.PathSeparator)) { + pathInArchive = header.Name + valuesYaml = content + } + } + + dir := filepath.Dir(filepath.Join(workspace, header.Name)) + if err := os.MkdirAll(dir, 0700); err != nil { + finalErr = errors.Wrap(err, "failed to create directory from filename") + return + } + + outFile, err := os.Create(filepath.Join(workspace, header.Name)) + if err != nil { + finalErr = errors.Wrap(err, "failed to create file") + return + } + defer outFile.Close() + if err := os.WriteFile(outFile.Name(), content, header.FileInfo().Mode()); err != nil { + finalErr = errors.Wrap(err, "failed to write file") + return + } + } + } + + return +} + +func configureChartValues(valuesYAML []byte, replicatedChartName string, isSubchart bool, u *types.Upstream, options types.WriteOptions) ([]byte, error) { + // unmarshal to insert the replicated values + var valuesNode yaml.Node + if err := yaml.Unmarshal([]byte(valuesYAML), &valuesNode); err != nil { + return nil, errors.Wrap(err, "unmarshal values") + } + + if len(valuesNode.Content) == 0 { + return nil, errors.New("no content") + } + + if replicatedChartName != "" { + err := addReplicatedValues(valuesNode.Content[0], replicatedChartName, isSubchart, u, options) + if err != nil { + return nil, errors.Wrap(err, "add replicated values") + } + } + + if err := addGlobalReplicatedValues(valuesNode.Content[0], u, options); err != nil { + return nil, errors.Wrap(err, "add global replicated values") + } + + updatedValues, err := kotsutil.NodeToYAML(&valuesNode) + if err != nil { + return nil, errors.Wrap(err, "node to yaml") + } + + return updatedValues, nil +} + +func addReplicatedValues(doc *yaml.Node, replicatedChartName string, isSubchart bool, u *types.Upstream, options types.WriteOptions) error { + replicatedValues, err := buildReplicatedValues(u, options) + if err != nil { + return errors.Wrap(err, "build replicated values") + } + + targetNode := doc + hasReplicatedValues := false + v := replicatedValues + + // if replicated sdk is included as a subchart, + // we make sure to add the values under the subchart name + // as helm expects the field name to match the subchart name + if isSubchart { + for i, n := range doc.Content { + if n.Value == replicatedChartName { // check if field already exists + targetNode = doc.Content[i+1] + hasReplicatedValues = true + break + } + } + if !hasReplicatedValues { + v = map[string]interface{}{ + replicatedChartName: replicatedValues, + } + } + } + + additionalYAML, err := yaml.Marshal(v) + if err != nil { + return errors.Wrap(err, "marshal additional values") + } + + var additionalNode yaml.Node + if err := yaml.Unmarshal([]byte(additionalYAML), &additionalNode); err != nil { + return errors.Wrap(err, "unmarshal additional values") + } + + if !hasReplicatedValues && isSubchart { + targetNode.Content = append(targetNode.Content, additionalNode.Content[0].Content...) + } else { + targetNode.Content = kotsutil.MergeYAMLNodes(targetNode.Content, additionalNode.Content[0].Content) + } + + return nil +} + +func buildReplicatedValues(u *types.Upstream, options types.WriteOptions) (map[string]interface{}, error) { + replicatedValues := map[string]interface{}{ + "replicatedID": options.KotsadmID, + "appID": options.AppID, + "userAgent": buildversion.GetUserAgent(), + "isAirgap": options.IsAirgap, + } + + // only add the license if this is an airgap install + // because the airgap builder doesn't have the license context + if u.License != nil && options.IsAirgap { + replicatedValues["license"] = string(MustMarshalLicense(u.License)) + } + + return replicatedValues, nil +} + +func addGlobalReplicatedValues(doc *yaml.Node, u *types.Upstream, options types.WriteOptions) error { + globalReplicatedValues, err := buildGlobalReplicatedValues(u, options) + if err != nil { + return errors.Wrap(err, "build global replicated values") + } + if len(globalReplicatedValues) == 0 { + return nil + } + + targetNode := doc + hasGlobal := false + for i, n := range doc.Content { + if n.Value == "global" { + targetNode = doc.Content[i+1] + hasGlobal = true + break + } + } + + hasGlobalReplicated := false + if hasGlobal { + for i, n := range targetNode.Content { + if n.Value == "replicated" { + targetNode = targetNode.Content[i+1] + hasGlobalReplicated = true + break + } + } + } + + v := globalReplicatedValues + if !hasGlobalReplicated { + v = map[string]interface{}{ + "replicated": v, + } + if !hasGlobal { + v = map[string]interface{}{ + "global": v, + } + } + } + + additionalYAML, err := yaml.Marshal(v) + if err != nil { + return errors.Wrap(err, "marshal additional values") + } + + var additionalNode yaml.Node + if err := yaml.Unmarshal([]byte(additionalYAML), &additionalNode); err != nil { + return errors.Wrap(err, "unmarshal additional values") + } + + if hasGlobalReplicated || hasGlobal { + targetNode.Content = kotsutil.MergeYAMLNodes(targetNode.Content, additionalNode.Content[0].Content) + } else { + targetNode.Content = append(targetNode.Content, additionalNode.Content[0].Content...) + } + + return nil +} + +func buildGlobalReplicatedValues(u *types.Upstream, options types.WriteOptions) (map[string]interface{}, error) { + globalReplicatedValues := map[string]interface{}{} + + // only add license related info if this is an airgap install + // because the airgap builder doesn't have the license context + if u.License != nil && options.IsAirgap { + globalReplicatedValues["channelName"] = u.License.Spec.ChannelName + globalReplicatedValues["customerName"] = u.License.Spec.CustomerName + globalReplicatedValues["customerEmail"] = u.License.Spec.CustomerEmail + globalReplicatedValues["licenseID"] = u.License.Spec.LicenseID + globalReplicatedValues["licenseType"] = u.License.Spec.LicenseType + + // we marshal and then unmarshal entitlements into an interface to evaluate entitlement values + // and end up with a single value instead of (intVal, boolVal, strVal, and type) + marshalledEntitlements, err := json.Marshal(u.License.Spec.Entitlements) + if err != nil { + return nil, errors.Wrap(err, "failed to marshal entitlements") + } + + var licenseFields map[string]interface{} + if err := json.Unmarshal(marshalledEntitlements, &licenseFields); err != nil { + return nil, errors.Wrap(err, "failed to unmarshal entitlements") + } + + // add the field name if missing + for k, v := range licenseFields { + if name, ok := v.(map[string]interface{})["name"]; !ok || name == "" { + licenseFields[k].(map[string]interface{})["name"] = k + } + } + + globalReplicatedValues["licenseFields"] = licenseFields + + // add docker config json + auth := fmt.Sprintf("%s:%s", u.License.Spec.LicenseID, u.License.Spec.LicenseID) + encodedAuth := base64.StdEncoding.EncodeToString([]byte(auth)) + dockercfg := map[string]interface{}{ + "auths": map[string]interface{}{ + u.ReplicatedProxyDomain: map[string]string{ + "auth": encodedAuth, + }, + u.ReplicatedRegistryDomain: map[string]string{ + "auth": encodedAuth, + }, + }, + } + + b, err := json.Marshal(dockercfg) + if err != nil { + return nil, errors.Wrap(err, "failed to marshal dockercfg") + } + + globalReplicatedValues["dockerconfigjson"] = base64.StdEncoding.EncodeToString(b) + } + + return globalReplicatedValues, nil +} + +func packageChartArchive(extractedArchiveRoot string) (string, error) { + configuredChartArchive, err := os.CreateTemp("", "configured-chart-") + if err != nil { + return "", errors.Wrap(err, "create temp file") + } + + gzipWriter := gzip.NewWriter(configuredChartArchive) + defer gzipWriter.Close() + + tarWriter := tar.NewWriter(gzipWriter) + defer tarWriter.Close() + + err = filepath.Walk(extractedArchiveRoot, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if info.IsDir() { + return nil + } + + file, err := os.Open(path) + if err != nil { + return errors.Wrapf(err, "open file '%s'", path) + } + defer file.Close() + + rel, err := filepath.Rel(extractedArchiveRoot, path) + if err != nil { + return errors.New(fmt.Sprintf("Could not get relative path for file '%s', got error '%s'", path, err.Error())) + } + + header := &tar.Header{ + Name: rel, + Size: info.Size(), + Mode: int64(info.Mode()), + ModTime: info.ModTime(), + } + + err = tarWriter.WriteHeader(header) + if err != nil { + return errors.New(fmt.Sprintf("Could not write header for file '%s', got error '%s'", path, err.Error())) + } + + _, err = io.Copy(tarWriter, file) + if err != nil { + return errors.New(fmt.Sprintf("Could not copy the file '%s' data to the tarball, got error '%s'", path, err.Error())) + } + + return nil + }) + if err != nil { + return "", errors.Wrap(err, "walk file tree") + } + + return configuredChartArchive.Name(), nil +} diff --git a/pkg/upstream/helm_test.go b/pkg/upstream/helm_test.go new file mode 100644 index 0000000000..a921624e7a --- /dev/null +++ b/pkg/upstream/helm_test.go @@ -0,0 +1,1287 @@ +package upstream + +import ( + "fmt" + "testing" + + "github.com/pmezard/go-difflib/difflib" + "github.com/replicatedhq/kots/pkg/upstream/types" + "github.com/replicatedhq/kots/pkg/util" + kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func Test_configureChart(t *testing.T) { + testReplicatedChartNames := []string{ + "replicated", + "replicated-sdk", + } + + type Test struct { + name string + isAirgap bool + chartContent map[string]string + want map[string]string + wantErr bool + } + + tests := []Test{ + { + name: "online - a standalone non-replicated chart", + isAirgap: false, + chartContent: map[string]string{ + "non-replicated/Chart.yaml": `apiVersion: v1 +name: not-replicated +version: 1.0.0 +description: Not a Replicated Chart +`, + "non-replicated/values.yaml": `# this values.yaml file should not change + +# do not change global values +global: + some: value + +# use this value to configure the chart +some: value +`, + }, + want: map[string]string{ + "non-replicated/Chart.yaml": `apiVersion: v1 +name: not-replicated +version: 1.0.0 +description: Not a Replicated Chart +`, + "non-replicated/values.yaml": `# this values.yaml file should not change + +# do not change global values +global: + some: value + +# use this value to configure the chart +some: value +`, + }, + wantErr: false, + }, + { + name: "airgap - a standalone non-replicated chart", + isAirgap: true, + chartContent: map[string]string{ + "non-replicated/Chart.yaml": `apiVersion: v1 +name: not-replicated +version: 1.0.0 +description: Not a Replicated Chart +`, + "non-replicated/values.yaml": `# this values.yaml file should not change + +# do not change global values +global: + some: value + +# use this value to configure the chart +some: value +`, + }, + want: map[string]string{ + "non-replicated/Chart.yaml": `apiVersion: v1 +name: not-replicated +version: 1.0.0 +description: Not a Replicated Chart +`, + "non-replicated/values.yaml": `# this values.yaml file should not change + +# do not change global values +global: + some: value + +# use this value to configure the chart +some: value +`, + }, + wantErr: false, + }, + { + name: "online - an nginx chart with the 'common' subchart only", + isAirgap: false, + chartContent: map[string]string{ + "nginx/Chart.yaml": `apiVersion: v2 +name: nginx +version: 12.0.1 +description: An NGINX Chart +`, + "nginx/values.yaml": `## @section Global parameters +## Global Docker image parameters +## Please, note that this will override the image parameters, including dependencies, configured to use the global value +## Current available global Docker image parameters: imageRegistry, imagePullSecrets and storageClass + +## @param global.imageRegistry Global Docker image registry +## @param global.imagePullSecrets Global Docker registry secret names as an array +## +global: + imageRegistry: "" + ## E.g. + ## imagePullSecrets: + ## - myRegistryKeySecretName + ## + imagePullSecrets: [] + +## @section Common parameters + +## @param nameOverride String to partially override nginx.fullname template (will maintain the release name) +## +nameOverride: "" +`, + "nginx/charts/common/Chart.yaml": `apiVersion: v2 +name: common +version: 1.13.1 +description: A Common Chart +`, + "nginx/charts/common/values.yaml": `# do not change this file + +# do not change global values +global: + some: value + +# keep this comment +another: value +`, + }, + want: map[string]string{ + "nginx/Chart.yaml": `apiVersion: v2 +name: nginx +version: 12.0.1 +description: An NGINX Chart +`, + "nginx/values.yaml": `## @section Global parameters +## Global Docker image parameters +## Please, note that this will override the image parameters, including dependencies, configured to use the global value +## Current available global Docker image parameters: imageRegistry, imagePullSecrets and storageClass + +## @param global.imageRegistry Global Docker image registry +## @param global.imagePullSecrets Global Docker registry secret names as an array +## +global: + imageRegistry: "" + ## E.g. + ## imagePullSecrets: + ## - myRegistryKeySecretName + ## + imagePullSecrets: [] + +## @section Common parameters + +## @param nameOverride String to partially override nginx.fullname template (will maintain the release name) +## +nameOverride: "" +`, + "nginx/charts/common/Chart.yaml": `apiVersion: v2 +name: common +version: 1.13.1 +description: A Common Chart +`, + "nginx/charts/common/values.yaml": `# do not change this file + +# do not change global values +global: + some: value + +# keep this comment +another: value +`, + }, + wantErr: false, + }, + { + name: "airgap - an nginx chart with the 'common' subchart only", + isAirgap: true, + chartContent: map[string]string{ + "nginx/Chart.yaml": `apiVersion: v2 +name: nginx +version: 12.0.1 +description: An NGINX Chart +`, + "nginx/values.yaml": `## @section Global parameters +## Global Docker image parameters +## Please, note that this will override the image parameters, including dependencies, configured to use the global value +## Current available global Docker image parameters: imageRegistry, imagePullSecrets and storageClass + +## @param global.imageRegistry Global Docker image registry +## @param global.imagePullSecrets Global Docker registry secret names as an array +## +global: + imageRegistry: "" + ## E.g. + ## imagePullSecrets: + ## - myRegistryKeySecretName + ## + imagePullSecrets: [] + +## @section Common parameters + +## @param nameOverride String to partially override nginx.fullname template (will maintain the release name) +## +nameOverride: "" +`, + "nginx/charts/common/Chart.yaml": `apiVersion: v2 +name: common +version: 1.13.1 +description: A Common Chart +`, + "nginx/charts/common/values.yaml": `# do not change this file + +# do not change global values +global: + some: value + +# keep this comment +another: value +`, + }, + want: map[string]string{ + "nginx/Chart.yaml": `apiVersion: v2 +name: nginx +version: 12.0.1 +description: An NGINX Chart +`, + "nginx/values.yaml": `## @section Global parameters +## Global Docker image parameters +## Please, note that this will override the image parameters, including dependencies, configured to use the global value +## Current available global Docker image parameters: imageRegistry, imagePullSecrets and storageClass + +## @param global.imageRegistry Global Docker image registry +## @param global.imagePullSecrets Global Docker registry secret names as an array +## +global: + imageRegistry: "" + ## E.g. + ## imagePullSecrets: + ## - myRegistryKeySecretName + ## + imagePullSecrets: [] + +## @section Common parameters + +## @param nameOverride String to partially override nginx.fullname template (will maintain the release name) +## +nameOverride: "" +`, + "nginx/charts/common/Chart.yaml": `apiVersion: v2 +name: common +version: 1.13.1 +description: A Common Chart +`, + "nginx/charts/common/values.yaml": `# do not change this file + +# do not change global values +global: + some: value + +# keep this comment +another: value +`, + }, + wantErr: false, + }, + } + + // Generate dynamic tests using the supported replicated chart names + for _, chartName := range testReplicatedChartNames { + tests = append(tests, Test{ + name: "online - a standalone replicated chart", + isAirgap: false, + chartContent: map[string]string{ + "replicated/Chart.yaml": fmt.Sprintf(`apiVersion: v1 +name: %s +version: 1.0.0 +description: A Replicated Chart +`, chartName), + "replicated/values.yaml": `# preserve this comment + +license: online-license +appName: online-app-name +channelID: online-channel-id +channelName: online-channel-name +channelSequence: 2 +releaseCreatedAt: "2023-10-02T00:00:00Z" +releaseNotes: override my release notes +releaseSequence: 1 +statusInformers: + - deployment/replicated + - service/replicated +versionLabel: 1.0.0 +# and this comment + +global: + replicated: + licenseID: online-license-id + channelName: online-channel-name + customerName: Online Customer Name + customerEmail: online-customer@example.com + licenseType: dev + dockerconfigjson: bm90LWEtZG9ja2VyLWNvbmZpZy1qc29uCg== + licenseFields: + expires_at: + name: expires_at + title: Expiration + description: License Expiration + value: "" + valueType: String + signature: + v1: nwZmD/sMFzKKxkd7JaAcKU/2uBE5m23w7+8xqLMXjUturMVCF5cF66EVMAibb2nHOqytie+N35GYSwIeTd16PKwbFBDd12c2E5M9COWwjVRcVTz4OnNWmHv9PEqZIbXhvfCLlyJ/aY3zV9Pno1VLFcYxGMrBugncEo4ecHkEbaVp3VLS4wn8EykAC1byvYBshzEXppYYd3c6a9cNw50Z6inI/IaKVxIForuz+Yn5uRAsjRyCY2auBCMeHMhY+CQ+4Vl5WtGjuJuE1g7t8AVZqt2JDBgDuxZAZX/JGncfzUaaDl87athMTtBKnFkTnCl34UXPkhsgM0LC4YoUiyKYjQ== +some: value +# and this comment as well +`, + }, + want: map[string]string{ + "replicated/Chart.yaml": fmt.Sprintf(`apiVersion: v1 +name: %s +version: 1.0.0 +description: A Replicated Chart +`, chartName), + "replicated/values.yaml": `# preserve this comment + +license: online-license +appName: online-app-name +channelID: online-channel-id +channelName: online-channel-name +channelSequence: 2 +releaseCreatedAt: "2023-10-02T00:00:00Z" +releaseNotes: override my release notes +releaseSequence: 1 +statusInformers: + - deployment/replicated + - service/replicated +versionLabel: 1.0.0 +# and this comment + +global: + replicated: + licenseID: online-license-id + channelName: online-channel-name + customerName: Online Customer Name + customerEmail: online-customer@example.com + licenseType: dev + dockerconfigjson: bm90LWEtZG9ja2VyLWNvbmZpZy1qc29uCg== + licenseFields: + expires_at: + name: expires_at + title: Expiration + description: License Expiration + value: "" + valueType: String + signature: + v1: nwZmD/sMFzKKxkd7JaAcKU/2uBE5m23w7+8xqLMXjUturMVCF5cF66EVMAibb2nHOqytie+N35GYSwIeTd16PKwbFBDd12c2E5M9COWwjVRcVTz4OnNWmHv9PEqZIbXhvfCLlyJ/aY3zV9Pno1VLFcYxGMrBugncEo4ecHkEbaVp3VLS4wn8EykAC1byvYBshzEXppYYd3c6a9cNw50Z6inI/IaKVxIForuz+Yn5uRAsjRyCY2auBCMeHMhY+CQ+4Vl5WtGjuJuE1g7t8AVZqt2JDBgDuxZAZX/JGncfzUaaDl87athMTtBKnFkTnCl34UXPkhsgM0LC4YoUiyKYjQ== +some: value +# and this comment as well + +appID: app-id +isAirgap: false +replicatedID: kotsadm-id +userAgent: KOTS/v0.0.0-unknown +`, + }, + wantErr: false, + }) + + tests = append(tests, Test{ + name: "airgap - a standalone replicated chart", + isAirgap: true, + chartContent: map[string]string{ + "replicated/Chart.yaml": fmt.Sprintf(`apiVersion: v1 +name: %s +version: 1.0.0 +description: A Replicated Chart +`, chartName), + "replicated/values.yaml": `# preserve this comment + +license: "" +appName: app-name +channelID: channel-id +channelName: channel-name +channelSequence: 2 +releaseCreatedAt: "2023-10-02T00:00:00Z" +releaseNotes: override my release notes +releaseSequence: 1 +statusInformers: + - deployment/replicated + - service/replicated +versionLabel: 1.0.0 +# and this comment + +some: value +# and this comment as well +`, + }, + want: map[string]string{ + "replicated/Chart.yaml": fmt.Sprintf(`apiVersion: v1 +name: %s +version: 1.0.0 +description: A Replicated Chart +`, chartName), + "replicated/values.yaml": `# preserve this comment + +license: | + apiVersion: kots.io/v1beta1 + kind: License + metadata: + creationTimestamp: null + name: kots-license + spec: + appSlug: app-slug + channelName: channel-name + customerEmail: customer@example.com + customerName: Customer Name + endpoint: https://replicated.app + entitlements: + license-field: + description: This is a license field + title: License Field + value: license-field-value + valueType: string + licenseID: license-id + licenseType: dev + signature: "" + status: {} +appName: app-name +channelID: channel-id +channelName: channel-name +channelSequence: 2 +releaseCreatedAt: "2023-10-02T00:00:00Z" +releaseNotes: override my release notes +releaseSequence: 1 +statusInformers: + - deployment/replicated + - service/replicated +versionLabel: 1.0.0 +# and this comment + +some: value +# and this comment as well + +appID: app-id +isAirgap: true +replicatedID: kotsadm-id +userAgent: KOTS/v0.0.0-unknown +global: + replicated: + channelName: channel-name + customerEmail: customer@example.com + customerName: Customer Name + dockerconfigjson: eyJhdXRocyI6eyJjdXN0b20ucHJveHkuY29tIjp7ImF1dGgiOiJiR2xqWlc1elpTMXBaRHBzYVdObGJuTmxMV2xrIn0sImN1c3RvbS5yZWdpc3RyeS5jb20iOnsiYXV0aCI6ImJHbGpaVzV6WlMxcFpEcHNhV05sYm5ObExXbGsifX19 + licenseFields: + license-field: + description: This is a license field + name: license-field + title: License Field + value: license-field-value + valueType: string + licenseID: license-id + licenseType: dev +`, + }, + wantErr: false, + }) + + tests = append(tests, Test{ + name: "online - a guestbook chart with the replicated subchart", + isAirgap: false, + chartContent: map[string]string{ + "guestbook/Chart.yaml": `apiVersion: v2 +name: guestbook +version: 1.16.0 +description: A Guestbook Chart +`, + "guestbook/values.yaml": fmt.Sprintf(`affinity: {} + +# use this value to override the chart name +fullnameOverride: "" + +# use this value to set the image pull policy +image: + pullPolicy: IfNotPresent +%s: + license: online-license + appName: online-app-name + channelID: online-channel-id + channelName: online-channel-name + channelSequence: 2 + releaseCreatedAt: "2023-10-02T00:00:00Z" + releaseNotes: override my release notes + releaseSequence: 1 + statusInformers: + - deployment/replicated + - service/replicated + versionLabel: 1.0.0 +global: + replicated: + licenseID: online-license-id + channelName: online-channel-name + customerName: Online Customer Name + customerEmail: online-customer@example.com + licenseType: dev + dockerconfigjson: bm90LWEtZG9ja2VyLWNvbmZpZy1qc29uCg== + licenseFields: + expires_at: + name: expires_at + title: Expiration + description: License Expiration + value: "" + valueType: String + signature: + v1: nwZmD/sMFzKKxkd7JaAcKU/2uBE5m23w7+8xqLMXjUturMVCF5cF66EVMAibb2nHOqytie+N35GYSwIeTd16PKwbFBDd12c2E5M9COWwjVRcVTz4OnNWmHv9PEqZIbXhvfCLlyJ/aY3zV9Pno1VLFcYxGMrBugncEo4ecHkEbaVp3VLS4wn8EykAC1byvYBshzEXppYYd3c6a9cNw50Z6inI/IaKVxIForuz+Yn5uRAsjRyCY2auBCMeHMhY+CQ+4Vl5WtGjuJuE1g7t8AVZqt2JDBgDuxZAZX/JGncfzUaaDl87athMTtBKnFkTnCl34UXPkhsgM0LC4YoUiyKYjQ== +`, chartName), + "guestbook/charts/replicated/Chart.yaml": fmt.Sprintf(`apiVersion: v1 +name: %s +version: 1.0.0 +description: A Replicated Chart +`, chartName), + "guestbook/charts/replicated/values.yaml": `# preserve this comment + +channelName: keep-this-channel-name +# and this comment + +some: value +# and this comment as well +`, + }, + want: map[string]string{ + "guestbook/Chart.yaml": `apiVersion: v2 +name: guestbook +version: 1.16.0 +description: A Guestbook Chart +`, + "guestbook/values.yaml": fmt.Sprintf(`affinity: {} +# use this value to override the chart name +fullnameOverride: "" +# use this value to set the image pull policy +image: + pullPolicy: IfNotPresent +%s: + license: online-license + appName: online-app-name + channelID: online-channel-id + channelName: online-channel-name + channelSequence: 2 + releaseCreatedAt: "2023-10-02T00:00:00Z" + releaseNotes: override my release notes + releaseSequence: 1 + statusInformers: + - deployment/replicated + - service/replicated + versionLabel: 1.0.0 + appID: app-id + isAirgap: false + replicatedID: kotsadm-id + userAgent: KOTS/v0.0.0-unknown +global: + replicated: + licenseID: online-license-id + channelName: online-channel-name + customerName: Online Customer Name + customerEmail: online-customer@example.com + licenseType: dev + dockerconfigjson: bm90LWEtZG9ja2VyLWNvbmZpZy1qc29uCg== + licenseFields: + expires_at: + name: expires_at + title: Expiration + description: License Expiration + value: "" + valueType: String + signature: + v1: nwZmD/sMFzKKxkd7JaAcKU/2uBE5m23w7+8xqLMXjUturMVCF5cF66EVMAibb2nHOqytie+N35GYSwIeTd16PKwbFBDd12c2E5M9COWwjVRcVTz4OnNWmHv9PEqZIbXhvfCLlyJ/aY3zV9Pno1VLFcYxGMrBugncEo4ecHkEbaVp3VLS4wn8EykAC1byvYBshzEXppYYd3c6a9cNw50Z6inI/IaKVxIForuz+Yn5uRAsjRyCY2auBCMeHMhY+CQ+4Vl5WtGjuJuE1g7t8AVZqt2JDBgDuxZAZX/JGncfzUaaDl87athMTtBKnFkTnCl34UXPkhsgM0LC4YoUiyKYjQ== +`, chartName), + "guestbook/charts/replicated/Chart.yaml": fmt.Sprintf(`apiVersion: v1 +name: %s +version: 1.0.0 +description: A Replicated Chart +`, chartName), + "guestbook/charts/replicated/values.yaml": `# preserve this comment + +channelName: keep-this-channel-name +# and this comment + +some: value +# and this comment as well +`, + }, + wantErr: false, + }) + + tests = append(tests, Test{ + name: "airgap - a guestbook chart with the replicated subchart", + isAirgap: true, + chartContent: map[string]string{ + "guestbook/Chart.yaml": `apiVersion: v2 +name: guestbook +version: 1.16.0 +description: A Guestbook Chart +`, + "guestbook/values.yaml": fmt.Sprintf(`affinity: {} + +# use this value to override the chart name +fullnameOverride: "" + +# use this value to set the image pull policy +image: + pullPolicy: IfNotPresent +%s: + appName: app-name + channelID: channel-id + channelName: channel-name + channelSequence: 2 + releaseCreatedAt: "2023-10-02T00:00:00Z" + releaseNotes: override my release notes + releaseSequence: 1 + statusInformers: + - deployment/replicated + - service/replicated + versionLabel: 1.0.0 +`, chartName), + "guestbook/charts/replicated/Chart.yaml": fmt.Sprintf(`apiVersion: v1 +name: %s +version: 1.0.0 +description: A Replicated Chart +`, chartName), + "guestbook/charts/replicated/values.yaml": `# preserve this comment + +channelName: keep-this-channel-name +# and this comment + +some: value +# and this comment as well +`, + }, + want: map[string]string{ + "guestbook/Chart.yaml": `apiVersion: v2 +name: guestbook +version: 1.16.0 +description: A Guestbook Chart +`, + "guestbook/values.yaml": fmt.Sprintf(`affinity: {} +# use this value to override the chart name +fullnameOverride: "" +# use this value to set the image pull policy +image: + pullPolicy: IfNotPresent +%s: + appName: app-name + channelID: channel-id + channelName: channel-name + channelSequence: 2 + releaseCreatedAt: "2023-10-02T00:00:00Z" + releaseNotes: override my release notes + releaseSequence: 1 + statusInformers: + - deployment/replicated + - service/replicated + versionLabel: 1.0.0 + appID: app-id + isAirgap: true + license: | + apiVersion: kots.io/v1beta1 + kind: License + metadata: + creationTimestamp: null + name: kots-license + spec: + appSlug: app-slug + channelName: channel-name + customerEmail: customer@example.com + customerName: Customer Name + endpoint: https://replicated.app + entitlements: + license-field: + description: This is a license field + title: License Field + value: license-field-value + valueType: string + licenseID: license-id + licenseType: dev + signature: "" + status: {} + replicatedID: kotsadm-id + userAgent: KOTS/v0.0.0-unknown +global: + replicated: + channelName: channel-name + customerEmail: customer@example.com + customerName: Customer Name + dockerconfigjson: eyJhdXRocyI6eyJjdXN0b20ucHJveHkuY29tIjp7ImF1dGgiOiJiR2xqWlc1elpTMXBaRHBzYVdObGJuTmxMV2xrIn0sImN1c3RvbS5yZWdpc3RyeS5jb20iOnsiYXV0aCI6ImJHbGpaVzV6WlMxcFpEcHNhV05sYm5ObExXbGsifX19 + licenseFields: + license-field: + description: This is a license field + name: license-field + title: License Field + value: license-field-value + valueType: string + licenseID: license-id + licenseType: dev +`, chartName), + "guestbook/charts/replicated/Chart.yaml": fmt.Sprintf(`apiVersion: v1 +name: %s +version: 1.0.0 +description: A Replicated Chart +`, chartName), + "guestbook/charts/replicated/values.yaml": `# preserve this comment + +channelName: keep-this-channel-name +# and this comment + +some: value +# and this comment as well +`, + }, + wantErr: false, + }) + + tests = append(tests, Test{ + name: "online - a redis chart with the replicated subchart and predefined replicated and global values", + isAirgap: false, + chartContent: map[string]string{ + "redis/Chart.yaml": `apiVersion: v1 +name: redis +version: 5.0.7 +description: A Redis Chart +`, + "redis/values.yaml": fmt.Sprintf(`## Global Docker image parameters +## Please, note that this will override the image parameters, including dependencies, configured to use the global value +## Current available global Docker image parameters: imageRegistry and imagePullSecrets +## +global: + # imageRegistry: myRegistryName + # imagePullSecrets: + # - myRegistryKeySecretName + # storageClass: myStorageClass + redis: {} + replicated: + some: value + licenseID: online-license-id + channelName: online-channel-name + customerName: Online Customer Name + customerEmail: online-customer@example.com + licenseType: dev + dockerconfigjson: bm90LWEtZG9ja2VyLWNvbmZpZy1qc29uCg== + licenseFields: + expires_at: + name: expires_at + title: Expiration + description: License Expiration + value: "" + valueType: String + signature: + v1: nwZmD/sMFzKKxkd7JaAcKU/2uBE5m23w7+8xqLMXjUturMVCF5cF66EVMAibb2nHOqytie+N35GYSwIeTd16PKwbFBDd12c2E5M9COWwjVRcVTz4OnNWmHv9PEqZIbXhvfCLlyJ/aY3zV9Pno1VLFcYxGMrBugncEo4ecHkEbaVp3VLS4wn8EykAC1byvYBshzEXppYYd3c6a9cNw50Z6inI/IaKVxIForuz+Yn5uRAsjRyCY2auBCMeHMhY+CQ+4Vl5WtGjuJuE1g7t8AVZqt2JDBgDuxZAZX/JGncfzUaaDl87athMTtBKnFkTnCl34UXPkhsgM0LC4YoUiyKYjQ== + +# values related to the replicated subchart +%s: + some: value + license: online-license + appName: online-app-name + channelID: online-channel-id + channelName: online-channel-name + channelSequence: 2 + releaseCreatedAt: "2023-10-02T00:00:00Z" + releaseNotes: override my release notes + releaseSequence: 1 + statusInformers: + - deployment/replicated + - service/replicated + versionLabel: 1.0.0 +`, chartName), + "redis/charts/replicated/Chart.yaml": fmt.Sprintf(`apiVersion: v1 +name: %s +version: 1.0.0 +description: A Replicated Chart +`, chartName), + "redis/charts/replicated/values.yaml": `# preserve this comment + +channelName: keep-this-channel-name +# and this comment + +some: value +# and this comment as well +`, + }, + want: map[string]string{ + "redis/Chart.yaml": `apiVersion: v1 +name: redis +version: 5.0.7 +description: A Redis Chart +`, + "redis/values.yaml": fmt.Sprintf(`## Global Docker image parameters +## Please, note that this will override the image parameters, including dependencies, configured to use the global value +## Current available global Docker image parameters: imageRegistry and imagePullSecrets +## +global: + # imageRegistry: myRegistryName + # imagePullSecrets: + # - myRegistryKeySecretName + # storageClass: myStorageClass + redis: {} + replicated: + some: value + licenseID: online-license-id + channelName: online-channel-name + customerName: Online Customer Name + customerEmail: online-customer@example.com + licenseType: dev + dockerconfigjson: bm90LWEtZG9ja2VyLWNvbmZpZy1qc29uCg== + licenseFields: + expires_at: + name: expires_at + title: Expiration + description: License Expiration + value: "" + valueType: String + signature: + v1: nwZmD/sMFzKKxkd7JaAcKU/2uBE5m23w7+8xqLMXjUturMVCF5cF66EVMAibb2nHOqytie+N35GYSwIeTd16PKwbFBDd12c2E5M9COWwjVRcVTz4OnNWmHv9PEqZIbXhvfCLlyJ/aY3zV9Pno1VLFcYxGMrBugncEo4ecHkEbaVp3VLS4wn8EykAC1byvYBshzEXppYYd3c6a9cNw50Z6inI/IaKVxIForuz+Yn5uRAsjRyCY2auBCMeHMhY+CQ+4Vl5WtGjuJuE1g7t8AVZqt2JDBgDuxZAZX/JGncfzUaaDl87athMTtBKnFkTnCl34UXPkhsgM0LC4YoUiyKYjQ== +# values related to the replicated subchart +%s: + some: value + license: online-license + appName: online-app-name + channelID: online-channel-id + channelName: online-channel-name + channelSequence: 2 + releaseCreatedAt: "2023-10-02T00:00:00Z" + releaseNotes: override my release notes + releaseSequence: 1 + statusInformers: + - deployment/replicated + - service/replicated + versionLabel: 1.0.0 + appID: app-id + isAirgap: false + replicatedID: kotsadm-id + userAgent: KOTS/v0.0.0-unknown +`, chartName), + "redis/charts/replicated/Chart.yaml": fmt.Sprintf(`apiVersion: v1 +name: %s +version: 1.0.0 +description: A Replicated Chart +`, chartName), + "redis/charts/replicated/values.yaml": `# preserve this comment + +channelName: keep-this-channel-name +# and this comment + +some: value +# and this comment as well +`, + }, + wantErr: false, + }) + + tests = append(tests, Test{ + name: "airgap - a redis chart with the replicated subchart and predefined replicated and global values", + isAirgap: true, + chartContent: map[string]string{ + "redis/Chart.yaml": `apiVersion: v1 +name: redis +version: 5.0.7 +description: A Redis Chart +`, + "redis/values.yaml": fmt.Sprintf(`## Global Docker image parameters +## Please, note that this will override the image parameters, including dependencies, configured to use the global value +## Current available global Docker image parameters: imageRegistry and imagePullSecrets +## +global: + # imageRegistry: myRegistryName + # imagePullSecrets: + # - myRegistryKeySecretName + # storageClass: myStorageClass + redis: {} + replicated: + some: value + +# values related to the replicated subchart +%s: + some: value + appName: app-name + channelID: channel-id + channelName: channel-name + channelSequence: 2 + releaseCreatedAt: "2023-10-02T00:00:00Z" + releaseNotes: override my release notes + releaseSequence: 1 + statusInformers: + - deployment/replicated + - service/replicated + versionLabel: 1.0.0 +`, chartName), + "redis/charts/replicated/Chart.yaml": fmt.Sprintf(`apiVersion: v1 +name: %s +version: 1.0.0 +description: A Replicated Chart +`, chartName), + "redis/charts/replicated/values.yaml": `# preserve this comment + +channelName: keep-this-channel-name +# and this comment + +some: value +# and this comment as well +`, + }, + want: map[string]string{ + "redis/Chart.yaml": `apiVersion: v1 +name: redis +version: 5.0.7 +description: A Redis Chart +`, + "redis/values.yaml": fmt.Sprintf(`## Global Docker image parameters +## Please, note that this will override the image parameters, including dependencies, configured to use the global value +## Current available global Docker image parameters: imageRegistry and imagePullSecrets +## +global: + # imageRegistry: myRegistryName + # imagePullSecrets: + # - myRegistryKeySecretName + # storageClass: myStorageClass + redis: {} + replicated: + some: value + channelName: channel-name + customerEmail: customer@example.com + customerName: Customer Name + dockerconfigjson: eyJhdXRocyI6eyJjdXN0b20ucHJveHkuY29tIjp7ImF1dGgiOiJiR2xqWlc1elpTMXBaRHBzYVdObGJuTmxMV2xrIn0sImN1c3RvbS5yZWdpc3RyeS5jb20iOnsiYXV0aCI6ImJHbGpaVzV6WlMxcFpEcHNhV05sYm5ObExXbGsifX19 + licenseFields: + license-field: + description: This is a license field + name: license-field + title: License Field + value: license-field-value + valueType: string + licenseID: license-id + licenseType: dev +# values related to the replicated subchart +%s: + some: value + appName: app-name + channelID: channel-id + channelName: channel-name + channelSequence: 2 + releaseCreatedAt: "2023-10-02T00:00:00Z" + releaseNotes: override my release notes + releaseSequence: 1 + statusInformers: + - deployment/replicated + - service/replicated + versionLabel: 1.0.0 + appID: app-id + isAirgap: true + license: | + apiVersion: kots.io/v1beta1 + kind: License + metadata: + creationTimestamp: null + name: kots-license + spec: + appSlug: app-slug + channelName: channel-name + customerEmail: customer@example.com + customerName: Customer Name + endpoint: https://replicated.app + entitlements: + license-field: + description: This is a license field + title: License Field + value: license-field-value + valueType: string + licenseID: license-id + licenseType: dev + signature: "" + status: {} + replicatedID: kotsadm-id + userAgent: KOTS/v0.0.0-unknown +`, chartName), + "redis/charts/replicated/Chart.yaml": fmt.Sprintf(`apiVersion: v1 +name: %s +version: 1.0.0 +description: A Replicated Chart +`, chartName), + "redis/charts/replicated/values.yaml": `# preserve this comment + +channelName: keep-this-channel-name +# and this comment + +some: value +# and this comment as well +`, + }, + wantErr: false, + }) + + tests = append(tests, Test{ + name: "online - a postgresql chart with replicated as subsubchart", + isAirgap: false, + chartContent: map[string]string{ + "postgresql/Chart.yaml": `apiVersion: v2 +name: postgresql +version: 11.6.0 +description: A Postgresql Chart +`, + "postgresql/values.yaml": `extraEnv: [] + +# override global values here +global: + postgresql: {} + +# additional values can be added here +`, + "postgresql/charts/guestbook/Chart.yaml": `apiVersion: v2 +name: guestbook +version: 1.16.0 +description: A Guestbook Chart +`, + "postgresql/charts/guestbook/values.yaml": `affinity: {} + +# use this value to override the chart name +fullnameOverride: "" + +# use this value to set the image pull policy +image: + pullPolicy: IfNotPresent +`, + "postgresql/charts/guestbook/charts/replicated/Chart.yaml": fmt.Sprintf(`apiVersion: v1 +name: %s +version: 1.0.0 +description: A Replicated Chart +`, chartName), + "postgresql/charts/guestbook/charts/replicated/values.yaml": `# this file should NOT change + +# global values should NOT be updated +global: + keep: this-value + +channelName: keep-this-channel-name +# and this comment + +some: value +# and this comment as well +`, + }, + want: map[string]string{ + "postgresql/Chart.yaml": `apiVersion: v2 +name: postgresql +version: 11.6.0 +description: A Postgresql Chart +`, + "postgresql/values.yaml": `extraEnv: [] + +# override global values here +global: + postgresql: {} + +# additional values can be added here +`, + "postgresql/charts/guestbook/Chart.yaml": `apiVersion: v2 +name: guestbook +version: 1.16.0 +description: A Guestbook Chart +`, + "postgresql/charts/guestbook/values.yaml": `affinity: {} + +# use this value to override the chart name +fullnameOverride: "" + +# use this value to set the image pull policy +image: + pullPolicy: IfNotPresent +`, + "postgresql/charts/guestbook/charts/replicated/Chart.yaml": fmt.Sprintf(`apiVersion: v1 +name: %s +version: 1.0.0 +description: A Replicated Chart +`, chartName), + "postgresql/charts/guestbook/charts/replicated/values.yaml": `# this file should NOT change + +# global values should NOT be updated +global: + keep: this-value + +channelName: keep-this-channel-name +# and this comment + +some: value +# and this comment as well +`, + }, + }) + + tests = append(tests, Test{ + name: "airgap - a postgresql chart with replicated as subsubchart", + isAirgap: true, + chartContent: map[string]string{ + "postgresql/Chart.yaml": `apiVersion: v2 +name: postgresql +version: 11.6.0 +description: A Postgresql Chart +`, + "postgresql/values.yaml": `extraEnv: [] + +# override global values here +global: + postgresql: {} + +# additional values can be added here +`, + "postgresql/charts/guestbook/Chart.yaml": `apiVersion: v2 +name: guestbook +version: 1.16.0 +description: A Guestbook Chart +`, + "postgresql/charts/guestbook/values.yaml": `affinity: {} + +# use this value to override the chart name +fullnameOverride: "" + +# use this value to set the image pull policy +image: + pullPolicy: IfNotPresent +`, + "postgresql/charts/guestbook/charts/replicated/Chart.yaml": fmt.Sprintf(`apiVersion: v1 +name: %s +version: 1.0.0 +description: A Replicated Chart +`, chartName), + "postgresql/charts/guestbook/charts/replicated/values.yaml": `# this file should NOT change + +# global values should NOT be updated +global: + keep: this-value + +channelName: keep-this-channel-name +# and this comment + +some: value +# and this comment as well +`, + }, + want: map[string]string{ + "postgresql/Chart.yaml": `apiVersion: v2 +name: postgresql +version: 11.6.0 +description: A Postgresql Chart +`, + "postgresql/values.yaml": `extraEnv: [] + +# override global values here +global: + postgresql: {} + +# additional values can be added here +`, + "postgresql/charts/guestbook/Chart.yaml": `apiVersion: v2 +name: guestbook +version: 1.16.0 +description: A Guestbook Chart +`, + "postgresql/charts/guestbook/values.yaml": `affinity: {} + +# use this value to override the chart name +fullnameOverride: "" + +# use this value to set the image pull policy +image: + pullPolicy: IfNotPresent +`, + "postgresql/charts/guestbook/charts/replicated/Chart.yaml": fmt.Sprintf(`apiVersion: v1 +name: %s +version: 1.0.0 +description: A Replicated Chart +`, chartName), + "postgresql/charts/guestbook/charts/replicated/values.yaml": `# this file should NOT change + +# global values should NOT be updated +global: + keep: this-value + +channelName: keep-this-channel-name +# and this comment + +some: value +# and this comment as well +`, + }, + }) + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + chartBytes, err := util.FilesToTGZ(tt.chartContent) + require.NoError(t, err) + + upstream := &types.Upstream{ + License: &kotsv1beta1.License{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "kots.io/v1beta1", + Kind: "License", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "kots-license", + }, + Spec: kotsv1beta1.LicenseSpec{ + LicenseID: "license-id", + AppSlug: "app-slug", + ChannelName: "channel-name", + Endpoint: "https://replicated.app", + Entitlements: map[string]kotsv1beta1.EntitlementField{ + "license-field": { + Title: "License Field", + Description: "This is a license field", + ValueType: "string", + Value: kotsv1beta1.EntitlementValue{ + Type: kotsv1beta1.String, + StrVal: "license-field-value", + }, + }, + }, + CustomerEmail: "customer@example.com", + CustomerName: "Customer Name", + LicenseType: "dev", + Signature: []byte{}, + }, + }, + ReplicatedRegistryDomain: "custom.registry.com", + ReplicatedProxyDomain: "custom.proxy.com", + ReplicatedChartNames: testReplicatedChartNames, + } + + writeOptions := types.WriteOptions{ + KotsadmID: "kotsadm-id", + AppID: "app-id", + IsAirgap: tt.isAirgap, + } + + got, err := configureChart(chartBytes, upstream, writeOptions) + if (err != nil) != tt.wantErr { + t.Errorf("configureChart() error = %v, wantErr %v", err, tt.wantErr) + return + } + + gotFiles, err := util.TGZToFiles(got) + require.NoError(t, err) + + for filename, wantContent := range tt.want { + gotContent := gotFiles[filename] + if gotContent != wantContent { + t.Errorf("configureChart() %s: %v", filename, diffString(gotContent, wantContent)) + } + } + }) + } +} + +func diffString(got, want string) string { + diff := difflib.UnifiedDiff{ + A: difflib.SplitLines(got), + B: difflib.SplitLines(want), + FromFile: "Got", + ToFile: "Want", + Context: 1, + } + diffStr, _ := difflib.GetUnifiedDiffString(diff) + return fmt.Sprintf("got:\n%s \n\nwant:\n%s \n\ndiff:\n%s", got, want, diffStr) +} diff --git a/pkg/upstream/replicated.go b/pkg/upstream/replicated.go index 78529b0349..2e4b09a65b 100644 --- a/pkg/upstream/replicated.go +++ b/pkg/upstream/replicated.go @@ -55,6 +55,7 @@ type Release struct { ReleasedAt *time.Time ReplicatedRegistryDomain string ReplicatedProxyDomain string + ReplicatedChartNames []string Manifests map[string][]byte } @@ -125,6 +126,7 @@ func downloadReplicated( isRequired bool, replicatedRegistryDomain string, replicatedProxyDomain string, + replicatedChartNames []string, appSlug string, appSequence int64, isAirgap bool, @@ -136,7 +138,7 @@ func downloadReplicated( var release *Release if localPath != "" { - parsedLocalRelease, err := readReplicatedAppFromLocalPath(localPath, updateCursor, versionLabel, isRequired, replicatedRegistryDomain, replicatedProxyDomain) + parsedLocalRelease, err := readReplicatedAppFromLocalPath(localPath, updateCursor, versionLabel, isRequired, replicatedRegistryDomain, replicatedProxyDomain, replicatedChartNames) if err != nil { return nil, errors.Wrap(err, "failed to read replicated app from local path") } @@ -287,6 +289,7 @@ func downloadReplicated( Files: files, Type: "replicated", UpdateCursor: release.UpdateCursor.Cursor, + License: license, ChannelID: channelID, ChannelName: channelName, VersionLabel: release.VersionLabel, @@ -295,12 +298,13 @@ func downloadReplicated( ReleasedAt: release.ReleasedAt, ReplicatedRegistryDomain: release.ReplicatedRegistryDomain, ReplicatedProxyDomain: release.ReplicatedProxyDomain, + ReplicatedChartNames: release.ReplicatedChartNames, } return upstream, nil } -func readReplicatedAppFromLocalPath(localPath string, localCursor replicatedapp.ReplicatedCursor, versionLabel string, isRequired bool, replicatedRegistryDomain string, replicatedProxyDomain string) (*Release, error) { +func readReplicatedAppFromLocalPath(localPath string, localCursor replicatedapp.ReplicatedCursor, versionLabel string, isRequired bool, replicatedRegistryDomain string, replicatedProxyDomain string, replicatedChartNames []string) (*Release, error) { release := Release{ Manifests: make(map[string][]byte), UpdateCursor: localCursor, @@ -308,6 +312,7 @@ func readReplicatedAppFromLocalPath(localPath string, localCursor replicatedapp. IsRequired: isRequired, ReplicatedRegistryDomain: replicatedRegistryDomain, ReplicatedProxyDomain: replicatedProxyDomain, + ReplicatedChartNames: replicatedChartNames, } err := filepath.Walk(localPath, @@ -370,6 +375,7 @@ func downloadReplicatedApp(replicatedUpstream *replicatedapp.ReplicatedUpstream, releasedAtStr := getResp.Header.Get("X-Replicated-ReleasedAt") replicatedRegistryDomain := getResp.Header.Get("X-Replicated-ReplicatedRegistryDomain") replicatedProxyDomain := getResp.Header.Get("X-Replicated-ReplicatedProxyDomain") + replicatedChartNamesStr := getResp.Header.Get("X-Replicated-ReplicatedChartNames") var releasedAt *time.Time r, err := time.Parse(time.RFC3339, releasedAtStr) @@ -379,6 +385,11 @@ func downloadReplicatedApp(replicatedUpstream *replicatedapp.ReplicatedUpstream, isRequired, _ := strconv.ParseBool(isRequiredStr) + var replicatedChartNames []string + if replicatedChartNamesStr != "" { + replicatedChartNames = strings.Split(replicatedChartNamesStr, ",") + } + gzf, err := gzip.NewReader(getResp.Body) if err != nil { return nil, errors.Wrap(err, "failed to create new gzip reader") @@ -396,6 +407,7 @@ func downloadReplicatedApp(replicatedUpstream *replicatedapp.ReplicatedUpstream, ReleasedAt: releasedAt, ReplicatedRegistryDomain: replicatedRegistryDomain, ReplicatedProxyDomain: replicatedProxyDomain, + ReplicatedChartNames: replicatedChartNames, // NOTE: release notes come from Application spec } tarReader := tar.NewReader(gzf) diff --git a/pkg/upstream/types/types.go b/pkg/upstream/types/types.go index ae9df8ee7a..6f24527b6f 100644 --- a/pkg/upstream/types/types.go +++ b/pkg/upstream/types/types.go @@ -27,6 +27,7 @@ type Upstream struct { Type string Files []UpstreamFile UpdateCursor string + License *kotsv1beta1.License ChannelID string ChannelName string VersionLabel string @@ -35,6 +36,7 @@ type Upstream struct { ReleasedAt *time.Time ReplicatedRegistryDomain string ReplicatedProxyDomain string + ReplicatedChartNames []string EncryptionKey string } @@ -67,6 +69,9 @@ type WriteOptions struct { NoProxyEnvValue string IsMinimalRBAC bool AdditionalNamespaces []string + IsAirgap bool + KotsadmID string + AppID string // This should be set to true when updating due to license sync, config update, registry settings update. // and should be false when it's an upstream update. // When true, the channel name in Installation yaml will not be changed. @@ -98,6 +103,7 @@ type FetchOptions struct { CurrentVersionIsRequired bool CurrentReplicatedRegistryDomain string CurrentReplicatedProxyDomain string + CurrentReplicatedChartNames []string ChannelChanged bool AppSlug string AppSequence int64 diff --git a/pkg/upstream/write.go b/pkg/upstream/write.go index 0d569f8cef..2c8d8f8fb9 100644 --- a/pkg/upstream/write.go +++ b/pkg/upstream/write.go @@ -3,11 +3,11 @@ package upstream import ( "bytes" "encoding/base64" - "io/ioutil" "os" "path" "github.com/pkg/errors" + "github.com/replicatedhq/kots/pkg/archives" "github.com/replicatedhq/kots/pkg/crypto" "github.com/replicatedhq/kots/pkg/kotsutil" "github.com/replicatedhq/kots/pkg/upstream/types" @@ -39,7 +39,7 @@ func WriteUpstream(u *types.Upstream, options types.WriteOptions) error { if err == nil { _, err = os.Stat(path.Join(renderDir, "userdata", "installation.yaml")) if err == nil { - c, err := ioutil.ReadFile(path.Join(renderDir, "userdata", "installation.yaml")) + c, err := os.ReadFile(path.Join(renderDir, "userdata", "installation.yaml")) if err != nil { return errors.Wrap(err, "failed to read existing installation") } @@ -100,7 +100,16 @@ func WriteUpstream(u *types.Upstream, options types.WriteOptions) error { u.Files[i] = file } - if err := ioutil.WriteFile(fileRenderPath, file.Content, 0644); err != nil { + if archives.IsTGZ(file.Content) { + updatedContent, err := configureChart(file.Content, u, options) + if err != nil { + return errors.Wrap(err, "failed to configure replicated sdk") + } + file.Content = updatedContent + u.Files[i] = file + } + + if err := os.WriteFile(fileRenderPath, file.Content, 0644); err != nil { return errors.Wrap(err, "failed to write upstream file") } } @@ -131,6 +140,7 @@ func WriteUpstream(u *types.Upstream, options types.WriteOptions) error { ReleaseNotes: u.ReleaseNotes, ReplicatedRegistryDomain: u.ReplicatedRegistryDomain, ReplicatedProxyDomain: u.ReplicatedProxyDomain, + ReplicatedChartNames: u.ReplicatedChartNames, EncryptionKey: encryptionKey, }, } @@ -147,7 +157,7 @@ func WriteUpstream(u *types.Upstream, options types.WriteOptions) error { } installationBytes := kotsutil.MustMarshalInstallation(&installation) - err = ioutil.WriteFile(path.Join(renderDir, "userdata", "installation.yaml"), installationBytes, 0644) + err = os.WriteFile(path.Join(renderDir, "userdata", "installation.yaml"), installationBytes, 0644) if err != nil { return errors.Wrap(err, "failed to write installation") } diff --git a/pkg/util/file.go b/pkg/util/file.go new file mode 100644 index 0000000000..5583a2a3ed --- /dev/null +++ b/pkg/util/file.go @@ -0,0 +1,74 @@ +package util + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "io" + "strings" + + "github.com/pkg/errors" +) + +func FilesToTGZ(files map[string]string) ([]byte, error) { + var buf bytes.Buffer + gw := gzip.NewWriter(&buf) + tw := tar.NewWriter(gw) + + for path, content := range files { + header := &tar.Header{ + Name: path, + Mode: 0644, + Size: int64(len(content)), + } + if err := tw.WriteHeader(header); err != nil { + return nil, errors.Wrapf(err, "failed to write tar header for %s", path) + } + _, err := io.Copy(tw, strings.NewReader(content)) + if err != nil { + return nil, errors.Wrapf(err, "failed to write %s to tar", path) + } + } + + if err := tw.Close(); err != nil { + return nil, errors.Wrap(err, "failed to close tar writer") + } + + if err := gw.Close(); err != nil { + return nil, errors.Wrap(err, "failed to close gzip writer") + } + + return buf.Bytes(), nil +} + +func TGZToFiles(tgzBytes []byte) (map[string]string, error) { + files := make(map[string]string) + + gr, err := gzip.NewReader(bytes.NewReader(tgzBytes)) + if err != nil { + return nil, errors.Wrap(err, "failed to create gzip reader") + } + defer gr.Close() + + tr := tar.NewReader(gr) + + for { + header, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + return nil, err + } + + if header.Typeflag == tar.TypeReg { + var contentBuf bytes.Buffer + if _, err := io.Copy(&contentBuf, tr); err != nil { + return nil, errors.Wrap(err, "failed to copy tar data") + } + files[header.Name] = contentBuf.String() + } + } + + return files, nil +} diff --git a/pkg/util/file_test.go b/pkg/util/file_test.go new file mode 100644 index 0000000000..31e188b3c9 --- /dev/null +++ b/pkg/util/file_test.go @@ -0,0 +1,51 @@ +package util + +import ( + "reflect" + "testing" +) + +func Test_filesToTGZAndTGZToFiles(t *testing.T) { + tests := []struct { + name string + files map[string]string + }{ + { + name: "SingleFile", + files: map[string]string{ + "file.txt": "File content", + }, + }, + { + name: "MultipleFiles", + files: map[string]string{ + "file1.txt": "File 1 content", + "file2.txt": "File 2 content", + }, + }, + { + name: "EmptyFiles", + files: map[string]string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tgzBytes, err := FilesToTGZ(tt.files) + if err != nil { + t.Errorf("FilesToTGZ() error = %v", err) + return + } + + actualFiles, err := TGZToFiles(tgzBytes) + if err != nil { + t.Errorf("TGZToFiles() error = %v", err) + return + } + + if !reflect.DeepEqual(actualFiles, tt.files) { + t.Errorf("filesToTGZAndTGZToFiles() = %v, want %v", actualFiles, tt.files) + } + }) + } +} diff --git a/web/src/Root.tsx b/web/src/Root.tsx index d21a4ad89e..2e7930a438 100644 --- a/web/src/Root.tsx +++ b/web/src/Root.tsx @@ -9,7 +9,8 @@ import GitOps from "./components/clusters/GitOps"; import PreflightResultPage from "./components/PreflightResultPage"; import AppConfig from "./features/AppConfig/components/AppConfig"; import { AppDetailPage } from "./components/apps/AppDetailPage"; -import ClusterNodes from "./components/apps/ClusterNodes"; +import KurlClusterManagement from "./components/apps/KurlClusterManagement"; +import HelmVMClusterManagement from "./components/apps/HelmVMClusterManagement"; import UnsupportedBrowser from "./components/static/UnsupportedBrowser"; import NotFound from "./components/static/NotFound"; import { Utilities, parseUpstreamUri } from "./utilities/utilities"; @@ -465,6 +466,7 @@ const Root = () => { refetchAppsList={getAppsList} fetchingMetadata={state.fetchingMetadata} isKurlEnabled={Boolean(state.adminConsoleMetadata?.isKurl)} + isHelmVMEnabled={Boolean(state.adminConsoleMetadata?.isHelmVM)} isGitOpsSupported={isGitOpsSupported()} isIdentityServiceSupported={isIdentityServiceSupported()} appsList={state.appsList} @@ -573,7 +575,13 @@ const Root = () => { } /> } + element={ + state.adminConsoleMetadata?.isKurl ? ( + + ) : ( + + ) + } /> { + try { + const res = await fetch(`${process.env.API_ENDPOINT}/helmvm/nodes`, { + headers: { + Accept: "application/json", + }, + credentials: "include", + method: "GET", + }); + if (!res.ok) { + if (res.status === 401) { + Utilities.logoutUser(); + return; + } + console.log( + "failed to get node status list, unexpected status code", + res.status + ); + return; + } + const response = await res.json(); + this.setState({ + helmvm: response, + // if cluster doesn't support ha, then primary will be disabled. Force into secondary + selectedNodeType: !response.ha + ? "secondary" + : this.state.selectedNodeType, + }); + return response; + } catch (err) { + console.log(err); + throw err; + } + }; + + deleteNode = (name) => { + this.setState({ + confirmDeleteNode: name, + }); + }; + + cancelDeleteNode = () => { + this.setState({ + confirmDeleteNode: "", + }); + }; + + reallyDeleteNode = () => { + const name = this.state.confirmDeleteNode; + this.cancelDeleteNode(); + + fetch(`${process.env.API_ENDPOINT}/helmvm/nodes/${name}`, { + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + method: "DELETE", + }) + .then(async (res) => { + if (!res.ok) { + if (res.status === 401) { + Utilities.logoutUser(); + return; + } + this.setState({ + deleteNodeError: `Delete failed with status ${res.status}`, + }); + } + }) + .catch((err) => { + console.log(err); + }); + }; + + generateWorkerAddNodeCommand = async () => { + this.setState({ + generating: true, + command: "", + expiry: null, + generateCommandErrMsg: "", + }); + + fetch( + `${process.env.API_ENDPOINT}/helmvm/generate-node-join-command-secondary`, + { + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + method: "POST", + } + ) + .then(async (res) => { + if (!res.ok) { + this.setState({ + generating: false, + generateCommandErrMsg: `Failed to generate command with status ${res.status}`, + }); + } else { + const data = await res.json(); + this.setState({ + generating: false, + command: data.command, + expiry: data.expiry, + }); + } + }) + .catch((err) => { + console.log(err); + this.setState({ + generating: false, + generateCommandErrMsg: err ? err.message : "Something went wrong", + }); + }); + }; + + onDrainNodeClick = (name) => { + this.setState({ + showConfirmDrainModal: true, + nodeNameToDrain: name, + }); + }; + + drainNode = async (name) => { + this.setState({ showConfirmDrainModal: false, drainingNodeName: name }); + fetch(`${process.env.API_ENDPOINT}/helmvm/nodes/${name}/drain`, { + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + method: "POST", + }) + .then(async (res) => { + this.setState({ drainNodeSuccessful: true }); + setTimeout(() => { + this.setState({ + drainingNodeName: null, + drainNodeSuccessful: false, + }); + }, 3000); + }) + .catch((err) => { + console.log(err); + this.setState({ + drainingNodeName: null, + drainNodeSuccessful: false, + }); + }); + }; + + generatePrimaryAddNodeCommand = async () => { + this.setState({ + generating: true, + command: "", + expiry: null, + generateCommandErrMsg: "", + }); + + fetch( + `${process.env.API_ENDPOINT}/helmvm/generate-node-join-command-primary`, + { + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + method: "POST", + } + ) + .then(async (res) => { + if (!res.ok) { + this.setState({ + generating: false, + generateCommandErrMsg: `Failed to generate command with status ${res.status}`, + }); + } else { + const data = await res.json(); + this.setState({ + generating: false, + command: data.command, + expiry: data.expiry, + }); + } + }) + .catch((err) => { + console.log(err); + this.setState({ + generating: false, + generateCommandErrMsg: err ? err.message : "Something went wrong", + }); + }); + }; + + onAddNodeClick = () => { + this.setState( + { + displayAddNode: true, + }, + async () => { + await this.generateWorkerAddNodeCommand(); + } + ); + }; + + onSelectNodeType = (event) => { + const value = event.currentTarget.value; + this.setState( + { + selectedNodeType: value, + }, + async () => { + if (this.state.selectedNodeType === "secondary") { + await this.generateWorkerAddNodeCommand(); + } else { + await this.generatePrimaryAddNodeCommand(); + } + } + ); + }; + + ackDeleteNodeError = () => { + this.setState({ deleteNodeError: "" }); + }; + + render() { + const { helmvm } = this.state; + const { displayAddNode, generateCommandErrMsg } = this.state; + + if (!helmvm) { + return ( +
+ +
+ ); + } + + return ( +
+ +
+
+
+

+ Your nodes +

+
+ {helmvm?.nodes && + helmvm?.nodes.map((node, i) => ( + + ))} +
+
+ {helmvm?.isHelmVMEnabled && + Utilities.sessionRolesHasOneOf([rbacRoles.CLUSTER_ADMIN]) ? ( + !displayAddNode ? ( +
+ +
+ ) : ( +
+
+

+ Add a Node +

+
+
+
+ + +
+
+ + +
+
+ {this.state.generating && ( +
+ +
+ )} + {!this.state.generating && this.state.command?.length > 0 ? ( + +

+ Run this command on the node you wish to join the + cluster +

+ + Command has been copied to your clipboard + + } + > + {[this.state.command.join(" \\\n ")]} + + {this.state.expiry && ( + + {`Expires on ${dayjs(this.state.expiry).format( + "MMM Do YYYY, h:mm:ss a z" + )} UTC${(-1 * new Date().getTimezoneOffset()) / 60}`} + + )} +
+ ) : ( + + {generateCommandErrMsg && ( +
+ + {generateCommandErrMsg} + +
+ )} +
+ )} +
+ ) + ) : null} +
+
+ {this.state.deleteNodeError && ( + + )} + +
+

+ Deleting this node may cause data loss. Are you sure you want to + proceed? +

+
+ + +
+
+
+ {this.state.showConfirmDrainModal && ( + + this.setState({ + showConfirmDrainModal: false, + nodeNameToDrain: "", + }) + } + shouldReturnFocusAfterClose={false} + contentLabel="Confirm Drain Node" + ariaHideApp={false} + className="Modal MediumSize" + > +
+

+ Are you sure you want to drain {this.state.nodeNameToDrain}? +

+

+ Draining this node may cause data loss. If you want to delete{" "} + {this.state.nodeNameToDrain} you must disconnect it after it has + been drained. +

+
+ + +
+
+
+ )} +
+ ); + } +} + +export default HelmVMClusterManagement; diff --git a/web/src/components/apps/HelmVMNodeRow.jsx b/web/src/components/apps/HelmVMNodeRow.jsx new file mode 100644 index 0000000000..93f2c5489c --- /dev/null +++ b/web/src/components/apps/HelmVMNodeRow.jsx @@ -0,0 +1,278 @@ +import React from "react"; +import classNames from "classnames"; +import Loader from "../shared/Loader"; +import { rbacRoles } from "../../constants/rbac"; +import { getPercentageStatus, Utilities } from "../../utilities/utilities"; +import Icon from "../Icon"; + +export default function HelmVMNodeRow(props) { + const { node } = props; + + const DrainDeleteNode = () => { + const { drainNode, drainNodeSuccessful, drainingNodeName } = props; + if (drainNode && Utilities.sessionRolesHasOneOf(rbacRoles.DRAIN_NODE)) { + if ( + !drainNodeSuccessful && + drainingNodeName && + drainingNodeName === node?.name + ) { + return ( +
+ + + + + Draining Node + +
+ ); + } else if (drainNodeSuccessful && drainingNodeName === node?.name) { + return ( +
+ + + Node successfully drained + +
+ ); + } else { + return ( +
+ +
+ ); + } + } + }; + + return ( +
+
+
+

+ {node?.name} +

+ {node?.isPrimaryNode && ( + + Primary node + + )} +
+
+
+

+ + {node?.isConnected ? "Connected" : "Disconnected"} +

+

+   +

+
+
+

+ + {node?.pods?.available === -1 + ? `${node?.pods?.capacity} pods` + : `${ + node?.pods?.available === 0 + ? "0" + : node?.pods?.capacity - node?.pods?.available + } pods used`} +

+ {node?.pods?.available !== -1 && ( +

+ of {node?.pods?.capacity} pods total +

+ )} +
+
+

+ + {node?.cpu?.available === -1 + ? `${node?.cpu?.capacity} ${ + node?.cpu?.available === "1" ? "core" : "cores" + }` + : `${ + node?.cpu?.available === 0 + ? "0" + : (node?.cpu?.capacity - node?.cpu?.available).toFixed(1) + } ${ + node?.cpu?.available === "1" ? "core used" : "cores used" + }`} +

+ {node?.pods?.available !== -1 && ( +

+ of {node?.cpu?.capacity}{" "} + {node?.cpu?.available === "1" ? "core total" : "cores total"} +

+ )} +
+
+

+ + {node?.memory?.available === -1 + ? `${node?.memory?.capacity?.toFixed(1)} GB` + : `${ + node?.memory?.available === 0 + ? "0" + : ( + node?.memory?.capacity - node?.memory?.available + ).toFixed(1) + } GB used`} +

+ {node?.pods?.available !== -1 && ( +

+ of {node?.memory?.capacity?.toFixed(1)} GB total +

+ )} +
+
+
+
+

+ + {node?.kubeletVersion} +

+
+
+

+ + {node?.conditions?.diskPressure + ? "No Space on Device" + : "No Disk Pressure"} +

+
+
+

+ + {node?.conditions?.pidPressure + ? "Pressure on CPU" + : "No CPU Pressure"} +

+
+
+

+ + {node?.conditions?.memoryPressure + ? "No Space on Memory" + : "No Memory Pressure"} +

+
+
+ {/* LABELS */} +
+ {node?.labels.length > 0 + ? node.labels.sort().map((label, i) => { + let labelToShow = label.replace(":", "="); + return ( +
+ {labelToShow} +
+ ); + }) + : null} +
+
+

+ For more details run{" "} + + kubectl describe node {node?.name} + +

+
+
+ +
+ ); +} diff --git a/web/src/components/apps/HelmVMNodeRow.test.js b/web/src/components/apps/HelmVMNodeRow.test.js new file mode 100644 index 0000000000..b7a4d8324c --- /dev/null +++ b/web/src/components/apps/HelmVMNodeRow.test.js @@ -0,0 +1,3 @@ +describe("HelmVMNodeRow", () => { + it.todo("upgrade to react 18 and add unit tests"); +}); diff --git a/web/src/components/apps/ClusterNodes.jsx b/web/src/components/apps/KurlClusterManagement.jsx similarity index 98% rename from web/src/components/apps/ClusterNodes.jsx rename to web/src/components/apps/KurlClusterManagement.jsx index d5307df3e0..46f6ade1fd 100644 --- a/web/src/components/apps/ClusterNodes.jsx +++ b/web/src/components/apps/KurlClusterManagement.jsx @@ -3,7 +3,7 @@ import classNames from "classnames"; import dayjs from "dayjs"; import { KotsPageTitle } from "@components/Head"; import CodeSnippet from "../shared/CodeSnippet"; -import NodeRow from "./NodeRow"; +import KurlNodeRow from "./KurlNodeRow"; import Loader from "../shared/Loader"; import { rbacRoles } from "../../constants/rbac"; import { Utilities } from "../../utilities/utilities"; @@ -11,10 +11,10 @@ import { Repeater } from "../../utilities/repeater"; import ErrorModal from "../modals/ErrorModal"; import Modal from "react-modal"; -import "@src/scss/components/apps/ClusterNodes.scss"; +import "@src/scss/components/apps/KurlClusterManagement.scss"; import Icon from "../Icon"; -export class ClusterNodes extends Component { +export class KurlClusterManagement extends Component { state = { generating: false, command: "", @@ -287,7 +287,7 @@ export class ClusterNodes extends Component { ); } return ( -
+
@@ -298,7 +298,7 @@ export class ClusterNodes extends Component {
{kurl?.nodes && kurl?.nodes.map((node, i) => ( - { @@ -59,7 +59,7 @@ export default function NodeRow(props) { }; return ( -
+

@@ -71,7 +71,7 @@ export default function NodeRow(props) { )}

-
+

-
+

diff --git a/web/src/components/apps/NodeRow.test.js b/web/src/components/apps/KurlNodeRow.test.js similarity index 64% rename from web/src/components/apps/NodeRow.test.js rename to web/src/components/apps/KurlNodeRow.test.js index 7c045a60a2..415445998a 100644 --- a/web/src/components/apps/NodeRow.test.js +++ b/web/src/components/apps/KurlNodeRow.test.js @@ -1,3 +1,3 @@ -describe("NodeRow", () => { +describe("KurlNodeRow", () => { it.todo("upgrade to react 18 and add unit tests"); }); diff --git a/web/src/components/shared/NavBar.tsx b/web/src/components/shared/NavBar.tsx index 48d60f8a4e..6f937c6226 100644 --- a/web/src/components/shared/NavBar.tsx +++ b/web/src/components/shared/NavBar.tsx @@ -19,6 +19,7 @@ type Props = { isHelmManaged: boolean; isIdentityServiceSupported: boolean; isKurlEnabled: boolean; + isHelmVMEnabled: boolean; isSnapshotsSupported: boolean; logo: string | null; onLogoutError: (message: string) => void; @@ -143,6 +144,7 @@ export class NavBar extends PureComponent { className, fetchingMetadata, isKurlEnabled, + isHelmVMEnabled, isGitOpsSupported, isIdentityServiceSupported, appsList, @@ -226,7 +228,7 @@ export class NavBar extends PureComponent {

)} - {isKurlEnabled && ( + {(isKurlEnabled || isHelmVMEnabled) && (
+
diff --git a/web/src/features/AppVersionHistory/YamlErrors.test.jsx b/web/src/features/AppVersionHistory/YamlErrors.test.jsx index 7c045a60a2..73e56f4c29 100644 --- a/web/src/features/AppVersionHistory/YamlErrors.test.jsx +++ b/web/src/features/AppVersionHistory/YamlErrors.test.jsx @@ -1,3 +1,3 @@ -describe("NodeRow", () => { +describe("YamlErrors", () => { it.todo("upgrade to react 18 and add unit tests"); }); diff --git a/web/src/scss/components/apps/AppVersionHistory.scss b/web/src/scss/components/apps/AppVersionHistory.scss index 8b58577224..6c9f353166 100644 --- a/web/src/scss/components/apps/AppVersionHistory.scss +++ b/web/src/scss/components/apps/AppVersionHistory.scss @@ -97,53 +97,6 @@ $cell-width: 140px; width: $cell-width; } -.ActiveDownstreamVersionRow--wrapper, -.NodeRow--wrapper { - padding: 12px; - border-top: 1px solid #dfdfdf; - - &.is-expired { - background-color: $sub-nav-color; - } - - &.is-deleting { - background-color: $sub-nav-color; - } - - .nodeTag { - background-color: $primary-light-color; - font-size: 12px; - line-height: 12px; - color: #73a3cd; - justify-content: center; - border-radius: 3px; - padding: 2px 4px 2px 4px; - } -} - -.NodeRow--items p { - max-width: 180px; - - .icon { - vertical-align: -3px; - margin-right: 4px; - } - - .node-status { - width: 6px; - height: 6px; - border-radius: 100%; - margin-right: 4px; - display: inline-block; - vertical-align: 1px; - background-color: #44bb66; - - &.disconnected { - background-color: #bc4752; - } - } -} - .gh-version-detail-text { position: relative; top: 1px; diff --git a/web/src/scss/components/apps/ClusterNodes.scss b/web/src/scss/components/apps/ClusterNodes.scss deleted file mode 100644 index 1177761a02..0000000000 --- a/web/src/scss/components/apps/ClusterNodes.scss +++ /dev/null @@ -1,31 +0,0 @@ -.ClusterNodes--wrapper { - .BoxedCheckbox { - height: 85px; - width: 200px; - } - - .timestamp { - position: relative; - margin-top: -10px; - z-index: -1; - } - - .node-label { - font-size: 12px; - font-weight: 500; - line-height: 12px; - color: #577981; - padding: 4px 6px; - border-radius: 20px; - background-color: #ffffff; - white-space: nowrap; - border: 1px solid #577981; - margin-right: 8px; - display: inline-block; - margin-top: 8px; - - &:last-child { - margin-right: 0; - } - } -} diff --git a/web/src/scss/components/apps/HelmVMClusterManagement.scss b/web/src/scss/components/apps/HelmVMClusterManagement.scss new file mode 100644 index 0000000000..cd8bc74a01 --- /dev/null +++ b/web/src/scss/components/apps/HelmVMClusterManagement.scss @@ -0,0 +1,79 @@ +@import "../../variables.scss"; + +.HelmVMClusterManagement--wrapper { + .BoxedCheckbox { + height: 85px; + width: 200px; + } + + .timestamp { + position: relative; + margin-top: -10px; + z-index: -1; + } + + .node-label { + font-size: 12px; + font-weight: 500; + line-height: 12px; + color: #577981; + padding: 4px 6px; + border-radius: 20px; + background-color: #ffffff; + white-space: nowrap; + border: 1px solid #577981; + margin-right: 8px; + display: inline-block; + margin-top: 8px; + + &:last-child { + margin-right: 0; + } + } +} + +.HelmVMNodeRow--wrapper { + padding: 12px; + border-top: 1px solid #dfdfdf; + + &.is-expired { + background-color: $sub-nav-color; + } + + &.is-deleting { + background-color: $sub-nav-color; + } + + .nodeTag { + background-color: $primary-light-color; + font-size: 12px; + line-height: 12px; + color: #73a3cd; + justify-content: center; + border-radius: 3px; + padding: 2px 4px 2px 4px; + } +} + +.HelmVMNodeRow--items p { + max-width: 180px; + + .icon { + vertical-align: -3px; + margin-right: 4px; + } + + .node-status { + width: 6px; + height: 6px; + border-radius: 100%; + margin-right: 4px; + display: inline-block; + vertical-align: 1px; + background-color: #44bb66; + + &.disconnected { + background-color: #bc4752; + } + } +} diff --git a/web/src/scss/components/apps/KurlClusterManagement.scss b/web/src/scss/components/apps/KurlClusterManagement.scss new file mode 100644 index 0000000000..5a8b39a8bf --- /dev/null +++ b/web/src/scss/components/apps/KurlClusterManagement.scss @@ -0,0 +1,79 @@ +@import "../../variables.scss"; + +.KurlClusterManagement--wrapper { + .BoxedCheckbox { + height: 85px; + width: 200px; + } + + .timestamp { + position: relative; + margin-top: -10px; + z-index: -1; + } + + .node-label { + font-size: 12px; + font-weight: 500; + line-height: 12px; + color: #577981; + padding: 4px 6px; + border-radius: 20px; + background-color: #ffffff; + white-space: nowrap; + border: 1px solid #577981; + margin-right: 8px; + display: inline-block; + margin-top: 8px; + + &:last-child { + margin-right: 0; + } + } +} + +.KurlNodeRow--wrapper { + padding: 12px; + border-top: 1px solid #dfdfdf; + + &.is-expired { + background-color: $sub-nav-color; + } + + &.is-deleting { + background-color: $sub-nav-color; + } + + .nodeTag { + background-color: $primary-light-color; + font-size: 12px; + line-height: 12px; + color: #73a3cd; + justify-content: center; + border-radius: 3px; + padding: 2px 4px 2px 4px; + } +} + +.KurlNodeRow--items p { + max-width: 180px; + + .icon { + vertical-align: -3px; + margin-right: 4px; + } + + .node-status { + width: 6px; + height: 6px; + border-radius: 100%; + margin-right: 4px; + display: inline-block; + vertical-align: 1px; + background-color: #44bb66; + + &.disconnected { + background-color: #bc4752; + } + } +} diff --git a/web/src/scss/components/troubleshoot/SupportBundleAnalysis.scss b/web/src/scss/components/troubleshoot/SupportBundleAnalysis.scss index 00b4f5f7d3..b3d61bd548 100644 --- a/web/src/scss/components/troubleshoot/SupportBundleAnalysis.scss +++ b/web/src/scss/components/troubleshoot/SupportBundleAnalysis.scss @@ -402,9 +402,14 @@ } } -.ActiveDownstreamVersionRow--wrapper .icon.u-iconFullArrowGray { - margin-left: 10px; - top: 2px; +.RedactorReportRow--wrapper { + padding: 12px; + border-top: 1px solid #dfdfdf; + + .icon.u-iconFullArrowGray { + margin-left: 100px; + top: 20px; + } } /* ≥ 960px */ diff --git a/web/src/types/index.ts b/web/src/types/index.ts index 787f3220a4..269d6d1b35 100644 --- a/web/src/types/index.ts +++ b/web/src/types/index.ts @@ -130,6 +130,7 @@ export type Entitlement = { export type Metadata = { isAirgap: boolean; isKurl: boolean; + isHelmVM: boolean; }; export type PreflightError = {