Skip to content

Commit

Permalink
add pending_cluster_management status for embedded clusters (#4461)
Browse files Browse the repository at this point in the history
  • Loading branch information
cbodonnell committed Feb 22, 2024
1 parent 0a7a43a commit dce1244
Show file tree
Hide file tree
Showing 15 changed files with 255 additions and 21 deletions.
105 changes: 105 additions & 0 deletions pkg/handlers/embedded_cluster_confirm_cluster_management.go
@@ -0,0 +1,105 @@
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"
"github.com/replicatedhq/kots/pkg/util"
)

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

func (h *Handler) ConfirmEmbeddedClusterManagement(w http.ResponseWriter, r *http.Request) {
if !util.IsEmbeddedCluster() {
logger.Errorf("not an embedded cluster")
w.WriteHeader(http.StatusBadRequest)
return
}

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 {
logger.Error(fmt.Errorf("pending version is not in pending_cluster_management status"))
w.WriteHeader(http.StatusBadRequest)
return
}

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
}
}

if err := store.GetStore().SetDownstreamVersionStatus(app.ID, pendingVersion.Sequence, downstreamVersionStatus, ""); 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(downstreamVersionStatus),
})
}
7 changes: 7 additions & 0 deletions pkg/handlers/embedded_cluster_delete_node.go
Expand Up @@ -8,11 +8,18 @@ import (
"github.com/replicatedhq/kots/pkg/embeddedcluster"
"github.com/replicatedhq/kots/pkg/k8sutil"
"github.com/replicatedhq/kots/pkg/logger"
"github.com/replicatedhq/kots/pkg/util"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func (h *Handler) DeleteEmbeddedClusterNode(w http.ResponseWriter, r *http.Request) {
if !util.IsEmbeddedCluster() {
logger.Errorf("not an embedded cluster")
w.WriteHeader(http.StatusBadRequest)
return
}

client, err := k8sutil.GetClientset()
if err != nil {
logger.Error(err)
Expand Down
7 changes: 7 additions & 0 deletions pkg/handlers/embedded_cluster_drain_node.go
Expand Up @@ -8,11 +8,18 @@ import (
"github.com/replicatedhq/kots/pkg/embeddedcluster"
"github.com/replicatedhq/kots/pkg/k8sutil"
"github.com/replicatedhq/kots/pkg/logger"
"github.com/replicatedhq/kots/pkg/util"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func (h *Handler) DrainEmbeddedClusterNode(w http.ResponseWriter, r *http.Request) {
if !util.IsEmbeddedCluster() {
logger.Errorf("not an embedded cluster")
w.WriteHeader(http.StatusBadRequest)
return
}

client, err := k8sutil.GetClientset()
if err != nil {
logger.Error(err)
Expand Down
19 changes: 19 additions & 0 deletions pkg/handlers/embedded_cluster_get.go
Expand Up @@ -7,13 +7,20 @@ import (
"github.com/replicatedhq/kots/pkg/embeddedcluster"
"github.com/replicatedhq/kots/pkg/k8sutil"
"github.com/replicatedhq/kots/pkg/logger"
"github.com/replicatedhq/kots/pkg/util"
)

type GetEmbeddedClusterRolesResponse struct {
Roles []string `json:"roles"`
}

func (h *Handler) GetEmbeddedClusterNodes(w http.ResponseWriter, r *http.Request) {
if !util.IsEmbeddedCluster() {
logger.Errorf("not an embedded cluster")
w.WriteHeader(http.StatusBadRequest)
return
}

client, err := k8sutil.GetClientset()
if err != nil {
logger.Error(err)
Expand All @@ -31,6 +38,12 @@ func (h *Handler) GetEmbeddedClusterNodes(w http.ResponseWriter, r *http.Request
}

func (h *Handler) GetEmbeddedClusterNode(w http.ResponseWriter, r *http.Request) {
if !util.IsEmbeddedCluster() {
logger.Errorf("not an embedded cluster")
w.WriteHeader(http.StatusBadRequest)
return
}

client, err := k8sutil.GetClientset()
if err != nil {
logger.Error(err)
Expand All @@ -49,6 +62,12 @@ func (h *Handler) GetEmbeddedClusterNode(w http.ResponseWriter, r *http.Request)
}

func (h *Handler) GetEmbeddedClusterRoles(w http.ResponseWriter, r *http.Request) {
if !util.IsEmbeddedCluster() {
logger.Errorf("not an embedded cluster")
w.WriteHeader(http.StatusBadRequest)
return
}

roles, err := embeddedcluster.GetRoles(r.Context())
if err != nil {
logger.Error(err)
Expand Down
13 changes: 13 additions & 0 deletions pkg/handlers/embedded_cluster_node_join_command.go
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/replicatedhq/kots/pkg/k8sutil"
"github.com/replicatedhq/kots/pkg/logger"
"github.com/replicatedhq/kots/pkg/store"
"github.com/replicatedhq/kots/pkg/util"
)

type GenerateEmbeddedClusterNodeJoinCommandResponse struct {
Expand All @@ -29,6 +30,12 @@ type GenerateEmbeddedClusterNodeJoinCommandRequest struct {
}

func (h *Handler) GenerateEmbeddedClusterNodeJoinCommand(w http.ResponseWriter, r *http.Request) {
if !util.IsEmbeddedCluster() {
logger.Errorf("not an embedded cluster")
w.WriteHeader(http.StatusBadRequest)
return
}

generateEmbeddedClusterNodeJoinCommandRequest := GenerateEmbeddedClusterNodeJoinCommandRequest{}
if err := json.NewDecoder(r.Body).Decode(&generateEmbeddedClusterNodeJoinCommandRequest); err != nil {
logger.Error(fmt.Errorf("failed to decode request body: %w", err))
Expand Down Expand Up @@ -63,6 +70,12 @@ func (h *Handler) GenerateEmbeddedClusterNodeJoinCommand(w http.ResponseWriter,

// this function relies on the token being valid for authentication
func (h *Handler) GetEmbeddedClusterNodeJoinCommand(w http.ResponseWriter, r *http.Request) {
if !util.IsEmbeddedCluster() {
logger.Errorf("not an embedded cluster")
w.WriteHeader(http.StatusBadRequest)
return
}

// read query string, ensure that the token is valid
token := r.URL.Query().Get("token")
roles, err := store.GetStore().GetEmbeddedClusterInstallCommandRoles(token)
Expand Down
2 changes: 2 additions & 0 deletions pkg/handlers/handlers.go
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("ConfirmEmbeddedClusterManagement").Path("/api/v1/embedded-cluster/management").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
10 changes: 10 additions & 0 deletions pkg/handlers/handlers_test.go
Expand Up @@ -1210,6 +1210,16 @@ var HandlerPolicyTests = map[string][]HandlerPolicyTest{
},

"EmbeddedCluster": {}, // Not implemented
"ConfirmEmbeddedClusterManagement": {
{
Roles: []rbactypes.Role{rbac.ClusterAdminRole},
SessionRoles: []string{rbac.ClusterAdminRoleID},
Calls: func(storeRecorder *mock_store.MockStoreMockRecorder, handlerRecorder *mock_handlers.MockKOTSHandlerMockRecorder) {
handlerRecorder.ConfirmEmbeddedClusterManagement(gomock.Any(), gomock.Any())
},
ExpectStatus: http.StatusOK,
},
},
"GenerateEmbeddedClusterNodeJoinCommand": {
{
Roles: []rbactypes.Role{rbac.ClusterAdminRole},
Expand Down
1 change: 1 addition & 0 deletions pkg/handlers/interface.go
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
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
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
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
)
9 changes: 4 additions & 5 deletions web/src/components/apps/AppDetailPage.tsx
Expand Up @@ -337,15 +337,14 @@ function AppDetailPage(props: Props) {
const firstVersion = downstream.pendingVersions.find(
(version: Version) => version?.sequence === 0
);
if (firstVersion?.status === "unknown" && props.isEmbeddedCluster) {
if (
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

0 comments on commit dce1244

Please sign in to comment.