Skip to content

Commit

Permalink
add pending_cluster_management status for embedded clusters
Browse files Browse the repository at this point in the history
  • Loading branch information
Craig O'Donnell committed Feb 21, 2024
1 parent 0a7a43a commit d5b5a96
Show file tree
Hide file tree
Showing 10 changed files with 188 additions and 21 deletions.
95 changes: 95 additions & 0 deletions pkg/handlers/embedded_cluster_confirm_cluster_management.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package handlers

import (
"fmt"
"net/http"
"os"

"github.com/pkg/errors"
"github.com/replicatedhq/kots/pkg/kotsutil"
"github.com/replicatedhq/kots/pkg/logger"
"github.com/replicatedhq/kots/pkg/preflight"
"github.com/replicatedhq/kots/pkg/store"
storetypes "github.com/replicatedhq/kots/pkg/store/types"
)

type ConfirmEmbeddedClusterManagementResponse struct {
VersionStatus string `json:"versionStatus"`
}

func (h *Handler) ConfirmEmbeddedClusterManagement(w http.ResponseWriter, r *http.Request) {
apps, err := store.GetStore().ListInstalledApps()
if err != nil {
logger.Error(fmt.Errorf("failed to list installed apps: %w", err))
w.WriteHeader(http.StatusInternalServerError)
return
}

if len(apps) == 0 {
logger.Error(fmt.Errorf("no installed apps found"))
w.WriteHeader(http.StatusInternalServerError)
return
}
app := apps[0]

downstreamVersions, err := store.GetStore().FindDownstreamVersions(app.ID, true)
if err != nil {
logger.Error(fmt.Errorf("failed to find downstream versions: %w", err))
w.WriteHeader(http.StatusInternalServerError)
return
}

if len(downstreamVersions.PendingVersions) == 0 {
logger.Error(fmt.Errorf("no pending versions found"))
w.WriteHeader(http.StatusInternalServerError)
return
}
pendingVersion := downstreamVersions.PendingVersions[0]

if pendingVersion.Status == storetypes.VersionPendingClusterManagement {
archiveDir, err := os.MkdirTemp("", "kotsadm")
if err != nil {
logger.Error(fmt.Errorf("failed to create temp dir: %w", err))
w.WriteHeader(http.StatusInternalServerError)
return
}
defer os.RemoveAll(archiveDir)

err = store.GetStore().GetAppVersionArchive(app.ID, pendingVersion.Sequence, archiveDir)
if err != nil {
logger.Error(fmt.Errorf("failed to get app version archive: %w", err))
w.WriteHeader(http.StatusInternalServerError)
return
}

kotsKinds, err := kotsutil.LoadKotsKinds(archiveDir)
if err != nil {
logger.Error(fmt.Errorf("failed to load kots kinds: %w", err))
w.WriteHeader(http.StatusInternalServerError)
return
}

downstreamVersionStatus := storetypes.VersionPending
if kotsKinds.IsConfigurable() {
downstreamVersionStatus = storetypes.VersionPendingConfig
} else if kotsKinds.HasPreflights() {
downstreamVersionStatus = storetypes.VersionPendingPreflight
if err := preflight.Run(app.ID, app.Slug, pendingVersion.Sequence, false, archiveDir); err != nil {
logger.Error(errors.Wrap(err, "failed to start preflights"))
w.WriteHeader(http.StatusInternalServerError)
return
}
}
pendingVersion.Status = downstreamVersionStatus

if err := store.GetStore().SetDownstreamVersionStatus(app.ID, pendingVersion.Sequence, pendingVersion.Status, ""); err != nil {
logger.Error(fmt.Errorf("failed to set downstream version status: %w", err))
w.WriteHeader(http.StatusInternalServerError)
return
}
}

JSON(w, http.StatusOK, ConfirmEmbeddedClusterManagementResponse{
VersionStatus: string(pendingVersion.Status),
})
}
2 changes: 2 additions & 0 deletions pkg/handlers/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,8 @@ func RegisterSessionAuthRoutes(r *mux.Router, kotsStore store.Store, handler KOT

// Embedded Cluster
r.Name("EmbeddedCluster").Path("/api/v1/embedded-cluster").HandlerFunc(NotImplemented)
r.Name("GenerateEmbeddedClusterNodeJoinCommand").Path("/api/v1/embedded-cluster/confirm").Methods("POST").
HandlerFunc(middleware.EnforceAccess(policy.ClusterWrite, handler.ConfirmEmbeddedClusterManagement))
r.Name("GenerateEmbeddedClusterNodeJoinCommand").Path("/api/v1/embedded-cluster/generate-node-join-command").Methods("POST").
HandlerFunc(middleware.EnforceAccess(policy.ClusterWrite, handler.GenerateEmbeddedClusterNodeJoinCommand))
r.Name("DrainEmbeddedClusterNode").Path("/api/v1/embedded-cluster/nodes/{nodeName}/drain").Methods("POST").
Expand Down
1 change: 1 addition & 0 deletions pkg/handlers/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ type KOTSHandler interface {
GetKurlNodes(w http.ResponseWriter, r *http.Request)

// EmbeddedCLuster
ConfirmEmbeddedClusterManagement(w http.ResponseWriter, r *http.Request)
GenerateEmbeddedClusterNodeJoinCommand(w http.ResponseWriter, r *http.Request)
DrainEmbeddedClusterNode(w http.ResponseWriter, r *http.Request)
DeleteEmbeddedClusterNode(w http.ResponseWriter, r *http.Request)
Expand Down
12 changes: 12 additions & 0 deletions pkg/handlers/mock/mock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions pkg/online/online.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,16 @@ func CreateAppFromOnline(opts CreateOnlineAppOpts) (_ *kotsutil.KotsKinds, final
return nil, errors.Wrap(err, "failed to load kotskinds from path")
}

status, err := store.GetStore().GetDownstreamVersionStatus(opts.PendingApp.ID, newSequence)
if err != nil {
return nil, errors.Wrap(err, "failed to get downstream version status")
}

if status == storetypes.VersionPendingClusterManagement {
// if pending cluster management, we don't want to deploy the app
return kotsKinds, nil
}

hasStrictPreflights, err := store.GetStore().HasStrictPreflights(opts.PendingApp.ID, newSequence)
if err != nil {
return nil, errors.Wrap(err, "failed to check if app preflight has strict analyzers")
Expand Down
5 changes: 4 additions & 1 deletion pkg/store/kotsstore/version_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -619,7 +619,10 @@ func (s *KOTSStore) upsertAppVersionStatements(appID string, sequence int64, bas
return nil, errors.Wrap(err, "failed to check strict preflights from spec")
}
downstreamStatus := types.VersionPending
if baseSequence == nil && kotsKinds.IsConfigurable() { // initial version should always require configuration (if exists) even if all required items are already set and have values (except for automated installs, which can override this later)
if baseSequence == nil && util.IsEmbeddedCluster() {
// embedded clusters always require cluster management on initial install
downstreamStatus = types.VersionPendingClusterManagement
} else if baseSequence == nil && kotsKinds.IsConfigurable() { // initial version should always require configuration (if exists) even if all required items are already set and have values (except for automated installs, which can override this later)
downstreamStatus = types.VersionPendingConfig
} else if kotsKinds.HasPreflights() && (!skipPreflights || hasStrictPreflights) {
downstreamStatus = types.VersionPendingPreflight
Expand Down
17 changes: 9 additions & 8 deletions pkg/store/types/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ package types
type DownstreamVersionStatus string

const (
VersionUnknown DownstreamVersionStatus = "unknown" // we don't know
VersionPendingConfig DownstreamVersionStatus = "pending_config" // needs required configuration
VersionPendingDownload DownstreamVersionStatus = "pending_download" // needs to be downloaded from the upstream source
VersionPendingPreflight DownstreamVersionStatus = "pending_preflight" // waiting for preflights to finish
VersionPending DownstreamVersionStatus = "pending" // can be deployed, but is not yet
VersionDeploying DownstreamVersionStatus = "deploying" // is being deployed
VersionDeployed DownstreamVersionStatus = "deployed" // did deploy successfully
VersionFailed DownstreamVersionStatus = "failed" // did not deploy successfully
VersionUnknown DownstreamVersionStatus = "unknown" // we don't know
VersionPendingClusterManagement DownstreamVersionStatus = "pending_cluster_management" // needs cluster configuration
VersionPendingConfig DownstreamVersionStatus = "pending_config" // needs required configuration
VersionPendingDownload DownstreamVersionStatus = "pending_download" // needs to be downloaded from the upstream source
VersionPendingPreflight DownstreamVersionStatus = "pending_preflight" // waiting for preflights to finish
VersionPending DownstreamVersionStatus = "pending" // can be deployed, but is not yet
VersionDeploying DownstreamVersionStatus = "deploying" // is being deployed
VersionDeployed DownstreamVersionStatus = "deployed" // did deploy successfully
VersionFailed DownstreamVersionStatus = "failed" // did not deploy successfully
)
6 changes: 1 addition & 5 deletions web/src/components/apps/AppDetailPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -337,15 +337,11 @@ function AppDetailPage(props: Props) {
const firstVersion = downstream.pendingVersions.find(
(version: Version) => version?.sequence === 0
);
if (firstVersion?.status === "unknown" && props.isEmbeddedCluster) {
if ((firstVersion?.status === "unknown" || firstVersion?.status === "pending_cluster_management") && props.isEmbeddedCluster) {
navigate(`/${appNeedsConfiguration.slug}/cluster/manage`);
return;
}
if (firstVersion?.status === "pending_config") {
if (props.isEmbeddedCluster) {
navigate(`/${appNeedsConfiguration.slug}/cluster/manage`);
return;
}
navigate(`/${appNeedsConfiguration.slug}/config`);
return;
}
Expand Down
60 changes: 53 additions & 7 deletions web/src/components/apps/EmbeddedClusterManagement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import classNames from "classnames";
import MaterialReactTable, { MRT_ColumnDef } from "material-react-table";
import { ChangeEvent, useEffect, useMemo, useReducer, useState } from "react";
import Modal from "react-modal";
import { Link, useParams } from "react-router-dom";
import { Link, useNavigate, useParams } from "react-router-dom";

import { KotsPageTitle } from "@components/Head";
import { useApps } from "@features/App";
Expand All @@ -13,6 +13,7 @@ import Icon from "../Icon";
import CodeSnippet from "../shared/CodeSnippet";

import "@src/scss/components/apps/EmbeddedClusterManagement.scss";
import { on } from "events";

const testData = {
nodes: undefined,
Expand Down Expand Up @@ -50,12 +51,17 @@ const EmbeddedClusterManagement = ({
);
const [selectedNodeTypes, setSelectedNodeTypes] = useState<string[]>([]);

const { data: appsData } = useApps();
const {
data: appsData,
refetch: refetchApps
} = useApps();
// we grab the first app because embeddedcluster users should only ever have one app
const app = appsData?.apps?.[0];

const { slug } = useParams();

const navigate = useNavigate();

// #region queries
type NodesResponse = {
ha: boolean;
Expand Down Expand Up @@ -360,6 +366,48 @@ const EmbeddedClusterManagement = ({
}, [nodesData?.nodes?.toString()]);
// #endregion

const onContinueClick = async () => {
const res = await fetch(
`${process.env.API_ENDPOINT}/embedded-cluster/confirm`,
{
headers: {
Accept: "application/json",
},
credentials: "include",
method: "POST",
}
);
if (!res.ok) {
if (res.status === 401) {
Utilities.logoutUser();
}
console.log(
"failed to update cluster management, unexpected status code",
res.status
);
try {
const error = await res.json();
throw new Error(
error?.error?.message || error?.error || error?.message
);
} catch (err) {
throw new Error("Unable to update cluster management, please try again later.");
}
}

await refetchApps();

const data = await res.json();

if (data.versionStatus === "pending_config") {
navigate(`/${app?.slug}/config`);
} else if (data.versionStatus === "pending_preflight") {
navigate(`/${app?.slug}/preflight`);
} else {
navigate(`/app/${app?.slug}`);
}
};

return (
<div className="EmbeddedClusterManagement--wrapper container u-overflow--auto u-paddingTop--50 tw-font-sans">
<KotsPageTitle pageName="Cluster Management" />
Expand Down Expand Up @@ -436,14 +484,12 @@ const EmbeddedClusterManagement = ({
)}
</div>
{fromLicenseFlow && (
<Link
<button
className="btn primary tw-w-fit tw-ml-auto"
to={
app?.isConfigurable ? `/${app?.slug}/config` : `/app/${app?.slug}`
}
onClick={() => onContinueClick()}
>
Continue
</Link>
</button>
)}
</div>
{/* MODALS */}
Expand Down
1 change: 1 addition & 0 deletions web/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ export type VersionStatus =
| "deploying"
| "failed"
| "pending"
| "pending_cluster_management"
| "pending_config"
| "pending_download"
| "pending_preflight"
Expand Down

0 comments on commit d5b5a96

Please sign in to comment.