diff --git a/kotsadm/pkg/apiserver/server.go b/kotsadm/pkg/apiserver/server.go index d1f97f5669..10052b28e4 100644 --- a/kotsadm/pkg/apiserver/server.go +++ b/kotsadm/pkg/apiserver/server.go @@ -105,6 +105,7 @@ func Start() { r.Path("/api/v1/app/{appSlug}/sequence/{sequence}/renderedcontents").Methods("OPTIONS", "GET").HandlerFunc(handlers.GetAppRenderedContents) r.Path("/api/v1/app/{appSlug}/sequence/{sequence}/contents").Methods("OPTIONS", "GET").HandlerFunc(handlers.GetAppContents) r.Path("/api/v1/app/{appSlug}/cluster/{clusterId}/dashboard").Methods("OPTIONS", "GET").HandlerFunc(handlers.GetAppDashboard) + r.Path("/api/v1/app/{appSlug}/cluster/{clusterId}/sequence/{sequence}/downstreamoutput").Methods("OPTIONS", "GET").HandlerFunc(handlers.GetDownstreamOutput) r.HandleFunc("/api/v1/login", handlers.Login) r.HandleFunc("/api/v1/logout", handlers.Logout) @@ -144,8 +145,6 @@ func Start() { // Find a home snapshot routes r.Path("/api/v1/snapshot/{backup}/logs").Methods("OPTIONS", "GET").HandlerFunc(handlers.DownloadSnapshotLogs) - // TODO - // KURL r.HandleFunc("/api/v1/kurl", handlers.NotImplemented) r.Path("/api/v1/kurl/generate-node-join-command-worker").Methods("OPTIONS", "POST").HandlerFunc(handlers.GenerateNodeJoinCommandWorker) diff --git a/kotsadm/pkg/downstream/downstream.go b/kotsadm/pkg/downstream/downstream.go index 69c00b3b6a..3aa3719b3f 100644 --- a/kotsadm/pkg/downstream/downstream.go +++ b/kotsadm/pkg/downstream/downstream.go @@ -2,9 +2,11 @@ package downstream import ( "database/sql" + "encoding/base64" "github.com/pkg/errors" "github.com/replicatedhq/kots/kotsadm/pkg/downstream/types" + "github.com/replicatedhq/kots/kotsadm/pkg/logger" "github.com/replicatedhq/kots/kotsadm/pkg/persistence" ) @@ -134,3 +136,78 @@ func SetIgnorePreflightPermissionErrors(appID string, sequence int64) error { return nil } + +func GetDownstreamOutput(appID string, clusterID string, sequence int64) (*types.DownstreamOutput, error) { + db := persistence.MustGetPGSession() + query := `SELECT + adv.status, + adv.status_info, + ado.dryrun_stdout, + ado.dryrun_stderr, + ado.apply_stdout, + ado.apply_stderr +FROM + app_downstream_version adv +LEFT JOIN + app_downstream_output ado +ON + adv.app_id = ado.app_id AND adv.cluster_id = ado.cluster_id AND adv.sequence = ado.downstream_sequence +WHERE + adv.app_id = $1 AND + adv.cluster_id = $2 AND + adv.sequence = $3` + row := db.QueryRow(query, appID, clusterID, sequence) + + var status sql.NullString + var statusInfo sql.NullString + var dryrunStdout sql.NullString + var dryrunStderr sql.NullString + var applyStdout sql.NullString + var applyStderr sql.NullString + + if err := row.Scan(&status, &statusInfo, &dryrunStdout, &dryrunStderr, &applyStdout, &applyStderr); err != nil { + if err == sql.ErrNoRows { + return &types.DownstreamOutput{}, nil + } + return nil, errors.Wrap(err, "failed to select downstream") + } + + renderError := "" + if status.String == "failed" { + renderError = statusInfo.String + } + + dryrunStdoutDecoded, err := base64.StdEncoding.DecodeString(dryrunStdout.String) + if err != nil { + logger.Error(errors.Wrap(err, "failed to decode dryrun stdout")) + dryrunStdoutDecoded = []byte("") + } + + dryrunStderrDecoded, err := base64.StdEncoding.DecodeString(dryrunStderr.String) + if err != nil { + logger.Error(errors.Wrap(err, "failed to decode dryrun stderr")) + dryrunStderrDecoded = []byte("") + } + + applyStdoutDecoded, err := base64.StdEncoding.DecodeString(applyStdout.String) + if err != nil { + logger.Error(errors.Wrap(err, "failed to decode apply stdout")) + applyStdoutDecoded = []byte("") + } + + applyStderrDecoded, err := base64.StdEncoding.DecodeString(applyStderr.String) + if err != nil { + logger.Error(errors.Wrap(err, "failed to decode apply stder")) + applyStderrDecoded = []byte("") + } + + output := &types.DownstreamOutput{ + DryrunStdout: string(dryrunStdoutDecoded), + DryrunStderr: string(dryrunStderrDecoded), + ApplyStdout: string(applyStdoutDecoded), + ApplyStderr: string(applyStderrDecoded), + RenderError: string(renderError), + } + + return output, nil +} diff --git a/kotsadm/pkg/downstream/types/types.go b/kotsadm/pkg/downstream/types/types.go index bd2efcb08a..f9af258423 100644 --- a/kotsadm/pkg/downstream/types/types.go +++ b/kotsadm/pkg/downstream/types/types.go @@ -5,3 +5,11 @@ type Downstream struct { Name string CurrentSequence int64 } + +type DownstreamOutput struct { + DryrunStdout string `json:"dryrunStdout"` + DryrunStderr string `json:"dryrunStderr"` + ApplyStdout string `json:"applyStdout"` + ApplyStderr string `json:"applyStderr"` + RenderError string `json:"renderError"` +} diff --git a/kotsadm/pkg/handlers/downstream.go b/kotsadm/pkg/handlers/downstream.go new file mode 100644 index 0000000000..11ecaf67f3 --- /dev/null +++ b/kotsadm/pkg/handlers/downstream.go @@ -0,0 +1,51 @@ +package handlers + +import ( + "net/http" + "strconv" + + "github.com/gorilla/mux" + "github.com/replicatedhq/kots/kotsadm/pkg/app" + "github.com/replicatedhq/kots/kotsadm/pkg/downstream" + "github.com/replicatedhq/kots/kotsadm/pkg/logger" +) + +func GetDownstreamOutput(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Headers", "content-type, origin, accept, authorization") + + if r.Method == "OPTIONS" { + w.WriteHeader(http.StatusOK) + return + } + + if err := requireValidSession(w, r); err != nil { + logger.Error(err) + return + } + + appSlug := mux.Vars(r)["appSlug"] + clusterID := mux.Vars(r)["clusterId"] + sequence, err := strconv.Atoi(mux.Vars(r)["sequence"]) + if err != nil { + logger.Error(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + a, err := app.GetFromSlug(appSlug) + if err != nil { + logger.Error(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + output, err := downstream.GetDownstreamOutput(a.ID, clusterID, int64(sequence)) + if err != nil { + logger.Error(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + JSON(w, http.StatusOK, output) +} diff --git a/kotsadm/web/src/components/apps/AppVersionHistory.jsx b/kotsadm/web/src/components/apps/AppVersionHistory.jsx index fb4e9fd987..6c934854b3 100644 --- a/kotsadm/web/src/components/apps/AppVersionHistory.jsx +++ b/kotsadm/web/src/components/apps/AppVersionHistory.jsx @@ -19,7 +19,7 @@ import DownstreamWatchVersionDiff from "@src/components/watches/DownstreamWatchV import AirgapUploadProgress from "@src/components/AirgapUploadProgress"; import UpdateCheckerModal from "@src/components/modals/UpdateCheckerModal"; import ShowDetailsModal from "@src/components/modals/ShowDetailsModal"; -import { getKotsDownstreamHistory, getKotsDownstreamOutput, getUpdateDownloadStatus } from "../../queries/AppsQueries"; +import { getKotsDownstreamHistory, getUpdateDownloadStatus } from "../../queries/AppsQueries"; import { Utilities, isAwaitingResults, secondsAgo, getPreflightResultState, getGitProviderDiffUrl, getCommitHashFromUrl } from "../../utilities/utilities"; import { Repeater } from "../../utilities/repeater"; import has from "lodash/has"; @@ -471,26 +471,30 @@ class AppVersionHistory extends Component { } handleViewLogs = async version => { - const { match, app } = this.props; - const clusterSlug = app.downstreams?.length && app.downstreams[0].cluster?.slug; - if (clusterSlug) { + try { + const { app } = this.props; + const clusterId = app.downstreams?.length && app.downstreams[0].cluster?.id; + this.setState({ logsLoading: true, showLogsModal: true }); - this.props.client.query({ - query: getKotsDownstreamOutput, - fetchPolicy: "no-cache", - variables: { - appSlug: match.params.slug, - clusterSlug: clusterSlug, - sequence: version.sequence - } - }).then(result => { - const logs = result.data.getKotsDownstreamOutput; + + const res = await fetch(`${window.env.API_ENDPOINT}/app/${app?.slug}/cluster/${clusterId}/sequence/${version?.sequence}/downstreamoutput`, { + headers: { + "Authorization": Utilities.getToken(), + "Content-Type": "application/json", + }, + method: "GET", + }); + if (res.ok && res.status === 200) { + const logs = await res.json(); const selectedTab = Object.keys(logs)[0]; this.setState({ logs, selectedTab, logsLoading: false }); - }).catch(err => { - console.log(err); + } else { + console.log("failed to view logs, unexpected status code", res.status); this.setState({ logsLoading: false }); - }); + } + } catch(err) { + console.log(err); + this.setState({ logsLoading: false }); } } @@ -1091,11 +1095,12 @@ class AppVersionHistory extends Component { ) : (
- {logs.renderError ? +
+ {!logs.renderError && this.renderLogsTabs()}
- : -
- {this.renderLogsTabs()} -
- -
-
- } +