diff --git a/.github/actions/copy-assets/Dockerfile b/.github/actions/copy-assets/Dockerfile index 77904db08b..5ae63d6592 100644 --- a/.github/actions/copy-assets/Dockerfile +++ b/.github/actions/copy-assets/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.21 as builder +FROM golang:1.22 as builder WORKDIR /action COPY . /action diff --git a/Makefile b/Makefile index 3a88f5b0bf..0efb26ab65 100644 --- a/Makefile +++ b/Makefile @@ -51,6 +51,8 @@ kots: capture-start-time kots-real report-metric .PHONY: kots-real kots-real: + mkdir -p web/dist + touch web/dist/README.md go build ${LDFLAGS} -o bin/kots $(BUILDFLAGS) github.com/replicatedhq/kots/cmd/kots .PHONY: fmt @@ -80,7 +82,7 @@ build: capture-start-time build-real report-metric .PHONY: build-real build-real: mkdir -p web/dist - touch web/dist/THIS_IS_OKTETO # we need this for go:embed, but it's not actually used in dev + touch web/dist/README.md go build ${LDFLAGS} ${GCFLAGS} -v -o bin/kotsadm $(BUILDFLAGS) ./cmd/kotsadm .PHONY: tidy @@ -112,9 +114,12 @@ debug-build: debug: debug-build LOG_LEVEL=$(LOG_LEVEL) dlv --listen=:2345 --headless=true --api-version=2 exec ./bin/kotsadm-debug api -.PHONY: build-ttl.sh -build-ttl.sh: kots build +.PHONY: web +web: source .image.env && ${MAKE} -C web build-kotsadm + +.PHONY: build-ttl.sh +build-ttl.sh: web kots build docker build -f deploy/Dockerfile -t ttl.sh/${CURRENT_USER}/kotsadm:24h . docker push ttl.sh/${CURRENT_USER}/kotsadm:24h diff --git a/cmd/kots/cli/install.go b/cmd/kots/cli/install.go index d2c268641f..e98110dfc8 100644 --- a/cmd/kots/cli/install.go +++ b/cmd/kots/cli/install.go @@ -37,8 +37,8 @@ import ( "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" storetypes "github.com/replicatedhq/kots/pkg/store/types" + "github.com/replicatedhq/kots/pkg/tasks" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" "github.com/replicatedhq/troubleshoot/pkg/preflight" "github.com/spf13/cobra" @@ -887,7 +887,7 @@ func ValidateAutomatedInstall(deployOptions kotsadmtypes.DeployOptions, authSlug return "", errors.New("timeout waiting for automated install. Use the --wait-duration flag to increase timeout.") } -func getAutomatedInstallStatus(url string, authSlug string) (*kotsstore.TaskStatus, error) { +func getAutomatedInstallStatus(url string, authSlug string) (*tasks.TaskStatus, error) { newReq, err := http.NewRequest("GET", url, nil) if err != nil { return nil, errors.Wrap(err, "failed to create request") @@ -910,7 +910,7 @@ func getAutomatedInstallStatus(url string, authSlug string) (*kotsstore.TaskStat return nil, errors.Wrap(err, "failed to read response body") } - taskStatus := kotsstore.TaskStatus{} + taskStatus := tasks.TaskStatus{} if err := json.Unmarshal(b, &taskStatus); err != nil { return nil, errors.Wrap(err, "failed to unmarshal task status") } diff --git a/cmd/kots/cli/root.go b/cmd/kots/cli/root.go index a7b9c9941f..d1c0de67fe 100644 --- a/cmd/kots/cli/root.go +++ b/cmd/kots/cli/root.go @@ -55,6 +55,7 @@ func RootCmd() *cobra.Command { cmd.AddCommand(CompletionCmd()) cmd.AddCommand(DockerRegistryCmd()) cmd.AddCommand(EnableHACmd()) + cmd.AddCommand(UpgradeServiceCmd()) viper.BindPFlags(cmd.Flags()) diff --git a/cmd/kots/cli/upgrade-service.go b/cmd/kots/cli/upgrade-service.go new file mode 100644 index 0000000000..f0ea172f65 --- /dev/null +++ b/cmd/kots/cli/upgrade-service.go @@ -0,0 +1,78 @@ +package cli + +import ( + "fmt" + "io" + "os" + + "github.com/replicatedhq/kots/pkg/upgradeservice" + "github.com/replicatedhq/kots/pkg/upgradeservice/types" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "gopkg.in/yaml.v3" +) + +func UpgradeServiceCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "upgrade-service", + Short: "KOTS Upgrade Service", + Hidden: true, + } + + cmd.AddCommand(UpgradeServiceStartCmd()) + + return cmd +} + +func UpgradeServiceStartCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "start [params-file]", + Short: "Starts a KOTS upgrade service using the provided params file", + Long: ``, + SilenceUsage: true, + SilenceErrors: false, + PreRun: func(cmd *cobra.Command, args []string) { + viper.BindPFlags(cmd.Flags()) + os.Setenv("IS_UPGRADE_SERVICE", "true") + }, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) != 1 { + cmd.Help() + os.Exit(1) + } + + paramsYAML, err := readUpgradeServiceParams(args[0]) + if err != nil { + return fmt.Errorf("failed to read config file: %v", err) + } + + var params types.UpgradeServiceParams + if err := yaml.Unmarshal(paramsYAML, ¶ms); err != nil { + return fmt.Errorf("failed to unmarshal config file: %v", err) + } + + if err := upgradeservice.Serve(params); err != nil { + return err + } + + return nil + }, + } + + return cmd +} + +func readUpgradeServiceParams(path string) ([]byte, error) { + if path == "-" { + b, err := io.ReadAll(os.Stdin) + if err != nil { + return nil, fmt.Errorf("read stdin: %w", err) + } + return b, nil + } + b, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("read file: %w", err) + } + return b, nil +} diff --git a/cmd/kots/cli/version.go b/cmd/kots/cli/version.go index ff3d682063..dbc64305f4 100644 --- a/cmd/kots/cli/version.go +++ b/cmd/kots/cli/version.go @@ -1,11 +1,12 @@ package cli import ( - "encoding/json" "fmt" + "log" + "net/http" - "github.com/pkg/errors" - "github.com/replicatedhq/kots/pkg/buildversion" + "github.com/gorilla/mux" + "github.com/replicatedhq/kots/pkg/handlers" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -25,37 +26,19 @@ func VersionCmd() *cobra.Command { viper.BindPFlags(cmd.Flags()) }, RunE: func(cmd *cobra.Command, args []string) error { - v := viper.GetViper() + r := mux.NewRouter() - output := v.GetString("output") + spa := handlers.SPAHandler{} + r.PathPrefix("/").Handler(spa) - isLatest, latestVer, err := buildversion.IsLatestRelease() - versionOutput := VersionOutput{ - Version: buildversion.Version(), - } - if err == nil && !isLatest { - versionOutput.LatestVersion = latestVer - versionOutput.InstallLatest = "curl https://kots.io/install | bash" + srv := &http.Server{ + Handler: r, + Addr: ":30888", } - if output != "json" && output != "" { - return errors.Errorf("output format %s not supported (allowed formats are: json)", output) - } else if output == "json" { - // marshal JSON - outputJSON, err := json.Marshal(versionOutput) - if err != nil { - return errors.Wrap(err, "error marshaling JSON") - } - fmt.Println(string(outputJSON)) - } else { - // print basic version info - fmt.Printf("Replicated KOTS %s\n", buildversion.Version()) + fmt.Printf("Starting KOTS SPA handler on port %d...\n", 30888) - // check if this is the latest release, and display possible upgrade instructions - if versionOutput.LatestVersion != "" { - fmt.Printf("\nVersion %s is available for kots. To install updates, run\n $ %s\n", versionOutput.LatestVersion, versionOutput.InstallLatest) - } - } + log.Fatal(srv.ListenAndServe()) return nil }, diff --git a/hack/dev/skaffold.Dockerfile b/hack/dev/skaffold.Dockerfile index 32ffe776e3..4f69c3e6f6 100644 --- a/hack/dev/skaffold.Dockerfile +++ b/hack/dev/skaffold.Dockerfile @@ -2,7 +2,6 @@ FROM kotsadm:cache AS builder ENV PROJECTPATH=/go/src/github.com/replicatedhq/kots WORKDIR $PROJECTPATH -RUN mkdir -p web/dist && touch web/dist/README.md COPY Makefile ./ COPY Makefile.build.mk ./ COPY go.mod go.sum ./ diff --git a/hack/dev/skaffoldcache.Dockerfile b/hack/dev/skaffoldcache.Dockerfile index 70f7d7efc7..f500b80bbd 100644 --- a/hack/dev/skaffoldcache.Dockerfile +++ b/hack/dev/skaffoldcache.Dockerfile @@ -4,7 +4,6 @@ RUN go install github.com/go-delve/delve/cmd/dlv@v1.7.2 ENV PROJECTPATH=/go/src/github.com/replicatedhq/kots WORKDIR $PROJECTPATH -RUN mkdir -p web/dist && touch web/dist/README.md COPY Makefile ./ COPY Makefile.build.mk ./ COPY go.mod go.sum ./ diff --git a/kurl_proxy/cmd/main.go b/kurl_proxy/cmd/main.go index 5efdc6629c..21f41391f2 100644 --- a/kurl_proxy/cmd/main.go +++ b/kurl_proxy/cmd/main.go @@ -472,8 +472,8 @@ func getHttpsServer(upstream, dexUpstream *url.URL, tlsSecretName string, secret // CSPMiddleware adds Content-Security-Policy and X-Frame-Options headers to the response. func CSPMiddleware(c *gin.Context) { - c.Writer.Header().Set("Content-Security-Policy", "frame-ancestors 'none';") - c.Writer.Header().Set("X-Frame-Options", "DENY") + c.Writer.Header().Set("Content-Security-Policy", "frame-ancestors 'self';") + c.Writer.Header().Set("X-Frame-Options", "SAMEORIGIN") c.Next() } diff --git a/kurl_proxy/cmd/main_test.go b/kurl_proxy/cmd/main_test.go index 1ce0406526..83dd9a91a7 100644 --- a/kurl_proxy/cmd/main_test.go +++ b/kurl_proxy/cmd/main_test.go @@ -181,8 +181,8 @@ func Test_httpServerCSPHeaders(t *testing.T) { httpServer: getHttpServer("some-fingerprint", true, tmpDir), path: "/assets/index.html", wantHeaders: map[string]string{ - "Content-Security-Policy": "frame-ancestors 'none';", - "X-Frame-Options": "DENY", + "Content-Security-Policy": "frame-ancestors 'self';", + "X-Frame-Options": "SAMEORIGIN", }, }, { @@ -191,8 +191,8 @@ func Test_httpServerCSPHeaders(t *testing.T) { isHttps: true, path: "/tls/assets/index.html", wantHeaders: map[string]string{ - "Content-Security-Policy": "frame-ancestors 'none';", - "X-Frame-Options": "DENY", + "Content-Security-Policy": "frame-ancestors 'self';", + "X-Frame-Options": "SAMEORIGIN", }, }, } @@ -275,15 +275,15 @@ func Test_generateDefaultCertSecret(t *testing.T) { func Test_generateCertHostnames(t *testing.T) { tests := []struct { - name string + name string namespace string hostname string - altNames []string + altNames []string }{ { - name: "with no namespace", - hostname: "kotsadm.default.svc.cluster.local", - altNames : []string{ + name: "with no namespace", + hostname: "kotsadm.default.svc.cluster.local", + altNames: []string{ "kotsadm", "kotsadm.default", "kotsadm.default.svc", @@ -292,10 +292,10 @@ func Test_generateCertHostnames(t *testing.T) { }, }, { - name: "with some other namespace", + name: "with some other namespace", namespace: "somecluster", hostname: "kotsadm.default.svc.cluster.local", - altNames : []string{ + altNames: []string{ "kotsadm", "kotsadm.default", "kotsadm.default.svc", diff --git a/kurl_proxy/skaffold.Dockerfile b/kurl_proxy/skaffold.Dockerfile index b66bda2104..c8d3ec5229 100644 --- a/kurl_proxy/skaffold.Dockerfile +++ b/kurl_proxy/skaffold.Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.21-bookworm +FROM golang:1.22-bookworm ENV PROJECTPATH=/go/src/github.com/replicatedhq/kots/kurl_proxy WORKDIR $PROJECTPATH diff --git a/migrations/tables/instance-report.yaml b/migrations/tables/instance-report.yaml deleted file mode 100644 index dae94afd31..0000000000 --- a/migrations/tables/instance-report.yaml +++ /dev/null @@ -1,66 +0,0 @@ -apiVersion: schemas.schemahero.io/v1alpha4 -kind: Table -metadata: - name: instance-report -spec: - name: instance_report - requires: [] - schema: - rqlite: - strict: true - primaryKey: - - created_at - columns: - - name: created_at - type: integer - - name: license_id - type: text - - name: instance_id - type: text - - name: cluster_id - type: text - - name: app_status - type: text - - name: is_kurl - type: integer - - name: kurl_node_count_total - type: integer - - name: kurl_node_count_ready - type: integer - - name: k8s_version - type: text - - name: kots_version - type: text - - name: kots_install_id - type: text - - name: kurl_install_id - type: text - - name: embedded_cluster_id - type: text - - name: embedded_cluster_version - type: text - - name: is_gitops_enabled - type: integer - - name: gitops_provider - type: text -# downstream stuff - - name: downstream_channel_id - type: text - - name: downstream_channel_sequence - type: integer - - name: downstream_channel_name - type: text - - name: downstream_sequence - type: integer - - name: downstream_source - type: text - - name: install_status - type: text - - name: preflight_state - type: text - - name: skip_preflights - type: integer - - name: repl_helm_installs - type: integer - - name: native_helm_installs - type: integer \ No newline at end of file diff --git a/migrations/tables/preflight-report.yaml b/migrations/tables/preflight-report.yaml deleted file mode 100644 index 775772d716..0000000000 --- a/migrations/tables/preflight-report.yaml +++ /dev/null @@ -1,35 +0,0 @@ -apiVersion: schemas.schemahero.io/v1alpha4 -kind: Table -metadata: - name: preflight-report -spec: - name: preflight_report - requires: [] - schema: - rqlite: - strict: true - primaryKey: - - created_at - columns: - - name: created_at - type: integer - - name: license_id - type: text - - name: instance_id - type: text - - name: cluster_id - type: text - - name: sequence - type: integer - - name: skip_preflights - type: integer - - name: install_status - type: text - - name: is_cli - type: integer - - name: preflight_status - type: text - - name: app_status - type: text - - name: kots_version - type: text diff --git a/pkg/airgap/airgap.go b/pkg/airgap/airgap.go index cb61e3eda7..c388d7fe1a 100644 --- a/pkg/airgap/airgap.go +++ b/pkg/airgap/airgap.go @@ -28,6 +28,7 @@ import ( storetypes "github.com/replicatedhq/kots/pkg/store/types" "github.com/replicatedhq/kots/pkg/supportbundle" supportbundletypes "github.com/replicatedhq/kots/pkg/supportbundle/types" + "github.com/replicatedhq/kots/pkg/tasks" "github.com/replicatedhq/kots/pkg/util" "github.com/replicatedhq/kots/pkg/version" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" @@ -54,7 +55,7 @@ type CreateAirgapAppOpts struct { // will also have a version func CreateAppFromAirgap(opts CreateAirgapAppOpts) (finalError error) { taskID := fmt.Sprintf("airgap-install-slug-%s", opts.PendingApp.Slug) - if err := store.GetStore().SetTaskStatus(taskID, "Processing package...", "running"); err != nil { + if err := tasks.SetTaskStatus(taskID, "Processing package...", "running"); err != nil { return errors.Wrap(err, "failed to set task status") } @@ -64,7 +65,7 @@ func CreateAppFromAirgap(opts CreateAirgapAppOpts) (finalError error) { for { select { case <-time.After(time.Second): - if err := store.GetStore().UpdateTaskStatusTimestamp(taskID); err != nil { + if err := tasks.UpdateTaskStatusTimestamp(taskID); err != nil { logger.Error(errors.Wrapf(err, "failed to update task %s", taskID)) } case <-finishedCh: @@ -75,14 +76,14 @@ func CreateAppFromAirgap(opts CreateAirgapAppOpts) (finalError error) { defer func() { if finalError == nil { - if err := store.GetStore().ClearTaskStatus(taskID); err != nil { + if err := tasks.ClearTaskStatus(taskID); err != nil { logger.Error(errors.Wrap(err, "failed to clear install task status")) } if err := store.GetStore().SetAppInstallState(opts.PendingApp.ID, "installed"); err != nil { logger.Error(errors.Wrap(err, "failed to set app status to installed")) } } else { - if err := store.GetStore().SetTaskStatus(taskID, finalError.Error(), "failed"); err != nil { + if err := tasks.SetTaskStatus(taskID, finalError.Error(), "failed"); err != nil { logger.Error(errors.Wrap(err, "failed to set error on install task status")) } if err := store.GetStore().SetAppInstallState(opts.PendingApp.ID, "airgap_upload_error"); err != nil { @@ -96,7 +97,7 @@ func CreateAppFromAirgap(opts CreateAirgapAppOpts) (finalError error) { } // Extract it - if err := store.GetStore().SetTaskStatus(taskID, "Extracting files...", "running"); err != nil { + if err := tasks.SetTaskStatus(taskID, "Extracting files...", "running"); err != nil { return errors.Wrap(err, "failed to set task status") } @@ -130,7 +131,7 @@ func CreateAppFromAirgap(opts CreateAirgapAppOpts) (finalError error) { } defer os.RemoveAll(tmpRoot) - if err := store.GetStore().SetTaskStatus(taskID, "Reading license data...", "running"); err != nil { + if err := tasks.SetTaskStatus(taskID, "Reading license data...", "running"); err != nil { return errors.Wrap(err, "failed to set task status") } @@ -154,7 +155,7 @@ func CreateAppFromAirgap(opts CreateAirgapAppOpts) (finalError error) { go func() { scanner := bufio.NewScanner(pipeReader) for scanner.Scan() { - if err := store.GetStore().SetTaskStatus(taskID, scanner.Text(), "running"); err != nil { + if err := tasks.SetTaskStatus(taskID, scanner.Text(), "running"); err != nil { logger.Error(errors.Wrapf(err, "failed to set status for task %s", taskID)) } } diff --git a/pkg/airgap/update.go b/pkg/airgap/update.go index b34cc56977..2bc3634b69 100644 --- a/pkg/airgap/update.go +++ b/pkg/airgap/update.go @@ -36,7 +36,7 @@ func UpdateAppFromAirgap(a *apptypes.App, airgapBundlePath string, deploy bool, finishedChan <- finalError }() - if err := store.GetStore().SetTaskStatus("update-download", "Extracting files...", "running"); err != nil { + if err := tasks.SetTaskStatus("update-download", "Extracting files...", "running"); err != nil { return errors.Wrap(err, "failed to set task status") } @@ -55,7 +55,7 @@ func UpdateAppFromAirgap(a *apptypes.App, airgapBundlePath string, deploy bool, } func UpdateAppFromPath(a *apptypes.App, airgapRoot string, airgapBundlePath string, deploy bool, skipPreflights bool, skipCompatibilityCheck bool) error { - if err := store.GetStore().SetTaskStatus("update-download", "Processing package...", "running"); err != nil { + if err := tasks.SetTaskStatus("update-download", "Processing package...", "running"); err != nil { return errors.Wrap(err, "failed to set tasks status") } @@ -97,7 +97,7 @@ func UpdateAppFromPath(a *apptypes.App, airgapRoot string, airgapBundlePath stri return err } - if err := store.GetStore().SetTaskStatus("update-download", "Processing app package...", "running"); err != nil { + if err := tasks.SetTaskStatus("update-download", "Processing app package...", "running"); err != nil { return errors.Wrap(err, "failed to set task status") } @@ -113,7 +113,7 @@ func UpdateAppFromPath(a *apptypes.App, airgapRoot string, airgapBundlePath stri downstreamNames = append(downstreamNames, d.Name) } - if err := store.GetStore().SetTaskStatus("update-download", "Creating app version...", "running"); err != nil { + if err := tasks.SetTaskStatus("update-download", "Creating app version...", "running"); err != nil { return errors.Wrap(err, "failed to set task status") } @@ -126,7 +126,7 @@ func UpdateAppFromPath(a *apptypes.App, airgapRoot string, airgapBundlePath stri go func() { scanner := bufio.NewScanner(pipeReader) for scanner.Scan() { - if err := store.GetStore().SetTaskStatus("update-download", scanner.Text(), "running"); err != nil { + if err := tasks.SetTaskStatus("update-download", scanner.Text(), "running"); err != nil { logger.Error(errors.Wrap(err, "failed to update download status")) } } diff --git a/pkg/api/reporting/types/types.go b/pkg/api/reporting/types/types.go index 4143e7fb87..b92e16c74e 100644 --- a/pkg/api/reporting/types/types.go +++ b/pkg/api/reporting/types/types.go @@ -1,52 +1,38 @@ package types -// This type is mimicked in the instance_report table. type ReportingInfo struct { - InstanceID string `json:"instance_id"` - ClusterID string `json:"cluster_id"` - Downstream DownstreamInfo `json:"downstream"` - AppStatus string `json:"app_status"` - IsKurl bool `json:"is_kurl"` - KurlNodeCountTotal int `json:"kurl_node_count_total"` - KurlNodeCountReady int `json:"kurl_node_count_ready"` - K8sVersion string `json:"k8s_version"` - K8sDistribution string `json:"k8s_distribution"` - UserAgent string `json:"user_agent"` - KOTSInstallID string `json:"kots_install_id"` - KURLInstallID string `json:"kurl_install_id"` - EmbeddedClusterID string `json:"embedded_cluster_id"` - EmbeddedClusterVersion string `json:"embedded_cluster_version"` - IsGitOpsEnabled bool `json:"is_gitops_enabled"` - GitOpsProvider string `json:"gitops_provider"` - SnapshotProvider string `json:"snapshot_provider"` - SnapshotFullSchedule string `json:"snapshot_full_schedule"` - SnapshotFullTTL string `json:"snapshot_full_ttl"` - SnapshotPartialSchedule string `json:"snapshot_partial_schedule"` - SnapshotPartialTTL string `json:"snapshot_partial_ttl"` + InstanceID string `json:"instance_id" yaml:"instance_id"` + ClusterID string `json:"cluster_id" yaml:"cluster_id"` + Downstream DownstreamInfo `json:"downstream" yaml:"downstream"` + AppStatus string `json:"app_status" yaml:"app_status"` + IsKurl bool `json:"is_kurl" yaml:"is_kurl"` + KurlNodeCountTotal int `json:"kurl_node_count_total" yaml:"kurl_node_count_total"` + KurlNodeCountReady int `json:"kurl_node_count_ready" yaml:"kurl_node_count_ready"` + K8sVersion string `json:"k8s_version" yaml:"k8s_version"` + K8sDistribution string `json:"k8s_distribution" yaml:"k8s_distribution"` + UserAgent string `json:"user_agent" yaml:"user_agent"` + KOTSInstallID string `json:"kots_install_id" yaml:"kots_install_id"` + KURLInstallID string `json:"kurl_install_id" yaml:"kurl_install_id"` + EmbeddedClusterID string `json:"embedded_cluster_id" yaml:"embedded_cluster_id"` + EmbeddedClusterVersion string `json:"embedded_cluster_version" yaml:"embedded_cluster_version"` + IsGitOpsEnabled bool `json:"is_gitops_enabled" yaml:"is_gitops_enabled"` + GitOpsProvider string `json:"gitops_provider" yaml:"gitops_provider"` + SnapshotProvider string `json:"snapshot_provider" yaml:"snapshot_provider"` + SnapshotFullSchedule string `json:"snapshot_full_schedule" yaml:"snapshot_full_schedule"` + SnapshotFullTTL string `json:"snapshot_full_ttl" yaml:"snapshot_full_ttl"` + SnapshotPartialSchedule string `json:"snapshot_partial_schedule" yaml:"snapshot_partial_schedule"` + SnapshotPartialTTL string `json:"snapshot_partial_ttl" yaml:"snapshot_partial_ttl"` } type DownstreamInfo struct { - Cursor string `json:"cursor"` - ChannelID string `json:"channel_id"` - ChannelName string `json:"channel_name"` - Sequence *int64 `json:"sequence"` - Source string `json:"source"` - Status string `json:"status"` - PreflightState string `json:"preflight_state"` - SkipPreflights bool `json:"skip_preflights"` - ReplHelmInstalls int `json:"repl_helm_installs"` - NativeHelmInstalls int `json:"native_helm_installs"` -} - -// This type is mimicked in the preflight_report table. -type PreflightStatus struct { - InstanceID string `json:"instance_id"` - ClusterID string `json:"cluster_id"` - Sequence int64 `json:"sequence"` - SkipPreflights bool `json:"skip_preflights"` - InstallStatus string `json:"install_status"` - IsCLI bool `json:"is_cli"` - PreflightStatus string `json:"preflight_status"` - AppStatus string `json:"app_status"` - KOTSVersion string `json:"kots_version"` + Cursor string `json:"cursor" yaml:"cursor"` + ChannelID string `json:"channel_id" yaml:"channel_id"` + ChannelName string `json:"channel_name" yaml:"channel_name"` + Sequence *int64 `json:"sequence" yaml:"sequence"` + Source string `json:"source" yaml:"source"` + Status string `json:"status" yaml:"status"` + PreflightState string `json:"preflight_state" yaml:"preflight_state"` + SkipPreflights bool `json:"skip_preflights" yaml:"skip_preflights"` + ReplHelmInstalls int `json:"repl_helm_installs" yaml:"repl_helm_installs"` + NativeHelmInstalls int `json:"native_helm_installs" yaml:"native_helm_installs"` } diff --git a/pkg/apiserver/server.go b/pkg/apiserver/server.go index 9323aa32c2..c85da776d8 100644 --- a/pkg/apiserver/server.go +++ b/pkg/apiserver/server.go @@ -29,6 +29,7 @@ import ( "github.com/replicatedhq/kots/pkg/store" "github.com/replicatedhq/kots/pkg/supportbundle" "github.com/replicatedhq/kots/pkg/updatechecker" + "github.com/replicatedhq/kots/pkg/upgradeservice" "github.com/replicatedhq/kots/pkg/util" "golang.org/x/crypto/bcrypt" ) @@ -194,8 +195,11 @@ func Start(params *APIServerParams) { * Static routes **********************************************************************/ - // to avoid confusion, we don't serve this in the dev env... - if os.Getenv("DISABLE_SPA_SERVING") != "1" { + // Serve the upgrade UI from the upgrade service + // CAUTION: modifying this route WILL break backwards compatibility + r.PathPrefix("/upgrade-service/app/{appSlug}").Methods("GET").HandlerFunc(upgradeservice.Proxy) + + if os.Getenv("DISABLE_SPA_SERVING") != "1" { // we don't serve this in the dev env spa := handlers.SPAHandler{} r.PathPrefix("/").Handler(spa) } else if os.Getenv("ENABLE_WEB_PROXY") == "1" { // for dev env diff --git a/pkg/automation/automation.go b/pkg/automation/automation.go index 997a2481a0..d39044ed34 100644 --- a/pkg/automation/automation.go +++ b/pkg/automation/automation.go @@ -27,6 +27,7 @@ import ( "github.com/replicatedhq/kots/pkg/replicatedapp" "github.com/replicatedhq/kots/pkg/store" storetypes "github.com/replicatedhq/kots/pkg/store/types" + "github.com/replicatedhq/kots/pkg/tasks" "github.com/replicatedhq/kots/pkg/util" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" "go.uber.org/zap" @@ -147,7 +148,7 @@ func installLicenseSecret(clientset *kubernetes.Clientset, licenseSecret corev1. return errors.Wrap(err, "failed to marshal task message") } taskID := fmt.Sprintf("automated-install-slug-%s", appSlug) - if err := store.GetStore().SetTaskStatus(taskID, string(taskMessage), AutomatedInstallRunning); err != nil { + if err := tasks.SetTaskStatus(taskID, string(taskMessage), AutomatedInstallRunning); err != nil { logger.Error(errors.Wrap(err, "failed to set task status")) } @@ -157,7 +158,7 @@ func installLicenseSecret(clientset *kubernetes.Clientset, licenseSecret corev1. for { select { case <-time.After(time.Second): - if err := store.GetStore().UpdateTaskStatusTimestamp(taskID); err != nil { + if err := tasks.UpdateTaskStatusTimestamp(taskID); err != nil { logger.Error(errors.Wrapf(err, "failed to update task %s", taskID)) } case <-finishedCh: @@ -183,7 +184,7 @@ func installLicenseSecret(clientset *kubernetes.Clientset, licenseSecret corev1. if err != nil { logger.Error(errors.Wrap(err, "failed to marshal task message")) } - if err := store.GetStore().SetTaskStatus(taskID, string(taskMessage), AutomatedInstallSuccess); err != nil { + if err := tasks.SetTaskStatus(taskID, string(taskMessage), AutomatedInstallSuccess); err != nil { logger.Error(errors.Wrap(err, "failed to set error on install task status")) } } else { @@ -191,7 +192,7 @@ func installLicenseSecret(clientset *kubernetes.Clientset, licenseSecret corev1. if err != nil { logger.Error(errors.Wrap(err, "failed to marshal task message")) } - if err := store.GetStore().SetTaskStatus(taskID, string(taskMessage), AutomatedInstallFailed); err != nil { + if err := tasks.SetTaskStatus(taskID, string(taskMessage), AutomatedInstallFailed); err != nil { logger.Error(errors.Wrap(err, "failed to set error on install task status")) } } @@ -226,7 +227,7 @@ func installLicenseSecret(clientset *kubernetes.Clientset, licenseSecret corev1. return errors.Wrapf(err, "failed to check if license already exists for app %s", appSlug) } if existingLicense != nil { - resolved, err := kotslicense.ResolveExistingLicense(verifiedLicense) + resolved, err := kotsadmlicense.ResolveExistingLicense(verifiedLicense) if err != nil { logger.Error(errors.Wrap(err, "failed to resolve existing license conflict")) } diff --git a/pkg/docker/registry/temp_registry.go b/pkg/docker/registry/temp_registry.go index 7917397994..6665a890ba 100644 --- a/pkg/docker/registry/temp_registry.go +++ b/pkg/docker/registry/temp_registry.go @@ -25,8 +25,8 @@ import ( var tempRegistryConfigYML string type TempRegistry struct { - process *os.Process - port string + cmd *exec.Cmd + port string } // Start will spin up a docker registry service in the background on a random port. @@ -52,11 +52,11 @@ func (r *TempRegistry) Start(rootDir string) (finalError error) { configYMLCopy := strings.Replace(tempRegistryConfigYML, "__ROOT_DIR__", rootDir, 1) configYMLCopy = strings.Replace(configYMLCopy, "__PORT__", freePort, 1) - configFile, err := ioutil.TempFile("", "registryconfig") + configFile, err := os.CreateTemp("", "registryconfig") if err != nil { return errors.Wrap(err, "failed to create temp file for config") } - if err := ioutil.WriteFile(configFile.Name(), []byte(configYMLCopy), 0644); err != nil { + if err := os.WriteFile(configFile.Name(), []byte(configYMLCopy), 0644); err != nil { return errors.Wrap(err, "failed to write config to temp file") } defer os.RemoveAll(configFile.Name()) @@ -70,8 +70,11 @@ func (r *TempRegistry) Start(rootDir string) (finalError error) { return errors.Wrap(err, "failed to start") } + // calling wait helps reap the zombie process + go cmd.Wait() + + r.cmd = cmd r.port = freePort - r.process = cmd.Process if err := r.WaitForReady(time.Second * 30); err != nil { return errors.Wrap(err, "failed to wait for registry to become ready") @@ -81,35 +84,37 @@ func (r *TempRegistry) Start(rootDir string) (finalError error) { } func (r *TempRegistry) Stop() { - if r.process != nil { - if err := r.process.Signal(os.Interrupt); err != nil { - logger.Debugf("Failed to stop registry process on port %s", r.port) + if r.cmd != nil && r.cmd.ProcessState == nil { + if err := r.cmd.Process.Signal(os.Interrupt); err != nil { + logger.Errorf("Failed to stop registry process on port %s", r.port) } } + r.cmd = nil r.port = "" - r.process = nil } func (r *TempRegistry) WaitForReady(timeout time.Duration) error { start := time.Now() - + var lasterr error for { - url := fmt.Sprintf("http://localhost:%s", r.port) - newRequest, err := http.NewRequest("GET", url, nil) - if err == nil { - resp, err := http.DefaultClient.Do(newRequest) - if err == nil { - if resp.StatusCode == http.StatusOK { - return nil - } - } + if time.Sleep(time.Second); time.Since(start) > timeout { + return errors.Errorf("Timeout waiting for registry to become ready on port %s. last error: %v", r.port, lasterr) } - - time.Sleep(time.Second) - - if time.Since(start) > timeout { - return errors.Errorf("Timeout waiting for registry to become ready on port %s", r.port) + request, err := http.NewRequest("GET", fmt.Sprintf("http://localhost:%s", r.port), nil) + if err != nil { + lasterr = errors.Wrap(err, "failed to create request") + continue + } + response, err := http.DefaultClient.Do(request) + if err != nil { + lasterr = errors.Wrap(err, "failed to do request") + continue + } + if response.StatusCode != http.StatusOK { + lasterr = errors.Errorf("unexpected status code %d", response.StatusCode) + continue } + return nil } } diff --git a/pkg/handlers/airgap.go b/pkg/handlers/airgap.go index a5be3abcbd..5300985d1a 100644 --- a/pkg/handlers/airgap.go +++ b/pkg/handlers/airgap.go @@ -19,6 +19,7 @@ import ( "github.com/replicatedhq/kots/pkg/kotsutil" "github.com/replicatedhq/kots/pkg/logger" "github.com/replicatedhq/kots/pkg/store" + "github.com/replicatedhq/kots/pkg/tasks" "github.com/replicatedhq/kots/pkg/util" ) @@ -322,7 +323,7 @@ func (h *Handler) UpdateAppFromAirgap(w http.ResponseWriter, r *http.Request) { } // this is to avoid a race condition where the UI polls the task status before it is set by the goroutine - if err := store.GetStore().SetTaskStatus("update-download", "Processing...", "running"); err != nil { + if err := tasks.SetTaskStatus("update-download", "Processing...", "running"); err != nil { logger.Error(errors.Wrap(err, "failed to set task status")) w.WriteHeader(http.StatusInternalServerError) return diff --git a/pkg/handlers/app.go b/pkg/handlers/app.go index 522bd33766..37a9f98afb 100644 --- a/pkg/handlers/app.go +++ b/pkg/handlers/app.go @@ -25,8 +25,8 @@ import ( "github.com/replicatedhq/kots/pkg/render" "github.com/replicatedhq/kots/pkg/session" "github.com/replicatedhq/kots/pkg/store" - "github.com/replicatedhq/kots/pkg/store/kotsstore" storetypes "github.com/replicatedhq/kots/pkg/store/types" + "github.com/replicatedhq/kots/pkg/tasks" "github.com/replicatedhq/kots/pkg/util" "github.com/replicatedhq/kots/pkg/version" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" @@ -656,13 +656,13 @@ func (h *Handler) GetLatestDeployableVersion(w http.ResponseWriter, r *http.Requ } func (h *Handler) GetAutomatedInstallStatus(w http.ResponseWriter, r *http.Request) { - status, msg, err := store.GetStore().GetTaskStatus(fmt.Sprintf("automated-install-slug-%s", mux.Vars(r)["appSlug"])) + status, msg, err := tasks.GetTaskStatus(fmt.Sprintf("automated-install-slug-%s", mux.Vars(r)["appSlug"])) if err != nil { logger.Error(errors.Wrapf(err, "failed to get install status for app %s", mux.Vars(r)["appSlug"])) w.WriteHeader(http.StatusInternalServerError) return } - response := kotsstore.TaskStatus{ + response := tasks.TaskStatus{ Status: status, Message: msg, } diff --git a/pkg/handlers/config.go b/pkg/handlers/config.go index 32bfa7c857..2938247c98 100644 --- a/pkg/handlers/config.go +++ b/pkg/handlers/config.go @@ -28,6 +28,7 @@ import ( registrytypes "github.com/replicatedhq/kots/pkg/registry/types" "github.com/replicatedhq/kots/pkg/render" rendertypes "github.com/replicatedhq/kots/pkg/render/types" + "github.com/replicatedhq/kots/pkg/reporting" "github.com/replicatedhq/kots/pkg/store" storetypes "github.com/replicatedhq/kots/pkg/store/types" "github.com/replicatedhq/kots/pkg/template" @@ -156,6 +157,21 @@ func (h *Handler) UpdateAppConfig(w http.ResponseWriter, r *http.Request) { return } + isEditable, err := isVersionConfigEditable(foundApp, updateAppConfigRequest.Sequence) + if err != nil { + updateAppConfigResponse.Error = "failed to check if version is editable" + logger.Error(errors.Wrap(err, updateAppConfigResponse.Error)) + JSON(w, http.StatusInternalServerError, updateAppConfigResponse) + return + } + + if !isEditable { + updateAppConfigResponse.Error = "this version cannot be edited" + logger.Error(errors.Wrap(err, updateAppConfigResponse.Error)) + JSON(w, http.StatusForbidden, updateAppConfigResponse) + return + } + validationErrors, err := configvalidation.ValidateConfigSpec(kotsv1beta1.ConfigSpec{Groups: updateAppConfigRequest.ConfigGroups}) if err != nil { updateAppConfigResponse.Error = "failed to validate config spec." @@ -532,6 +548,30 @@ func (h *Handler) CurrentAppConfig(w http.ResponseWriter, r *http.Request) { JSON(w, http.StatusOK, currentAppConfigResponse) } +func isVersionConfigEditable(app *apptypes.App, sequence int64) (bool, error) { + if !util.IsEmbeddedCluster() { + return true, nil + } + // in embedded cluster, past versions cannot be edited + downstreams, err := store.GetStore().ListDownstreamsForApp(app.ID) + if err != nil { + return false, errors.Wrap(err, "failed to list downstreams for app") + } + if len(downstreams) == 0 { + return false, errors.New("no downstreams found for app") + } + versions, err := store.GetStore().GetDownstreamVersions(app.ID, downstreams[0].ClusterID, true) + if err != nil { + return false, errors.Wrap(err, "failed to get downstream versions") + } + for _, v := range versions.PastVersions { + if v.Sequence == sequence { + return false, nil + } + } + return true, nil +} + func shouldCreateNewAppVersion(archiveDir string, appID string, sequence int64) (bool, error) { // Updates are allowed for any version that does not have base rendered. if _, err := os.Stat(filepath.Join(archiveDir, "base")); err != nil { @@ -611,7 +651,7 @@ func updateAppConfig(updateApp *apptypes.App, sequence int64, configGroups []kot return updateAppConfigResponse, err } - requiredItems, requiredItemsTitles := getMissingRequiredConfig(configGroups) + requiredItems, requiredItemsTitles := kotsadmconfig.GetMissingRequiredConfig(configGroups) // not having all the required items is only a failure for the version that the user intended to edit if len(requiredItems) > 0 && isPrimaryVersion { @@ -624,7 +664,7 @@ func updateAppConfig(updateApp *apptypes.App, sequence int64, configGroups []kot // so we don't need the complex logic in kots, we can just write if kotsKinds.ConfigValues != nil { values := kotsKinds.ConfigValues.Spec.Values - kotsKinds.ConfigValues.Spec.Values = updateAppConfigValues(values, configGroups) + kotsKinds.ConfigValues.Spec.Values = kotsadmconfig.UpdateAppConfigValues(values, configGroups) configValuesSpec, err := kotsKinds.Marshal("kots.io", "v1beta1", "ConfigValues") if err != nil { @@ -697,6 +737,7 @@ func updateAppConfig(updateApp *apptypes.App, sequence int64, configGroups []kot Downstreams: downstreams, RegistrySettings: registrySettings, Sequence: renderSequence, + ReportingInfo: reporting.GetReportingInfo(app.ID), }) if err != nil { cause := errors.Cause(err) @@ -761,28 +802,6 @@ func updateAppConfig(updateApp *apptypes.App, sequence int64, configGroups []kot return updateAppConfigResponse, nil } -func getMissingRequiredConfig(configGroups []kotsv1beta1.ConfigGroup) ([]string, []string) { - requiredItems := make([]string, 0, 0) - requiredItemsTitles := make([]string, 0, 0) - for _, group := range configGroups { - if group.When == "false" { - continue - } - for _, item := range group.Items { - if kotsadmconfig.IsRequiredItem(item) && kotsadmconfig.IsUnsetItem(item) { - requiredItems = append(requiredItems, item.Name) - if item.Title != "" { - requiredItemsTitles = append(requiredItemsTitles, item.Title) - } else { - requiredItemsTitles = append(requiredItemsTitles, item.Name) - } - } - } - } - - return requiredItems, requiredItemsTitles -} - func updateAppConfigValues(values map[string]kotsv1beta1.ConfigValue, configGroups []kotsv1beta1.ConfigGroup) map[string]kotsv1beta1.ConfigValue { for _, group := range configGroups { for _, item := range group.Items { diff --git a/pkg/handlers/gitops.go b/pkg/handlers/gitops.go index b254789f9b..0d946a7c99 100644 --- a/pkg/handlers/gitops.go +++ b/pkg/handlers/gitops.go @@ -13,6 +13,7 @@ import ( "github.com/replicatedhq/kots/pkg/logger" "github.com/replicatedhq/kots/pkg/reporting" "github.com/replicatedhq/kots/pkg/store" + "github.com/replicatedhq/kots/pkg/tasks" ) type UpdateAppGitOpsRequest struct { @@ -103,7 +104,7 @@ func (h *Handler) DisableAppGitOps(w http.ResponseWriter, r *http.Request) { } func (h *Handler) InitGitOpsConnection(w http.ResponseWriter, r *http.Request) { - currentStatus, _, err := store.GetStore().GetTaskStatus("gitops-init") + currentStatus, _, err := tasks.GetTaskStatus("gitops-init") if err != nil { logger.Error(err) w.WriteHeader(http.StatusInternalServerError) @@ -187,7 +188,7 @@ func (h *Handler) InitGitOpsConnection(w http.ResponseWriter, r *http.Request) { } go func() { - if err := store.GetStore().SetTaskStatus("gitops-init", "Creating commits ...", "running"); err != nil { + if err := tasks.SetTaskStatus("gitops-init", "Creating commits ...", "running"); err != nil { logger.Error(errors.Wrap(err, "failed to set task status running")) return } @@ -195,11 +196,11 @@ func (h *Handler) InitGitOpsConnection(w http.ResponseWriter, r *http.Request) { var finalError error defer func() { if finalError == nil { - if err := store.GetStore().ClearTaskStatus("gitops-init"); err != nil { + if err := tasks.ClearTaskStatus("gitops-init"); err != nil { logger.Error(errors.Wrap(err, "failed to clear task status")) } } else { - if err := store.GetStore().SetTaskStatus("gitops-init", finalError.Error(), "failed"); err != nil { + if err := tasks.SetTaskStatus("gitops-init", finalError.Error(), "failed"); err != nil { logger.Error(errors.Wrap(err, "failed to set task status error")) } } diff --git a/pkg/handlers/handlers.go b/pkg/handlers/handlers.go index 8e0a75eacc..ade31dc8af 100644 --- a/pkg/handlers/handlers.go +++ b/pkg/handlers/handlers.go @@ -9,6 +9,7 @@ import ( "github.com/replicatedhq/kots/pkg/logger" "github.com/replicatedhq/kots/pkg/policy" "github.com/replicatedhq/kots/pkg/store" + "github.com/replicatedhq/kots/pkg/upgradeservice" kotsscheme "github.com/replicatedhq/kotskinds/client/kotsclientset/scheme" troubleshootscheme "github.com/replicatedhq/troubleshoot/pkg/client/troubleshootclientset/scheme" yaml "github.com/replicatedhq/yaml/v3" @@ -109,6 +110,8 @@ func RegisterSessionAuthRoutes(r *mux.Router, kotsStore store.Store, handler KOT HandlerFunc(middleware.EnforceAccess(policy.AppRead, handler.GetLatestDeployableVersion)) r.Name("GetUpdateDownloadStatus").Path("/api/v1/app/{appSlug}/task/updatedownload").Methods("GET"). HandlerFunc(middleware.EnforceAccess(policy.AppRead, handler.GetUpdateDownloadStatus)) // NOTE: appSlug is unused + r.Name("GetAvailableUpdates").Path("/api/v1/app/{appSlug}/updates").Methods("GET"). + HandlerFunc(middleware.EnforceAccess(policy.AppDownstreamRead, handler.GetAvailableUpdates)) // Airgap r.Name("AirgapBundleProgress").Path("/api/v1/app/{appSlug}/airgap/bundleprogress/{identifier}/{totalChunks}").Methods("GET"). @@ -315,6 +318,15 @@ func RegisterSessionAuthRoutes(r *mux.Router, kotsStore store.Store, handler KOT // Password change r.Name("ChangePassword").Path("/api/v1/password/change").Methods("PUT"). HandlerFunc(middleware.EnforceAccess(policy.PasswordChange, handler.ChangePassword)) + + // Start upgrade service + r.Name("StartUpgradeService").Path("/api/v1/app/{appSlug}/start-upgrade-service").Methods("POST"). + HandlerFunc(middleware.EnforceAccess(policy.AppUpdate, handler.StartUpgradeService)) + + // Proxy upgrade service requests to the upgrade service + // CAUTION: modifying this route WILL break backwards compatibility + r.Name("UpgradeServiceProxy").PathPrefix("/api/v1/upgrade-service/app/{appSlug}").Methods("GET", "POST", "PUT"). + HandlerFunc(middleware.EnforceAccess(policy.AppUpdate, upgradeservice.Proxy)) } func JSON(w http.ResponseWriter, code int, payload interface{}) { diff --git a/pkg/handlers/identity.go b/pkg/handlers/identity.go index a309ba3aa8..a1eec9f571 100644 --- a/pkg/handlers/identity.go +++ b/pkg/handlers/identity.go @@ -25,6 +25,7 @@ import ( "github.com/replicatedhq/kots/pkg/rbac" "github.com/replicatedhq/kots/pkg/render" rendertypes "github.com/replicatedhq/kots/pkg/render/types" + "github.com/replicatedhq/kots/pkg/reporting" "github.com/replicatedhq/kots/pkg/store" "github.com/replicatedhq/kots/pkg/util" "github.com/replicatedhq/kots/pkg/version" @@ -461,6 +462,7 @@ func (h *Handler) ConfigureAppIdentityService(w http.ResponseWriter, r *http.Req Downstreams: downstreams, RegistrySettings: registrySettings, Sequence: nextAppSequence, + ReportingInfo: reporting.GetReportingInfo(a.ID), }) if err != nil { err = errors.Wrap(err, "failed to render archive directory") diff --git a/pkg/handlers/image_rewrite_status.go b/pkg/handlers/image_rewrite_status.go index fe8cba3e6a..611eb178a4 100644 --- a/pkg/handlers/image_rewrite_status.go +++ b/pkg/handlers/image_rewrite_status.go @@ -4,7 +4,7 @@ import ( "net/http" "github.com/replicatedhq/kots/pkg/logger" - "github.com/replicatedhq/kots/pkg/store" + "github.com/replicatedhq/kots/pkg/tasks" ) type GetImageRewriteStatusResponse struct { @@ -13,7 +13,7 @@ type GetImageRewriteStatusResponse struct { } func (h *Handler) GetImageRewriteStatus(w http.ResponseWriter, r *http.Request) { - status, message, err := store.GetStore().GetTaskStatus("image-rewrite") + status, message, err := tasks.GetTaskStatus("image-rewrite") if err != nil { logger.Error(err) w.WriteHeader(500) diff --git a/pkg/handlers/interface.go b/pkg/handlers/interface.go index 6e10c76a5a..d3130c526d 100644 --- a/pkg/handlers/interface.go +++ b/pkg/handlers/interface.go @@ -49,6 +49,7 @@ type KOTSHandler interface { GetLatestDeployableVersion(w http.ResponseWriter, r *http.Request) GetUpdateDownloadStatus(w http.ResponseWriter, r *http.Request) // NOTE: appSlug is unused GetPendingApp(w http.ResponseWriter, r *http.Request) + GetAvailableUpdates(w http.ResponseWriter, r *http.Request) // Airgap AirgapBundleProgress(w http.ResponseWriter, r *http.Request) @@ -161,4 +162,7 @@ type KOTSHandler interface { // Password change ChangePassword(w http.ResponseWriter, r *http.Request) + + // Upgrade service + StartUpgradeService(w http.ResponseWriter, r *http.Request) } diff --git a/pkg/handlers/license.go b/pkg/handlers/license.go index cc28386f00..af3ef995f6 100644 --- a/pkg/handlers/license.go +++ b/pkg/handlers/license.go @@ -15,7 +15,7 @@ import ( "github.com/replicatedhq/kots/pkg/k8sutil" "github.com/replicatedhq/kots/pkg/kotsadm" kotsadmtypes "github.com/replicatedhq/kots/pkg/kotsadm/types" - license "github.com/replicatedhq/kots/pkg/kotsadmlicense" + kotsadmlicense "github.com/replicatedhq/kots/pkg/kotsadmlicense" "github.com/replicatedhq/kots/pkg/kotsutil" kotslicense "github.com/replicatedhq/kots/pkg/license" "github.com/replicatedhq/kots/pkg/logger" @@ -24,6 +24,7 @@ import ( "github.com/replicatedhq/kots/pkg/replicatedapp" "github.com/replicatedhq/kots/pkg/store" "github.com/replicatedhq/kots/pkg/updatechecker" + updatecheckertypes "github.com/replicatedhq/kots/pkg/updatechecker/types" "github.com/replicatedhq/kots/pkg/util" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" serializer "k8s.io/apimachinery/pkg/runtime/serializer/json" @@ -134,7 +135,7 @@ func (h *Handler) SyncLicense(w http.ResponseWriter, r *http.Request) { return } - latestLicense, isSynced, err := license.Sync(foundApp, syncLicenseRequest.LicenseData, true) + latestLicense, isSynced, err := kotsadmlicense.Sync(foundApp, syncLicenseRequest.LicenseData, true) if err != nil { syncLicenseResponse.Error = "failed to sync license" logger.Error(errors.Wrap(err, syncLicenseResponse.Error)) @@ -145,7 +146,7 @@ func (h *Handler) SyncLicense(w http.ResponseWriter, r *http.Request) { if !foundApp.IsAirgap && currentLicense.Spec.ChannelID != latestLicense.Spec.ChannelID { // channel changed and this is an online installation, fetch the latest release for the new channel go func(appID string) { - opts := updatechecker.CheckForUpdatesOpts{ + opts := updatecheckertypes.CheckForUpdatesOpts{ AppID: appID, } _, err := updatechecker.CheckForUpdates(opts) @@ -299,7 +300,7 @@ func (h *Handler) UploadNewLicense(w http.ResponseWriter, r *http.Request) { } // check if license already exists - existingLicense, err := license.CheckIfLicenseExists([]byte(licenseString)) + existingLicense, err := kotsadmlicense.CheckIfLicenseExists([]byte(licenseString)) if err != nil { logger.Error(errors.Wrap(err, "failed to check if license already exists")) uploadLicenseResponse.Error = err.Error() @@ -308,7 +309,7 @@ func (h *Handler) UploadNewLicense(w http.ResponseWriter, r *http.Request) { } if existingLicense != nil { - resolved, err := kotslicense.ResolveExistingLicense(verifiedLicense) + resolved, err := kotsadmlicense.ResolveExistingLicense(verifiedLicense) if err != nil { logger.Error(errors.Wrap(err, "failed to resolve existing license conflict")) } @@ -619,7 +620,7 @@ func (h *Handler) ChangeLicense(w http.ResponseWriter, r *http.Request) { return } - newLicense, err := license.Change(foundApp, changeLicenseRequest.LicenseData) + newLicense, err := kotsadmlicense.Change(foundApp, changeLicenseRequest.LicenseData) if err != nil { logger.Error(errors.Wrap(err, "failed to change license")) changeLicenseResponse.Error = errors.Cause(err).Error() @@ -630,7 +631,7 @@ func (h *Handler) ChangeLicense(w http.ResponseWriter, r *http.Request) { if !foundApp.IsAirgap && currentLicense.Spec.ChannelID != newLicense.Spec.ChannelID { // channel changed and this is an online installation, fetch the latest release for the new channel go func(appID string) { - opts := updatechecker.CheckForUpdatesOpts{ + opts := updatecheckertypes.CheckForUpdatesOpts{ AppID: appID, } _, err := updatechecker.CheckForUpdates(opts) diff --git a/pkg/handlers/mock/mock.go b/pkg/handlers/mock/mock.go index 0eb87eb88c..972f92b95d 100644 --- a/pkg/handlers/mock/mock.go +++ b/pkg/handlers/mock/mock.go @@ -670,6 +670,18 @@ func (mr *MockKOTSHandlerMockRecorder) GetAutomaticUpdatesConfig(w, r interface{ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAutomaticUpdatesConfig", reflect.TypeOf((*MockKOTSHandler)(nil).GetAutomaticUpdatesConfig), w, r) } +// GetAvailableUpdates mocks base method. +func (m *MockKOTSHandler) GetAvailableUpdates(w http.ResponseWriter, r *http.Request) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "GetAvailableUpdates", w, r) +} + +// GetAvailableUpdates indicates an expected call of GetAvailableUpdates. +func (mr *MockKOTSHandlerMockRecorder) GetAvailableUpdates(w, r interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAvailableUpdates", reflect.TypeOf((*MockKOTSHandler)(nil).GetAvailableUpdates), w, r) +} + // GetBackup mocks base method. func (m *MockKOTSHandler) GetBackup(w http.ResponseWriter, r *http.Request) { m.ctrl.T.Helper() @@ -1390,6 +1402,18 @@ func (mr *MockKOTSHandlerMockRecorder) StartPreflightChecks(w, r interface{}) *g return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartPreflightChecks", reflect.TypeOf((*MockKOTSHandler)(nil).StartPreflightChecks), w, r) } +// StartUpgradeService mocks base method. +func (m *MockKOTSHandler) StartUpgradeService(w http.ResponseWriter, r *http.Request) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "StartUpgradeService", w, r) +} + +// StartUpgradeService indicates an expected call of StartUpgradeService. +func (mr *MockKOTSHandlerMockRecorder) StartUpgradeService(w, r interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartUpgradeService", reflect.TypeOf((*MockKOTSHandler)(nil).StartUpgradeService), w, r) +} + // SyncLicense mocks base method. func (m *MockKOTSHandler) SyncLicense(w http.ResponseWriter, r *http.Request) { m.ctrl.T.Helper() diff --git a/pkg/handlers/registry.go b/pkg/handlers/registry.go index 42c4594d6c..9a068897d8 100644 --- a/pkg/handlers/registry.go +++ b/pkg/handlers/registry.go @@ -22,6 +22,7 @@ import ( registrytypes "github.com/replicatedhq/kots/pkg/registry/types" "github.com/replicatedhq/kots/pkg/render" "github.com/replicatedhq/kots/pkg/store" + "github.com/replicatedhq/kots/pkg/tasks" "github.com/replicatedhq/kots/pkg/version" corev1 "k8s.io/api/core/v1" "k8s.io/client-go/kubernetes/scheme" @@ -132,7 +133,7 @@ func (h *Handler) UpdateAppRegistry(w http.ResponseWriter, r *http.Request) { return } - currentStatus, _, err := store.GetStore().GetTaskStatus("image-rewrite") + currentStatus, _, err := tasks.GetTaskStatus("image-rewrite") if err != nil { logger.Error(errors.Wrap(err, "failed to get image-rewrite taks status")) updateAppRegistryResponse.Error = err.Error() @@ -148,7 +149,7 @@ func (h *Handler) UpdateAppRegistry(w http.ResponseWriter, r *http.Request) { return } - if err := store.GetStore().ClearTaskStatus("image-rewrite"); err != nil { + if err := tasks.ClearTaskStatus("image-rewrite"); err != nil { logger.Error(errors.Wrap(err, "failed to clear image-rewrite taks status")) updateAppRegistryResponse.Error = err.Error() JSON(w, http.StatusInternalServerError, updateAppRegistryResponse) @@ -225,7 +226,7 @@ func (h *Handler) UpdateAppRegistry(w http.ResponseWriter, r *http.Request) { } // set task status before starting the goroutine so that the UI can show the status - if err := store.GetStore().SetTaskStatus("image-rewrite", "Updating registry settings", "running"); err != nil { + if err := tasks.SetTaskStatus("image-rewrite", "Updating registry settings", "running"); err != nil { logger.Error(errors.Wrap(err, "failed to set task status")) updateAppRegistryResponse.Error = err.Error() JSON(w, http.StatusInternalServerError, updateAppRegistryResponse) diff --git a/pkg/handlers/status.go b/pkg/handlers/status.go index 94210dd1ac..8d073a45ef 100644 --- a/pkg/handlers/status.go +++ b/pkg/handlers/status.go @@ -8,7 +8,7 @@ import ( "github.com/gorilla/mux" "github.com/pkg/errors" "github.com/replicatedhq/kots/pkg/logger" - "github.com/replicatedhq/kots/pkg/store" + "github.com/replicatedhq/kots/pkg/tasks" ) type GetUpdateDownloadStatusResponse struct { @@ -17,7 +17,7 @@ type GetUpdateDownloadStatusResponse struct { } func (h *Handler) GetUpdateDownloadStatus(w http.ResponseWriter, r *http.Request) { - status, message, err := store.GetStore().GetTaskStatus("update-download") + status, message, err := tasks.GetTaskStatus("update-download") if err != nil { w.WriteHeader(http.StatusInternalServerError) logger.Error(err) @@ -49,7 +49,7 @@ func (h *Handler) GetAppVersionDownloadStatus(w http.ResponseWriter, r *http.Req } taskID := fmt.Sprintf("update-download.%d", sequence) - status, message, err := store.GetStore().GetTaskStatus(taskID) + status, message, err := tasks.GetTaskStatus(taskID) if err != nil { errMsg := fmt.Sprintf("failed to get %s task status", taskID) logger.Error(errors.Wrap(err, errMsg)) diff --git a/pkg/handlers/update.go b/pkg/handlers/update.go index 24aa6a48e8..85aa963c51 100644 --- a/pkg/handlers/update.go +++ b/pkg/handlers/update.go @@ -17,12 +17,14 @@ import ( "github.com/replicatedhq/kots/pkg/airgap" "github.com/replicatedhq/kots/pkg/k8sutil" "github.com/replicatedhq/kots/pkg/kotsadm" + license "github.com/replicatedhq/kots/pkg/kotsadmlicense" "github.com/replicatedhq/kots/pkg/kurl" "github.com/replicatedhq/kots/pkg/logger" "github.com/replicatedhq/kots/pkg/reporting" "github.com/replicatedhq/kots/pkg/store" "github.com/replicatedhq/kots/pkg/tasks" "github.com/replicatedhq/kots/pkg/updatechecker" + updatecheckertypes "github.com/replicatedhq/kots/pkg/updatechecker/types" "github.com/replicatedhq/kots/pkg/util" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" ) @@ -63,7 +65,7 @@ func (h *Handler) AppUpdateCheck(w http.ResponseWriter, r *http.Request) { } if contentType == "application/json" { - opts := updatechecker.CheckForUpdatesOpts{ + opts := updatecheckertypes.CheckForUpdatesOpts{ AppID: app.GetID(), DeployLatest: deploy, DeployVersionLabel: deployVersionLabel, @@ -207,6 +209,55 @@ func (h *Handler) AppUpdateCheck(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusBadRequest) } +type AvailableUpdatesResponse struct { + Success bool `json:"success"` + Updates []updatecheckertypes.AvailableUpdate `json:"updates,omitempty"` +} + +func (h *Handler) GetAvailableUpdates(w http.ResponseWriter, r *http.Request) { + if kotsadm.IsAirgap() { + w.WriteHeader(http.StatusForbidden) + return + } + + availableUpdatesResponse := AvailableUpdatesResponse{ + Success: false, + } + + appSlug, ok := mux.Vars(r)["appSlug"] + if !ok { + logger.Error(errors.New("appSlug is required")) + JSON(w, http.StatusBadRequest, availableUpdatesResponse) + return + } + + store := store.GetStore() + app, err := store.GetAppFromSlug(appSlug) + if err != nil { + logger.Error(errors.Wrap(err, "failed to get app from slug")) + JSON(w, http.StatusInternalServerError, availableUpdatesResponse) + return + } + + latestLicense, _, err := license.Sync(app, "", false) + if err != nil { + logger.Error(errors.Wrap(err, "failed to sync license")) + JSON(w, http.StatusInternalServerError, availableUpdatesResponse) + return + } + + updates, err := updatechecker.GetAvailableUpdates(store, app, latestLicense) + if err != nil { + logger.Error(errors.Wrap(err, "failed to get available app updates")) + JSON(w, http.StatusInternalServerError, availableUpdatesResponse) + return + } + + availableUpdatesResponse.Success = true + availableUpdatesResponse.Updates = updates + JSON(w, http.StatusOK, availableUpdatesResponse) +} + type UpdateAdminConsoleResponse struct { Success bool `json:"success"` UpdateStatus string `json:"updateStatus"` diff --git a/pkg/handlers/upgrade_service.go b/pkg/handlers/upgrade_service.go new file mode 100644 index 0000000000..5227f04168 --- /dev/null +++ b/pkg/handlers/upgrade_service.go @@ -0,0 +1,160 @@ +package handlers + +import ( + "encoding/json" + "net/http" + + "github.com/gorilla/mux" + "github.com/pkg/errors" + apptypes "github.com/replicatedhq/kots/pkg/app/types" + "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/store" + "github.com/replicatedhq/kots/pkg/updatechecker" + "github.com/replicatedhq/kots/pkg/upgradeservice" + upgradeservicetypes "github.com/replicatedhq/kots/pkg/upgradeservice/types" +) + +type StartUpgradeServiceRequest struct { + VersionLabel string `json:"versionLabel"` + UpdateCursor string `json:"updateCursor"` + ChannelID string `json:"channelId"` +} + +type StartUpgradeServiceResponse struct { + Success bool `json:"success"` + Error string `json:"error,omitempty"` +} + +func (h *Handler) StartUpgradeService(w http.ResponseWriter, r *http.Request) { + response := StartUpgradeServiceResponse{ + Success: false, + } + + request := StartUpgradeServiceRequest{} + if err := json.NewDecoder(r.Body).Decode(&request); err != nil { + response.Error = "failed to decode request body" + logger.Error(errors.Wrap(err, response.Error)) + JSON(w, http.StatusBadRequest, response) + return + } + + appSlug := mux.Vars(r)["appSlug"] + + foundApp, err := store.GetStore().GetAppFromSlug(appSlug) + if err != nil { + response.Error = "failed to get app from app slug" + logger.Error(errors.Wrap(err, response.Error)) + JSON(w, http.StatusInternalServerError, response) + return + } + + canStart, reason, err := canStartUpgradeService(foundApp, request) + if err != nil { + response.Error = "failed to check if upgrade service can start" + logger.Error(errors.Wrap(err, response.Error)) + JSON(w, http.StatusInternalServerError, response) + return + } + if !canStart { + response.Error = reason + logger.Error(errors.New(response.Error)) + JSON(w, http.StatusBadRequest, response) + return + } + + params, err := getUpgradeServiceParams(foundApp, request) + if err != nil { + response.Error = err.Error() + logger.Error(errors.Wrap(err, response.Error)) + JSON(w, http.StatusInternalServerError, response) + return + } + + if err := upgradeservice.Start(*params); err != nil { + response.Error = "failed to start upgrade service" + logger.Error(errors.Wrap(err, response.Error)) + JSON(w, http.StatusInternalServerError, response) + return + } + + response.Success = true + + JSON(w, http.StatusOK, response) +} + +func canStartUpgradeService(a *apptypes.App, r StartUpgradeServiceRequest) (bool, string, error) { + currLicense, err := kotsutil.LoadLicenseFromBytes([]byte(a.License)) + if err != nil { + return false, "", errors.Wrap(err, "failed to parse app license") + } + ll, err := replicatedapp.GetLatestLicense(currLicense) + if err != nil { + return false, "", errors.Wrap(err, "failed to get latest license") + } + if currLicense.Spec.ChannelID != ll.License.Spec.ChannelID || r.ChannelID != ll.License.Spec.ChannelID { + return false, "license channel has changed, please sync the license", nil + } + + updates, err := updatechecker.GetAvailableUpdates(store.GetStore(), a, currLicense) + if err != nil { + return false, "", errors.Wrap(err, "failed to get available updates") + } + + isDeployable, nonDeployableCause := false, "update not found" + for _, u := range updates { + if u.UpdateCursor == r.UpdateCursor { + isDeployable, nonDeployableCause = u.IsDeployable, u.NonDeployableCause + break + } + } + if !isDeployable { + return false, nonDeployableCause, nil + } + + return true, "", nil +} + +func getUpgradeServiceParams(a *apptypes.App, r StartUpgradeServiceRequest) (*upgradeservicetypes.UpgradeServiceParams, error) { + registrySettings, err := store.GetStore().GetRegistryDetailsForApp(a.ID) + if err != nil { + return nil, errors.Wrap(err, "failed to get registry details for app") + } + + baseArchive, baseSequence, err := store.GetStore().GetAppVersionBaseArchive(a.ID, r.VersionLabel) + if err != nil { + return nil, errors.Wrap(err, "failed to get app version base archive") + } + + nextSequence, err := store.GetStore().GetNextAppSequence(a.ID) + if err != nil { + return nil, errors.Wrap(err, "failed to get next app sequence") + } + + return &upgradeservicetypes.UpgradeServiceParams{ + AppID: a.ID, + AppSlug: a.Slug, + AppName: a.Name, + AppIsAirgap: a.IsAirgap, + AppIsGitOps: a.IsGitOps, + AppLicense: a.License, + + BaseArchive: baseArchive, + BaseSequence: baseSequence, + NextSequence: nextSequence, + + UpdateVersionLabel: r.VersionLabel, + UpdateCursor: r.UpdateCursor, + UpdateChannelID: r.ChannelID, + + RegistryEndpoint: registrySettings.Hostname, + RegistryUsername: registrySettings.Username, + RegistryPassword: registrySettings.Password, + RegistryNamespace: registrySettings.Namespace, + RegistryIsReadOnly: registrySettings.IsReadOnly, + + ReportingInfo: reporting.GetReportingInfo(a.ID), + }, nil +} diff --git a/pkg/handlers/upload.go b/pkg/handlers/upload.go index fcd2a14ba9..2f0dd5ba21 100644 --- a/pkg/handlers/upload.go +++ b/pkg/handlers/upload.go @@ -16,6 +16,7 @@ import ( "github.com/replicatedhq/kots/pkg/preflight" "github.com/replicatedhq/kots/pkg/render" rendertypes "github.com/replicatedhq/kots/pkg/render/types" + "github.com/replicatedhq/kots/pkg/reporting" "github.com/replicatedhq/kots/pkg/store" storetypes "github.com/replicatedhq/kots/pkg/store/types" "github.com/replicatedhq/kots/pkg/util" @@ -165,6 +166,7 @@ func (h *Handler) UploadExistingApp(w http.ResponseWriter, r *http.Request) { Downstreams: downstreams, RegistrySettings: registrySettings, Sequence: nextAppSequence, + ReportingInfo: reporting.GetReportingInfo(a.ID), }) if err != nil { cause := errors.Cause(err) diff --git a/pkg/kotsadmconfig/config.go b/pkg/kotsadmconfig/config.go index e887f5cb19..3b335ffa38 100644 --- a/pkg/kotsadmconfig/config.go +++ b/pkg/kotsadmconfig/config.go @@ -2,10 +2,14 @@ package kotsadmconfig import ( "context" + "encoding/base64" + "fmt" "os" + "strconv" "github.com/pkg/errors" kotsconfig "github.com/replicatedhq/kots/pkg/config" + "github.com/replicatedhq/kots/pkg/crypto" "github.com/replicatedhq/kots/pkg/k8sutil" "github.com/replicatedhq/kots/pkg/kotsutil" "github.com/replicatedhq/kots/pkg/logger" @@ -13,6 +17,7 @@ import ( "github.com/replicatedhq/kots/pkg/template" "github.com/replicatedhq/kots/pkg/util" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" + "github.com/replicatedhq/kotskinds/multitype" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -91,6 +96,76 @@ func NeedsConfiguration(appSlug string, sequence int64, isAirgap bool, kotsKinds return false, nil } +func GetMissingRequiredConfig(configGroups []kotsv1beta1.ConfigGroup) ([]string, []string) { + requiredItems := make([]string, 0, 0) + requiredItemsTitles := make([]string, 0, 0) + for _, group := range configGroups { + if group.When == "false" { + continue + } + for _, item := range group.Items { + if IsRequiredItem(item) && IsUnsetItem(item) { + requiredItems = append(requiredItems, item.Name) + if item.Title != "" { + requiredItemsTitles = append(requiredItemsTitles, item.Title) + } else { + requiredItemsTitles = append(requiredItemsTitles, item.Name) + } + } + } + } + + return requiredItems, requiredItemsTitles +} + +func UpdateAppConfigValues(values map[string]kotsv1beta1.ConfigValue, configGroups []kotsv1beta1.ConfigGroup) map[string]kotsv1beta1.ConfigValue { + for _, group := range configGroups { + for _, item := range group.Items { + if item.Type == "file" { + v := values[item.Name] + v.Filename = item.Filename + values[item.Name] = v + } + if item.Value.Type == multitype.Bool { + updatedValue := item.Value.BoolVal + v := values[item.Name] + v.Value = strconv.FormatBool(updatedValue) + values[item.Name] = v + } else if item.Value.Type == multitype.String { + updatedValue := item.Value.String() + if item.Type == "password" { + // encrypt using the key + // if the decryption succeeds, don't encrypt again + _, err := util.DecryptConfigValue(updatedValue) + if err != nil { + updatedValue = base64.StdEncoding.EncodeToString(crypto.Encrypt([]byte(updatedValue))) + } + } + + v := values[item.Name] + v.Value = updatedValue + values[item.Name] = v + } + for _, repeatableValues := range item.ValuesByGroup { + // clear out all variadic values for this group first + for name, value := range values { + if value.RepeatableItem == item.Name { + delete(values, name) + } + } + // add variadic groups back in declaratively + for itemName, valueItem := range repeatableValues { + v := values[itemName] + v.Value = fmt.Sprintf("%v", valueItem) + v.RepeatableItem = item.Name + values[itemName] = v + } + } + } + } + return values +} + func ReadConfigValuesFromInClusterSecret() (string, error) { log := logger.NewCLILogger(os.Stdout) diff --git a/pkg/kotsadmlicense/license.go b/pkg/kotsadmlicense/license.go index f8312706f3..c5cf7581bb 100644 --- a/pkg/kotsadmlicense/license.go +++ b/pkg/kotsadmlicense/license.go @@ -12,6 +12,7 @@ import ( "github.com/replicatedhq/kots/pkg/preflight" "github.com/replicatedhq/kots/pkg/render" "github.com/replicatedhq/kots/pkg/replicatedapp" + "github.com/replicatedhq/kots/pkg/reporting" "github.com/replicatedhq/kots/pkg/store" "github.com/replicatedhq/kots/pkg/version" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" @@ -84,7 +85,8 @@ func Sync(a *apptypes.App, licenseString string, failOnVersionCreate bool) (*kot if updatedLicense.Spec.ChannelID != currentLicense.Spec.ChannelID { channelChanged = true } - newSequence, err := store.GetStore().UpdateAppLicense(a.ID, latestSequence, archiveDir, updatedLicense, licenseString, channelChanged, failOnVersionCreate, &version.DownstreamGitOps{}, &render.Renderer{}) + reportingInfo := reporting.GetReportingInfo(a.ID) + newSequence, err := store.GetStore().UpdateAppLicense(a.ID, latestSequence, archiveDir, updatedLicense, licenseString, channelChanged, failOnVersionCreate, &version.DownstreamGitOps{}, &render.Renderer{}, reportingInfo) if err != nil { return nil, false, errors.Wrap(err, "failed to update license") } @@ -161,7 +163,7 @@ func Change(a *apptypes.App, newLicenseString string) (*kotsv1beta1.License, err return nil, errors.Wrap(err, "failed to check if license exists") } if existingLicense != nil { - resolved, err := kotslicense.ResolveExistingLicense(newLicense) + resolved, err := ResolveExistingLicense(newLicense) if err != nil { logger.Error(errors.Wrap(err, "failed to resolve existing license conflict")) } @@ -190,7 +192,8 @@ func Change(a *apptypes.App, newLicenseString string) (*kotsv1beta1.License, err if newLicense.Spec.ChannelID != currentLicense.Spec.ChannelID { channelChanged = true } - newSequence, err := store.GetStore().UpdateAppLicense(a.ID, latestSequence, archiveDir, newLicense, newLicenseString, channelChanged, true, &version.DownstreamGitOps{}, &render.Renderer{}) + reportingInfo := reporting.GetReportingInfo(a.ID) + newSequence, err := store.GetStore().UpdateAppLicense(a.ID, latestSequence, archiveDir, newLicense, newLicenseString, channelChanged, true, &version.DownstreamGitOps{}, &render.Renderer{}, reportingInfo) if err != nil { return nil, errors.Wrap(err, "failed to update license") } @@ -223,3 +226,40 @@ func CheckIfLicenseExists(license []byte) (*kotsv1beta1.License, error) { return nil, nil } + +func ResolveExistingLicense(newLicense *kotsv1beta1.License) (bool, error) { + notInstalledApps, err := store.GetStore().ListFailedApps() + if err != nil { + logger.Error(errors.Wrap(err, "failed to list failed apps")) + return false, err + } + + for _, app := range notInstalledApps { + decode := scheme.Codecs.UniversalDeserializer().Decode + obj, _, err := decode([]byte(app.License), nil, nil) + if err != nil { + continue + } + license := obj.(*kotsv1beta1.License) + if license.Spec.LicenseID != newLicense.Spec.LicenseID { + continue + } + + if err := store.GetStore().RemoveApp(app.ID); err != nil { + return false, errors.Wrap(err, "failed to remove existing app record") + } + } + + // check if license still exists + allLicenses, err := store.GetStore().GetAllAppLicenses() + if err != nil { + return false, errors.Wrap(err, "failed to get all app licenses") + } + for _, l := range allLicenses { + if l.Spec.LicenseID == newLicense.Spec.LicenseID { + return false, nil + } + } + + return true, nil +} diff --git a/pkg/kotsadmupstream/upstream.go b/pkg/kotsadmupstream/upstream.go index 3db3095c3c..4a932ec926 100644 --- a/pkg/kotsadmupstream/upstream.go +++ b/pkg/kotsadmupstream/upstream.go @@ -17,6 +17,7 @@ import ( "github.com/replicatedhq/kots/pkg/render" "github.com/replicatedhq/kots/pkg/reporting" "github.com/replicatedhq/kots/pkg/store" + "github.com/replicatedhq/kots/pkg/tasks" "github.com/replicatedhq/kots/pkg/upstream" "github.com/replicatedhq/kots/pkg/upstream/types" "github.com/replicatedhq/kots/pkg/util" @@ -37,7 +38,7 @@ func DownloadUpdate(appID string, update types.Update, skipPreflights bool, skip for { select { case <-time.After(time.Second): - if err := store.GetStore().UpdateTaskStatusTimestamp(taskID); err != nil { + if err := tasks.UpdateTaskStatusTimestamp(taskID); err != nil { logger.Error(errors.Wrapf(err, "failed to update %s task status timestamp", taskID)) } case <-finishedCh: @@ -47,7 +48,7 @@ func DownloadUpdate(appID string, update types.Update, skipPreflights bool, skip }() } - if err := store.GetStore().SetTaskStatus(taskID, "Fetching update...", "running"); err != nil { + if err := tasks.SetTaskStatus(taskID, "Fetching update...", "running"); err != nil { finalError = errors.Wrap(err, "failed to set task status") return } @@ -66,7 +67,7 @@ func DownloadUpdate(appID string, update types.Update, skipPreflights bool, skip logger.Error(errors.Wrapf(err, "failed to update next app version diff summary for base sequence %d", *update.AppSequence)) } } - err := store.GetStore().ClearTaskStatus(taskID) + err := tasks.ClearTaskStatus(taskID) if err != nil { logger.Error(errors.Wrapf(err, "failed to clear %s task status", taskID)) } @@ -92,7 +93,7 @@ func DownloadUpdate(appID string, update types.Update, skipPreflights bool, skip if update.AppSequence != nil || finalSequence != nil { // a version already exists or has been created - err := store.GetStore().SetTaskStatus(taskID, errMsg, "failed") + err := tasks.SetTaskStatus(taskID, errMsg, "failed") if err != nil { logger.Error(errors.Wrapf(err, "failed to set %s task status", taskID)) } @@ -103,7 +104,7 @@ func DownloadUpdate(appID string, update types.Update, skipPreflights bool, skip newSequence, err := store.GetStore().CreatePendingDownloadAppVersion(appID, update, kotsApplication, license) if err != nil { logger.Error(errors.Wrapf(err, "failed to create pending download app version for update %s", update.VersionLabel)) - if err := store.GetStore().SetTaskStatus(taskID, errMsg, "failed"); err != nil { + if err := tasks.SetTaskStatus(taskID, errMsg, "failed"); err != nil { logger.Error(errors.Wrapf(err, "failed to set %s task status", taskID)) } return @@ -113,10 +114,10 @@ func DownloadUpdate(appID string, update types.Update, skipPreflights bool, skip // a pending download version has been created, bind the download error to it // clear the global task status at the end to avoid a race condition with the UI sequenceTaskID := fmt.Sprintf("update-download.%d", *finalSequence) - if err := store.GetStore().SetTaskStatus(sequenceTaskID, errMsg, "failed"); err != nil { + if err := tasks.SetTaskStatus(sequenceTaskID, errMsg, "failed"); err != nil { logger.Error(errors.Wrapf(err, "failed to set %s task status", sequenceTaskID)) } - if err := store.GetStore().ClearTaskStatus(taskID); err != nil { + if err := tasks.ClearTaskStatus(taskID); err != nil { logger.Error(errors.Wrapf(err, "failed to clear %s task status", taskID)) } }() @@ -143,7 +144,7 @@ func DownloadUpdate(appID string, update types.Update, skipPreflights bool, skip go func() { scanner := bufio.NewScanner(pipeReader) for scanner.Scan() { - if err := store.GetStore().SetTaskStatus(taskID, scanner.Text(), "running"); err != nil { + if err := tasks.SetTaskStatus(taskID, scanner.Text(), "running"); err != nil { logger.Error(err) } } diff --git a/pkg/kotsutil/kots.go b/pkg/kotsutil/kots.go index c7413f60d1..81a545fee7 100644 --- a/pkg/kotsutil/kots.go +++ b/pkg/kotsutil/kots.go @@ -7,6 +7,8 @@ import ( "context" "encoding/base64" "fmt" + "io" + "net/http" "os" "path" "path/filepath" @@ -1517,3 +1519,58 @@ func SaveInstallation(installation *kotsv1beta1.Installation, upstreamDir string } return nil } + +// TODO NOW: download via replicated.app +func DownloadKOTSBinary(version string) (string, error) { + url := fmt.Sprintf("https://github.com/replicatedhq/kots/releases/download/%s/kots_linux_amd64.tar.gz", version) + resp, err := http.Get(url) + if err != nil { + return "", errors.Wrap(err, "failed to get") + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", errors.Errorf("unexpected status code %d", resp.StatusCode) + } + + tmpFile, err := os.CreateTemp("", "kots") + if err != nil { + return "", errors.Wrap(err, "failed to create temp file") + } + defer tmpFile.Close() + + gzipReader, err := gzip.NewReader(resp.Body) + if err != nil { + return "", errors.Wrap(err, "failed to get new gzip reader") + } + defer gzipReader.Close() + + tarReader := tar.NewReader(gzipReader) + for { + header, err := tarReader.Next() + if err == io.EOF { + break + } + if err != nil { + return "", errors.Wrap(err, "failed to get read archive") + } + + if header.Typeflag != tar.TypeReg { + continue + } + if header.Name != "kots" { + continue + } + + if _, err := io.Copy(tmpFile, tarReader); err != nil { + return "", errors.Wrap(err, "failed to copy kots binary") + } + if err := os.Chmod(tmpFile.Name(), 0755); err != nil { + return "", errors.Wrap(err, "failed to set file permissions") + } + + return tmpFile.Name(), nil + } + + return "", errors.New("kots binary not found in archive") +} diff --git a/pkg/license/license.go b/pkg/license/license.go index 8cb8afa740..3eb4e7d669 100644 --- a/pkg/license/license.go +++ b/pkg/license/license.go @@ -4,49 +4,9 @@ import ( "time" "github.com/pkg/errors" - "github.com/replicatedhq/kots/pkg/logger" - "github.com/replicatedhq/kots/pkg/store" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" - "github.com/replicatedhq/kotskinds/client/kotsclientset/scheme" ) -func ResolveExistingLicense(newLicense *kotsv1beta1.License) (bool, error) { - notInstalledApps, err := store.GetStore().ListFailedApps() - if err != nil { - logger.Error(errors.Wrap(err, "failed to list failed apps")) - return false, err - } - - for _, app := range notInstalledApps { - decode := scheme.Codecs.UniversalDeserializer().Decode - obj, _, err := decode([]byte(app.License), nil, nil) - if err != nil { - continue - } - license := obj.(*kotsv1beta1.License) - if license.Spec.LicenseID != newLicense.Spec.LicenseID { - continue - } - - if err := store.GetStore().RemoveApp(app.ID); err != nil { - return false, errors.Wrap(err, "failed to remove existing app record") - } - } - - // check if license still exists - allLicenses, err := store.GetStore().GetAllAppLicenses() - if err != nil { - return false, errors.Wrap(err, "failed to get all app licenses") - } - for _, l := range allLicenses { - if l.Spec.LicenseID == newLicense.Spec.LicenseID { - return false, nil - } - } - - return true, nil -} - func LicenseIsExpired(license *kotsv1beta1.License) (bool, error) { val, found := license.Spec.Entitlements["expires_at"] if !found { diff --git a/pkg/online/online.go b/pkg/online/online.go index f22152d721..ba37592242 100644 --- a/pkg/online/online.go +++ b/pkg/online/online.go @@ -22,6 +22,7 @@ import ( storetypes "github.com/replicatedhq/kots/pkg/store/types" "github.com/replicatedhq/kots/pkg/supportbundle" supportbundletypes "github.com/replicatedhq/kots/pkg/supportbundle/types" + "github.com/replicatedhq/kots/pkg/tasks" "github.com/replicatedhq/kots/pkg/updatechecker" "github.com/replicatedhq/kots/pkg/util" "github.com/replicatedhq/kots/pkg/version" @@ -40,7 +41,7 @@ func CreateAppFromOnline(opts CreateOnlineAppOpts) (_ *kotsutil.KotsKinds, final logger.Debug("creating app from online", zap.String("upstreamURI", opts.UpstreamURI)) - if err := store.GetStore().SetTaskStatus("online-install", "Uploading license...", "running"); err != nil { + if err := tasks.SetTaskStatus("online-install", "Uploading license...", "running"); err != nil { return nil, errors.Wrap(err, "failed to set task status") } @@ -50,7 +51,7 @@ func CreateAppFromOnline(opts CreateOnlineAppOpts) (_ *kotsutil.KotsKinds, final for { select { case <-time.After(time.Second): - if err := store.GetStore().UpdateTaskStatusTimestamp("online-install"); err != nil { + if err := tasks.UpdateTaskStatusTimestamp("online-install"); err != nil { logger.Error(err) } case <-finishedCh: @@ -62,7 +63,7 @@ func CreateAppFromOnline(opts CreateOnlineAppOpts) (_ *kotsutil.KotsKinds, final var app *apptypes.App defer func() { if finalError == nil { - if err := store.GetStore().ClearTaskStatus("online-install"); err != nil { + if err := tasks.ClearTaskStatus("online-install"); err != nil { logger.Error(errors.Wrap(err, "failed to clear install task status")) } if err := store.GetStore().SetAppInstallState(opts.PendingApp.ID, "installed"); err != nil { @@ -72,7 +73,7 @@ func CreateAppFromOnline(opts CreateOnlineAppOpts) (_ *kotsutil.KotsKinds, final logger.Error(errors.Wrap(err, "failed to configure update checker")) } } else { - if err := store.GetStore().SetTaskStatus("online-install", finalError.Error(), "failed"); err != nil { + if err := tasks.SetTaskStatus("online-install", finalError.Error(), "failed"); err != nil { logger.Error(errors.Wrap(err, "failed to set error on install task status")) } if err := store.GetStore().SetAppInstallState(opts.PendingApp.ID, "install_error"); err != nil { @@ -85,7 +86,7 @@ func CreateAppFromOnline(opts CreateOnlineAppOpts) (_ *kotsutil.KotsKinds, final go func() { scanner := bufio.NewScanner(pipeReader) for scanner.Scan() { - if err := store.GetStore().SetTaskStatus("online-install", scanner.Text(), "running"); err != nil { + if err := tasks.SetTaskStatus("online-install", scanner.Text(), "running"); err != nil { logger.Error(err) } } diff --git a/pkg/preflight/execute.go b/pkg/preflight/execute.go index 4b73d70648..e54132945d 100644 --- a/pkg/preflight/execute.go +++ b/pkg/preflight/execute.go @@ -1,7 +1,6 @@ package preflight import ( - "encoding/json" "strings" "sync" "time" @@ -10,45 +9,16 @@ import ( "github.com/replicatedhq/kots/pkg/k8sutil" "github.com/replicatedhq/kots/pkg/logger" "github.com/replicatedhq/kots/pkg/preflight/types" - "github.com/replicatedhq/kots/pkg/store" troubleshootanalyze "github.com/replicatedhq/troubleshoot/pkg/analyze" troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" troubleshootcollect "github.com/replicatedhq/troubleshoot/pkg/collect" "github.com/replicatedhq/troubleshoot/pkg/preflight" troubleshootpreflight "github.com/replicatedhq/troubleshoot/pkg/preflight" - "go.uber.org/zap" ) -func setPreflightResult(appID string, sequence int64, preflightResults *types.PreflightResults, preflightRunError error) error { - if preflightRunError != nil { - if preflightResults.Errors == nil { - preflightResults.Errors = []*types.PreflightError{} - } - preflightResults.Errors = append(preflightResults.Errors, &types.PreflightError{ - Error: preflightRunError.Error(), - IsRBAC: false, - }) - } - - b, err := json.Marshal(preflightResults) - if err != nil { - return errors.Wrap(err, "failed to marshal preflight results") - } - - if err := store.GetStore().SetPreflightResults(appID, sequence, b); err != nil { - return errors.Wrap(err, "failed to set preflight results") - } - - return nil -} - -// execute will execute the preflights using spec in preflightSpec. +// 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.Info("executing preflight checks", - zap.String("appID", appID), - zap.Int64("sequence", sequence)) - +func Execute(preflightSpec *troubleshootv1beta2.Preflight, ignorePermissionErrors bool, setProgress func(progress map[string]interface{}) error, setResults func(results *types.PreflightResults) error) (*types.PreflightResults, error) { progressChan := make(chan interface{}, 0) // non-zero buffer will result in missed messages defer close(progressChan) @@ -73,26 +43,25 @@ func execute(appID string, sequence int64, preflightSpec *troubleshootv1beta2.Pr } } - progress, ok := msg.(preflight.CollectProgress) + collectProgress, ok := msg.(preflight.CollectProgress) if !ok { continue } // TODO: We need a nice title to display - progressBytes, err := json.Marshal(map[string]interface{}{ - "completedCount": progress.CompletedCount, - "totalCount": progress.TotalCount, - "currentName": progress.CurrentName, - "currentStatus": progress.CurrentStatus, + progress := map[string]interface{}{ + "completedCount": collectProgress.CompletedCount, + "totalCount": collectProgress.TotalCount, + "currentName": collectProgress.CurrentName, + "currentStatus": collectProgress.CurrentStatus, "updatedAt": time.Now().Format(time.RFC3339), - }) - if err != nil { - continue } completeMx.Lock() if !isComplete { - _ = store.GetStore().SetPreflightProgress(appID, sequence, string(progressBytes)) + if err := setProgress(progress); err != nil { + logger.Error(errors.Wrap(err, "failed to set preflight progress")) + } } completeMx.Unlock() } @@ -104,7 +73,17 @@ func execute(appID string, sequence int64, preflightSpec *troubleshootv1beta2.Pr defer completeMx.Unlock() isComplete = true - if err := setPreflightResult(appID, sequence, uploadPreflightResults, preflightRunError); err != nil { + + if preflightRunError != nil { + if uploadPreflightResults.Errors == nil { + uploadPreflightResults.Errors = []*types.PreflightError{} + } + uploadPreflightResults.Errors = append(uploadPreflightResults.Errors, &types.PreflightError{ + Error: preflightRunError.Error(), + IsRBAC: false, + }) + } + if err := setResults(uploadPreflightResults); err != nil { logger.Error(errors.Wrap(err, "failed to set preflight results")) return } diff --git a/pkg/preflight/preflight.go b/pkg/preflight/preflight.go index d32e69a03c..54650628c1 100644 --- a/pkg/preflight/preflight.go +++ b/pkg/preflight/preflight.go @@ -3,6 +3,7 @@ package preflight import ( "bytes" "context" + "encoding/json" "fmt" "os" "path/filepath" @@ -90,7 +91,7 @@ func Run(appID string, appSlug string, sequence int64, isAirgap bool, archiveDir preflight = troubleshootpreflight.ConcatPreflightSpec(preflight, &v) } - injectDefaultPreflights(preflight, kotsKinds, registrySettings) + InjectDefaultPreflights(preflight, kotsKinds, registrySettings) numAnalyzers := 0 for _, analyzer := range preflight.Spec.Analyzers { @@ -125,7 +126,7 @@ func Run(appID string, appSlug string, sequence int64, isAirgap bool, archiveDir return errors.Wrap(err, "failed to load rendered preflight") } - injectDefaultPreflights(preflight, kotsKinds, registrySettings) + InjectDefaultPreflights(preflight, kotsKinds, registrySettings) numAnalyzers := 0 for _, analyzer := range preflight.Spec.Analyzers { @@ -141,8 +142,15 @@ func Run(appID string, appSlug string, sequence int64, isAirgap bool, archiveDir var preflightErr error defer func() { if preflightErr != nil { - err := setPreflightResult(appID, sequence, &types.PreflightResults{}, preflightErr) - if err != nil { + preflightResults := &types.PreflightResults{ + Errors: []*types.PreflightError{ + &types.PreflightError{ + Error: preflightErr.Error(), + IsRBAC: false, + }, + }, + } + if err := setPreflightResults(appID, sequence, preflightResults); err != nil { logger.Error(errors.Wrap(err, "failed to set preflight results")) return } @@ -170,8 +178,17 @@ func Run(appID string, appSlug string, sequence int64, isAirgap bool, archiveDir preflight.Spec.Collectors = collectors go func() { - logger.Info("preflight checks beginning") - uploadPreflightResults, err := execute(appID, sequence, preflight, ignoreRBAC) + logger.Info("preflight checks beginning", + zap.String("appID", appID), + zap.Int64("sequence", sequence)) + + setProgress := func(progress map[string]interface{}) error { + return setPreflightProgress(appID, sequence, progress) + } + setResults := func(results *types.PreflightResults) error { + return setPreflightResults(appID, sequence, results) + } + uploadPreflightResults, err := Execute(preflight, ignoreRBAC, setProgress, setResults) if err != nil { logger.Error(errors.Wrap(err, "failed to run preflight checks")) return @@ -239,6 +256,28 @@ func Run(appID string, appSlug string, sequence int64, isAirgap bool, archiveDir return nil } +func setPreflightProgress(appID string, sequence int64, progress map[string]interface{}) error { + b, err := json.Marshal(progress) + if err != nil { + return errors.Wrap(err, "failed to marshal preflight progress") + } + if err := store.GetStore().SetPreflightProgress(appID, sequence, string(b)); err != nil { + return errors.Wrap(err, "failed to set preflight progress") + } + return nil +} + +func setPreflightResults(appID string, sequence int64, preflightResults *types.PreflightResults) error { + b, err := json.Marshal(preflightResults) + if err != nil { + return errors.Wrap(err, "failed to marshal preflight results") + } + if err := store.GetStore().SetPreflightResults(appID, sequence, b); err != nil { + return errors.Wrap(err, "failed to set preflight results") + } + return nil +} + // GetPreflightCheckState returns the state of a single preflight check result func GetPreflightCheckState(p *troubleshootpreflight.UploadPreflightResult) string { if p == nil { @@ -371,7 +410,7 @@ func CreateRenderedSpec(app *apptypes.App, sequence int64, origin string, inClus return errors.Wrap(err, "failed to get registry settings for app") } - injectDefaultPreflights(builtPreflight, kotsKinds, registrySettings) + InjectDefaultPreflights(builtPreflight, kotsKinds, registrySettings) collectors, err := registry.UpdateCollectorSpecsWithRegistryData(builtPreflight.Spec.Collectors, registrySettings, kotsKinds.Installation, kotsKinds.License, &kotsKinds.KotsApplication) if err != nil { @@ -448,7 +487,7 @@ func CreateRenderedSpec(app *apptypes.App, sequence int64, origin string, inClus return nil } -func injectDefaultPreflights(preflight *troubleshootv1beta2.Preflight, kotskinds *kotsutil.KotsKinds, registrySettings registrytypes.RegistrySettings) { +func InjectDefaultPreflights(preflight *troubleshootv1beta2.Preflight, kotskinds *kotsutil.KotsKinds, registrySettings registrytypes.RegistrySettings) { if registrySettings.IsValid() && registrySettings.IsReadOnly { // Get images from Installation.KnownImages, see UpdateCollectorSpecsWithRegistryData images := []string{} diff --git a/pkg/registry/images.go b/pkg/registry/images.go index 3dac1a5f08..c4ba47c845 100644 --- a/pkg/registry/images.go +++ b/pkg/registry/images.go @@ -28,6 +28,7 @@ import ( "github.com/replicatedhq/kots/pkg/registry/types" kotss3 "github.com/replicatedhq/kots/pkg/s3" "github.com/replicatedhq/kots/pkg/store" + "github.com/replicatedhq/kots/pkg/tasks" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -198,7 +199,7 @@ func deleteUnusedImages(ctx context.Context, registry types.RegistrySettings, us return nil } - currentStatus, _, err := store.GetStore().GetTaskStatus(deleteImagesTaskID) + currentStatus, _, err := tasks.GetTaskStatus(deleteImagesTaskID) if err != nil { return errors.Wrap(err, "failed to get task status") } @@ -208,7 +209,7 @@ func deleteUnusedImages(ctx context.Context, registry types.RegistrySettings, us return nil } - if err := store.GetStore().SetTaskStatus(deleteImagesTaskID, "Searching registry...", "running"); err != nil { + if err := tasks.SetTaskStatus(deleteImagesTaskID, "Searching registry...", "running"); err != nil { return errors.Wrap(err, "failed to set task status") } @@ -344,11 +345,11 @@ func startDeleteImagesTaskMonitor(finishedChan <-chan error) { var finalError error defer func() { if finalError == nil { - if err := store.GetStore().ClearTaskStatus(deleteImagesTaskID); err != nil { + if err := tasks.ClearTaskStatus(deleteImagesTaskID); err != nil { logger.Error(errors.Wrapf(err, "failed to clear %q task status", deleteImagesTaskID)) } } else { - if err := store.GetStore().SetTaskStatus(deleteImagesTaskID, finalError.Error(), "failed"); err != nil { + if err := tasks.SetTaskStatus(deleteImagesTaskID, finalError.Error(), "failed"); err != nil { logger.Error(errors.Wrapf(err, "failed to set error on %q task status", deleteImagesTaskID)) } } @@ -357,7 +358,7 @@ func startDeleteImagesTaskMonitor(finishedChan <-chan error) { for { select { case <-time.After(time.Second): - if err := store.GetStore().UpdateTaskStatusTimestamp(deleteImagesTaskID); err != nil { + if err := tasks.UpdateTaskStatusTimestamp(deleteImagesTaskID); err != nil { logger.Error(err) } case err := <-finishedChan: diff --git a/pkg/registry/registry.go b/pkg/registry/registry.go index 7c72b3cc11..3c7a35d6f8 100644 --- a/pkg/registry/registry.go +++ b/pkg/registry/registry.go @@ -19,6 +19,7 @@ import ( "github.com/replicatedhq/kots/pkg/reporting" "github.com/replicatedhq/kots/pkg/rewrite" "github.com/replicatedhq/kots/pkg/store" + "github.com/replicatedhq/kots/pkg/tasks" "github.com/replicatedhq/kots/pkg/util" ) @@ -26,7 +27,7 @@ import ( // and create a new version of the application // the caller is responsible for deleting the appDir returned func RewriteImages(appID string, sequence int64, hostname string, username string, password string, namespace string, isReadOnly bool) (appDir string, finalError error) { - if err := store.GetStore().SetTaskStatus("image-rewrite", "Updating registry settings", "running"); err != nil { + if err := tasks.SetTaskStatus("image-rewrite", "Updating registry settings", "running"); err != nil { return "", errors.Wrap(err, "failed to set task status") } @@ -36,7 +37,7 @@ func RewriteImages(appID string, sequence int64, hostname string, username strin for { select { case <-time.After(time.Second): - if err := store.GetStore().UpdateTaskStatusTimestamp("image-rewrite"); err != nil { + if err := tasks.UpdateTaskStatusTimestamp("image-rewrite"); err != nil { logger.Error(err) } case <-finishedCh: @@ -47,13 +48,13 @@ func RewriteImages(appID string, sequence int64, hostname string, username strin defer func() { if finalError == nil { - if err := store.GetStore().ClearTaskStatus("image-rewrite"); err != nil { + if err := tasks.ClearTaskStatus("image-rewrite"); err != nil { logger.Error(errors.Wrap(err, "failed to clear image rewrite task status")) } } else { // do not show the stack trace to the user causeErr := errors.Cause(finalError) - if err := store.GetStore().SetTaskStatus("image-rewrite", causeErr.Error(), "failed"); err != nil { + if err := tasks.SetTaskStatus("image-rewrite", causeErr.Error(), "failed"); err != nil { logger.Error(errors.Wrap(err, "failed to set image rewrite task status as failed")) } } @@ -97,7 +98,7 @@ func RewriteImages(appID string, sequence int64, hostname string, username strin go func() { scanner := bufio.NewScanner(pipeReader) for scanner.Scan() { - if err := store.GetStore().SetTaskStatus("image-rewrite", scanner.Text(), "running"); err != nil { + if err := tasks.SetTaskStatus("image-rewrite", scanner.Text(), "running"); err != nil { logger.Error(err) } } diff --git a/pkg/render/render.go b/pkg/render/render.go index d63e10ae14..a5bb8cc3d4 100644 --- a/pkg/render/render.go +++ b/pkg/render/render.go @@ -10,7 +10,6 @@ import ( "github.com/replicatedhq/kots/pkg/kotsutil" registrytypes "github.com/replicatedhq/kots/pkg/registry/types" types "github.com/replicatedhq/kots/pkg/render/types" - "github.com/replicatedhq/kots/pkg/reporting" "github.com/replicatedhq/kots/pkg/rewrite" "github.com/replicatedhq/kots/pkg/template" "github.com/replicatedhq/kots/pkg/util" @@ -145,7 +144,7 @@ func RenderDir(opts types.RenderDirOptions) error { AppSlug: opts.App.Slug, IsGitOps: opts.App.IsGitOps, AppSequence: opts.Sequence, - ReportingInfo: reporting.GetReportingInfo(opts.App.ID), + ReportingInfo: opts.ReportingInfo, RegistrySettings: opts.RegistrySettings, // TODO: pass in as arguments if this is ever called from CLI diff --git a/pkg/render/types/interface.go b/pkg/render/types/interface.go index 8bc5d85808..a56ce9fb99 100644 --- a/pkg/render/types/interface.go +++ b/pkg/render/types/interface.go @@ -2,6 +2,7 @@ package types import ( downstreamtypes "github.com/replicatedhq/kots/pkg/api/downstream/types" + reportingtypes "github.com/replicatedhq/kots/pkg/api/reporting/types" apptypes "github.com/replicatedhq/kots/pkg/app/types" "github.com/replicatedhq/kots/pkg/kotsutil" registrytypes "github.com/replicatedhq/kots/pkg/registry/types" @@ -23,6 +24,7 @@ type RenderDirOptions struct { Downstreams []downstreamtypes.Downstream RegistrySettings registrytypes.RegistrySettings Sequence int64 + ReportingInfo *reportingtypes.ReportingInfo } type Renderer interface { diff --git a/pkg/replicatedapp/upstream.go b/pkg/replicatedapp/upstream.go index d4baf20612..c96aa418b1 100644 --- a/pkg/replicatedapp/upstream.go +++ b/pkg/replicatedapp/upstream.go @@ -17,7 +17,6 @@ type ReplicatedUpstream struct { Channel *string AppSlug string VersionLabel *string - Sequence *int } func ParseReplicatedURL(u *url.URL) (*ReplicatedUpstream, error) { diff --git a/pkg/reporting/app_online.go b/pkg/reporting/app_online.go index 32b40bfb66..0686edac25 100644 --- a/pkg/reporting/app_online.go +++ b/pkg/reporting/app_online.go @@ -8,8 +8,10 @@ import ( "time" "github.com/pkg/errors" + "github.com/replicatedhq/kots/pkg/api/reporting/types" "github.com/replicatedhq/kots/pkg/store" "github.com/replicatedhq/kots/pkg/util" + kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" ) var onlineAppInfoMtx sync.Mutex @@ -35,11 +37,20 @@ func (r *OnlineReporter) SubmitAppInfo(appID string) error { return errors.Wrap(err, "failed to get license for app") } + reportingInfo := GetReportingInfo(a.ID) + + if err := SendOnlineAppInfo(license, reportingInfo); err != nil { + return errors.Wrap(err, "failed to send online app info") + } + + return nil +} + +func SendOnlineAppInfo(license *kotsv1beta1.License, reportingInfo *types.ReportingInfo) error { endpoint := license.Spec.Endpoint if !canReport(endpoint) { return nil } - url := fmt.Sprintf("%s/kots_metrics/license_instance/info", endpoint) postReq, err := util.NewRequest("POST", url, nil) @@ -49,7 +60,6 @@ func (r *OnlineReporter) SubmitAppInfo(appID string) error { postReq.Header.Set("Authorization", fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", license.Spec.LicenseID, license.Spec.LicenseID))))) postReq.Header.Set("Content-Type", "application/json") - reportingInfo := GetReportingInfo(a.ID) InjectReportingInfoHeaders(postReq, reportingInfo) resp, err := http.DefaultClient.Do(postReq) diff --git a/pkg/rewrite/rewrite.go b/pkg/rewrite/rewrite.go index 95dde7e9cd..edfaceeb27 100644 --- a/pkg/rewrite/rewrite.go +++ b/pkg/rewrite/rewrite.go @@ -21,7 +21,6 @@ import ( "github.com/replicatedhq/kots/pkg/midstream" registrytypes "github.com/replicatedhq/kots/pkg/registry/types" "github.com/replicatedhq/kots/pkg/rendered" - "github.com/replicatedhq/kots/pkg/store" "github.com/replicatedhq/kots/pkg/upstream" upstreamtypes "github.com/replicatedhq/kots/pkg/upstream/types" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" @@ -311,10 +310,6 @@ func Rewrite(rewriteOptions RewriteOptions) error { return errors.Wrap(err, "failed to write downstreams") } - if err := store.GetStore().UpdateAppVersionInstallationSpec(rewriteOptions.AppID, rewriteOptions.AppSequence, renderedKotsKinds.Installation); err != nil { - return errors.Wrap(err, "failed to update installation spec") - } - if err := rendered.WriteRenderedApp(&rendered.WriteOptions{ BaseDir: u.GetBaseDir(writeUpstreamOptions), OverlaysDir: u.GetOverlaysDir(writeUpstreamOptions), diff --git a/pkg/store/kotsstore/airgap_store.go b/pkg/store/kotsstore/airgap_store.go index baca30276d..3e6a013c16 100644 --- a/pkg/store/kotsstore/airgap_store.go +++ b/pkg/store/kotsstore/airgap_store.go @@ -7,6 +7,7 @@ import ( "github.com/replicatedhq/kots/pkg/airgap/types" airgaptypes "github.com/replicatedhq/kots/pkg/airgap/types" "github.com/replicatedhq/kots/pkg/persistence" + "github.com/replicatedhq/kots/pkg/tasks" "github.com/rqlite/gorqlite" ) @@ -71,7 +72,7 @@ func (s *KOTSStore) GetAirgapInstallStatus(appID string) (*airgaptypes.InstallSt return nil, errors.Wrap(err, "failed to scan") } - _, message, err := s.GetTaskStatus(fmt.Sprintf("airgap-install-slug-%s", slug)) + _, message, err := tasks.GetTaskStatus(fmt.Sprintf("airgap-install-slug-%s", slug)) if err != nil { return nil, errors.Wrap(err, "failed to get task status") } diff --git a/pkg/store/kotsstore/downstream_store.go b/pkg/store/kotsstore/downstream_store.go index 345fad66d8..1a87320104 100644 --- a/pkg/store/kotsstore/downstream_store.go +++ b/pkg/store/kotsstore/downstream_store.go @@ -14,6 +14,7 @@ import ( "github.com/replicatedhq/kots/pkg/logger" "github.com/replicatedhq/kots/pkg/persistence" "github.com/replicatedhq/kots/pkg/store/types" + "github.com/replicatedhq/kots/pkg/tasks" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" "github.com/rqlite/gorqlite" ) @@ -613,7 +614,7 @@ func (s *KOTSStore) AddDownstreamVersionsDetails(appID string, clusterID string, if version.Status == types.VersionPendingDownload { downloadTaskID := fmt.Sprintf("update-download.%d", version.Sequence) - downloadStatus, downloadStatusMessage, err := s.GetTaskStatus(downloadTaskID) + downloadStatus, downloadStatusMessage, err := tasks.GetTaskStatus(downloadTaskID) if err != nil { // don't fail on this logger.Error(errors.Wrap(err, fmt.Sprintf("failed to get %s task status", downloadTaskID))) diff --git a/pkg/store/kotsstore/installation_store.go b/pkg/store/kotsstore/installation_store.go index f53e68b5be..7899aae48a 100644 --- a/pkg/store/kotsstore/installation_store.go +++ b/pkg/store/kotsstore/installation_store.go @@ -6,6 +6,7 @@ import ( "github.com/pkg/errors" installationtypes "github.com/replicatedhq/kots/pkg/online/types" "github.com/replicatedhq/kots/pkg/persistence" + "github.com/replicatedhq/kots/pkg/tasks" "github.com/rqlite/gorqlite" ) @@ -29,7 +30,7 @@ func (s *KOTSStore) GetPendingInstallationStatus() (*installationtypes.InstallSt return nil, errors.Wrap(err, "failed to scan") } - _, message, err := s.GetTaskStatus("online-install") + _, message, err := tasks.GetTaskStatus("online-install") if err != nil { return nil, errors.Wrap(err, "failed to get task status") } diff --git a/pkg/store/kotsstore/kots_store.go b/pkg/store/kotsstore/kots_store.go index 9492fc2d0f..acec529aad 100644 --- a/pkg/store/kotsstore/kots_store.go +++ b/pkg/store/kotsstore/kots_store.go @@ -28,16 +28,9 @@ var ( ErrNotFound = errors.New("not found") ) -type cachedTaskStatus struct { - expirationTime time.Time - taskStatus TaskStatus -} - type KOTSStore struct { sessionSecret *corev1.Secret sessionExpiration time.Time - - cachedTaskStatus map[string]*cachedTaskStatus } func init() { @@ -164,9 +157,7 @@ func canIgnoreEtcdError(err error) bool { } func StoreFromEnv() *KOTSStore { - return &KOTSStore{ - cachedTaskStatus: make(map[string]*cachedTaskStatus), - } + return &KOTSStore{} } func (s *KOTSStore) getConfigmap(name string) (*corev1.ConfigMap, error) { diff --git a/pkg/store/kotsstore/license_store.go b/pkg/store/kotsstore/license_store.go index bcf32fb9fc..56429d16cd 100644 --- a/pkg/store/kotsstore/license_store.go +++ b/pkg/store/kotsstore/license_store.go @@ -8,6 +8,7 @@ import ( "time" "github.com/pkg/errors" + reportingtypes "github.com/replicatedhq/kots/pkg/api/reporting/types" gitopstypes "github.com/replicatedhq/kots/pkg/gitops/types" "github.com/replicatedhq/kots/pkg/logger" "github.com/replicatedhq/kots/pkg/persistence" @@ -106,7 +107,7 @@ func (s *KOTSStore) GetAllAppLicenses() ([]*kotsv1beta1.License, error) { return licenses, nil } -func (s *KOTSStore) UpdateAppLicense(appID string, baseSequence int64, archiveDir string, newLicense *kotsv1beta1.License, originalLicenseData string, channelChanged bool, failOnVersionCreate bool, gitops gitopstypes.DownstreamGitOps, renderer rendertypes.Renderer) (int64, error) { +func (s *KOTSStore) UpdateAppLicense(appID string, baseSequence int64, archiveDir string, newLicense *kotsv1beta1.License, originalLicenseData string, channelChanged bool, failOnVersionCreate bool, gitops gitopstypes.DownstreamGitOps, renderer rendertypes.Renderer, reportingInfo *reportingtypes.ReportingInfo) (int64, error) { db := persistence.MustGetDBSession() statements := []gorqlite.ParameterizedStatement{} @@ -127,7 +128,7 @@ func (s *KOTSStore) UpdateAppLicense(appID string, baseSequence int64, archiveDi Arguments: []interface{}{originalLicenseData, time.Now().Unix(), channelChanged, appID}, }) - appVersionStatements, newSeq, err := s.createNewVersionForLicenseChangeStatements(appID, baseSequence, archiveDir, gitops, renderer) + appVersionStatements, newSeq, err := s.createNewVersionForLicenseChangeStatements(appID, baseSequence, archiveDir, gitops, renderer, reportingInfo) if err != nil { // ignore error here to prevent a failure to render the current version // preventing the end-user from updating the application @@ -164,7 +165,7 @@ func (s *KOTSStore) UpdateAppLicenseSyncNow(appID string) error { return nil } -func (s *KOTSStore) createNewVersionForLicenseChangeStatements(appID string, baseSequence int64, archiveDir string, gitops gitopstypes.DownstreamGitOps, renderer rendertypes.Renderer) ([]gorqlite.ParameterizedStatement, int64, error) { +func (s *KOTSStore) createNewVersionForLicenseChangeStatements(appID string, baseSequence int64, archiveDir string, gitops gitopstypes.DownstreamGitOps, renderer rendertypes.Renderer, reportingInfo *reportingtypes.ReportingInfo) ([]gorqlite.ParameterizedStatement, int64, error) { registrySettings, err := s.GetRegistryDetailsForApp(appID) if err != nil { return nil, int64(0), errors.Wrap(err, "failed to get registry settings for app") @@ -191,6 +192,7 @@ func (s *KOTSStore) createNewVersionForLicenseChangeStatements(appID string, bas Downstreams: downstreams, RegistrySettings: registrySettings, Sequence: nextAppSequence, + ReportingInfo: reportingInfo, }); err != nil { return nil, int64(0), errors.Wrap(err, "failed to render new version") } diff --git a/pkg/store/kotsstore/migrations.go b/pkg/store/kotsstore/migrations.go index 1b414e3953..a48f2cb94e 100644 --- a/pkg/store/kotsstore/migrations.go +++ b/pkg/store/kotsstore/migrations.go @@ -9,6 +9,7 @@ import ( "github.com/replicatedhq/kots/pkg/kotsutil" "github.com/replicatedhq/kots/pkg/logger" "github.com/replicatedhq/kots/pkg/persistence" + "github.com/replicatedhq/kots/pkg/tasks" "github.com/rqlite/gorqlite" ) @@ -48,7 +49,7 @@ func (s *KOTSStore) RunMigrations() { if err := s.migrateSupportBundlesFromRqlite(); err != nil { logger.Error(errors.Wrap(err, "failed to migrate support bundles")) } - if err := s.migrateTasksFromRqlite(); err != nil { + if err := tasks.MigrateTasksFromRqlite(); err != nil { logger.Error(errors.Wrap(err, "failed to migrate tasks")) } } diff --git a/pkg/store/kotsstore/task_store.go b/pkg/store/kotsstore/task_store.go deleted file mode 100644 index 7a2db0babe..0000000000 --- a/pkg/store/kotsstore/task_store.go +++ /dev/null @@ -1,254 +0,0 @@ -package kotsstore - -import ( - "encoding/json" - "fmt" - "sync" - "time" - - "github.com/pkg/errors" - "github.com/replicatedhq/kots/pkg/persistence" - "github.com/rqlite/gorqlite" -) - -const ( - TaskStatusConfigMapName = `kotsadm-tasks` - ConfgConfigMapName = `kotsadm-confg` - - taskCacheTTL = 1 * time.Minute -) - -var ( - taskStatusLock = sync.Mutex{} -) - -type TaskStatus struct { - Message string `json:"message"` - Status string `json:"status"` - UpdatedAt time.Time `json:"updatedAt"` -} - -func (s *KOTSStore) migrateTasksFromRqlite() error { - db := persistence.MustGetDBSession() - - query := `select updated_at, current_message, status from api_task_status` - rows, err := db.QueryOne(query) - if err != nil { - return fmt.Errorf("failed to select tasks for migration: %v: %v", err, rows.Err) - } - - taskCm, err := s.getConfigmap(TaskStatusConfigMapName) - if err != nil { - return errors.Wrap(err, "failed to get task status configmap") - } - - if taskCm.Data == nil { - taskCm.Data = map[string]string{} - } - - for rows.Next() { - var id string - var status gorqlite.NullString - var message gorqlite.NullString - - ts := TaskStatus{} - if err := rows.Scan(&id, &ts.UpdatedAt, &message, &status); err != nil { - return errors.Wrap(err, "failed to scan task status") - } - - if status.Valid { - ts.Status = status.String - } - if message.Valid { - ts.Message = message.String - } - - b, err := json.Marshal(ts) - if err != nil { - return errors.Wrap(err, "failed to marshal task status") - } - - taskCm.Data[id] = string(b) - } - - if err := s.updateConfigmap(taskCm); err != nil { - return errors.Wrap(err, "failed to update task status configmap") - } - - query = `delete from api_task_status` - if wr, err := db.WriteOne(query); err != nil { - return fmt.Errorf("failed to delete tasks from db: %v: %v", err, wr.Err) - } - - return nil -} - -func (s *KOTSStore) SetTaskStatus(id string, message string, status string) error { - taskStatusLock.Lock() - defer taskStatusLock.Unlock() - - cached := s.cachedTaskStatus[id] - if cached == nil { - cached = &cachedTaskStatus{} - s.cachedTaskStatus[id] = cached - } - cached.taskStatus.Message = message - cached.taskStatus.Status = status - cached.taskStatus.UpdatedAt = time.Now() - cached.expirationTime = time.Now().Add(taskCacheTTL) - - configmap, err := s.getConfigmap(TaskStatusConfigMapName) - if err != nil { - if canIgnoreEtcdError(err) { - return nil - } - return errors.Wrap(err, "failed to get task status configmap") - } - - if configmap.Data == nil { - configmap.Data = map[string]string{} - } - - b, err := json.Marshal(cached.taskStatus) - if err != nil { - return errors.Wrap(err, "failed to marshal task status") - } - - configmap.Data[id] = string(b) - - if err := s.updateConfigmap(configmap); err != nil { - if canIgnoreEtcdError(err) { - return nil - } - return errors.Wrap(err, "failed to update task status configmap") - } - - return nil -} - -func (s *KOTSStore) UpdateTaskStatusTimestamp(id string) error { - taskStatusLock.Lock() - defer taskStatusLock.Unlock() - - cached := s.cachedTaskStatus[id] - if cached != nil { - cached.taskStatus.UpdatedAt = time.Now() - cached.expirationTime = time.Now().Add(taskCacheTTL) - } - - configmap, err := s.getConfigmap(TaskStatusConfigMapName) - if err != nil { - if canIgnoreEtcdError(err) && cached != nil { - return nil - } - return errors.Wrap(err, "failed to get task status configmap") - } - - if configmap.Data == nil { - configmap.Data = map[string]string{} - } - - data, ok := configmap.Data[id] - if !ok { - return nil // copied from s3pgstore - } - - ts := TaskStatus{} - if err := json.Unmarshal([]byte(data), &ts); err != nil { - return errors.Wrap(err, "failed to unmarshal task status") - } - - ts.UpdatedAt = time.Now() - - b, err := json.Marshal(ts) - if err != nil { - return errors.Wrap(err, "failed to marshal task status") - } - - configmap.Data[id] = string(b) - - if err := s.updateConfigmap(configmap); err != nil { - if canIgnoreEtcdError(err) && cached != nil { - return nil - } - return errors.Wrap(err, "failed to update task status configmap") - } - - return nil -} - -func (s *KOTSStore) ClearTaskStatus(id string) error { - taskStatusLock.Lock() - defer taskStatusLock.Unlock() - - defer delete(s.cachedTaskStatus, id) - - configmap, err := s.getConfigmap(TaskStatusConfigMapName) - if err != nil { - return errors.Wrap(err, "failed to get task status configmap") - } - - if configmap.Data == nil { - configmap.Data = map[string]string{} - } - - _, ok := configmap.Data[id] - if !ok { - return nil // copied from s3pgstore - } - - delete(configmap.Data, id) - - if err := s.updateConfigmap(configmap); err != nil { - return errors.Wrap(err, "failed to update task status configmap") - } - - return nil -} - -func (s *KOTSStore) GetTaskStatus(id string) (string, string, error) { - taskStatusLock.Lock() - defer taskStatusLock.Unlock() - - cached := s.cachedTaskStatus[id] - if cached != nil && time.Now().Before(cached.expirationTime) { - return cached.taskStatus.Status, cached.taskStatus.Message, nil - } - - if cached == nil { - cached = &cachedTaskStatus{ - expirationTime: time.Now().Add(taskCacheTTL), - } - s.cachedTaskStatus[id] = cached - } - - configmap, err := s.getConfigmap(TaskStatusConfigMapName) - if err != nil { - if canIgnoreEtcdError(err) && cached != nil { - return cached.taskStatus.Status, cached.taskStatus.Message, nil - } - return "", "", errors.Wrap(err, "failed to get task status configmap") - } - - if configmap.Data == nil { - return "", "", nil - } - - marshalled, ok := configmap.Data[id] - if !ok { - return "", "", nil - } - - ts := TaskStatus{} - if err := json.Unmarshal([]byte(marshalled), &ts); err != nil { - return "", "", errors.Wrap(err, "error unmarshalling task status") - } - - if ts.UpdatedAt.Before(time.Now().Add(-10 * time.Second)) { - return "", "", nil - } - - cached.taskStatus = ts - - return ts.Status, ts.Message, nil -} diff --git a/pkg/store/kotsstore/version_store.go b/pkg/store/kotsstore/version_store.go index 081f38dac5..adfc0b5339 100644 --- a/pkg/store/kotsstore/version_store.go +++ b/pkg/store/kotsstore/version_store.go @@ -1,7 +1,6 @@ package kotsstore import ( - "bytes" "encoding/base64" "encoding/json" "fmt" @@ -38,7 +37,6 @@ import ( "github.com/rqlite/gorqlite" velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - serializer "k8s.io/apimachinery/pkg/runtime/serializer/json" "k8s.io/client-go/kubernetes/scheme" "sigs.k8s.io/application/api/v1beta1" ) @@ -977,24 +975,6 @@ func (s *KOTSStore) UpdateNextAppVersionDiffSummary(appID string, baseSequence i return nil } -func (s *KOTSStore) UpdateAppVersionInstallationSpec(appID string, sequence int64, installation kotsv1beta1.Installation) error { - ser := serializer.NewYAMLSerializer(serializer.DefaultMetaFactory, scheme.Scheme, scheme.Scheme) - var b bytes.Buffer - if err := ser.Encode(&installation, &b); err != nil { - return errors.Wrap(err, "failed to encode installation") - } - - db := persistence.MustGetDBSession() - wr, err := db.WriteOneParameterized(gorqlite.ParameterizedStatement{ - Query: `UPDATE app_version SET kots_installation_spec = ? WHERE app_id = ? AND sequence = ?`, - Arguments: []interface{}{b.String(), appID, sequence}, - }) - if err != nil { - return fmt.Errorf("failed to write: %v: %v", err, wr.Err) - } - return nil -} - func (s *KOTSStore) GetNextAppSequence(appID string) (int64, error) { db := persistence.MustGetDBSession() diff --git a/pkg/store/mock/mock.go b/pkg/store/mock/mock.go index 9deaf4936c..bbad145a29 100644 --- a/pkg/store/mock/mock.go +++ b/pkg/store/mock/mock.go @@ -13,20 +13,21 @@ import ( v1beta1 "github.com/replicatedhq/embedded-cluster-kinds/apis/v1beta1" types "github.com/replicatedhq/kots/pkg/airgap/types" types0 "github.com/replicatedhq/kots/pkg/api/downstream/types" - types1 "github.com/replicatedhq/kots/pkg/api/version/types" - types2 "github.com/replicatedhq/kots/pkg/app/types" - types3 "github.com/replicatedhq/kots/pkg/appstate/types" - types4 "github.com/replicatedhq/kots/pkg/gitops/types" - types5 "github.com/replicatedhq/kots/pkg/kotsadmsnapshot/types" - types6 "github.com/replicatedhq/kots/pkg/online/types" - types7 "github.com/replicatedhq/kots/pkg/preflight/types" - types8 "github.com/replicatedhq/kots/pkg/registry/types" - types9 "github.com/replicatedhq/kots/pkg/render/types" - types10 "github.com/replicatedhq/kots/pkg/session/types" - types11 "github.com/replicatedhq/kots/pkg/store/types" - types12 "github.com/replicatedhq/kots/pkg/supportbundle/types" - types13 "github.com/replicatedhq/kots/pkg/upstream/types" - types14 "github.com/replicatedhq/kots/pkg/user/types" + types1 "github.com/replicatedhq/kots/pkg/api/reporting/types" + types2 "github.com/replicatedhq/kots/pkg/api/version/types" + types3 "github.com/replicatedhq/kots/pkg/app/types" + types4 "github.com/replicatedhq/kots/pkg/appstate/types" + types5 "github.com/replicatedhq/kots/pkg/gitops/types" + types6 "github.com/replicatedhq/kots/pkg/kotsadmsnapshot/types" + types7 "github.com/replicatedhq/kots/pkg/online/types" + types8 "github.com/replicatedhq/kots/pkg/preflight/types" + types9 "github.com/replicatedhq/kots/pkg/registry/types" + types10 "github.com/replicatedhq/kots/pkg/render/types" + types11 "github.com/replicatedhq/kots/pkg/session/types" + types12 "github.com/replicatedhq/kots/pkg/store/types" + types13 "github.com/replicatedhq/kots/pkg/supportbundle/types" + types14 "github.com/replicatedhq/kots/pkg/upstream/types" + types15 "github.com/replicatedhq/kots/pkg/user/types" v1beta10 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" redact "github.com/replicatedhq/troubleshoot/pkg/redact" ) @@ -96,25 +97,11 @@ func (mr *MockStoreMockRecorder) AddDownstreamVersionsDetails(appID, clusterID, return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddDownstreamVersionsDetails", reflect.TypeOf((*MockStore)(nil).AddDownstreamVersionsDetails), appID, clusterID, versions, checkIfDeployable) } -// ClearTaskStatus mocks base method. -func (m *MockStore) ClearTaskStatus(taskID string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ClearTaskStatus", taskID) - ret0, _ := ret[0].(error) - return ret0 -} - -// ClearTaskStatus indicates an expected call of ClearTaskStatus. -func (mr *MockStoreMockRecorder) ClearTaskStatus(taskID interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClearTaskStatus", reflect.TypeOf((*MockStore)(nil).ClearTaskStatus), taskID) -} - // CreateApp mocks base method. -func (m *MockStore) CreateApp(name, upstreamURI, licenseData string, isAirgapEnabled, skipImagePush, registryIsReadOnly bool) (*types2.App, error) { +func (m *MockStore) CreateApp(name, upstreamURI, licenseData string, isAirgapEnabled, skipImagePush, registryIsReadOnly bool) (*types3.App, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CreateApp", name, upstreamURI, licenseData, isAirgapEnabled, skipImagePush, registryIsReadOnly) - ret0, _ := ret[0].(*types2.App) + ret0, _ := ret[0].(*types3.App) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -126,7 +113,7 @@ func (mr *MockStoreMockRecorder) CreateApp(name, upstreamURI, licenseData, isAir } // CreateAppVersion mocks base method. -func (m *MockStore) CreateAppVersion(appID string, baseSequence *int64, filesInDir, source string, skipPreflights bool, gitops types4.DownstreamGitOps, renderer types9.Renderer) (int64, error) { +func (m *MockStore) CreateAppVersion(appID string, baseSequence *int64, filesInDir, source string, skipPreflights bool, gitops types5.DownstreamGitOps, renderer types10.Renderer) (int64, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CreateAppVersion", appID, baseSequence, filesInDir, source, skipPreflights, gitops, renderer) ret0, _ := ret[0].(int64) @@ -155,7 +142,7 @@ func (mr *MockStoreMockRecorder) CreateAppVersionArchive(appID, sequence, archiv } // CreateInProgressSupportBundle mocks base method. -func (m *MockStore) CreateInProgressSupportBundle(supportBundle *types12.SupportBundle) error { +func (m *MockStore) CreateInProgressSupportBundle(supportBundle *types13.SupportBundle) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CreateInProgressSupportBundle", supportBundle) ret0, _ := ret[0].(error) @@ -199,7 +186,7 @@ func (mr *MockStoreMockRecorder) CreateNewCluster(userID, isAllUsers, title, tok } // CreatePendingDownloadAppVersion mocks base method. -func (m *MockStore) CreatePendingDownloadAppVersion(appID string, update types13.Update, kotsApplication *v1beta10.Application, license *v1beta10.License) (int64, error) { +func (m *MockStore) CreatePendingDownloadAppVersion(appID string, update types14.Update, kotsApplication *v1beta10.Application, license *v1beta10.License) (int64, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CreatePendingDownloadAppVersion", appID, update, kotsApplication, license) ret0, _ := ret[0].(int64) @@ -242,10 +229,10 @@ func (mr *MockStoreMockRecorder) CreateScheduledSnapshot(snapshotID, appID, time } // CreateSession mocks base method. -func (m *MockStore) CreateSession(user *types14.User, issuedAt, expiresAt time.Time, roles []string) (*types10.Session, error) { +func (m *MockStore) CreateSession(user *types15.User, issuedAt, expiresAt time.Time, roles []string) (*types11.Session, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CreateSession", user, issuedAt, expiresAt, roles) - ret0, _ := ret[0].(*types10.Session) + ret0, _ := ret[0].(*types11.Session) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -257,10 +244,10 @@ func (mr *MockStoreMockRecorder) CreateSession(user, issuedAt, expiresAt, roles } // CreateSupportBundle mocks base method. -func (m *MockStore) CreateSupportBundle(bundleID, appID, archivePath string, marshalledTree []byte) (*types12.SupportBundle, error) { +func (m *MockStore) CreateSupportBundle(bundleID, appID, archivePath string, marshalledTree []byte) (*types13.SupportBundle, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CreateSupportBundle", bundleID, appID, archivePath, marshalledTree) - ret0, _ := ret[0].(*types12.SupportBundle) + ret0, _ := ret[0].(*types13.SupportBundle) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -429,10 +416,10 @@ func (mr *MockStoreMockRecorder) GetAllAppLicenses() *gomock.Call { } // GetApp mocks base method. -func (m *MockStore) GetApp(appID string) (*types2.App, error) { +func (m *MockStore) GetApp(appID string) (*types3.App, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetApp", appID) - ret0, _ := ret[0].(*types2.App) + ret0, _ := ret[0].(*types3.App) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -444,10 +431,10 @@ func (mr *MockStoreMockRecorder) GetApp(appID interface{}) *gomock.Call { } // GetAppFromSlug mocks base method. -func (m *MockStore) GetAppFromSlug(slug string) (*types2.App, error) { +func (m *MockStore) GetAppFromSlug(slug string) (*types3.App, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetAppFromSlug", slug) - ret0, _ := ret[0].(*types2.App) + ret0, _ := ret[0].(*types3.App) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -489,10 +476,10 @@ func (mr *MockStoreMockRecorder) GetAppIDsFromRegistry(hostname interface{}) *go } // GetAppStatus mocks base method. -func (m *MockStore) GetAppStatus(appID string) (*types3.AppStatus, error) { +func (m *MockStore) GetAppStatus(appID string) (*types4.AppStatus, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetAppStatus", appID) - ret0, _ := ret[0].(*types3.AppStatus) + ret0, _ := ret[0].(*types4.AppStatus) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -504,10 +491,10 @@ func (mr *MockStoreMockRecorder) GetAppStatus(appID interface{}) *gomock.Call { } // GetAppVersion mocks base method. -func (m *MockStore) GetAppVersion(appID string, sequence int64) (*types1.AppVersion, error) { +func (m *MockStore) GetAppVersion(appID string, sequence int64) (*types2.AppVersion, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetAppVersion", appID, sequence) - ret0, _ := ret[0].(*types1.AppVersion) + ret0, _ := ret[0].(*types2.AppVersion) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -714,10 +701,10 @@ func (mr *MockStoreMockRecorder) GetDownstreamVersionSource(appID, sequence inte } // GetDownstreamVersionStatus mocks base method. -func (m *MockStore) GetDownstreamVersionStatus(appID string, sequence int64) (types11.DownstreamVersionStatus, error) { +func (m *MockStore) GetDownstreamVersionStatus(appID string, sequence int64) (types12.DownstreamVersionStatus, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetDownstreamVersionStatus", appID, sequence) - ret0, _ := ret[0].(types11.DownstreamVersionStatus) + ret0, _ := ret[0].(types12.DownstreamVersionStatus) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -986,10 +973,10 @@ func (mr *MockStoreMockRecorder) GetPendingAirgapUploadApp() *gomock.Call { } // GetPendingInstallationStatus mocks base method. -func (m *MockStore) GetPendingInstallationStatus() (*types6.InstallStatus, error) { +func (m *MockStore) GetPendingInstallationStatus() (*types7.InstallStatus, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetPendingInstallationStatus") - ret0, _ := ret[0].(*types6.InstallStatus) + ret0, _ := ret[0].(*types7.InstallStatus) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -1016,10 +1003,10 @@ func (mr *MockStoreMockRecorder) GetPreflightProgress(appID, sequence interface{ } // GetPreflightResults mocks base method. -func (m *MockStore) GetPreflightResults(appID string, sequence int64) (*types7.PreflightResult, error) { +func (m *MockStore) GetPreflightResults(appID string, sequence int64) (*types8.PreflightResult, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetPreflightResults", appID, sequence) - ret0, _ := ret[0].(*types7.PreflightResult) + ret0, _ := ret[0].(*types8.PreflightResult) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -1076,10 +1063,10 @@ func (mr *MockStoreMockRecorder) GetRedactions(bundleID interface{}) *gomock.Cal } // GetRegistryDetailsForApp mocks base method. -func (m *MockStore) GetRegistryDetailsForApp(appID string) (types8.RegistrySettings, error) { +func (m *MockStore) GetRegistryDetailsForApp(appID string) (types9.RegistrySettings, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetRegistryDetailsForApp", appID) - ret0, _ := ret[0].(types8.RegistrySettings) + ret0, _ := ret[0].(types9.RegistrySettings) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -1091,10 +1078,10 @@ func (mr *MockStoreMockRecorder) GetRegistryDetailsForApp(appID interface{}) *go } // GetSession mocks base method. -func (m *MockStore) GetSession(sessionID string) (*types10.Session, error) { +func (m *MockStore) GetSession(sessionID string) (*types11.Session, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetSession", sessionID) - ret0, _ := ret[0].(*types10.Session) + ret0, _ := ret[0].(*types11.Session) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -1121,10 +1108,10 @@ func (mr *MockStoreMockRecorder) GetSharedPasswordBcrypt() *gomock.Call { } // GetStatusForVersion mocks base method. -func (m *MockStore) GetStatusForVersion(appID, clusterID string, sequence int64) (types11.DownstreamVersionStatus, error) { +func (m *MockStore) GetStatusForVersion(appID, clusterID string, sequence int64) (types12.DownstreamVersionStatus, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetStatusForVersion", appID, clusterID, sequence) - ret0, _ := ret[0].(types11.DownstreamVersionStatus) + ret0, _ := ret[0].(types12.DownstreamVersionStatus) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -1136,10 +1123,10 @@ func (mr *MockStoreMockRecorder) GetStatusForVersion(appID, clusterID, sequence } // GetSupportBundle mocks base method. -func (m *MockStore) GetSupportBundle(bundleID string) (*types12.SupportBundle, error) { +func (m *MockStore) GetSupportBundle(bundleID string) (*types13.SupportBundle, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetSupportBundle", bundleID) - ret0, _ := ret[0].(*types12.SupportBundle) + ret0, _ := ret[0].(*types13.SupportBundle) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -1151,10 +1138,10 @@ func (mr *MockStoreMockRecorder) GetSupportBundle(bundleID interface{}) *gomock. } // GetSupportBundleAnalysis mocks base method. -func (m *MockStore) GetSupportBundleAnalysis(bundleID string) (*types12.SupportBundleAnalysis, error) { +func (m *MockStore) GetSupportBundleAnalysis(bundleID string) (*types13.SupportBundleAnalysis, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetSupportBundleAnalysis", bundleID) - ret0, _ := ret[0].(*types12.SupportBundleAnalysis) + ret0, _ := ret[0].(*types13.SupportBundleAnalysis) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -1195,22 +1182,6 @@ func (mr *MockStoreMockRecorder) GetTargetKotsVersionForVersion(appID, sequence return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTargetKotsVersionForVersion", reflect.TypeOf((*MockStore)(nil).GetTargetKotsVersionForVersion), appID, sequence) } -// GetTaskStatus mocks base method. -func (m *MockStore) GetTaskStatus(taskID string) (string, string, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetTaskStatus", taskID) - ret0, _ := ret[0].(string) - ret1, _ := ret[1].(string) - ret2, _ := ret[2].(error) - return ret0, ret1, ret2 -} - -// GetTaskStatus indicates an expected call of GetTaskStatus. -func (mr *MockStoreMockRecorder) GetTaskStatus(taskID interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTaskStatus", reflect.TypeOf((*MockStore)(nil).GetTaskStatus), taskID) -} - // HasStrictPreflights mocks base method. func (m *MockStore) HasStrictPreflights(appID string, sequence int64) (bool, error) { m.ctrl.T.Helper() @@ -1346,7 +1317,7 @@ func (mr *MockStoreMockRecorder) IsRollbackSupportedForVersion(appID, sequence i } // IsSnapshotsSupportedForVersion mocks base method. -func (m *MockStore) IsSnapshotsSupportedForVersion(a *types2.App, sequence int64, renderer types9.Renderer) (bool, error) { +func (m *MockStore) IsSnapshotsSupportedForVersion(a *types3.App, sequence int64, renderer types10.Renderer) (bool, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "IsSnapshotsSupportedForVersion", a, sequence, renderer) ret0, _ := ret[0].(bool) @@ -1361,10 +1332,10 @@ func (mr *MockStoreMockRecorder) IsSnapshotsSupportedForVersion(a, sequence, ren } // ListAppsForDownstream mocks base method. -func (m *MockStore) ListAppsForDownstream(clusterID string) ([]*types2.App, error) { +func (m *MockStore) ListAppsForDownstream(clusterID string) ([]*types3.App, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ListAppsForDownstream", clusterID) - ret0, _ := ret[0].([]*types2.App) + ret0, _ := ret[0].([]*types3.App) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -1406,10 +1377,10 @@ func (mr *MockStoreMockRecorder) ListDownstreamsForApp(appID interface{}) *gomoc } // ListFailedApps mocks base method. -func (m *MockStore) ListFailedApps() ([]*types2.App, error) { +func (m *MockStore) ListFailedApps() ([]*types3.App, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ListFailedApps") - ret0, _ := ret[0].([]*types2.App) + ret0, _ := ret[0].([]*types3.App) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -1436,10 +1407,10 @@ func (mr *MockStoreMockRecorder) ListInstalledAppSlugs() *gomock.Call { } // ListInstalledApps mocks base method. -func (m *MockStore) ListInstalledApps() ([]*types2.App, error) { +func (m *MockStore) ListInstalledApps() ([]*types3.App, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ListInstalledApps") - ret0, _ := ret[0].([]*types2.App) + ret0, _ := ret[0].([]*types3.App) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -1451,10 +1422,10 @@ func (mr *MockStoreMockRecorder) ListInstalledApps() *gomock.Call { } // ListPendingScheduledInstanceSnapshots mocks base method. -func (m *MockStore) ListPendingScheduledInstanceSnapshots(clusterID string) ([]types5.ScheduledInstanceSnapshot, error) { +func (m *MockStore) ListPendingScheduledInstanceSnapshots(clusterID string) ([]types6.ScheduledInstanceSnapshot, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ListPendingScheduledInstanceSnapshots", clusterID) - ret0, _ := ret[0].([]types5.ScheduledInstanceSnapshot) + ret0, _ := ret[0].([]types6.ScheduledInstanceSnapshot) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -1466,10 +1437,10 @@ func (mr *MockStoreMockRecorder) ListPendingScheduledInstanceSnapshots(clusterID } // ListPendingScheduledSnapshots mocks base method. -func (m *MockStore) ListPendingScheduledSnapshots(appID string) ([]types5.ScheduledSnapshot, error) { +func (m *MockStore) ListPendingScheduledSnapshots(appID string) ([]types6.ScheduledSnapshot, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ListPendingScheduledSnapshots", appID) - ret0, _ := ret[0].([]types5.ScheduledSnapshot) + ret0, _ := ret[0].([]types6.ScheduledSnapshot) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -1481,10 +1452,10 @@ func (mr *MockStoreMockRecorder) ListPendingScheduledSnapshots(appID interface{} } // ListSupportBundles mocks base method. -func (m *MockStore) ListSupportBundles(appID string) ([]*types12.SupportBundle, error) { +func (m *MockStore) ListSupportBundles(appID string) ([]*types13.SupportBundle, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ListSupportBundles", appID) - ret0, _ := ret[0].([]*types12.SupportBundle) + ret0, _ := ret[0].([]*types13.SupportBundle) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -1606,7 +1577,7 @@ func (mr *MockStoreMockRecorder) SetAppIsAirgap(appID, isAirgap interface{}) *go } // SetAppStatus mocks base method. -func (m *MockStore) SetAppStatus(appID string, resourceStates types3.ResourceStates, updatedAt time.Time, sequence int64) error { +func (m *MockStore) SetAppStatus(appID string, resourceStates types4.ResourceStates, updatedAt time.Time, sequence int64) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SetAppStatus", appID, resourceStates, updatedAt, sequence) ret0, _ := ret[0].(error) @@ -1620,7 +1591,7 @@ func (mr *MockStoreMockRecorder) SetAppStatus(appID, resourceStates, updatedAt, } // SetAutoDeploy mocks base method. -func (m *MockStore) SetAutoDeploy(appID string, autoDeploy types2.AutoDeploy) error { +func (m *MockStore) SetAutoDeploy(appID string, autoDeploy types3.AutoDeploy) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SetAutoDeploy", appID, autoDeploy) ret0, _ := ret[0].(error) @@ -1634,7 +1605,7 @@ func (mr *MockStoreMockRecorder) SetAutoDeploy(appID, autoDeploy interface{}) *g } // SetDownstreamVersionStatus mocks base method. -func (m *MockStore) SetDownstreamVersionStatus(appID string, sequence int64, status types11.DownstreamVersionStatus, statusInfo string) error { +func (m *MockStore) SetDownstreamVersionStatus(appID string, sequence int64, status types12.DownstreamVersionStatus, statusInfo string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SetDownstreamVersionStatus", appID, sequence, status, statusInfo) ret0, _ := ret[0].(error) @@ -1844,20 +1815,6 @@ func (mr *MockStoreMockRecorder) SetSupportBundleAnalysis(bundleID, insights int return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetSupportBundleAnalysis", reflect.TypeOf((*MockStore)(nil).SetSupportBundleAnalysis), bundleID, insights) } -// SetTaskStatus mocks base method. -func (m *MockStore) SetTaskStatus(taskID, message, status string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SetTaskStatus", taskID, message, status) - ret0, _ := ret[0].(error) - return ret0 -} - -// SetTaskStatus indicates an expected call of SetTaskStatus. -func (mr *MockStoreMockRecorder) SetTaskStatus(taskID, message, status interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetTaskStatus", reflect.TypeOf((*MockStore)(nil).SetTaskStatus), taskID, message, status) -} - // SetUpdateCheckerSpec mocks base method. func (m *MockStore) SetUpdateCheckerSpec(appID, updateCheckerSpec string) error { m.ctrl.T.Helper() @@ -1873,18 +1830,18 @@ func (mr *MockStoreMockRecorder) SetUpdateCheckerSpec(appID, updateCheckerSpec i } // UpdateAppLicense mocks base method. -func (m *MockStore) UpdateAppLicense(appID string, sequence int64, archiveDir string, newLicense *v1beta10.License, originalLicenseData string, channelChanged, failOnVersionCreate bool, gitops types4.DownstreamGitOps, renderer types9.Renderer) (int64, error) { +func (m *MockStore) UpdateAppLicense(appID string, sequence int64, archiveDir string, newLicense *v1beta10.License, originalLicenseData string, channelChanged, failOnVersionCreate bool, gitops types5.DownstreamGitOps, renderer types10.Renderer, reportingInfo *types1.ReportingInfo) (int64, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateAppLicense", appID, sequence, archiveDir, newLicense, originalLicenseData, channelChanged, failOnVersionCreate, gitops, renderer) + ret := m.ctrl.Call(m, "UpdateAppLicense", appID, sequence, archiveDir, newLicense, originalLicenseData, channelChanged, failOnVersionCreate, gitops, renderer, reportingInfo) ret0, _ := ret[0].(int64) ret1, _ := ret[1].(error) return ret0, ret1 } // UpdateAppLicense indicates an expected call of UpdateAppLicense. -func (mr *MockStoreMockRecorder) UpdateAppLicense(appID, sequence, archiveDir, newLicense, originalLicenseData, channelChanged, failOnVersionCreate, gitops, renderer interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateAppLicense(appID, sequence, archiveDir, newLicense, originalLicenseData, channelChanged, failOnVersionCreate, gitops, renderer, reportingInfo interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAppLicense", reflect.TypeOf((*MockStore)(nil).UpdateAppLicense), appID, sequence, archiveDir, newLicense, originalLicenseData, channelChanged, failOnVersionCreate, gitops, renderer) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAppLicense", reflect.TypeOf((*MockStore)(nil).UpdateAppLicense), appID, sequence, archiveDir, newLicense, originalLicenseData, channelChanged, failOnVersionCreate, gitops, renderer, reportingInfo) } // UpdateAppLicenseSyncNow mocks base method. @@ -1902,7 +1859,7 @@ func (mr *MockStoreMockRecorder) UpdateAppLicenseSyncNow(appID interface{}) *gom } // UpdateAppVersion mocks base method. -func (m *MockStore) UpdateAppVersion(appID string, sequence int64, baseSequence *int64, filesInDir, source string, skipPreflights bool, gitops types4.DownstreamGitOps, renderer types9.Renderer) error { +func (m *MockStore) UpdateAppVersion(appID string, sequence int64, baseSequence *int64, filesInDir, source string, skipPreflights bool, gitops types5.DownstreamGitOps, renderer types10.Renderer) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "UpdateAppVersion", appID, sequence, baseSequence, filesInDir, source, skipPreflights, gitops, renderer) ret0, _ := ret[0].(error) @@ -1915,20 +1872,6 @@ func (mr *MockStoreMockRecorder) UpdateAppVersion(appID, sequence, baseSequence, return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAppVersion", reflect.TypeOf((*MockStore)(nil).UpdateAppVersion), appID, sequence, baseSequence, filesInDir, source, skipPreflights, gitops, renderer) } -// UpdateAppVersionInstallationSpec mocks base method. -func (m *MockStore) UpdateAppVersionInstallationSpec(appID string, sequence int64, spec v1beta10.Installation) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateAppVersionInstallationSpec", appID, sequence, spec) - ret0, _ := ret[0].(error) - return ret0 -} - -// UpdateAppVersionInstallationSpec indicates an expected call of UpdateAppVersionInstallationSpec. -func (mr *MockStoreMockRecorder) UpdateAppVersionInstallationSpec(appID, sequence, spec interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAppVersionInstallationSpec", reflect.TypeOf((*MockStore)(nil).UpdateAppVersionInstallationSpec), appID, sequence, spec) -} - // UpdateDownstreamDeployStatus mocks base method. func (m *MockStore) UpdateDownstreamDeployStatus(appID, clusterID string, sequence int64, isError bool, output types0.DownstreamOutput) error { m.ctrl.T.Helper() @@ -2014,7 +1957,7 @@ func (mr *MockStoreMockRecorder) UpdateSessionExpiresAt(sessionID, expiresAt int } // UpdateSupportBundle mocks base method. -func (m *MockStore) UpdateSupportBundle(bundle *types12.SupportBundle) error { +func (m *MockStore) UpdateSupportBundle(bundle *types13.SupportBundle) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "UpdateSupportBundle", bundle) ret0, _ := ret[0].(error) @@ -2027,20 +1970,6 @@ func (mr *MockStoreMockRecorder) UpdateSupportBundle(bundle interface{}) *gomock return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateSupportBundle", reflect.TypeOf((*MockStore)(nil).UpdateSupportBundle), bundle) } -// UpdateTaskStatusTimestamp mocks base method. -func (m *MockStore) UpdateTaskStatusTimestamp(taskID string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateTaskStatusTimestamp", taskID) - ret0, _ := ret[0].(error) - return ret0 -} - -// UpdateTaskStatusTimestamp indicates an expected call of UpdateTaskStatusTimestamp. -func (mr *MockStoreMockRecorder) UpdateTaskStatusTimestamp(taskID interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTaskStatusTimestamp", reflect.TypeOf((*MockStore)(nil).UpdateTaskStatusTimestamp), taskID) -} - // UploadSupportBundle mocks base method. func (m *MockStore) UploadSupportBundle(bundleID, archivePath string, marshalledTree []byte) error { m.ctrl.T.Helper() @@ -2143,10 +2072,10 @@ func (mr *MockRegistryStoreMockRecorder) GetAppIDsFromRegistry(hostname interfac } // GetRegistryDetailsForApp mocks base method. -func (m *MockRegistryStore) GetRegistryDetailsForApp(appID string) (types8.RegistrySettings, error) { +func (m *MockRegistryStore) GetRegistryDetailsForApp(appID string) (types9.RegistrySettings, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetRegistryDetailsForApp", appID) - ret0, _ := ret[0].(types8.RegistrySettings) + ret0, _ := ret[0].(types9.RegistrySettings) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -2195,7 +2124,7 @@ func (m *MockSupportBundleStore) EXPECT() *MockSupportBundleStoreMockRecorder { } // CreateInProgressSupportBundle mocks base method. -func (m *MockSupportBundleStore) CreateInProgressSupportBundle(supportBundle *types12.SupportBundle) error { +func (m *MockSupportBundleStore) CreateInProgressSupportBundle(supportBundle *types13.SupportBundle) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CreateInProgressSupportBundle", supportBundle) ret0, _ := ret[0].(error) @@ -2209,10 +2138,10 @@ func (mr *MockSupportBundleStoreMockRecorder) CreateInProgressSupportBundle(supp } // CreateSupportBundle mocks base method. -func (m *MockSupportBundleStore) CreateSupportBundle(bundleID, appID, archivePath string, marshalledTree []byte) (*types12.SupportBundle, error) { +func (m *MockSupportBundleStore) CreateSupportBundle(bundleID, appID, archivePath string, marshalledTree []byte) (*types13.SupportBundle, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CreateSupportBundle", bundleID, appID, archivePath, marshalledTree) - ret0, _ := ret[0].(*types12.SupportBundle) + ret0, _ := ret[0].(*types13.SupportBundle) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -2253,10 +2182,10 @@ func (mr *MockSupportBundleStoreMockRecorder) GetRedactions(bundleID interface{} } // GetSupportBundle mocks base method. -func (m *MockSupportBundleStore) GetSupportBundle(bundleID string) (*types12.SupportBundle, error) { +func (m *MockSupportBundleStore) GetSupportBundle(bundleID string) (*types13.SupportBundle, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetSupportBundle", bundleID) - ret0, _ := ret[0].(*types12.SupportBundle) + ret0, _ := ret[0].(*types13.SupportBundle) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -2268,10 +2197,10 @@ func (mr *MockSupportBundleStoreMockRecorder) GetSupportBundle(bundleID interfac } // GetSupportBundleAnalysis mocks base method. -func (m *MockSupportBundleStore) GetSupportBundleAnalysis(bundleID string) (*types12.SupportBundleAnalysis, error) { +func (m *MockSupportBundleStore) GetSupportBundleAnalysis(bundleID string) (*types13.SupportBundleAnalysis, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetSupportBundleAnalysis", bundleID) - ret0, _ := ret[0].(*types12.SupportBundleAnalysis) + ret0, _ := ret[0].(*types13.SupportBundleAnalysis) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -2298,10 +2227,10 @@ func (mr *MockSupportBundleStoreMockRecorder) GetSupportBundleArchive(bundleID i } // ListSupportBundles mocks base method. -func (m *MockSupportBundleStore) ListSupportBundles(appID string) ([]*types12.SupportBundle, error) { +func (m *MockSupportBundleStore) ListSupportBundles(appID string) ([]*types13.SupportBundle, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ListSupportBundles", appID) - ret0, _ := ret[0].([]*types12.SupportBundle) + ret0, _ := ret[0].([]*types13.SupportBundle) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -2341,7 +2270,7 @@ func (mr *MockSupportBundleStoreMockRecorder) SetSupportBundleAnalysis(bundleID, } // UpdateSupportBundle mocks base method. -func (m *MockSupportBundleStore) UpdateSupportBundle(bundle *types12.SupportBundle) error { +func (m *MockSupportBundleStore) UpdateSupportBundle(bundle *types13.SupportBundle) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "UpdateSupportBundle", bundle) ret0, _ := ret[0].(error) @@ -2407,10 +2336,10 @@ func (mr *MockPreflightStoreMockRecorder) GetPreflightProgress(appID, sequence i } // GetPreflightResults mocks base method. -func (m *MockPreflightStore) GetPreflightResults(appID string, sequence int64) (*types7.PreflightResult, error) { +func (m *MockPreflightStore) GetPreflightResults(appID string, sequence int64) (*types8.PreflightResult, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetPreflightResults", appID, sequence) - ret0, _ := ret[0].(*types7.PreflightResult) + ret0, _ := ret[0].(*types8.PreflightResult) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -2610,87 +2539,6 @@ func (mr *MockAirgapStoreMockRecorder) SetAppIsAirgap(appID, isAirgap interface{ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetAppIsAirgap", reflect.TypeOf((*MockAirgapStore)(nil).SetAppIsAirgap), appID, isAirgap) } -// MockTaskStore is a mock of TaskStore interface. -type MockTaskStore struct { - ctrl *gomock.Controller - recorder *MockTaskStoreMockRecorder -} - -// MockTaskStoreMockRecorder is the mock recorder for MockTaskStore. -type MockTaskStoreMockRecorder struct { - mock *MockTaskStore -} - -// NewMockTaskStore creates a new mock instance. -func NewMockTaskStore(ctrl *gomock.Controller) *MockTaskStore { - mock := &MockTaskStore{ctrl: ctrl} - mock.recorder = &MockTaskStoreMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockTaskStore) EXPECT() *MockTaskStoreMockRecorder { - return m.recorder -} - -// ClearTaskStatus mocks base method. -func (m *MockTaskStore) ClearTaskStatus(taskID string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ClearTaskStatus", taskID) - ret0, _ := ret[0].(error) - return ret0 -} - -// ClearTaskStatus indicates an expected call of ClearTaskStatus. -func (mr *MockTaskStoreMockRecorder) ClearTaskStatus(taskID interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClearTaskStatus", reflect.TypeOf((*MockTaskStore)(nil).ClearTaskStatus), taskID) -} - -// GetTaskStatus mocks base method. -func (m *MockTaskStore) GetTaskStatus(taskID string) (string, string, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetTaskStatus", taskID) - ret0, _ := ret[0].(string) - ret1, _ := ret[1].(string) - ret2, _ := ret[2].(error) - return ret0, ret1, ret2 -} - -// GetTaskStatus indicates an expected call of GetTaskStatus. -func (mr *MockTaskStoreMockRecorder) GetTaskStatus(taskID interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTaskStatus", reflect.TypeOf((*MockTaskStore)(nil).GetTaskStatus), taskID) -} - -// SetTaskStatus mocks base method. -func (m *MockTaskStore) SetTaskStatus(taskID, message, status string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SetTaskStatus", taskID, message, status) - ret0, _ := ret[0].(error) - return ret0 -} - -// SetTaskStatus indicates an expected call of SetTaskStatus. -func (mr *MockTaskStoreMockRecorder) SetTaskStatus(taskID, message, status interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetTaskStatus", reflect.TypeOf((*MockTaskStore)(nil).SetTaskStatus), taskID, message, status) -} - -// UpdateTaskStatusTimestamp mocks base method. -func (m *MockTaskStore) UpdateTaskStatusTimestamp(taskID string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateTaskStatusTimestamp", taskID) - ret0, _ := ret[0].(error) - return ret0 -} - -// UpdateTaskStatusTimestamp indicates an expected call of UpdateTaskStatusTimestamp. -func (mr *MockTaskStoreMockRecorder) UpdateTaskStatusTimestamp(taskID interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTaskStatusTimestamp", reflect.TypeOf((*MockTaskStore)(nil).UpdateTaskStatusTimestamp), taskID) -} - // MockSessionStore is a mock of SessionStore interface. type MockSessionStore struct { ctrl *gomock.Controller @@ -2715,10 +2563,10 @@ func (m *MockSessionStore) EXPECT() *MockSessionStoreMockRecorder { } // CreateSession mocks base method. -func (m *MockSessionStore) CreateSession(user *types14.User, issuedAt, expiresAt time.Time, roles []string) (*types10.Session, error) { +func (m *MockSessionStore) CreateSession(user *types15.User, issuedAt, expiresAt time.Time, roles []string) (*types11.Session, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CreateSession", user, issuedAt, expiresAt, roles) - ret0, _ := ret[0].(*types10.Session) + ret0, _ := ret[0].(*types11.Session) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -2758,10 +2606,10 @@ func (mr *MockSessionStoreMockRecorder) DeleteSession(sessionID interface{}) *go } // GetSession mocks base method. -func (m *MockSessionStore) GetSession(sessionID string) (*types10.Session, error) { +func (m *MockSessionStore) GetSession(sessionID string) (*types11.Session, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetSession", sessionID) - ret0, _ := ret[0].(*types10.Session) + ret0, _ := ret[0].(*types11.Session) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -2810,10 +2658,10 @@ func (m *MockAppStatusStore) EXPECT() *MockAppStatusStoreMockRecorder { } // GetAppStatus mocks base method. -func (m *MockAppStatusStore) GetAppStatus(appID string) (*types3.AppStatus, error) { +func (m *MockAppStatusStore) GetAppStatus(appID string) (*types4.AppStatus, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetAppStatus", appID) - ret0, _ := ret[0].(*types3.AppStatus) + ret0, _ := ret[0].(*types4.AppStatus) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -2825,7 +2673,7 @@ func (mr *MockAppStatusStoreMockRecorder) GetAppStatus(appID interface{}) *gomoc } // SetAppStatus mocks base method. -func (m *MockAppStatusStore) SetAppStatus(appID string, resourceStates types3.ResourceStates, updatedAt time.Time, sequence int64) error { +func (m *MockAppStatusStore) SetAppStatus(appID string, resourceStates types4.ResourceStates, updatedAt time.Time, sequence int64) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SetAppStatus", appID, resourceStates, updatedAt, sequence) ret0, _ := ret[0].(error) @@ -2876,10 +2724,10 @@ func (mr *MockAppStoreMockRecorder) AddAppToAllDownstreams(appID interface{}) *g } // CreateApp mocks base method. -func (m *MockAppStore) CreateApp(name, upstreamURI, licenseData string, isAirgapEnabled, skipImagePush, registryIsReadOnly bool) (*types2.App, error) { +func (m *MockAppStore) CreateApp(name, upstreamURI, licenseData string, isAirgapEnabled, skipImagePush, registryIsReadOnly bool) (*types3.App, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CreateApp", name, upstreamURI, licenseData, isAirgapEnabled, skipImagePush, registryIsReadOnly) - ret0, _ := ret[0].(*types2.App) + ret0, _ := ret[0].(*types3.App) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -2891,10 +2739,10 @@ func (mr *MockAppStoreMockRecorder) CreateApp(name, upstreamURI, licenseData, is } // GetApp mocks base method. -func (m *MockAppStore) GetApp(appID string) (*types2.App, error) { +func (m *MockAppStore) GetApp(appID string) (*types3.App, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetApp", appID) - ret0, _ := ret[0].(*types2.App) + ret0, _ := ret[0].(*types3.App) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -2906,10 +2754,10 @@ func (mr *MockAppStoreMockRecorder) GetApp(appID interface{}) *gomock.Call { } // GetAppFromSlug mocks base method. -func (m *MockAppStore) GetAppFromSlug(slug string) (*types2.App, error) { +func (m *MockAppStore) GetAppFromSlug(slug string) (*types3.App, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetAppFromSlug", slug) - ret0, _ := ret[0].(*types2.App) + ret0, _ := ret[0].(*types3.App) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -2966,10 +2814,10 @@ func (mr *MockAppStoreMockRecorder) IsGitOpsEnabledForApp(appID interface{}) *go } // ListAppsForDownstream mocks base method. -func (m *MockAppStore) ListAppsForDownstream(clusterID string) ([]*types2.App, error) { +func (m *MockAppStore) ListAppsForDownstream(clusterID string) ([]*types3.App, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ListAppsForDownstream", clusterID) - ret0, _ := ret[0].([]*types2.App) + ret0, _ := ret[0].([]*types3.App) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -2996,10 +2844,10 @@ func (mr *MockAppStoreMockRecorder) ListDownstreamsForApp(appID interface{}) *go } // ListFailedApps mocks base method. -func (m *MockAppStore) ListFailedApps() ([]*types2.App, error) { +func (m *MockAppStore) ListFailedApps() ([]*types3.App, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ListFailedApps") - ret0, _ := ret[0].([]*types2.App) + ret0, _ := ret[0].([]*types3.App) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -3026,10 +2874,10 @@ func (mr *MockAppStoreMockRecorder) ListInstalledAppSlugs() *gomock.Call { } // ListInstalledApps mocks base method. -func (m *MockAppStore) ListInstalledApps() ([]*types2.App, error) { +func (m *MockAppStore) ListInstalledApps() ([]*types3.App, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ListInstalledApps") - ret0, _ := ret[0].([]*types2.App) + ret0, _ := ret[0].([]*types3.App) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -3083,7 +2931,7 @@ func (mr *MockAppStoreMockRecorder) SetAppInstallState(appID, state interface{}) } // SetAutoDeploy mocks base method. -func (m *MockAppStore) SetAutoDeploy(appID string, autoDeploy types2.AutoDeploy) error { +func (m *MockAppStore) SetAutoDeploy(appID string, autoDeploy types3.AutoDeploy) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SetAutoDeploy", appID, autoDeploy) ret0, _ := ret[0].(error) @@ -3309,10 +3157,10 @@ func (mr *MockDownstreamStoreMockRecorder) GetDownstreamVersionSource(appID, seq } // GetDownstreamVersionStatus mocks base method. -func (m *MockDownstreamStore) GetDownstreamVersionStatus(appID string, sequence int64) (types11.DownstreamVersionStatus, error) { +func (m *MockDownstreamStore) GetDownstreamVersionStatus(appID string, sequence int64) (types12.DownstreamVersionStatus, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetDownstreamVersionStatus", appID, sequence) - ret0, _ := ret[0].(types11.DownstreamVersionStatus) + ret0, _ := ret[0].(types12.DownstreamVersionStatus) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -3401,10 +3249,10 @@ func (mr *MockDownstreamStoreMockRecorder) GetPreviouslyDeployedSequence(appID, } // GetStatusForVersion mocks base method. -func (m *MockDownstreamStore) GetStatusForVersion(appID, clusterID string, sequence int64) (types11.DownstreamVersionStatus, error) { +func (m *MockDownstreamStore) GetStatusForVersion(appID, clusterID string, sequence int64) (types12.DownstreamVersionStatus, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetStatusForVersion", appID, clusterID, sequence) - ret0, _ := ret[0].(types11.DownstreamVersionStatus) + ret0, _ := ret[0].(types12.DownstreamVersionStatus) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -3461,7 +3309,7 @@ func (mr *MockDownstreamStoreMockRecorder) MarkAsCurrentDownstreamVersion(appID, } // SetDownstreamVersionStatus mocks base method. -func (m *MockDownstreamStore) SetDownstreamVersionStatus(appID string, sequence int64, status types11.DownstreamVersionStatus, statusInfo string) error { +func (m *MockDownstreamStore) SetDownstreamVersionStatus(appID string, sequence int64, status types12.DownstreamVersionStatus, statusInfo string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SetDownstreamVersionStatus", appID, sequence, status, statusInfo) ret0, _ := ret[0].(error) @@ -3568,10 +3416,10 @@ func (mr *MockSnapshotStoreMockRecorder) DeletePendingScheduledSnapshots(appID i } // ListPendingScheduledInstanceSnapshots mocks base method. -func (m *MockSnapshotStore) ListPendingScheduledInstanceSnapshots(clusterID string) ([]types5.ScheduledInstanceSnapshot, error) { +func (m *MockSnapshotStore) ListPendingScheduledInstanceSnapshots(clusterID string) ([]types6.ScheduledInstanceSnapshot, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ListPendingScheduledInstanceSnapshots", clusterID) - ret0, _ := ret[0].([]types5.ScheduledInstanceSnapshot) + ret0, _ := ret[0].([]types6.ScheduledInstanceSnapshot) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -3583,10 +3431,10 @@ func (mr *MockSnapshotStoreMockRecorder) ListPendingScheduledInstanceSnapshots(c } // ListPendingScheduledSnapshots mocks base method. -func (m *MockSnapshotStore) ListPendingScheduledSnapshots(appID string) ([]types5.ScheduledSnapshot, error) { +func (m *MockSnapshotStore) ListPendingScheduledSnapshots(appID string) ([]types6.ScheduledSnapshot, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ListPendingScheduledSnapshots", appID) - ret0, _ := ret[0].([]types5.ScheduledSnapshot) + ret0, _ := ret[0].([]types6.ScheduledSnapshot) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -3649,7 +3497,7 @@ func (m *MockVersionStore) EXPECT() *MockVersionStoreMockRecorder { } // CreateAppVersion mocks base method. -func (m *MockVersionStore) CreateAppVersion(appID string, baseSequence *int64, filesInDir, source string, skipPreflights bool, gitops types4.DownstreamGitOps, renderer types9.Renderer) (int64, error) { +func (m *MockVersionStore) CreateAppVersion(appID string, baseSequence *int64, filesInDir, source string, skipPreflights bool, gitops types5.DownstreamGitOps, renderer types10.Renderer) (int64, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CreateAppVersion", appID, baseSequence, filesInDir, source, skipPreflights, gitops, renderer) ret0, _ := ret[0].(int64) @@ -3678,7 +3526,7 @@ func (mr *MockVersionStoreMockRecorder) CreateAppVersionArchive(appID, sequence, } // CreatePendingDownloadAppVersion mocks base method. -func (m *MockVersionStore) CreatePendingDownloadAppVersion(appID string, update types13.Update, kotsApplication *v1beta10.Application, license *v1beta10.License) (int64, error) { +func (m *MockVersionStore) CreatePendingDownloadAppVersion(appID string, update types14.Update, kotsApplication *v1beta10.Application, license *v1beta10.License) (int64, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CreatePendingDownloadAppVersion", appID, update, kotsApplication, license) ret0, _ := ret[0].(int64) @@ -3693,10 +3541,10 @@ func (mr *MockVersionStoreMockRecorder) CreatePendingDownloadAppVersion(appID, u } // GetAppVersion mocks base method. -func (m *MockVersionStore) GetAppVersion(appID string, sequence int64) (*types1.AppVersion, error) { +func (m *MockVersionStore) GetAppVersion(appID string, sequence int64) (*types2.AppVersion, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetAppVersion", appID, sequence) - ret0, _ := ret[0].(*types1.AppVersion) + ret0, _ := ret[0].(*types2.AppVersion) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -3873,7 +3721,7 @@ func (mr *MockVersionStoreMockRecorder) IsRollbackSupportedForVersion(appID, seq } // IsSnapshotsSupportedForVersion mocks base method. -func (m *MockVersionStore) IsSnapshotsSupportedForVersion(a *types2.App, sequence int64, renderer types9.Renderer) (bool, error) { +func (m *MockVersionStore) IsSnapshotsSupportedForVersion(a *types3.App, sequence int64, renderer types10.Renderer) (bool, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "IsSnapshotsSupportedForVersion", a, sequence, renderer) ret0, _ := ret[0].(bool) @@ -3888,7 +3736,7 @@ func (mr *MockVersionStoreMockRecorder) IsSnapshotsSupportedForVersion(a, sequen } // UpdateAppVersion mocks base method. -func (m *MockVersionStore) UpdateAppVersion(appID string, sequence int64, baseSequence *int64, filesInDir, source string, skipPreflights bool, gitops types4.DownstreamGitOps, renderer types9.Renderer) error { +func (m *MockVersionStore) UpdateAppVersion(appID string, sequence int64, baseSequence *int64, filesInDir, source string, skipPreflights bool, gitops types5.DownstreamGitOps, renderer types10.Renderer) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "UpdateAppVersion", appID, sequence, baseSequence, filesInDir, source, skipPreflights, gitops, renderer) ret0, _ := ret[0].(error) @@ -3901,20 +3749,6 @@ func (mr *MockVersionStoreMockRecorder) UpdateAppVersion(appID, sequence, baseSe return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAppVersion", reflect.TypeOf((*MockVersionStore)(nil).UpdateAppVersion), appID, sequence, baseSequence, filesInDir, source, skipPreflights, gitops, renderer) } -// UpdateAppVersionInstallationSpec mocks base method. -func (m *MockVersionStore) UpdateAppVersionInstallationSpec(appID string, sequence int64, spec v1beta10.Installation) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateAppVersionInstallationSpec", appID, sequence, spec) - ret0, _ := ret[0].(error) - return ret0 -} - -// UpdateAppVersionInstallationSpec indicates an expected call of UpdateAppVersionInstallationSpec. -func (mr *MockVersionStoreMockRecorder) UpdateAppVersionInstallationSpec(appID, sequence, spec interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAppVersionInstallationSpec", reflect.TypeOf((*MockVersionStore)(nil).UpdateAppVersionInstallationSpec), appID, sequence, spec) -} - // UpdateNextAppVersionDiffSummary mocks base method. func (m *MockVersionStore) UpdateNextAppVersionDiffSummary(appID string, baseSequence int64) error { m.ctrl.T.Helper() @@ -3998,18 +3832,18 @@ func (mr *MockLicenseStoreMockRecorder) GetLicenseForAppVersion(appID, sequence } // UpdateAppLicense mocks base method. -func (m *MockLicenseStore) UpdateAppLicense(appID string, sequence int64, archiveDir string, newLicense *v1beta10.License, originalLicenseData string, channelChanged, failOnVersionCreate bool, gitops types4.DownstreamGitOps, renderer types9.Renderer) (int64, error) { +func (m *MockLicenseStore) UpdateAppLicense(appID string, sequence int64, archiveDir string, newLicense *v1beta10.License, originalLicenseData string, channelChanged, failOnVersionCreate bool, gitops types5.DownstreamGitOps, renderer types10.Renderer, reportingInfo *types1.ReportingInfo) (int64, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateAppLicense", appID, sequence, archiveDir, newLicense, originalLicenseData, channelChanged, failOnVersionCreate, gitops, renderer) + ret := m.ctrl.Call(m, "UpdateAppLicense", appID, sequence, archiveDir, newLicense, originalLicenseData, channelChanged, failOnVersionCreate, gitops, renderer, reportingInfo) ret0, _ := ret[0].(int64) ret1, _ := ret[1].(error) return ret0, ret1 } // UpdateAppLicense indicates an expected call of UpdateAppLicense. -func (mr *MockLicenseStoreMockRecorder) UpdateAppLicense(appID, sequence, archiveDir, newLicense, originalLicenseData, channelChanged, failOnVersionCreate, gitops, renderer interface{}) *gomock.Call { +func (mr *MockLicenseStoreMockRecorder) UpdateAppLicense(appID, sequence, archiveDir, newLicense, originalLicenseData, channelChanged, failOnVersionCreate, gitops, renderer, reportingInfo interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAppLicense", reflect.TypeOf((*MockLicenseStore)(nil).UpdateAppLicense), appID, sequence, archiveDir, newLicense, originalLicenseData, channelChanged, failOnVersionCreate, gitops, renderer) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAppLicense", reflect.TypeOf((*MockLicenseStore)(nil).UpdateAppLicense), appID, sequence, archiveDir, newLicense, originalLicenseData, channelChanged, failOnVersionCreate, gitops, renderer, reportingInfo) } // UpdateAppLicenseSyncNow mocks base method. @@ -4242,10 +4076,10 @@ func (m *MockInstallationStore) EXPECT() *MockInstallationStoreMockRecorder { } // GetPendingInstallationStatus mocks base method. -func (m *MockInstallationStore) GetPendingInstallationStatus() (*types6.InstallStatus, error) { +func (m *MockInstallationStore) GetPendingInstallationStatus() (*types7.InstallStatus, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetPendingInstallationStatus") - ret0, _ := ret[0].(*types6.InstallStatus) + ret0, _ := ret[0].(*types7.InstallStatus) ret1, _ := ret[1].(error) return ret0, ret1 } diff --git a/pkg/store/store.go b/pkg/store/store.go index 409eb32e97..a0c1f9f425 100644 --- a/pkg/store/store.go +++ b/pkg/store/store.go @@ -1,6 +1,8 @@ package store import ( + "os" + "github.com/replicatedhq/kots/pkg/store/kotsstore" ) @@ -12,11 +14,13 @@ var ( var _ Store = (*kotsstore.KOTSStore)(nil) func GetStore() Store { + if os.Getenv("IS_UPGRADE_SERVICE") == "true" { + panic("store should not be used in the upgrade service") + } if !hasStore { globalStore = storeFromEnv() hasStore = true } - return globalStore } diff --git a/pkg/store/store_interface.go b/pkg/store/store_interface.go index 05f1ee395e..6e503719e6 100644 --- a/pkg/store/store_interface.go +++ b/pkg/store/store_interface.go @@ -7,6 +7,7 @@ import ( embeddedclusterv1beta1 "github.com/replicatedhq/embedded-cluster-kinds/apis/v1beta1" airgaptypes "github.com/replicatedhq/kots/pkg/airgap/types" downstreamtypes "github.com/replicatedhq/kots/pkg/api/downstream/types" + reportingtypes "github.com/replicatedhq/kots/pkg/api/reporting/types" versiontypes "github.com/replicatedhq/kots/pkg/api/version/types" apptypes "github.com/replicatedhq/kots/pkg/app/types" appstatetypes "github.com/replicatedhq/kots/pkg/appstate/types" @@ -32,7 +33,6 @@ type Store interface { PreflightStore PrometheusStore AirgapStore - TaskStore SessionStore AppStatusStore AppStore @@ -99,13 +99,6 @@ type AirgapStore interface { SetAppIsAirgap(appID string, isAirgap bool) error } -type TaskStore interface { - SetTaskStatus(taskID string, message string, status string) error - UpdateTaskStatusTimestamp(taskID string) error - ClearTaskStatus(taskID string) error - GetTaskStatus(taskID string) (status string, message string, err error) -} - type SessionStore interface { CreateSession(user *usertypes.User, issuedAt time.Time, expiresAt time.Time, roles []string) (*sessiontypes.Session, error) DeleteSession(sessionID string) error @@ -196,7 +189,6 @@ type VersionStore interface { GetAppVersion(appID string, sequence int64) (*versiontypes.AppVersion, error) GetLatestAppSequence(appID string, downloadedOnly bool) (int64, error) UpdateNextAppVersionDiffSummary(appID string, baseSequence int64) error - UpdateAppVersionInstallationSpec(appID string, sequence int64, spec kotsv1beta1.Installation) error GetNextAppSequence(appID string) (int64, error) GetCurrentUpdateCursor(appID string, channelID string) (string, error) HasStrictPreflights(appID string, sequence int64) (bool, error) @@ -209,7 +201,7 @@ type LicenseStore interface { GetAllAppLicenses() ([]*kotsv1beta1.License, error) // originalLicenseData is the data received from the replicated API that was never marshalled locally so all fields are intact - UpdateAppLicense(appID string, sequence int64, archiveDir string, newLicense *kotsv1beta1.License, originalLicenseData string, channelChanged bool, failOnVersionCreate bool, gitops gitopstypes.DownstreamGitOps, renderer rendertypes.Renderer) (int64, error) + UpdateAppLicense(appID string, sequence int64, archiveDir string, newLicense *kotsv1beta1.License, originalLicenseData string, channelChanged bool, failOnVersionCreate bool, gitops gitopstypes.DownstreamGitOps, renderer rendertypes.Renderer, reportingInfo *reportingtypes.ReportingInfo) (int64, error) UpdateAppLicenseSyncNow(appID string) error } diff --git a/pkg/tasks/tasks.go b/pkg/tasks/tasks.go index c342f6716e..a8a342efa9 100644 --- a/pkg/tasks/tasks.go +++ b/pkg/tasks/tasks.go @@ -1,20 +1,54 @@ package tasks import ( + "context" + "encoding/json" + "fmt" + "strings" + "sync" "time" "github.com/pkg/errors" + "github.com/replicatedhq/kots/pkg/k8sutil" + kotsadmtypes "github.com/replicatedhq/kots/pkg/kotsadm/types" "github.com/replicatedhq/kots/pkg/logger" - "github.com/replicatedhq/kots/pkg/store" + "github.com/replicatedhq/kots/pkg/persistence" "github.com/replicatedhq/kots/pkg/util" + "github.com/rqlite/gorqlite" + corev1 "k8s.io/api/core/v1" + kuberneteserrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +const ( + TaskStatusConfigMapName = `kotsadm-tasks` + ConfgConfigMapName = `kotsadm-confg` + + taskCacheTTL = 1 * time.Minute +) + +var ( + taskStatusLock = sync.Mutex{} + cachedTaskStatus = map[string]*CachedTaskStatus{} +) + +type TaskStatus struct { + Message string `json:"message"` + Status string `json:"status"` + UpdatedAt time.Time `json:"updatedAt"` +} + +type CachedTaskStatus struct { + expirationTime time.Time + taskStatus TaskStatus +} + func StartUpdateTaskMonitor(taskID string, finishedChan <-chan error) { go func() { var finalError error defer func() { if finalError == nil { - if err := store.GetStore().ClearTaskStatus(taskID); err != nil { + if err := ClearTaskStatus(taskID); err != nil { logger.Error(errors.Wrap(err, "failed to clear update-download task status")) } } else { @@ -22,7 +56,7 @@ func StartUpdateTaskMonitor(taskID string, finishedChan <-chan error) { if cause, ok := errors.Cause(finalError).(util.ActionableError); ok { errMsg = cause.Error() } - if err := store.GetStore().SetTaskStatus(taskID, errMsg, "failed"); err != nil { + if err := SetTaskStatus(taskID, errMsg, "failed"); err != nil { logger.Error(errors.Wrap(err, "failed to set error on update-download task status")) } } @@ -31,7 +65,7 @@ func StartUpdateTaskMonitor(taskID string, finishedChan <-chan error) { for { select { case <-time.After(time.Second): - if err := store.GetStore().UpdateTaskStatusTimestamp(taskID); err != nil { + if err := UpdateTaskStatusTimestamp(taskID); err != nil { logger.Error(err) } case err := <-finishedChan: @@ -41,3 +75,296 @@ func StartUpdateTaskMonitor(taskID string, finishedChan <-chan error) { } }() } + +func SetTaskStatus(id string, message string, status string) error { + taskStatusLock.Lock() + defer taskStatusLock.Unlock() + + cached := cachedTaskStatus[id] + if cached == nil { + cached = &CachedTaskStatus{} + cachedTaskStatus[id] = cached + } + cached.taskStatus.Message = message + cached.taskStatus.Status = status + cached.taskStatus.UpdatedAt = time.Now() + cached.expirationTime = time.Now().Add(taskCacheTTL) + + configmap, err := getConfigmap() + if err != nil { + if canIgnoreEtcdError(err) { + return nil + } + return errors.Wrap(err, "failed to get task status configmap") + } + + if configmap.Data == nil { + configmap.Data = map[string]string{} + } + + b, err := json.Marshal(cached.taskStatus) + if err != nil { + return errors.Wrap(err, "failed to marshal task status") + } + + configmap.Data[id] = string(b) + + if err := updateConfigmap(configmap); err != nil { + if canIgnoreEtcdError(err) { + return nil + } + return errors.Wrap(err, "failed to update task status configmap") + } + + return nil +} + +func UpdateTaskStatusTimestamp(id string) error { + taskStatusLock.Lock() + defer taskStatusLock.Unlock() + + cached := cachedTaskStatus[id] + if cached != nil { + cached.taskStatus.UpdatedAt = time.Now() + cached.expirationTime = time.Now().Add(taskCacheTTL) + } + + configmap, err := getConfigmap() + if err != nil { + if canIgnoreEtcdError(err) && cached != nil { + return nil + } + return errors.Wrap(err, "failed to get task status configmap") + } + + if configmap.Data == nil { + configmap.Data = map[string]string{} + } + + data, ok := configmap.Data[id] + if !ok { + return nil // copied from s3pgstore + } + + ts := TaskStatus{} + if err := json.Unmarshal([]byte(data), &ts); err != nil { + return errors.Wrap(err, "failed to unmarshal task status") + } + + ts.UpdatedAt = time.Now() + + b, err := json.Marshal(ts) + if err != nil { + return errors.Wrap(err, "failed to marshal task status") + } + + configmap.Data[id] = string(b) + + if err := updateConfigmap(configmap); err != nil { + if canIgnoreEtcdError(err) && cached != nil { + return nil + } + return errors.Wrap(err, "failed to update task status configmap") + } + + return nil +} + +func ClearTaskStatus(id string) error { + taskStatusLock.Lock() + defer taskStatusLock.Unlock() + + defer delete(cachedTaskStatus, id) + + configmap, err := getConfigmap() + if err != nil { + return errors.Wrap(err, "failed to get task status configmap") + } + + if configmap.Data == nil { + configmap.Data = map[string]string{} + } + + _, ok := configmap.Data[id] + if !ok { + return nil // copied from s3pgstore + } + + delete(configmap.Data, id) + + if err := updateConfigmap(configmap); err != nil { + return errors.Wrap(err, "failed to update task status configmap") + } + + return nil +} + +func GetTaskStatus(id string) (string, string, error) { + taskStatusLock.Lock() + defer taskStatusLock.Unlock() + + cached := cachedTaskStatus[id] + if cached != nil && time.Now().Before(cached.expirationTime) { + return cached.taskStatus.Status, cached.taskStatus.Message, nil + } + + if cached == nil { + cached = &CachedTaskStatus{ + expirationTime: time.Now().Add(taskCacheTTL), + } + cachedTaskStatus[id] = cached + } + + configmap, err := getConfigmap() + if err != nil { + if canIgnoreEtcdError(err) && cached != nil { + return cached.taskStatus.Status, cached.taskStatus.Message, nil + } + return "", "", errors.Wrap(err, "failed to get task status configmap") + } + + if configmap.Data == nil { + return "", "", nil + } + + marshalled, ok := configmap.Data[id] + if !ok { + return "", "", nil + } + + ts := TaskStatus{} + if err := json.Unmarshal([]byte(marshalled), &ts); err != nil { + return "", "", errors.Wrap(err, "error unmarshalling task status") + } + + if ts.UpdatedAt.Before(time.Now().Add(-10 * time.Second)) { + return "", "", nil + } + + cached.taskStatus = ts + + return ts.Status, ts.Message, nil +} + +func getConfigmap() (*corev1.ConfigMap, error) { + clientset, err := k8sutil.GetClientset() + if err != nil { + return nil, errors.Wrap(err, "failed to get clientset") + } + + existingConfigmap, err := clientset.CoreV1().ConfigMaps(util.PodNamespace).Get(context.TODO(), TaskStatusConfigMapName, metav1.GetOptions{}) + if err != nil && !kuberneteserrors.IsNotFound(err) { + return nil, errors.Wrap(err, "failed to get configmap") + } else if kuberneteserrors.IsNotFound(err) { + configmap := corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "ConfigMap", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: TaskStatusConfigMapName, + Namespace: util.PodNamespace, + Labels: kotsadmtypes.GetKotsadmLabels(), + }, + Data: map[string]string{}, + } + + createdConfigmap, err := clientset.CoreV1().ConfigMaps(util.PodNamespace).Create(context.TODO(), &configmap, metav1.CreateOptions{}) + if err != nil { + return nil, errors.Wrap(err, "failed to create configmap") + } + + return createdConfigmap, nil + } + + return existingConfigmap, nil +} + +func updateConfigmap(configmap *corev1.ConfigMap) error { + clientset, err := k8sutil.GetClientset() + if err != nil { + return errors.Wrap(err, "failed to get clientset") + } + + _, err = clientset.CoreV1().ConfigMaps(util.PodNamespace).Update(context.Background(), configmap, metav1.UpdateOptions{}) + if err != nil { + return errors.Wrap(err, "failed to update config map") + } + + return nil +} + +func canIgnoreEtcdError(err error) bool { + if err == nil { + return true + } + + if strings.Contains(err.Error(), "connection refused") { + return true + } + + if strings.Contains(err.Error(), "request timed out") { + return true + } + + if strings.Contains(err.Error(), "EOF") { + return true + } + + return false +} + +func MigrateTasksFromRqlite() error { + db := persistence.MustGetDBSession() + + query := `select updated_at, current_message, status from api_task_status` + rows, err := db.QueryOne(query) + if err != nil { + return fmt.Errorf("failed to select tasks for migration: %v: %v", err, rows.Err) + } + + taskCm, err := getConfigmap() + if err != nil { + return errors.Wrap(err, "failed to get task status configmap") + } + + if taskCm.Data == nil { + taskCm.Data = map[string]string{} + } + + for rows.Next() { + var id string + var status gorqlite.NullString + var message gorqlite.NullString + + ts := TaskStatus{} + if err := rows.Scan(&id, &ts.UpdatedAt, &message, &status); err != nil { + return errors.Wrap(err, "failed to scan task status") + } + + if status.Valid { + ts.Status = status.String + } + if message.Valid { + ts.Message = message.String + } + + b, err := json.Marshal(ts) + if err != nil { + return errors.Wrap(err, "failed to marshal task status") + } + + taskCm.Data[id] = string(b) + } + + if err := updateConfigmap(taskCm); err != nil { + return errors.Wrap(err, "failed to update task status configmap") + } + + query = `delete from api_task_status` + if wr, err := db.WriteOne(query); err != nil { + return fmt.Errorf("failed to delete tasks from db: %v: %v", err, wr.Err) + } + + return nil +} diff --git a/pkg/tests/renderdir/renderdir_test.go b/pkg/tests/renderdir/renderdir_test.go index 9e95b4a37f..3eb9d5ccb1 100644 --- a/pkg/tests/renderdir/renderdir_test.go +++ b/pkg/tests/renderdir/renderdir_test.go @@ -13,8 +13,6 @@ import ( cp "github.com/otiai10/copy" "github.com/replicatedhq/kots/pkg/render" rendertypes "github.com/replicatedhq/kots/pkg/render/types" - "github.com/replicatedhq/kots/pkg/store" - mock_store "github.com/replicatedhq/kots/pkg/store/mock" "github.com/replicatedhq/kots/pkg/util" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" "github.com/stretchr/testify/assert" @@ -36,12 +34,6 @@ func TestKotsRenderDir(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - mockStore := mock_store.NewMockStore(ctrl) - store.SetStore(mockStore) - defer store.SetStore(nil) - - mockStore.EXPECT().UpdateAppVersionInstallationSpec(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() - t.Setenv("USE_MOCK_REPORTING", "1") defer os.Unsetenv("USE_MOCK_REPORTING") diff --git a/pkg/updatechecker/types/types.go b/pkg/updatechecker/types/types.go new file mode 100644 index 0000000000..c260b07d00 --- /dev/null +++ b/pkg/updatechecker/types/types.go @@ -0,0 +1,37 @@ +package types + +import "time" + +type CheckForUpdatesOpts struct { + AppID string + DeployLatest bool + DeployVersionLabel string + IsAutomatic bool + SkipPreflights bool + SkipCompatibilityCheck bool + IsCLI bool + Wait bool +} + +type UpdateCheckResponse struct { + AvailableUpdates int64 + CurrentRelease *UpdateCheckRelease + AvailableReleases []UpdateCheckRelease + DeployingRelease *UpdateCheckRelease +} + +type UpdateCheckRelease struct { + Sequence int64 + Version string +} + +type AvailableUpdate struct { + VersionLabel string `json:"versionLabel"` + UpdateCursor string `json:"updateCursor"` + ChannelID string `json:"channelId"` + IsRequired bool `json:"isRequired"` + UpstreamReleasedAt *time.Time `json:"upstreamReleasedAt,omitempty"` + ReleaseNotes string `json:"releaseNotes,omitempty"` + IsDeployable bool `json:"isDeployable,omitempty"` + NonDeployableCause string `json:"nonDeployableCause,omitempty"` +} diff --git a/pkg/updatechecker/updatechecker.go b/pkg/updatechecker/updatechecker.go index 287eca9ebb..aa916b697c 100644 --- a/pkg/updatechecker/updatechecker.go +++ b/pkg/updatechecker/updatechecker.go @@ -3,6 +3,7 @@ package updatechecker import ( "encoding/json" "fmt" + "strings" "sync" "time" @@ -15,16 +16,19 @@ import ( upstream "github.com/replicatedhq/kots/pkg/kotsadmupstream" "github.com/replicatedhq/kots/pkg/logger" "github.com/replicatedhq/kots/pkg/preflight" - "github.com/replicatedhq/kots/pkg/preflight/types" + preflighttypes "github.com/replicatedhq/kots/pkg/preflight/types" kotspull "github.com/replicatedhq/kots/pkg/pull" "github.com/replicatedhq/kots/pkg/reporting" kotssemver "github.com/replicatedhq/kots/pkg/semver" storepkg "github.com/replicatedhq/kots/pkg/store" storetypes "github.com/replicatedhq/kots/pkg/store/types" "github.com/replicatedhq/kots/pkg/tasks" + "github.com/replicatedhq/kots/pkg/updatechecker/types" + upstreampkg "github.com/replicatedhq/kots/pkg/upstream" upstreamtypes "github.com/replicatedhq/kots/pkg/upstream/types" "github.com/replicatedhq/kots/pkg/util" "github.com/replicatedhq/kots/pkg/version" + kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" cron "github.com/robfig/cron/v3" "go.uber.org/zap" "k8s.io/apimachinery/pkg/util/wait" @@ -112,7 +116,7 @@ func Configure(a *apptypes.App, updateCheckerSpec string) error { _, err := job.AddFunc(cronSpec, func() { logger.Debug("checking updates for app", zap.String("slug", jobAppSlug)) - opts := CheckForUpdatesOpts{ + opts := types.CheckForUpdatesOpts{ AppID: jobAppID, IsAutomatic: true, } @@ -154,36 +158,13 @@ func Stop(appID string) { } } -type CheckForUpdatesOpts struct { - AppID string - DeployLatest bool - DeployVersionLabel string - IsAutomatic bool - SkipPreflights bool - SkipCompatibilityCheck bool - IsCLI bool - Wait bool -} - -type UpdateCheckResponse struct { - AvailableUpdates int64 - CurrentRelease *UpdateCheckRelease - AvailableReleases []UpdateCheckRelease - DeployingRelease *UpdateCheckRelease -} - -type UpdateCheckRelease struct { - Sequence int64 - Version string -} - // CheckForUpdates checks, downloads, and makes sure the desired version for a specific app is deployed. // if "DeployLatest" is set to true, the latest version will be deployed. // otherwise, if "DeployVersionLabel" is set to true, then the version with the corresponding version label will be deployed (if found). // otherwise, if "IsAutomatic" is set to true (which means it's an automatic update check), then the version that matches the auto deploy configuration (if enabled) will be deployed. // returns the number of available updates. -func CheckForUpdates(opts CheckForUpdatesOpts) (ucr *UpdateCheckResponse, finalError error) { - currentStatus, _, err := store.GetTaskStatus("update-download") +func CheckForUpdates(opts types.CheckForUpdatesOpts) (ucr *types.UpdateCheckResponse, finalError error) { + currentStatus, _, err := tasks.GetTaskStatus("update-download") if err != nil { return nil, errors.Wrap(err, "failed to get task status") } @@ -192,7 +173,7 @@ func CheckForUpdates(opts CheckForUpdatesOpts) (ucr *UpdateCheckResponse, finalE return nil, nil } - if err := store.SetTaskStatus("update-download", "Checking for updates...", "running"); err != nil { + if err := tasks.SetTaskStatus("update-download", "Checking for updates...", "running"); err != nil { return nil, errors.Wrap(err, "failed to set task status") } @@ -216,7 +197,7 @@ func CheckForUpdates(opts CheckForUpdatesOpts) (ucr *UpdateCheckResponse, finalE return } -func checkForKotsAppUpdates(opts CheckForUpdatesOpts, finishedChan chan<- error) (*UpdateCheckResponse, error) { +func checkForKotsAppUpdates(opts types.CheckForUpdatesOpts, finishedChan chan<- error) (*types.UpdateCheckResponse, error) { a, err := store.GetApp(opts.AppID) if err != nil { return nil, errors.Wrap(err, "failed to get app") @@ -276,24 +257,24 @@ func checkForKotsAppUpdates(opts CheckForUpdatesOpts, finishedChan chan<- error) filteredUpdates := removeOldUpdates(updates.Updates, appVersions, latestLicense.Spec.IsSemverRequired) - var availableReleases []UpdateCheckRelease + var availableReleases []types.UpdateCheckRelease availableSequence := appVersions.AllVersions[0].Sequence + 1 for _, u := range filteredUpdates { - availableReleases = append(availableReleases, UpdateCheckRelease{ + availableReleases = append(availableReleases, types.UpdateCheckRelease{ Sequence: availableSequence, Version: u.VersionLabel, }) availableSequence++ } - ucr := UpdateCheckResponse{ + ucr := types.UpdateCheckResponse{ AvailableUpdates: int64(len(filteredUpdates)), AvailableReleases: availableReleases, DeployingRelease: getVersionToDeploy(opts, d.ClusterID, availableReleases), } if appVersions.CurrentVersion != nil { - ucr.CurrentRelease = &UpdateCheckRelease{ + ucr.CurrentRelease = &types.UpdateCheckRelease{ Sequence: appVersions.CurrentVersion.Sequence, Version: appVersions.CurrentVersion.VersionLabel, } @@ -311,7 +292,7 @@ func checkForKotsAppUpdates(opts CheckForUpdatesOpts, finishedChan chan<- error) // this is to avoid a race condition where the UI polls the task status before it is set by the goroutine status := fmt.Sprintf("%d Updates available...", ucr.AvailableUpdates) - if err := store.SetTaskStatus("update-download", status, "running"); err != nil { + if err := tasks.SetTaskStatus("update-download", status, "running"); err != nil { return nil, errors.Wrap(err, "failed to set task status") } @@ -333,7 +314,7 @@ func checkForKotsAppUpdates(opts CheckForUpdatesOpts, finishedChan chan<- error) return &ucr, nil } -func downloadAppUpdates(opts CheckForUpdatesOpts, appID string, clusterID string, updates []upstreamtypes.Update, updateCheckTime time.Time) error { +func downloadAppUpdates(opts types.CheckForUpdatesOpts, appID string, clusterID string, updates []upstreamtypes.Update, updateCheckTime time.Time) error { for index, update := range updates { appSequence, err := upstream.DownloadUpdate(appID, update, opts.SkipPreflights, opts.SkipCompatibilityCheck) if appSequence != nil { @@ -361,7 +342,7 @@ func downloadAppUpdates(opts CheckForUpdatesOpts, appID string, clusterID string return nil } -func ensureDesiredVersionIsDeployed(opts CheckForUpdatesOpts, clusterID string) error { +func ensureDesiredVersionIsDeployed(opts types.CheckForUpdatesOpts, clusterID string) error { if opts.DeployLatest { if err := deployLatestVersion(opts, clusterID); err != nil { return errors.Wrap(err, "failed to deploy latest version") @@ -390,7 +371,7 @@ func ensureDesiredVersionIsDeployed(opts CheckForUpdatesOpts, clusterID string) return nil } -func getVersionToDeploy(opts CheckForUpdatesOpts, clusterID string, availableReleases []UpdateCheckRelease) *UpdateCheckRelease { +func getVersionToDeploy(opts types.CheckForUpdatesOpts, clusterID string, availableReleases []types.UpdateCheckRelease) *types.UpdateCheckRelease { appVersions, err := store.GetDownstreamVersions(opts.AppID, clusterID, true) if err != nil { return nil @@ -405,7 +386,7 @@ func getVersionToDeploy(opts CheckForUpdatesOpts, clusterID string, availableRel } if opts.DeployLatest && appVersions.AllVersions[0].Sequence != appVersions.CurrentVersion.Sequence { - return &UpdateCheckRelease{ + return &types.UpdateCheckRelease{ Sequence: appVersions.AllVersions[0].Sequence, Version: appVersions.AllVersions[0].VersionLabel, } @@ -421,7 +402,7 @@ func getVersionToDeploy(opts CheckForUpdatesOpts, clusterID string, availableRel } if versionToDeploy != nil && versionToDeploy.Sequence != appVersions.CurrentVersion.Sequence { - return &UpdateCheckRelease{ + return &types.UpdateCheckRelease{ Sequence: versionToDeploy.Sequence, Version: versionToDeploy.VersionLabel, } @@ -433,7 +414,7 @@ func getVersionToDeploy(opts CheckForUpdatesOpts, clusterID string, availableRel return nil } -func deployLatestVersion(opts CheckForUpdatesOpts, clusterID string) error { +func deployLatestVersion(opts types.CheckForUpdatesOpts, clusterID string) error { appVersions, err := store.GetDownstreamVersions(opts.AppID, clusterID, true) if err != nil { return errors.Wrapf(err, "failed to get app versions for app %s", opts.AppID) @@ -450,7 +431,7 @@ func deployLatestVersion(opts CheckForUpdatesOpts, clusterID string) error { return nil } -func deployVersionLabel(opts CheckForUpdatesOpts, clusterID string, versionLabel string) error { +func deployVersionLabel(opts types.CheckForUpdatesOpts, clusterID string, versionLabel string) error { appVersions, err := store.GetDownstreamVersions(opts.AppID, clusterID, true) if err != nil { return errors.Wrapf(err, "failed to get app versions for app %s", opts.AppID) @@ -479,7 +460,7 @@ func deployVersionLabel(opts CheckForUpdatesOpts, clusterID string, versionLabel return nil } -func autoDeploy(opts CheckForUpdatesOpts, clusterID string, autoDeploy apptypes.AutoDeploy) error { +func autoDeploy(opts types.CheckForUpdatesOpts, clusterID string, autoDeploy apptypes.AutoDeploy) error { if autoDeploy == "" || autoDeploy == apptypes.AutoDeployDisabled { return nil } @@ -589,7 +570,7 @@ func waitForPreflightsToFinish(appID string, sequence int64) error { return errors.New("failed to find a preflight spec") } - var preflightResults *types.PreflightResults + var preflightResults *preflighttypes.PreflightResults if err = json.Unmarshal([]byte(preflightResult.Result), &preflightResults); err != nil { return errors.Wrap(err, "failed to parse preflight results") } @@ -602,7 +583,7 @@ func waitForPreflightsToFinish(appID string, sequence int64) error { return nil } -func deployVersion(opts CheckForUpdatesOpts, clusterID string, appVersions *downstreamtypes.DownstreamVersions, versionToDeploy *downstreamtypes.DownstreamVersion) error { +func deployVersion(opts types.CheckForUpdatesOpts, clusterID string, appVersions *downstreamtypes.DownstreamVersions, versionToDeploy *downstreamtypes.DownstreamVersion) error { if appVersions.CurrentVersion != nil { isPastVersion := false for _, p := range appVersions.PastVersions { @@ -728,3 +709,68 @@ func removeOldUpdates(updates []upstreamtypes.Update, appVersions *downstreamtyp return fileteredUpdates } + +func GetAvailableUpdates(kotsStore storepkg.Store, app *apptypes.App, license *kotsv1beta1.License) ([]types.AvailableUpdate, error) { + updateCursor, err := kotsStore.GetCurrentUpdateCursor(app.ID, license.Spec.ChannelID) + if err != nil { + return nil, errors.Wrap(err, "failed to get current update cursor") + } + + upstreamURI := fmt.Sprintf("replicated://%s", license.Spec.AppSlug) + fetchOptions := &upstreamtypes.FetchOptions{ + License: license, + LastUpdateCheckAt: app.LastUpdateCheckAt, + CurrentCursor: updateCursor, + CurrentChannelID: license.Spec.ChannelID, + CurrentChannelName: license.Spec.ChannelName, + ChannelChanged: app.ChannelChanged, + SortOrder: "desc", // get the latest updates first + ReportingInfo: reporting.GetReportingInfo(app.ID), + } + updates, err := upstreampkg.GetUpdatesUpstream(upstreamURI, fetchOptions) + if err != nil { + return nil, errors.Wrap(err, "failed to get updates") + } + + availableUpdates := []types.AvailableUpdate{} + for _, u := range updates.Updates { + deployable, cause := isUpdateDeployable(u.Cursor, updates.Updates) + availableUpdates = append(availableUpdates, types.AvailableUpdate{ + VersionLabel: u.VersionLabel, + UpdateCursor: u.Cursor, + ChannelID: u.ChannelID, + IsRequired: u.IsRequired, + UpstreamReleasedAt: u.ReleasedAt, + ReleaseNotes: u.ReleaseNotes, + IsDeployable: deployable, + NonDeployableCause: cause, + }) + } + + return availableUpdates, nil +} + +func isUpdateDeployable(updateCursor string, updates []upstreamtypes.Update) (bool, string) { + // iterate over updates in reverse since they are sorted in descending order + requiredUpdates := []upstreamtypes.Update{} + for i := len(updates) - 1; i >= 0; i-- { + if updates[i].Cursor == updateCursor { + break + } + if updates[i].IsRequired { + requiredUpdates = append(requiredUpdates, updates[i]) + } + } + if len(requiredUpdates) > 0 { + versionLabels := []string{} + for _, v := range requiredUpdates { + versionLabels = append([]string{v.VersionLabel}, versionLabels...) + } + versionLabelsStr := strings.Join(versionLabels, ", ") + if len(requiredUpdates) == 1 { + return false, fmt.Sprintf("This version cannot be deployed because version %s is required and must be deployed first.", versionLabelsStr) + } + return false, fmt.Sprintf("This version cannot be deployed because versions %s are required and must be deployed first.", versionLabelsStr) + } + return true, "" +} diff --git a/pkg/updatechecker/updatechecker_test.go b/pkg/updatechecker/updatechecker_test.go index 773424f519..9950abbd6b 100644 --- a/pkg/updatechecker/updatechecker_test.go +++ b/pkg/updatechecker/updatechecker_test.go @@ -1,8 +1,12 @@ package updatechecker import ( + "encoding/json" + "net/http" + "net/http/httptest" "strings" "testing" + "time" "github.com/blang/semver" "github.com/golang/mock/gomock" @@ -11,15 +15,20 @@ import ( apptypes "github.com/replicatedhq/kots/pkg/app/types" "github.com/replicatedhq/kots/pkg/cursor" preflighttypes "github.com/replicatedhq/kots/pkg/preflight/types" + storepkg "github.com/replicatedhq/kots/pkg/store" mock_store "github.com/replicatedhq/kots/pkg/store/mock" storetypes "github.com/replicatedhq/kots/pkg/store/types" + "github.com/replicatedhq/kots/pkg/updatechecker/types" + "github.com/replicatedhq/kots/pkg/upstream" upstreamtypes "github.com/replicatedhq/kots/pkg/upstream/types" + kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestAutoDeployDoesNotExecuteIfDisabled(t *testing.T) { var autoDeployType = apptypes.AutoDeployDisabled - var opts = CheckForUpdatesOpts{} + var opts = types.CheckForUpdatesOpts{} err := autoDeploy(opts, "cluster-id", autoDeployType) if err != nil { @@ -28,7 +37,7 @@ func TestAutoDeployDoesNotExecuteIfDisabled(t *testing.T) { } func TestAutoDeployDoesNotExecuteIfNotSet(t *testing.T) { - var opts = CheckForUpdatesOpts{} + var opts = types.CheckForUpdatesOpts{} var clusterID = "some-cluster-id" err := autoDeploy(opts, clusterID, "") @@ -41,7 +50,7 @@ func TestAutoDeployFailedToGetAppVersionsErrors(t *testing.T) { var autoDeployType = apptypes.AutoDeploySemverPatch var appID = "some-app" var clusterID = "some-cluster-id" - var opts = CheckForUpdatesOpts{AppID: appID} + var opts = types.CheckForUpdatesOpts{AppID: appID} var downstreamVersions = &downstreamtypes.DownstreamVersions{} ctrl := gomock.NewController(t) @@ -64,7 +73,7 @@ func TestAutoDeployAppVersionsIsEmptyErrors(t *testing.T) { var autoDeployType = apptypes.AutoDeploySemverPatch var appID = "some-app" var clusterID = "some-cluster-id" - var opts = CheckForUpdatesOpts{AppID: appID} + var opts = types.CheckForUpdatesOpts{AppID: appID} var downstreamVersions = &downstreamtypes.DownstreamVersions{ AllVersions: []*downstreamtypes.DownstreamVersion{}, } @@ -86,7 +95,7 @@ func TestAutoDeployCurrentVersionIsNilDoesNothing(t *testing.T) { var autoDeployType = apptypes.AutoDeploySemverPatch var appID = "some-app" var clusterID = "some-cluster-id" - var opts = CheckForUpdatesOpts{AppID: appID} + var opts = types.CheckForUpdatesOpts{AppID: appID} var downstreamVersions = &downstreamtypes.DownstreamVersions{ CurrentVersion: nil, AllVersions: []*downstreamtypes.DownstreamVersion{ @@ -111,7 +120,7 @@ func TestAutoDeployCurrentVersionSemverIsNilDoesNothing(t *testing.T) { var autoDeployType = apptypes.AutoDeploySemverPatch var appID = "some-app" var clusterID = "some-cluster-id" - var opts = CheckForUpdatesOpts{AppID: appID} + var opts = types.CheckForUpdatesOpts{AppID: appID} var downstreamVersions = &downstreamtypes.DownstreamVersions{ CurrentVersion: &downstreamtypes.DownstreamVersion{ Semver: nil, @@ -140,7 +149,7 @@ func TestAutoDeploySequenceQuitsIfCurrentVersionSequenceIsGreaterThanOrEqualToMo var clusterID = "some-cluster-id" var currentSequence = int64(1) var upgradeSequence = int64(1) - var opts = CheckForUpdatesOpts{AppID: appID} + var opts = types.CheckForUpdatesOpts{AppID: appID} var downstreamVersions = &downstreamtypes.DownstreamVersions{ CurrentVersion: &downstreamtypes.DownstreamVersion{ Semver: &semver.Version{}, @@ -170,7 +179,7 @@ func TestAutoDeploySequenceDeploysSequenceUpgradeIfCurrentVersionLessThanMostRec var autoDeployType = apptypes.AutoDeploySequence var appID = "some-app" var clusterID = "some-cluster-id" - var opts = CheckForUpdatesOpts{AppID: appID} + var opts = types.CheckForUpdatesOpts{AppID: appID} var currentCursor = cursor.MustParse("1") var upgradeCursor = cursor.MustParse("2") var downstreamVersions = &downstreamtypes.DownstreamVersions{ @@ -204,7 +213,7 @@ func TestAutoDeploySequenceDoesNotDeployIfCurrentVersionIsSameUpstream(t *testin var autoDeployType = apptypes.AutoDeploySequence var appID = "some-app" var clusterID = "some-cluster-id" - var opts = CheckForUpdatesOpts{AppID: appID} + var opts = types.CheckForUpdatesOpts{AppID: appID} var currentCursor = cursor.MustParse("2") var upgradeCursor = cursor.MustParse("2") var downstreamVersions = &downstreamtypes.DownstreamVersions{ @@ -237,7 +246,7 @@ func TestAutoDeploySemverRequiredAllVersionsIndexIsNil(t *testing.T) { var autoDeployType = apptypes.AutoDeploySemverPatch var appID = "some-app" var clusterID = "some-cluster-id" - var opts = CheckForUpdatesOpts{AppID: appID} + var opts = types.CheckForUpdatesOpts{AppID: appID} var downstreamVersions = &downstreamtypes.DownstreamVersions{ CurrentVersion: &downstreamtypes.DownstreamVersion{ Semver: &semver.Version{}, @@ -263,7 +272,7 @@ func TestAutoDeploySemverRequiredAllVersionsHasNilSemver(t *testing.T) { var autoDeployType = apptypes.AutoDeploySemverPatch var appID = "some-app" var clusterID = "some-cluster-id" - var opts = CheckForUpdatesOpts{AppID: appID} + var opts = types.CheckForUpdatesOpts{AppID: appID} var downstreamVersions = &downstreamtypes.DownstreamVersions{ CurrentVersion: &downstreamtypes.DownstreamVersion{ Semver: &semver.Version{}, @@ -296,7 +305,7 @@ func TestAutoDeploySemverRequiredNoNewVersionToDeploy(t *testing.T) { var major = uint64(1) var minor = uint64(2) var patch = uint64(1) - var opts = CheckForUpdatesOpts{AppID: appID} + var opts = types.CheckForUpdatesOpts{AppID: appID} var downstreamVersions = &downstreamtypes.DownstreamVersions{ CurrentVersion: &downstreamtypes.DownstreamVersion{ Semver: &semver.Version{ @@ -336,7 +345,7 @@ func TestAutoDeploySemverRequiredPatchUpdateMajorsDontMatch(t *testing.T) { var clusterID = "some-cluster-id" var currentMajor = uint64(1) var updateMajor = uint64(2) - var opts = CheckForUpdatesOpts{AppID: appID} + var opts = types.CheckForUpdatesOpts{AppID: appID} var downstreamVersions = &downstreamtypes.DownstreamVersions{ CurrentVersion: &downstreamtypes.DownstreamVersion{ Semver: &semver.Version{ @@ -373,7 +382,7 @@ func TestAutoDeploySemverRequiredPatchUpdateMajorsMatchMinorsDontMatch(t *testin var major = uint64(1) var currentMinor = uint64(2) var updateMinor = uint64(2) - var opts = CheckForUpdatesOpts{AppID: appID} + var opts = types.CheckForUpdatesOpts{AppID: appID} var downstreamVersions = &downstreamtypes.DownstreamVersions{ CurrentVersion: &downstreamtypes.DownstreamVersion{ Semver: &semver.Version{ @@ -414,7 +423,7 @@ func TestAutoDeploySemverRequiredPatchUpdateMajorsMatchMinorsMatchWillUpgrade(t var minor = uint64(2) var currentPatch = uint64(1) var upgradePatch = uint64(2) - var opts = CheckForUpdatesOpts{AppID: appID} + var opts = types.CheckForUpdatesOpts{AppID: appID} var downstreamVersions = &downstreamtypes.DownstreamVersions{ CurrentVersion: &downstreamtypes.DownstreamVersion{ Semver: &semver.Version{ @@ -456,7 +465,7 @@ func TestAutoDeploySemverRequiredMinorUpdateMajorsDontMatch(t *testing.T) { var sequence = int64(0) var currentMajor = uint64(1) var upgradeMajor = uint64(2) - var opts = CheckForUpdatesOpts{AppID: appID} + var opts = types.CheckForUpdatesOpts{AppID: appID} var downstreamVersions = &downstreamtypes.DownstreamVersions{ CurrentVersion: &downstreamtypes.DownstreamVersion{ Semver: &semver.Version{ @@ -495,7 +504,7 @@ func TestAutoDeploySemverRequiredMinorUpdateMajorsMatchWillUpgrade(t *testing.T) var major = uint64(1) var currentMinor = uint64(1) var upgradeMinor = uint64(2) - var opts = CheckForUpdatesOpts{AppID: appID} + var opts = types.CheckForUpdatesOpts{AppID: appID} var downstreamVersions = &downstreamtypes.DownstreamVersions{ CurrentVersion: &downstreamtypes.DownstreamVersion{ Semver: &semver.Version{ @@ -535,7 +544,7 @@ func TestAutoDeploySemverRequiredMajorUpdateWillUpgrade(t *testing.T) { var sequence = int64(0) var currentMajor = uint64(1) var upgradeMajor = uint64(2) - var opts = CheckForUpdatesOpts{AppID: appID} + var opts = types.CheckForUpdatesOpts{AppID: appID} var downstreamVersions = &downstreamtypes.DownstreamVersions{ CurrentVersion: &downstreamtypes.DownstreamVersion{ Semver: &semver.Version{ @@ -918,3 +927,244 @@ func Test_removeOldUpdates(t *testing.T) { req.Equal(test.want, got) } } + +func TestGetAvailableUpdates(t *testing.T) { + ctrl := gomock.NewController(t) + mockStore := mock_store.NewMockStore(ctrl) + + testTime := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC) + + type args struct { + kotsStore storepkg.Store + app *apptypes.App + license *kotsv1beta1.License + } + tests := []struct { + name string + args args + channelReleases []upstream.ChannelRelease + setup func(t *testing.T, args args, mockServerEndpoint string) + want []types.AvailableUpdate + wantErr bool + }{ + { + name: "no updates", + args: args{ + kotsStore: mockStore, + app: &apptypes.App{ + ID: "app-id", + }, + license: &kotsv1beta1.License{ + Spec: kotsv1beta1.LicenseSpec{ + ChannelID: "channel-id", + ChannelName: "channel-name", + AppSlug: "app-slug", + LicenseID: "license-id", + }, + }, + }, + channelReleases: []upstream.ChannelRelease{}, + setup: func(t *testing.T, args args, licenseEndpoint string) { + t.Setenv("USE_MOCK_REPORTING", "1") + args.license.Spec.Endpoint = licenseEndpoint + mockStore.EXPECT().GetCurrentUpdateCursor(args.app.ID, args.license.Spec.ChannelID).Return("1", nil) + }, + want: []types.AvailableUpdate{}, + wantErr: false, + }, + { + name: "has updates", + args: args{ + kotsStore: mockStore, + app: &apptypes.App{ + ID: "app-id", + }, + license: &kotsv1beta1.License{ + Spec: kotsv1beta1.LicenseSpec{ + ChannelID: "channel-id", + ChannelName: "channel-name", + AppSlug: "app-slug", + LicenseID: "license-id", + }, + }, + }, + channelReleases: []upstream.ChannelRelease{ + { + ChannelSequence: 2, + ReleaseSequence: 2, + VersionLabel: "0.0.2", + IsRequired: false, + CreatedAt: testTime.Format(time.RFC3339), + ReleaseNotes: "release notes", + }, + { + ChannelSequence: 1, + ReleaseSequence: 1, + VersionLabel: "0.0.1", + IsRequired: true, + CreatedAt: testTime.Format(time.RFC3339), + ReleaseNotes: "release notes", + }, + }, + setup: func(t *testing.T, args args, licenseEndpoint string) { + t.Setenv("USE_MOCK_REPORTING", "1") + args.license.Spec.Endpoint = licenseEndpoint + mockStore.EXPECT().GetCurrentUpdateCursor(args.app.ID, args.license.Spec.ChannelID).Return("1", nil) + }, + want: []types.AvailableUpdate{ + { + VersionLabel: "0.0.2", + UpdateCursor: "2", + ChannelID: "channel-id", + IsRequired: false, + UpstreamReleasedAt: &testTime, + ReleaseNotes: "release notes", + IsDeployable: false, + NonDeployableCause: "This version cannot be deployed because version 0.0.1 is required and must be deployed first.", + }, + { + VersionLabel: "0.0.1", + UpdateCursor: "1", + ChannelID: "channel-id", + IsRequired: true, + UpstreamReleasedAt: &testTime, + ReleaseNotes: "release notes", + IsDeployable: true, + }, + }, + wantErr: false, + }, + { + name: "fails to fetch updates", + args: args{ + kotsStore: mockStore, + app: &apptypes.App{ + ID: "app-id", + }, + license: &kotsv1beta1.License{ + Spec: kotsv1beta1.LicenseSpec{ + ChannelID: "channel-id", + ChannelName: "channel-name", + AppSlug: "app-slug", + LicenseID: "license-id", + }, + }, + }, + channelReleases: []upstream.ChannelRelease{}, + setup: func(t *testing.T, args args, licenseEndpoint string) { + t.Setenv("USE_MOCK_REPORTING", "1") + args.license.Spec.Endpoint = licenseEndpoint + mockStore.EXPECT().GetCurrentUpdateCursor(args.app.ID, args.license.Spec.ChannelID).Return("1", nil) + }, + want: []types.AvailableUpdate{}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := require.New(t) + mockServer := newMockServerWithReleases(tt.channelReleases, tt.wantErr) + defer mockServer.Close() + tt.setup(t, tt.args, mockServer.URL) + got, err := GetAvailableUpdates(tt.args.kotsStore, tt.args.app, tt.args.license) + if tt.wantErr { + req.Error(err) + return + } + req.NoError(err) + req.Equal(tt.want, got) + }) + } +} + +func newMockServerWithReleases(channelReleases []upstream.ChannelRelease, wantErr bool) *httptest.Server { + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if wantErr { + http.Error(w, "error", http.StatusInternalServerError) + return + } + var response struct { + ChannelReleases []upstream.ChannelRelease `json:"channelReleases"` + } + response.ChannelReleases = channelReleases + w.Header().Set("X-Replicated-UpdateCheckAt", time.Now().Format(time.RFC3339)) + if err := json.NewEncoder(w).Encode(response); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + })) +} +func TestIsUpdateDeployable(t *testing.T) { + tests := []struct { + name string + updateCursor string + updates []upstreamtypes.Update + want bool + wantCause string + }{ + { + name: "one update", + updateCursor: "3", + updates: []upstreamtypes.Update{ + {VersionLabel: "1.0.3", Cursor: "3", IsRequired: false}, + }, + want: true, + wantCause: "", + }, + { + name: "no required updates", + updateCursor: "3", + updates: []upstreamtypes.Update{ + {VersionLabel: "1.0.4", Cursor: "4", IsRequired: false}, + {VersionLabel: "1.0.3", Cursor: "3", IsRequired: false}, + {VersionLabel: "1.0.2", Cursor: "2", IsRequired: false}, + {VersionLabel: "1.0.1", Cursor: "1", IsRequired: false}, + }, + want: true, + wantCause: "", + }, + { + name: "no required releases before it", + updateCursor: "3", + updates: []upstreamtypes.Update{ + {VersionLabel: "1.0.4", Cursor: "4", IsRequired: true}, + {VersionLabel: "1.0.3", Cursor: "3", IsRequired: false}, + {VersionLabel: "1.0.2", Cursor: "2", IsRequired: false}, + {VersionLabel: "1.0.1", Cursor: "1", IsRequired: false}, + }, + want: true, + wantCause: "", + }, + { + name: "one required release before it", + updateCursor: "3", + updates: []upstreamtypes.Update{ + {VersionLabel: "1.0.4", Cursor: "4", IsRequired: false}, + {VersionLabel: "1.0.3", Cursor: "3", IsRequired: false}, + {VersionLabel: "1.0.2", Cursor: "2", IsRequired: true}, + {VersionLabel: "1.0.1", Cursor: "1", IsRequired: false}, + }, + want: false, + wantCause: "This version cannot be deployed because version 1.0.2 is required and must be deployed first.", + }, + { + name: "two required releases before it", + updateCursor: "3", + updates: []upstreamtypes.Update{ + {VersionLabel: "1.0.4", Cursor: "4", IsRequired: false}, + {VersionLabel: "1.0.3", Cursor: "3", IsRequired: false}, + {VersionLabel: "1.0.2", Cursor: "2", IsRequired: true}, + {VersionLabel: "1.0.1", Cursor: "1", IsRequired: true}, + }, + want: false, + wantCause: "This version cannot be deployed because versions 1.0.2, 1.0.1 are required and must be deployed first.", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, msg := isUpdateDeployable(tt.updateCursor, tt.updates) + assert.Equal(t, tt.want, result) + assert.Equal(t, tt.wantCause, msg) + }) + } +} diff --git a/pkg/upgradeservice/bootstrap.go b/pkg/upgradeservice/bootstrap.go new file mode 100644 index 0000000000..7d57e1402b --- /dev/null +++ b/pkg/upgradeservice/bootstrap.go @@ -0,0 +1,109 @@ +package upgradeservice + +import ( + "bufio" + "fmt" + "io" + "os" + "path/filepath" + + "github.com/pkg/errors" + identity "github.com/replicatedhq/kots/pkg/kotsadmidentity" + "github.com/replicatedhq/kots/pkg/kotsutil" + "github.com/replicatedhq/kots/pkg/logger" + "github.com/replicatedhq/kots/pkg/pull" + registrytypes "github.com/replicatedhq/kots/pkg/registry/types" + "github.com/replicatedhq/kots/pkg/tasks" + "github.com/replicatedhq/kots/pkg/upgradeservice/types" + "github.com/replicatedhq/kots/pkg/util" +) + +func bootstrap(params types.UpgradeServiceParams) error { + // TODO NOW: airgap mode + + if err := pullArchiveFromOnline(params); err != nil { + return errors.Wrap(err, "failed to pull archive from online") + } + + return nil +} + +func pullArchiveFromOnline(params types.UpgradeServiceParams) (finalError error) { + license, err := kotsutil.LoadLicenseFromBytes([]byte(params.AppLicense)) + if err != nil { + return errors.Wrap(err, "failed to load license from bytes") + } + + beforeKotsKinds, err := kotsutil.LoadKotsKinds(params.BaseArchive) + if err != nil { + return errors.Wrap(err, "failed to load current kotskinds") + } + + if err := pull.CleanBaseArchive(params.BaseArchive); err != nil { + return errors.Wrap(err, "failed to clean base archive") + } + + registrySettings := registrytypes.RegistrySettings{ + Hostname: params.RegistryEndpoint, + Username: params.RegistryUsername, + Password: params.RegistryPassword, + Namespace: params.RegistryNamespace, + IsReadOnly: params.RegistryIsReadOnly, + } + + identityConfigFile := filepath.Join(params.BaseArchive, "upstream", "userdata", "identityconfig.yaml") + if _, err := os.Stat(identityConfigFile); os.IsNotExist(err) { + file, err := identity.InitAppIdentityConfig(params.AppSlug) + if err != nil { + return errors.Wrap(err, "failed to init identity config") + } + identityConfigFile = file + defer os.Remove(identityConfigFile) + } else if err != nil { + return errors.Wrap(err, "failed to get stat identity config file") + } + + pipeReader, pipeWriter := io.Pipe() + defer func() { + pipeWriter.CloseWithError(finalError) + }() + go func() { + scanner := bufio.NewScanner(pipeReader) + for scanner.Scan() { + if err := tasks.SetTaskStatus("update-download", scanner.Text(), "running"); err != nil { + logger.Error(err) + } + } + pipeReader.CloseWithError(scanner.Err()) + }() + + pullOptions := pull.PullOptions{ + LicenseObj: license, + Namespace: util.AppNamespace(), + ConfigFile: filepath.Join(params.BaseArchive, "upstream", "userdata", "config.yaml"), + IdentityConfigFile: identityConfigFile, + InstallationFile: filepath.Join(params.BaseArchive, "upstream", "userdata", "installation.yaml"), + UpdateCursor: params.UpdateCursor, + RootDir: params.BaseArchive, + Downstreams: []string{"this-cluster"}, + ExcludeKotsKinds: true, + ExcludeAdminConsole: true, + CreateAppDir: false, + ReportWriter: pipeWriter, + AppID: params.AppID, + AppSlug: params.AppSlug, + AppSequence: params.NextSequence, + IsGitOps: params.AppIsGitOps, + ReportingInfo: params.ReportingInfo, + RewriteImages: registrySettings.IsValid(), + RewriteImageOptions: registrySettings, + KotsKinds: beforeKotsKinds, + } + + _, err = pull.Pull(fmt.Sprintf("replicated://%s", license.Spec.AppSlug), pullOptions) + if err != nil { + return errors.Wrap(err, "failed to pull") + } + + return nil +} diff --git a/pkg/upgradeservice/handlers/config.go b/pkg/upgradeservice/handlers/config.go new file mode 100644 index 0000000000..da901e4c79 --- /dev/null +++ b/pkg/upgradeservice/handlers/config.go @@ -0,0 +1,486 @@ +package handlers + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "net/http" + "os" + "path/filepath" + "strconv" + "strings" + + "github.com/gorilla/mux" + "github.com/pkg/errors" + downstreamtypes "github.com/replicatedhq/kots/pkg/api/downstream/types" + apptypes "github.com/replicatedhq/kots/pkg/app/types" + "github.com/replicatedhq/kots/pkg/config" + kotsconfig "github.com/replicatedhq/kots/pkg/config" + "github.com/replicatedhq/kots/pkg/kotsadmconfig" + configtypes "github.com/replicatedhq/kots/pkg/kotsadmconfig/types" + configvalidation "github.com/replicatedhq/kots/pkg/kotsadmconfig/validation" + "github.com/replicatedhq/kots/pkg/kotsutil" + "github.com/replicatedhq/kots/pkg/logger" + registrytypes "github.com/replicatedhq/kots/pkg/registry/types" + "github.com/replicatedhq/kots/pkg/render" + rendertypes "github.com/replicatedhq/kots/pkg/render/types" + "github.com/replicatedhq/kots/pkg/reporting" + "github.com/replicatedhq/kots/pkg/template" + upgradepreflight "github.com/replicatedhq/kots/pkg/upgradeservice/preflight" + "github.com/replicatedhq/kots/pkg/util" + kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" + "github.com/replicatedhq/kotskinds/multitype" +) + +type CurrentAppConfigResponse struct { + Success bool `json:"success"` + Error string `json:"error,omitempty"` + ConfigGroups []kotsv1beta1.ConfigGroup `json:"configGroups"` + ValidationErrors []configtypes.ConfigGroupValidationError `json:"validationErrors,omitempty"` +} + +type LiveAppConfigRequest struct { + ConfigGroups []kotsv1beta1.ConfigGroup `json:"configGroups"` +} + +type LiveAppConfigResponse struct { + Success bool `json:"success"` + Error string `json:"error,omitempty"` + ConfigGroups []kotsv1beta1.ConfigGroup `json:"configGroups"` + ValidationErrors []configtypes.ConfigGroupValidationError `json:"validationErrors,omitempty"` +} + +type SaveAppConfigRequest struct { + ConfigGroups []kotsv1beta1.ConfigGroup `json:"configGroups"` +} + +type SaveAppConfigResponse struct { + Success bool `json:"success"` + Error string `json:"error,omitempty"` + RequiredItems []string `json:"requiredItems,omitempty"` + ValidationErrors []configtypes.ConfigGroupValidationError `json:"validationErrors,omitempty"` +} + +type DownloadFileFromConfigResponse struct { + Success bool `json:"success"` + Error string `json:"error,omitempty"` +} + +func (h *Handler) CurrentAppConfig(w http.ResponseWriter, r *http.Request) { + currentAppConfigResponse := CurrentAppConfigResponse{ + Success: false, + } + + params := GetContextParams(r) + appSlug := mux.Vars(r)["appSlug"] + + if params.AppSlug != appSlug { + currentAppConfigResponse.Error = "app slug does not match" + JSON(w, http.StatusForbidden, currentAppConfigResponse) + return + } + + appLicense, err := kotsutil.LoadLicenseFromBytes([]byte(params.AppLicense)) + if err != nil { + currentAppConfigResponse.Error = "failed to load license from bytes" + logger.Error(errors.Wrap(err, currentAppConfigResponse.Error)) + JSON(w, http.StatusInternalServerError, currentAppConfigResponse) + return + } + + kotsKinds, err := kotsutil.LoadKotsKinds(params.BaseArchive) // TODO NOW: rename BaseArchive + if err != nil { + currentAppConfigResponse.Error = "failed to load kots kinds from path" + logger.Error(errors.Wrap(err, currentAppConfigResponse.Error)) + JSON(w, http.StatusInternalServerError, currentAppConfigResponse) + return + } + + // get the non-rendered config from the upstream directory because we have to re-render it with the new values + nonRenderedConfig, err := kotsutil.FindConfigInPath(filepath.Join(params.BaseArchive, "upstream")) + if err != nil { + currentAppConfigResponse.Error = "failed to find non-rendered config" + logger.Error(errors.Wrap(err, currentAppConfigResponse.Error)) + JSON(w, http.StatusInternalServerError, currentAppConfigResponse) + return + } + + localRegistry := registrytypes.RegistrySettings{ + Hostname: params.RegistryEndpoint, + Username: params.RegistryUsername, + Password: params.RegistryPassword, + Namespace: params.RegistryNamespace, + IsReadOnly: params.RegistryIsReadOnly, + } + + // get values from saved app version + configValues := map[string]template.ItemValue{} + + if kotsKinds.ConfigValues != nil { + for key, value := range kotsKinds.ConfigValues.Spec.Values { + generatedValue := template.ItemValue{ + Default: value.Default, + Value: value.Value, + Filename: value.Filename, + RepeatableItem: value.RepeatableItem, + } + configValues[key] = generatedValue + } + } + + versionInfo := template.VersionInfoFromInstallationSpec(params.NextSequence, params.AppIsAirgap, kotsKinds.Installation.Spec) + appInfo := template.ApplicationInfo{Slug: params.AppSlug} + renderedConfig, err := kotsconfig.TemplateConfigObjects(nonRenderedConfig, configValues, appLicense, &kotsKinds.KotsApplication, localRegistry, &versionInfo, &appInfo, kotsKinds.IdentityConfig, util.PodNamespace, false) + if err != nil { + logger.Error(err) + currentAppConfigResponse.Error = "failed to render templates" + JSON(w, http.StatusInternalServerError, currentAppConfigResponse) + return + } + + currentAppConfigResponse.ConfigGroups = []kotsv1beta1.ConfigGroup{} + if renderedConfig != nil { + currentAppConfigResponse.ConfigGroups = renderedConfig.Spec.Groups + } + + currentAppConfigResponse.Success = true + JSON(w, http.StatusOK, currentAppConfigResponse) +} + +func (h *Handler) LiveAppConfig(w http.ResponseWriter, r *http.Request) { + liveAppConfigResponse := LiveAppConfigResponse{ + Success: false, + } + + params := GetContextParams(r) + appSlug := mux.Vars(r)["appSlug"] + + if params.AppSlug != appSlug { + liveAppConfigResponse.Error = "app slug does not match" + JSON(w, http.StatusForbidden, liveAppConfigResponse) + return + } + + appLicense, err := kotsutil.LoadLicenseFromBytes([]byte(params.AppLicense)) + if err != nil { + liveAppConfigResponse.Error = "failed to load license from bytes" + logger.Error(errors.Wrap(err, liveAppConfigResponse.Error)) + JSON(w, http.StatusInternalServerError, liveAppConfigResponse) + return + } + + liveAppConfigRequest := LiveAppConfigRequest{} + if err := json.NewDecoder(r.Body).Decode(&liveAppConfigRequest); err != nil { + logger.Error(err) + liveAppConfigResponse.Error = "failed to decode request body" + JSON(w, http.StatusBadRequest, liveAppConfigResponse) + return + } + + kotsKinds, err := kotsutil.LoadKotsKinds(params.BaseArchive) + if err != nil { + liveAppConfigResponse.Error = "failed to load kots kinds from path" + logger.Error(errors.Wrap(err, liveAppConfigResponse.Error)) + JSON(w, http.StatusInternalServerError, liveAppConfigResponse) + return + } + + // get the non-rendered config from the upstream directory because we have to re-render it with the new values + nonRenderedConfig, err := kotsutil.FindConfigInPath(filepath.Join(params.BaseArchive, "upstream")) + if err != nil { + liveAppConfigResponse.Error = "failed to find non-rendered config" + logger.Error(errors.Wrap(err, liveAppConfigResponse.Error)) + JSON(w, http.StatusInternalServerError, liveAppConfigResponse) + return + } + + localRegistry := registrytypes.RegistrySettings{ + Hostname: params.RegistryEndpoint, + Username: params.RegistryUsername, + Password: params.RegistryPassword, + Namespace: params.RegistryNamespace, + IsReadOnly: params.RegistryIsReadOnly, + } + + // sequence +1 because the sequence will be incremented on save (and we want the preview to be accurate) + configValues := configValuesFromConfigGroups(liveAppConfigRequest.ConfigGroups) + versionInfo := template.VersionInfoFromInstallationSpec(params.NextSequence, params.AppIsAirgap, kotsKinds.Installation.Spec) + appInfo := template.ApplicationInfo{Slug: params.AppSlug} + + renderedConfig, err := kotsconfig.TemplateConfigObjects(nonRenderedConfig, configValues, appLicense, &kotsKinds.KotsApplication, localRegistry, &versionInfo, &appInfo, kotsKinds.IdentityConfig, util.PodNamespace, false) + if err != nil { + liveAppConfigResponse.Error = "failed to render templates" + logger.Error(errors.Wrap(err, liveAppConfigResponse.Error)) + JSON(w, http.StatusInternalServerError, liveAppConfigResponse) + return + } + + liveAppConfigResponse.ConfigGroups = []kotsv1beta1.ConfigGroup{} + if renderedConfig != nil { + validationErrors, err := configvalidation.ValidateConfigSpec(renderedConfig.Spec) + if err != nil { + liveAppConfigResponse.Error = "failed to validate config spec" + logger.Error(errors.Wrap(err, liveAppConfigResponse.Error)) + JSON(w, http.StatusInternalServerError, liveAppConfigResponse) + return + } + + liveAppConfigResponse.ConfigGroups = renderedConfig.Spec.Groups + if len(validationErrors) > 0 { + liveAppConfigResponse.ValidationErrors = validationErrors + logger.Warnf("Validation errors found for config spec: %v", validationErrors) + } + } + + liveAppConfigResponse.Success = true + JSON(w, http.StatusOK, liveAppConfigResponse) +} + +func configValuesFromConfigGroups(configGroups []kotsv1beta1.ConfigGroup) map[string]template.ItemValue { + configValues := map[string]template.ItemValue{} + + for _, group := range configGroups { + for _, item := range group.Items { + // collect all repeatable items + // Future Note: This could be refactored to use CountByGroup as the control. Front end provides the exact CountByGroup it wants, back end takes care of ValuesByGroup entries. + // this way the front end doesn't have to add anything to ValuesByGroup, it just sets values there. + if item.Repeatable { + for valuesByGroupName, groupValues := range item.ValuesByGroup { + config.CreateVariadicValues(&item, valuesByGroupName) + + for fieldName, subItem := range groupValues { + itemValue := template.ItemValue{ + Value: subItem, + RepeatableItem: item.Name, + } + if item.Filename != "" { + itemValue.Filename = fieldName + } + configValues[fieldName] = itemValue + } + } + continue + } + + generatedValue := template.ItemValue{} + if item.Value.Type == multitype.String { + generatedValue.Value = item.Value.StrVal + } else { + generatedValue.Value = item.Value.BoolVal + } + if item.Default.Type == multitype.String { + generatedValue.Default = item.Default.StrVal + } else { + generatedValue.Default = item.Default.BoolVal + } + if item.Type == "file" { + generatedValue.Filename = item.Filename + } + configValues[item.Name] = generatedValue + } + } + + return configValues +} + +func (h *Handler) SaveAppConfig(w http.ResponseWriter, r *http.Request) { + saveAppConfigResponse := SaveAppConfigResponse{ + Success: false, + } + + params := GetContextParams(r) + appSlug := mux.Vars(r)["appSlug"] + + if params.AppSlug != appSlug { + saveAppConfigResponse.Error = "app slug does not match" + JSON(w, http.StatusForbidden, saveAppConfigResponse) + return + } + + appLicense, err := kotsutil.LoadLicenseFromBytes([]byte(params.AppLicense)) + if err != nil { + saveAppConfigResponse.Error = "failed to load license from bytes" + logger.Error(errors.Wrap(err, saveAppConfigResponse.Error)) + JSON(w, http.StatusInternalServerError, saveAppConfigResponse) + return + } + + saveAppConfigRequest := SaveAppConfigRequest{} + if err := json.NewDecoder(r.Body).Decode(&saveAppConfigRequest); err != nil { + logger.Error(err) + saveAppConfigResponse.Error = "failed to decode request body" + JSON(w, http.StatusBadRequest, saveAppConfigResponse) + return + } + + validationErrors, err := configvalidation.ValidateConfigSpec(kotsv1beta1.ConfigSpec{Groups: saveAppConfigRequest.ConfigGroups}) + if err != nil { + saveAppConfigResponse.Error = "failed to validate config spec." + logger.Error(errors.Wrap(err, saveAppConfigResponse.Error)) + JSON(w, http.StatusInternalServerError, saveAppConfigResponse) + return + } + + if len(validationErrors) > 0 { + saveAppConfigResponse.Error = "invalid config values" + saveAppConfigResponse.ValidationErrors = validationErrors + logger.Errorf("%v, validation errors: %+v", saveAppConfigResponse.Error, validationErrors) + JSON(w, http.StatusBadRequest, saveAppConfigResponse) + return + } + + requiredItems, requiredItemsTitles := kotsadmconfig.GetMissingRequiredConfig(saveAppConfigRequest.ConfigGroups) + if len(requiredItems) > 0 { + saveAppConfigResponse.RequiredItems = requiredItems + saveAppConfigResponse.Error = fmt.Sprintf("The following fields are required: %s", strings.Join(requiredItemsTitles, ", ")) + logger.Errorf("%v, required items: %+v", saveAppConfigResponse.Error, requiredItems) + JSON(w, http.StatusBadRequest, saveAppConfigResponse) + return + } + + localRegistry := registrytypes.RegistrySettings{ + Hostname: params.RegistryEndpoint, + Username: params.RegistryUsername, + Password: params.RegistryPassword, + Namespace: params.RegistryNamespace, + IsReadOnly: params.RegistryIsReadOnly, + } + + app := &apptypes.App{ + ID: params.AppID, + Slug: params.AppSlug, + IsAirgap: params.AppIsAirgap, + IsGitOps: params.AppIsGitOps, + } + + kotsKinds, err := kotsutil.LoadKotsKinds(params.BaseArchive) + if err != nil { + saveAppConfigResponse.Error = "failed to load kots kinds from path" + logger.Error(errors.Wrap(err, saveAppConfigResponse.Error)) + JSON(w, http.StatusInternalServerError, saveAppConfigResponse) + return + } + + if kotsKinds.ConfigValues == nil { + err = errors.New("config values not found") + saveAppConfigResponse.Error = err.Error() + logger.Error(err) + JSON(w, http.StatusInternalServerError, saveAppConfigResponse) + return + } + + values := kotsKinds.ConfigValues.Spec.Values + kotsKinds.ConfigValues.Spec.Values = kotsadmconfig.UpdateAppConfigValues(values, saveAppConfigRequest.ConfigGroups) + + configValuesSpec, err := kotsKinds.Marshal("kots.io", "v1beta1", "ConfigValues") + if err != nil { + saveAppConfigResponse.Error = "failed to marshal config values" + logger.Error(errors.Wrap(err, saveAppConfigResponse.Error)) + JSON(w, http.StatusInternalServerError, saveAppConfigResponse) + return + } + + if err := os.WriteFile(filepath.Join(params.BaseArchive, "upstream", "userdata", "config.yaml"), []byte(configValuesSpec), 0644); err != nil { + saveAppConfigResponse.Error = "failed to write config values" + logger.Error(errors.Wrap(err, saveAppConfigResponse.Error)) + JSON(w, http.StatusInternalServerError, saveAppConfigResponse) + return + } + + err = render.RenderDir(rendertypes.RenderDirOptions{ + ArchiveDir: params.BaseArchive, + App: app, + Downstreams: []downstreamtypes.Downstream{{Name: "this-cluster"}}, + RegistrySettings: localRegistry, + Sequence: params.NextSequence, + ReportingInfo: params.ReportingInfo, + }) + if err != nil { + cause := errors.Cause(err) + if _, ok := cause.(util.ActionableError); ok { + saveAppConfigResponse.Error = err.Error() + JSON(w, http.StatusInternalServerError, saveAppConfigResponse) + return + } else { + saveAppConfigResponse.Error = "failed to render templates" + logger.Error(errors.Wrap(err, saveAppConfigResponse.Error)) + JSON(w, http.StatusInternalServerError, saveAppConfigResponse) + return + } + } + + reportingFn := func() error { + if params.AppIsAirgap { + // TODO NOW: airgap reporting + return nil + } + return reporting.SendOnlineAppInfo(appLicense, params.ReportingInfo) + } + + if err := upgradepreflight.Run(app, params.BaseArchive, int64(params.NextSequence), localRegistry, false, reportingFn); err != nil { + saveAppConfigResponse.Error = "failed to run preflights" + logger.Error(errors.Wrap(err, saveAppConfigResponse.Error)) + JSON(w, http.StatusInternalServerError, saveAppConfigResponse) + return + } + + saveAppConfigResponse.Success = true + JSON(w, http.StatusOK, saveAppConfigResponse) +} + +func (h *Handler) DownloadFileFromConfig(w http.ResponseWriter, r *http.Request) { + downloadFileFromConfigResponse := DownloadFileFromConfigResponse{ + Success: false, + } + + params := GetContextParams(r) + appSlug := mux.Vars(r)["appSlug"] + + if params.AppSlug != appSlug { + downloadFileFromConfigResponse.Error = "app slug does not match" + JSON(w, http.StatusForbidden, downloadFileFromConfigResponse) + return + } + + filename := mux.Vars(r)["filename"] + if filename == "" { + logger.Error(errors.New("filename parameter is empty")) + downloadFileFromConfigResponse.Error = "failed to parse filename, parameter was empty" + JSON(w, http.StatusInternalServerError, downloadFileFromConfigResponse) + return + } + + kotsKinds, err := kotsutil.LoadKotsKinds(params.BaseArchive) + if err != nil { + downloadFileFromConfigResponse.Error = "failed to load kots kinds from path" + logger.Error(errors.Wrap(err, downloadFileFromConfigResponse.Error)) + JSON(w, http.StatusInternalServerError, downloadFileFromConfigResponse) + return + } + + var configValue *string + for _, v := range kotsKinds.ConfigValues.Spec.Values { + if v.Filename == filename { + configValue = &v.Value + break + } + } + if configValue == nil { + logger.Error(errors.New("could not find requested file")) + downloadFileFromConfigResponse.Error = "could not find requested file" + JSON(w, http.StatusInternalServerError, downloadFileFromConfigResponse) + return + } + + decoded, err := base64.StdEncoding.DecodeString(*configValue) + if err != nil { + logger.Error(err) + downloadFileFromConfigResponse.Error = "failed to decode config value" + JSON(w, http.StatusInternalServerError, downloadFileFromConfigResponse) + } + w.Header().Set("Content-Type", "application/octet-stream") + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename)) + w.Header().Set("Content-Length", strconv.Itoa(len(decoded))) + w.WriteHeader(http.StatusOK) + w.Write(decoded) +} diff --git a/pkg/upgradeservice/handlers/handlers.go b/pkg/upgradeservice/handlers/handlers.go new file mode 100644 index 0000000000..4a98a28d62 --- /dev/null +++ b/pkg/upgradeservice/handlers/handlers.go @@ -0,0 +1,50 @@ +package handlers + +import ( + "encoding/json" + "net/http" + + "github.com/gorilla/mux" + "github.com/replicatedhq/kots/pkg/logger" + kotsscheme "github.com/replicatedhq/kotskinds/client/kotsclientset/scheme" + troubleshootscheme "github.com/replicatedhq/troubleshoot/pkg/client/troubleshootclientset/scheme" + veleroscheme "github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned/scheme" + "k8s.io/client-go/kubernetes/scheme" +) + +var _ UpgradeServiceHandler = (*Handler)(nil) + +type Handler struct { +} + +func init() { + kotsscheme.AddToScheme(scheme.Scheme) + troubleshootscheme.AddToScheme(scheme.Scheme) + veleroscheme.AddToScheme(scheme.Scheme) +} + +func RegisterRoutes(r *mux.Router, handler UpgradeServiceHandler) { + // CAUTION: modifying this prefix WILL break backwards compatibility + subRouter := r.PathPrefix("/api/v1/upgrade-service/app/{appSlug}").Subrouter() + subRouter.Use(LoggingMiddleware) + + subRouter.Path("/ping").Methods("GET").HandlerFunc(handler.Ping) + + subRouter.Path("/config").Methods("GET").HandlerFunc(handler.CurrentAppConfig) + subRouter.Path("/liveconfig").Methods("POST").HandlerFunc(handler.LiveAppConfig) + subRouter.Path("/config").Methods("PUT").HandlerFunc(handler.SaveAppConfig) + subRouter.Path("/config/{filename}/download").Methods("GET").HandlerFunc(handler.DownloadFileFromConfig) +} + +func JSON(w http.ResponseWriter, code int, payload interface{}) { + response, err := json.Marshal(payload) + if err != nil { + logger.Error(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(code) + w.Write(response) +} diff --git a/pkg/upgradeservice/handlers/interface.go b/pkg/upgradeservice/handlers/interface.go new file mode 100644 index 0000000000..6bcb314e17 --- /dev/null +++ b/pkg/upgradeservice/handlers/interface.go @@ -0,0 +1,12 @@ +package handlers + +import "net/http" + +type UpgradeServiceHandler interface { + Ping(w http.ResponseWriter, r *http.Request) + + CurrentAppConfig(w http.ResponseWriter, r *http.Request) + LiveAppConfig(w http.ResponseWriter, r *http.Request) + SaveAppConfig(w http.ResponseWriter, r *http.Request) + DownloadFileFromConfig(w http.ResponseWriter, r *http.Request) +} diff --git a/pkg/upgradeservice/handlers/middleware.go b/pkg/upgradeservice/handlers/middleware.go new file mode 100644 index 0000000000..b0e80969ea --- /dev/null +++ b/pkg/upgradeservice/handlers/middleware.go @@ -0,0 +1,68 @@ +package handlers + +import ( + "context" + "net/http" + "os" + "time" + + "github.com/gorilla/mux" + "github.com/replicatedhq/kots/pkg/logger" + "github.com/replicatedhq/kots/pkg/upgradeservice/types" +) + +type paramsKey struct{} + +func SetContextParams(r *http.Request, params types.UpgradeServiceParams) *http.Request { + return r.WithContext(context.WithValue(r.Context(), paramsKey{}, params)) +} + +func GetContextParams(r *http.Request) types.UpgradeServiceParams { + val := r.Context().Value(paramsKey{}) + params, _ := val.(types.UpgradeServiceParams) + return params +} + +func ParamsMiddleware(params types.UpgradeServiceParams) mux.MiddlewareFunc { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + r = SetContextParams(r, params) + next.ServeHTTP(w, r) + }) + } +} + +type loggingResponseWriter struct { + http.ResponseWriter + StatusCode int +} + +func NewLoggingResponseWriter(w http.ResponseWriter) *loggingResponseWriter { + return &loggingResponseWriter{w, http.StatusOK} +} + +func (lrw *loggingResponseWriter) WriteHeader(code int) { + lrw.StatusCode = code + lrw.ResponseWriter.WriteHeader(code) +} + +func LoggingMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + startTime := time.Now() + + lrw := NewLoggingResponseWriter(w) + next.ServeHTTP(lrw, r) + + if os.Getenv("DEBUG") != "true" && lrw.StatusCode < http.StatusBadRequest { + return + } + + logger.Infof( + "method=%s status=%d duration=%s request=%s", + r.Method, + lrw.StatusCode, + time.Since(startTime).String(), + r.RequestURI, + ) + }) +} diff --git a/pkg/upgradeservice/handlers/ping.go b/pkg/upgradeservice/handlers/ping.go new file mode 100644 index 0000000000..f2c193804c --- /dev/null +++ b/pkg/upgradeservice/handlers/ping.go @@ -0,0 +1,15 @@ +package handlers + +import ( + "net/http" +) + +type PingResponse struct { + Ping string `json:"ping"` +} + +func (h *Handler) Ping(w http.ResponseWriter, r *http.Request) { + pingResponse := PingResponse{} + pingResponse.Ping = "pong" + JSON(w, http.StatusOK, pingResponse) +} diff --git a/pkg/upgradeservice/handlers/spa.go b/pkg/upgradeservice/handlers/spa.go new file mode 100644 index 0000000000..cfa68de9a7 --- /dev/null +++ b/pkg/upgradeservice/handlers/spa.go @@ -0,0 +1,59 @@ +package handlers + +import ( + "context" + "io/fs" + "net/http" + "os" + "path/filepath" + + "github.com/replicatedhq/kots/web" +) + +// SPAHandler implements the http.Handler interface, so we can use it +// to respond to HTTP requests. The path to the static directory and +// path to the index file within that static directory are used to +// serve the SPA in the given static directory. +type SPAHandler struct { +} + +// ServeHTTP inspects the URL path to locate a file within the static dir +// on the SPA handler. If a file is found, it will be served. If not, the +// file located at the index path on the SPA handler will be served. This +// is suitable behavior for serving an SPA (single page application). +func (h SPAHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // get the absolute path to prevent directory traversal + path, err := filepath.Abs(r.URL.Path) + if err != nil { + // if we failed to get the absolute path respond with a 400 bad request + // and stop + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // prepend the path with the path to the static directory + + // check whether a file exists at the given path + fsys, err := fs.Sub(web.Content, "dist") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + _, err = fs.Stat(fsys, filepath.Join(".", path)) // because ... fs.Sub seems to require this + + rr := r /// because the docs say to not modify request, and we might need to, so lets clone + if os.IsNotExist(err) { + rr = r.Clone(context.Background()) + // file does not exist, serve index.html + rr.URL.Path = "/" + } else if err != nil { + // if we got an error (that wasn't that the file doesn't exist) stating the + // file, return a 500 internal server error and stop + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // otherwise, use http.FileServer to serve the static dir + http.FileServer(http.FS(fsys)).ServeHTTP(w, rr) +} diff --git a/pkg/upgradeservice/handlers/static.go b/pkg/upgradeservice/handlers/static.go new file mode 100644 index 0000000000..8a908e463a --- /dev/null +++ b/pkg/upgradeservice/handlers/static.go @@ -0,0 +1,13 @@ +package handlers + +import ( + "net/http" +) + +type StatusNotFoundHandler struct { +} + +func (h StatusNotFoundHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + http.Error(w, "", http.StatusNotFound) + return +} diff --git a/pkg/upgradeservice/preflight/preflight.go b/pkg/upgradeservice/preflight/preflight.go new file mode 100644 index 0000000000..7428964192 --- /dev/null +++ b/pkg/upgradeservice/preflight/preflight.go @@ -0,0 +1,197 @@ +package preflight + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + + "github.com/pkg/errors" + apptypes "github.com/replicatedhq/kots/pkg/app/types" + "github.com/replicatedhq/kots/pkg/kotsutil" + "github.com/replicatedhq/kots/pkg/logger" + preflightpkg "github.com/replicatedhq/kots/pkg/preflight" + "github.com/replicatedhq/kots/pkg/preflight/types" + "github.com/replicatedhq/kots/pkg/registry" + registrytypes "github.com/replicatedhq/kots/pkg/registry/types" + "github.com/replicatedhq/kots/pkg/render" + rendertypes "github.com/replicatedhq/kots/pkg/render/types" + "github.com/replicatedhq/kots/pkg/util" + troubleshootanalyze "github.com/replicatedhq/troubleshoot/pkg/analyze" + troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" + troubleshootpreflight "github.com/replicatedhq/troubleshoot/pkg/preflight" + "go.uber.org/zap" +) + +type PreflightData struct { + Progress map[string]interface{} `json:"progress"` + Results *types.PreflightResults `json:"results"` +} + +var PreflightDataFilepath string + +func init() { + tmpDir, err := os.MkdirTemp("", "preflights") + if err != nil { + panic(errors.Wrap(err, "failed to create preflights data dir")) + } + PreflightDataFilepath = filepath.Join(tmpDir, "preflights.json") +} + +func Run(app *apptypes.App, archiveDir string, sequence int64, registrySettings registrytypes.RegistrySettings, ignoreRBAC bool, reportingFn func() error) error { + kotsKinds, err := kotsutil.LoadKotsKinds(archiveDir) + if err != nil { + return errors.Wrap(err, "failed to load rendered kots kinds") + } + + tsKinds, err := kotsutil.LoadTSKindsFromPath(filepath.Join(archiveDir, "rendered")) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("failed to load troubleshoot kinds from path: %s", filepath.Join(archiveDir, "rendered"))) + } + + var preflight *troubleshootv1beta2.Preflight + if tsKinds.PreflightsV1Beta2 != nil { + for _, v := range tsKinds.PreflightsV1Beta2 { + preflight = troubleshootpreflight.ConcatPreflightSpec(preflight, &v) + } + } else if kotsKinds.Preflight != nil { + renderedMarshalledPreflights, err := kotsKinds.Marshal("troubleshoot.replicated.com", "v1beta1", "Preflight") + if err != nil { + return errors.Wrap(err, "failed to marshal rendered preflight") + } + renderedPreflight, err := render.RenderFile(rendertypes.RenderFileOptions{ + KotsKinds: kotsKinds, + RegistrySettings: registrySettings, + AppSlug: app.Slug, + Sequence: sequence, + IsAirgap: app.IsAirgap, + Namespace: util.PodNamespace, + InputContent: []byte(renderedMarshalledPreflights), + }) + if err != nil { + return errors.Wrap(err, "failed to render preflights") + } + preflight, err = kotsutil.LoadPreflightFromContents(renderedPreflight) + if err != nil { + return errors.Wrap(err, "failed to load rendered preflight") + } + } + + if preflight == nil { + logger.Info("no preflight spec found, not running preflights") + return nil + } + + preflightpkg.InjectDefaultPreflights(preflight, kotsKinds, registrySettings) + + numAnalyzers := 0 + for _, analyzer := range preflight.Spec.Analyzers { + exclude := troubleshootanalyze.GetExcludeFlag(analyzer).BoolOrDefaultFalse() + if !exclude { + numAnalyzers += 1 + } + } + if numAnalyzers == 0 { + logger.Info("no analyzers found, not running preflights") + return nil + } + + var preflightErr error + defer func() { + if preflightErr != nil { + preflightResults := &types.PreflightResults{ + Errors: []*types.PreflightError{ + &types.PreflightError{ + Error: preflightErr.Error(), + IsRBAC: false, + }, + }, + } + if err := setPreflightResults(preflightResults); err != nil { + logger.Error(errors.Wrap(err, "failed to set preflight results")) + return + } + } + }() + + collectors, err := registry.UpdateCollectorSpecsWithRegistryData(preflight.Spec.Collectors, registrySettings, kotsKinds.Installation, kotsKinds.License, &kotsKinds.KotsApplication) + if err != nil { + preflightErr = errors.Wrap(err, "failed to rewrite images in preflight") + return preflightErr + } + preflight.Spec.Collectors = collectors + + go func() { + logger.Info("preflight checks beginning", + zap.String("appID", app.ID), + zap.Int64("sequence", sequence)) + + _, err := preflightpkg.Execute(preflight, ignoreRBAC, setPreflightProgress, setPreflightResults) + if err != nil { + logger.Error(errors.Wrap(err, "failed to run preflight checks")) + return + } + + go func() { + if err := reportingFn(); err != nil { + logger.Debugf("failed to report app info: %v", err) + } + }() + }() + + return nil +} + +func setPreflightResults(results *types.PreflightResults) error { + preflightData, err := getPreflightData() + if err != nil { + return errors.Wrap(err, "failed to get preflight data") + } + preflightData.Results = results + if err := setPreflightData(preflightData); err != nil { + return errors.Wrap(err, "failed to set preflight results") + } + return nil +} + +func setPreflightProgress(progress map[string]interface{}) error { + preflightData, err := getPreflightData() + if err != nil { + return errors.Wrap(err, "failed to get preflight data") + } + preflightData.Progress = progress + if err := setPreflightData(preflightData); err != nil { + return errors.Wrap(err, "failed to set preflight progress") + } + return nil +} + +func getPreflightData() (*PreflightData, error) { + var preflightData *PreflightData + if _, err := os.Stat(PreflightDataFilepath); err != nil { + if !os.IsNotExist(err) { + return nil, errors.Wrap(err, "failed to stat existing preflight data") + } + preflightData = &PreflightData{} + } else { + existingBytes, err := os.ReadFile(PreflightDataFilepath) + if err != nil { + return nil, errors.Wrap(err, "failed to read existing preflight data") + } + if err := json.Unmarshal(existingBytes, &preflightData); err != nil { + return nil, errors.Wrap(err, "failed to unmarshal existing preflight data") + } + } + return preflightData, nil +} + +func setPreflightData(preflightData *PreflightData) error { + b, err := json.Marshal(preflightData) + if err != nil { + return errors.Wrap(err, "failed to marshal preflight data") + } + if err := os.WriteFile(PreflightDataFilepath, b, 0644); err != nil { + return errors.Wrap(err, "failed to write preflight data") + } + return nil +} diff --git a/pkg/upgradeservice/process.go b/pkg/upgradeservice/process.go new file mode 100644 index 0000000000..85bed37a7f --- /dev/null +++ b/pkg/upgradeservice/process.go @@ -0,0 +1,174 @@ +package upgradeservice + +import ( + _ "embed" + "fmt" + "net/http" + "net/http/httputil" + "net/url" + "os" + "os/exec" + "strings" + "sync" + "time" + + "github.com/gorilla/mux" + "github.com/phayes/freeport" + "github.com/pkg/errors" + "github.com/replicatedhq/kots/pkg/kotsutil" + "github.com/replicatedhq/kots/pkg/logger" + "github.com/replicatedhq/kots/pkg/upgradeservice/types" + "gopkg.in/yaml.v3" +) + +type UpgradeService struct { + cmd *exec.Cmd + port string +} + +// map of app slug to upgrade service +var upgradeServiceMap = map[string]*UpgradeService{} +var upgradeServiceMtx = &sync.Mutex{} + +// Start spins up an upgrade service for an app in the background on a random port and waits for it to be ready. +// If an upgrade service is already running for the app, it will be stopped and a new one will be started. +func Start(params types.UpgradeServiceParams) (finalError error) { + svc, err := start(params) + if err != nil { + return errors.Wrap(err, "failed to create new upgrade service") + } + if err := svc.waitForReady(params.AppSlug); err != nil { + return errors.Wrap(err, "failed to wait for upgrade service to become ready") + } + return nil +} + +// Proxy proxies the request to the app's upgrade service. +func Proxy(w http.ResponseWriter, r *http.Request) { + appSlug := mux.Vars(r)["appSlug"] + if appSlug == "" { + logger.Error(errors.New("upgrade service requires app slug in path")) + w.WriteHeader(http.StatusBadRequest) + return + } + + svc, ok := upgradeServiceMap[appSlug] + if !ok { + logger.Error(errors.Errorf("upgrade service not found for app %s", appSlug)) + w.WriteHeader(http.StatusServiceUnavailable) + return + } + + if !svc.isRunning() { + logger.Error(errors.Errorf("upgrade service is not running for app %s", appSlug)) + w.WriteHeader(http.StatusServiceUnavailable) + return + } + + remote, err := url.Parse(fmt.Sprintf("http://localhost:%s", svc.port)) + if err != nil { + logger.Error(errors.Wrap(err, "failed to parse upgrade service url")) + w.WriteHeader(http.StatusInternalServerError) + return + } + + proxy := httputil.NewSingleHostReverseProxy(remote) + proxy.ServeHTTP(w, r) +} + +func start(params types.UpgradeServiceParams) (*UpgradeService, error) { + upgradeServiceMtx.Lock() + defer upgradeServiceMtx.Unlock() + + // stop the current service + currSvc, _ := upgradeServiceMap[params.AppSlug] + if currSvc != nil { + currSvc.stop() + } + + fp, err := freeport.GetFreePort() + if err != nil { + return nil, errors.Wrap(err, "failed to get free port") + } + params.Port = fmt.Sprintf("%d", fp) + + paramsYAML, err := yaml.Marshal(params) + if err != nil { + return nil, errors.Wrap(err, "failed to marshal params") + } + + // TODO NOW: use local /kots bin if: + // - version is the same as the one running + // - OR it's a dev env + + // TODO NOW: uncomment this + // kotsBin, err := kotsutil.DownloadKOTSBinary(request.KOTSVersion) + // if err != nil { + // return nil, errors.Wrapf(err, "failed to download kots binary version %s", kotsVersion) + // } + + // TODO NOW: use target binary + kotsBin := kotsutil.GetKOTSBinPath() + + cmd := exec.Command(kotsBin, "upgrade-service", "start", "-") + cmd.Stdin = strings.NewReader(string(paramsYAML)) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Start(); err != nil { + return nil, errors.Wrap(err, "failed to start") + } + + // calling wait helps populate the process state and reap the zombie process + go cmd.Wait() + + // create a new service + newSvc := &UpgradeService{ + cmd: cmd, + port: params.Port, + } + upgradeServiceMap[params.AppSlug] = newSvc + + return newSvc, nil +} + +func (s *UpgradeService) stop() { + if !s.isRunning() { + return + } + logger.Infof("Stopping upgrade service on port %s", s.port) + if err := s.cmd.Process.Signal(os.Interrupt); err != nil { + logger.Errorf("Failed to stop upgrade service on port %s: %v", s.port, err) + } +} + +func (s *UpgradeService) isRunning() bool { + return s != nil && s.cmd != nil && s.cmd.ProcessState == nil +} + +func (s *UpgradeService) waitForReady(appSlug string) error { + var lasterr error + for { + time.Sleep(time.Second) + if s == nil || s.cmd == nil { + return errors.New("upgrade service not found") + } + if s.cmd.ProcessState != nil { + return errors.Errorf("upgrade service terminated. last error: %v", lasterr) + } + request, err := http.NewRequest("GET", fmt.Sprintf("http://localhost:%s/api/v1/upgrade-service/app/%s/ping", s.port, appSlug), nil) + if err != nil { + lasterr = errors.Wrap(err, "failed to create request") + continue + } + response, err := http.DefaultClient.Do(request) + if err != nil { + lasterr = errors.Wrap(err, "failed to do request") + continue + } + if response.StatusCode != http.StatusOK { + lasterr = errors.Errorf("unexpected status code %d", response.StatusCode) + continue + } + return nil + } +} diff --git a/pkg/upgradeservice/server.go b/pkg/upgradeservice/server.go new file mode 100644 index 0000000000..a662c664a4 --- /dev/null +++ b/pkg/upgradeservice/server.go @@ -0,0 +1,65 @@ +package upgradeservice + +import ( + "fmt" + "net/http" + "net/http/httputil" + "net/url" + "os" + + "github.com/gorilla/mux" + "github.com/pkg/errors" + "github.com/replicatedhq/kots/pkg/buildversion" + "github.com/replicatedhq/kots/pkg/upgradeservice/handlers" + "github.com/replicatedhq/kots/pkg/upgradeservice/types" +) + +func Serve(params types.UpgradeServiceParams) error { + fmt.Printf("Starting KOTS Upgrade Service version %s on port %s\n", buildversion.Version(), params.Port) + + if err := bootstrap(params); err != nil { + return errors.Wrap(err, "failed to bootstrap") + } + + r := mux.NewRouter() + r.Use(handlers.ParamsMiddleware(params)) + + handler := &handlers.Handler{} + handlers.RegisterRoutes(r, handler) + + // Prevent API requests that don't match anything in this router from returning UI content + r.PathPrefix("/api").Handler(handlers.StatusNotFoundHandler{}) + + /********************************************************************** + * Static routes + **********************************************************************/ + + if os.Getenv("DISABLE_SPA_SERVING") != "1" { // we don't serve this in the dev env + spa := handlers.SPAHandler{} + r.PathPrefix("/").Handler(spa) + } else if os.Getenv("ENABLE_WEB_PROXY") == "1" { // for dev env + u, err := url.Parse("http://kotsadm-web:8080") + if err != nil { + return errors.Wrap(err, "failed to parse kotsadm-web url") + } + upstream := httputil.NewSingleHostReverseProxy(u) + webProxy := func(upstream *httputil.ReverseProxy) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + r.Header.Set("X-Forwarded-Host", r.Header.Get("Host")) + upstream.ServeHTTP(w, r) + } + }(upstream) + r.PathPrefix("/").HandlerFunc(webProxy) + } + + srv := &http.Server{ + Handler: r, + Addr: fmt.Sprintf(":%s", params.Port), + } + + if err := srv.ListenAndServe(); err != nil { + return errors.Wrap(err, "failed to listen and serve") + } + + return nil +} diff --git a/pkg/upgradeservice/types/types.go b/pkg/upgradeservice/types/types.go new file mode 100644 index 0000000000..fe8f5e6d0d --- /dev/null +++ b/pkg/upgradeservice/types/types.go @@ -0,0 +1,32 @@ +package types + +import ( + reportingtypes "github.com/replicatedhq/kots/pkg/api/reporting/types" +) + +type UpgradeServiceParams struct { + Port string `yaml:"port"` + + AppID string `yaml:"appId"` + AppSlug string `yaml:"appSlug"` + AppName string `yaml:"appName"` + AppIsAirgap bool `yaml:"appIsAirgap"` + AppIsGitOps bool `yaml:"appIsGitOps"` + AppLicense string `yaml:"appLicense"` + + BaseArchive string `yaml:"baseArchive"` + BaseSequence int64 `yaml:"baseSequence"` + NextSequence int64 `yaml:"nextSequence"` + + UpdateVersionLabel string `yaml:"updateVersionLabel"` + UpdateCursor string `yaml:"updateCursor"` + UpdateChannelID string `yaml:"updateChannelID"` + + RegistryEndpoint string `yaml:"registryEndpoint"` + RegistryUsername string `yaml:"registryUsername"` + RegistryPassword string `yaml:"registryPassword"` + RegistryNamespace string `yaml:"registryNamespace"` + RegistryIsReadOnly bool `yaml:"registryIsReadOnly"` + + ReportingInfo *reportingtypes.ReportingInfo `yaml:"reportingInfo"` +} diff --git a/pkg/upstream/peek.go b/pkg/upstream/peek.go index 708be962e5..908194e59e 100644 --- a/pkg/upstream/peek.go +++ b/pkg/upstream/peek.go @@ -18,7 +18,7 @@ func GetUpdatesUpstream(upstreamURI string, fetchOptions *types.FetchOptions) (* return nil, errors.Wrap(err, "parse request uri failed") } if u.Scheme == "replicated" { - return getUpdatesReplicated(u, fetchOptions) + return getUpdatesReplicated(fetchOptions) } return nil, errors.Errorf("unknown protocol scheme %q", u.Scheme) diff --git a/pkg/upstream/replicated.go b/pkg/upstream/replicated.go index f083699374..8569db4841 100644 --- a/pkg/upstream/replicated.go +++ b/pkg/upstream/replicated.go @@ -69,7 +69,7 @@ type ChannelRelease struct { ReleaseNotes string `json:"releaseNotes"` } -func getUpdatesReplicated(u *url.URL, fetchOptions *types.FetchOptions) (*types.UpdateCheckResult, error) { +func getUpdatesReplicated(fetchOptions *types.FetchOptions) (*types.UpdateCheckResult, error) { currentCursor := replicatedapp.ReplicatedCursor{ ChannelID: fetchOptions.CurrentChannelID, ChannelName: fetchOptions.CurrentChannelName, @@ -81,12 +81,7 @@ func getUpdatesReplicated(u *url.URL, fetchOptions *types.FetchOptions) (*types. return nil, errors.New("No license was provided") } - replicatedUpstream, err := replicatedapp.ParseReplicatedURL(u) - if err != nil { - return nil, errors.Wrap(err, "failed to parse replicated upstream") - } - - pendingReleases, updateCheckTime, err := listPendingChannelReleases(replicatedUpstream, fetchOptions.License, fetchOptions.LastUpdateCheckAt, currentCursor, fetchOptions.ChannelChanged, fetchOptions.ReportingInfo) + pendingReleases, updateCheckTime, err := listPendingChannelReleases(fetchOptions.License, fetchOptions.LastUpdateCheckAt, currentCursor, fetchOptions.ChannelChanged, fetchOptions.SortOrder, fetchOptions.ReportingInfo) if err != nil { return nil, errors.Wrap(err, "failed to list replicated app releases") } @@ -364,7 +359,7 @@ func downloadReplicatedApp(replicatedUpstream *replicatedapp.ReplicatedUpstream, defer getResp.Body.Close() if getResp.StatusCode >= 300 { - body, _ := ioutil.ReadAll(getResp.Body) + body, _ := io.ReadAll(getResp.Body) if len(body) > 0 { return nil, util.ActionableError{Message: string(body)} } @@ -446,7 +441,7 @@ func downloadReplicatedApp(replicatedUpstream *replicatedapp.ReplicatedUpstream, return &release, nil } -func listPendingChannelReleases(replicatedUpstream *replicatedapp.ReplicatedUpstream, license *kotsv1beta1.License, lastUpdateCheckAt *time.Time, currentCursor replicatedapp.ReplicatedCursor, channelChanged bool, reportingInfo *reportingtypes.ReportingInfo) ([]ChannelRelease, *time.Time, error) { +func listPendingChannelReleases(license *kotsv1beta1.License, lastUpdateCheckAt *time.Time, currentCursor replicatedapp.ReplicatedCursor, channelChanged bool, sortOrder string, reportingInfo *reportingtypes.ReportingInfo) ([]ChannelRelease, *time.Time, error) { u, err := url.Parse(license.Spec.Endpoint) if err != nil { return nil, nil, errors.Wrap(err, "failed to parse endpoint from license") @@ -471,6 +466,10 @@ func listPendingChannelReleases(replicatedUpstream *replicatedapp.ReplicatedUpst urlValues.Add("lastUpdateCheckAt", lastUpdateCheckAt.UTC().Format(time.RFC3339)) } + if sortOrder != "" { + urlValues.Add("sortOrder", sortOrder) + } + url := fmt.Sprintf("%s://%s/release/%s/pending?%s", u.Scheme, hostname, license.Spec.AppSlug, urlValues.Encode()) req, err := util.NewRequest("GET", url, nil) diff --git a/pkg/upstream/types/types.go b/pkg/upstream/types/types.go index e7548cb19f..6ce7cc80e2 100644 --- a/pkg/upstream/types/types.go +++ b/pkg/upstream/types/types.go @@ -107,6 +107,7 @@ type FetchOptions struct { CurrentReplicatedChartNames []string CurrentEmbeddedClusterArtifacts *kotsv1beta1.EmbeddedClusterArtifacts ChannelChanged bool + SortOrder string AppSlug string AppSequence int64 AppVersionLabel string diff --git a/web/.gitignore b/web/.gitignore index 7f912b4d0b..812dac8b48 100644 --- a/web/.gitignore +++ b/web/.gitignore @@ -25,7 +25,6 @@ yarn-error.log* package-lock.json dist/* -!dist/README.md /history #editors diff --git a/web/dist/README.md b/web/dist/README.md deleted file mode 100644 index 7f1a1f8cb0..0000000000 --- a/web/dist/README.md +++ /dev/null @@ -1,3 +0,0 @@ -The dist directory is used in the go build to embed the compiled web resources into the kotsadm binary. Because -web isn't always built (testing, okteto, etc), this README.md will allow compiling of the go binary without first -building web. \ No newline at end of file diff --git a/web/src/Root.tsx b/web/src/Root.tsx index 0a7d547c89..bff5cd495e 100644 --- a/web/src/Root.tsx +++ b/web/src/Root.tsx @@ -34,11 +34,6 @@ import TroubleshootContainer from "@components/troubleshoot/TroubleshootContaine import Footer from "./components/shared/Footer"; import NavBar from "./components/shared/NavBar"; - -// scss -import "./scss/index.scss"; -// tailwind -import "./index.css"; import connectHistory from "./services/matomo"; // types @@ -103,6 +98,7 @@ type State = { snapshotInProgressApps: string[]; isEmbeddedClusterWaitingForNodes: boolean; themeState: ThemeState; + shouldShowUpgradeServiceModal: boolean; }; let interval: ReturnType | undefined; @@ -135,6 +131,7 @@ const Root = () => { navbarLogo: null, }, app: null, + shouldShowUpgradeServiceModal: false, } ); @@ -493,6 +490,9 @@ const Root = () => { } /> @@ -758,6 +758,9 @@ const Root = () => { } /> @@ -864,6 +867,17 @@ const Root = () => { /> )} + { + setState({ shouldShowUpgradeServiceModal: false }); + }} + contentLabel="KOTS Upgrade Service Modal" + ariaHideApp={false} + className="Modal LargeSize" + > +