Skip to content

Commit

Permalink
disaster recovery UI for embedded cluster (#4570)
Browse files Browse the repository at this point in the history
  • Loading branch information
Craig O'Donnell authored Apr 25, 2024
1 parent 6337f69 commit 63cecc4
Show file tree
Hide file tree
Showing 25 changed files with 300 additions and 167 deletions.
2 changes: 1 addition & 1 deletion hack/dev/skaffoldcache.Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM golang:1.21 as deps
FROM golang:1.22 as deps

RUN go install github.com/go-delve/delve/cmd/[email protected]

Expand Down
6 changes: 6 additions & 0 deletions pkg/handlers/backup.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ func (h *Handler) CreateApplicationBackup(w http.ResponseWriter, r *http.Request
Success: false,
}

if util.IsEmbeddedCluster() {
createApplicationBackupResponse.Error = "application backups are not supported in embedded clusters"
JSON(w, http.StatusForbidden, createApplicationBackupResponse)
return
}

// check minimal rbac
if err := requiresKotsadmVeleroAccess(w, r); err != nil {
return
Expand Down
8 changes: 7 additions & 1 deletion pkg/snapshot/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import (
kotss3 "github.com/replicatedhq/kots/pkg/s3"
"github.com/replicatedhq/kots/pkg/snapshot/providers"
"github.com/replicatedhq/kots/pkg/snapshot/types"
"github.com/replicatedhq/kots/pkg/util"
velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
veleroclientv1 "github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned/typed/velero/v1"
"google.golang.org/api/option"
Expand Down Expand Up @@ -1023,7 +1024,12 @@ func mapAWSBackupStorageLocationToStore(kotsadmVeleroBackendStorageLocation *vel
return errors.Wrap(err, "failed to parse s3 url")
}
// without endpoint, the ui has no logic to figure if it is amazon-s3 or other-s3 compatible storages
if !isS3Compatible || strings.HasSuffix(u.Hostname(), ".amazonaws.com") {
shouldMapToAWS := !isS3Compatible || strings.HasSuffix(u.Hostname(), ".amazonaws.com")
if util.IsEmbeddedCluster() {
// embedded clusters only support other s3 compatible storage
shouldMapToAWS = false
}
if shouldMapToAWS {
store.AWS = &types.StoreAWS{
Region: kotsadmVeleroBackendStorageLocation.Spec.Config["region"],
}
Expand Down
16 changes: 15 additions & 1 deletion pkg/snapshot/velero.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/replicatedhq/kots/pkg/k8sutil"
kotsadmresources "github.com/replicatedhq/kots/pkg/kotsadm/resources"
kotsadmtypes "github.com/replicatedhq/kots/pkg/kotsadm/types"
"github.com/replicatedhq/kots/pkg/util"
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
"github.com/vmware-tanzu/velero/pkg/cmd/cli/serverstatus"
veleroclientv1 "github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned/typed/velero/v1"
Expand Down Expand Up @@ -388,9 +389,16 @@ func getVeleroPod(ctx context.Context, clientset *kubernetes.Clientset, namespac
"component": "velero",
"deploy": "velero",
}
labelSelector := labels.SelectorFromSet(veleroLabels)

if util.IsEmbeddedCluster() {
labelSelector = labels.SelectorFromSet(map[string]string{
"name": "velero",
})
}

veleroPods, err := clientset.CoreV1().Pods(namespace).List(context.TODO(), metav1.ListOptions{
LabelSelector: labels.SelectorFromSet(veleroLabels).String(),
LabelSelector: labelSelector.String(),
})
if err != nil {
return "", errors.Wrap(err, "failed to list velero pods before restarting")
Expand Down Expand Up @@ -421,6 +429,12 @@ func getNodeAgentPods(ctx context.Context, clientset *kubernetes.Clientset, name
labelSelector := labels.NewSelector()
labelSelector = labelSelector.Add(*componentReq, *nameReq)

if util.IsEmbeddedCluster() {
labelSelector = labels.SelectorFromSet(map[string]string{
"name": "node-agent",
})
}

nodeAgentPods, err := clientset.CoreV1().Pods(namespace).List(context.TODO(), metav1.ListOptions{
LabelSelector: labelSelector.String(),
})
Expand Down
12 changes: 12 additions & 0 deletions web/src/Root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -635,6 +635,9 @@ const Root = () => {
<SnapshotsWrapper
appName={state.selectedAppName}
isKurlEnabled={state.adminConsoleMetadata?.isKurl}
isEmbeddedCluster={
state.adminConsoleMetadata?.isEmbeddedCluster
}
appsList={state.appsList}
/>
}
Expand All @@ -644,6 +647,9 @@ const Root = () => {
element={
<Snapshots
isKurlEnabled={state.adminConsoleMetadata?.isKurl}
isEmbeddedCluster={
state.adminConsoleMetadata?.isEmbeddedCluster
}
appsList={state.appsList}
/>
}
Expand All @@ -655,6 +661,9 @@ const Root = () => {
// @ts-ignore
<SnapshotSettings
isKurlEnabled={state.adminConsoleMetadata?.isKurl}
isEmbeddedCluster={
state.adminConsoleMetadata?.isEmbeddedCluster
}
appsList={state.appsList}
/>
}
Expand All @@ -664,6 +673,9 @@ const Root = () => {
element={
<SnapshotDetails
isKurlEnabled={state.adminConsoleMetadata?.isKurl}
isEmbeddedCluster={
state.adminConsoleMetadata?.isEmbeddedCluster
}
appsList={state.appsList}
/>
}
Expand Down
1 change: 1 addition & 0 deletions web/src/components/apps/AppDetailPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,7 @@ function AppDetailPage(props: Props) {
app={selectedApp}
isVeleroInstalled={isVeleroInstalled}
isHelmManaged={props.isHelmManaged}
isEmbeddedCluster={props.isEmbeddedCluster}
/>
<Outlet context={context} />
</Fragment>
Expand Down
12 changes: 8 additions & 4 deletions web/src/components/modals/DeleteSnapshotModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Utilities } from "@src/utilities/utilities";
import { Snapshot } from "@src/types";

interface DeleteSnapshotModalProps {
featureName: string;
deleteSnapshotModal: boolean;
toggleConfirmDeleteModal: (snapshot: Snapshot | {}) => void;
snapshotToDelete: Snapshot;
Expand All @@ -14,6 +15,7 @@ interface DeleteSnapshotModalProps {

export default function DeleteSnapshotModal(props: DeleteSnapshotModalProps) {
const {
featureName,
deleteSnapshotModal,
toggleConfirmDeleteModal,
snapshotToDelete,
Expand All @@ -37,16 +39,16 @@ export default function DeleteSnapshotModal(props: DeleteSnapshotModalProps) {
<div className="Modal-body">
<div className="flex flex-column">
<p className="u-fontSize--largest u-fontWeight--bold u-textColor--primary u-lineHeight--normal u-marginBottom--more">
Delete snapshot
Delete {featureName}
</p>
{deleteErr ? (
<p className="u-textColor--error u-fontSize--small u-fontWeight--medium u-lineHeight--normal">
{deleteErrorMsg}
</p>
) : null}
<p className="u-fontSize--normal u-fontWeight--normal u-textColor--bodyCopy u-lineHeight--normal">
Are you sure you want do permanently delete a snapshot? This action
cannot be reversed.
Are you sure you want to permanently delete a {featureName}? This
action cannot be reversed.
</p>
<div className="flex flex1 justifyContent--spaceBetween u-marginTop--20">
<div className="flex flex-column">
Expand Down Expand Up @@ -87,7 +89,9 @@ export default function DeleteSnapshotModal(props: DeleteSnapshotModalProps) {
}}
disabled={deletingSnapshot}
>
{deletingSnapshot ? "Deleting snapshot" : "Delete snapshot"}
{deletingSnapshot
? `Deleting ${featureName}`
: `Delete ${featureName}`}
</button>
</div>
</div>
Expand Down
5 changes: 4 additions & 1 deletion web/src/components/shared/NavBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,9 @@ export class NavBar extends PureComponent<Props, State> {
onClick={this.handleGoToSnapshots}
className="flex flex1 u-cursor--pointer alignItems--center text u-fontSize--normal u-fontWeight--medium flex"
>
Snapshots
{isEmbeddedClusterEnabled
? "Disaster Recovery"
: "Snapshots"}
</span>
</div>
)}
Expand All @@ -290,6 +292,7 @@ export class NavBar extends PureComponent<Props, State> {
<NavBarDropdown
handleLogOut={this.handleLogOut}
isHelmManaged={this.props.isHelmManaged}
isEmbeddedCluster={isEmbeddedClusterEnabled}
/>
</>
)}
Expand Down
4 changes: 2 additions & 2 deletions web/src/components/shared/NavBarDropdown.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Icon from "../Icon";
import ChangePasswordModal from "../modals/ChangePasswordModal/ChangePasswordModal";
import { useEffect, useRef, useState } from "react";

const NavBarDropdown = ({ handleLogOut, isHelmManaged }) => {
const NavBarDropdown = ({ handleLogOut, isHelmManaged, isEmbeddedCluster }) => {
const [showDropdown, setShowDropdown] = useState(false);
const [showModal, setShowModal] = useState(false);
const testRef = useRef(null);
Expand Down Expand Up @@ -49,7 +49,7 @@ const NavBarDropdown = ({ handleLogOut, isHelmManaged }) => {
<li>
<p onClick={() => setShowModal(true)}>Change password</p>
</li>
{!isHelmManaged && (
{!isHelmManaged && !isEmbeddedCluster && (
<li onMouseDown={handleNav}>
<p>Add new application</p>
</li>
Expand Down
6 changes: 5 additions & 1 deletion web/src/components/shared/SubNavBar/SubNavBar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export default function SubNavBar({
isAccess = false,
isSnapshots = false,
isHelmManaged,
isEmbeddedCluster,
}) {
let { slug } = app;

Expand Down Expand Up @@ -50,13 +51,14 @@ export default function SubNavBar({
const snapshotsConfig = [
{
tabName: activeTab === slug ? slug : "snapshots",
displayName: "Full Snapshots (Instance)",
displayName: isEmbeddedCluster ? "Backups" : "Full Snapshots (Instance)",
to: () => `/snapshots`,
},
{
tabName: "partial",
displayName: "Partial Snapshots (Application)",
to: (slug) => `/snapshots/partial/${slug}`,
hide: isEmbeddedCluster,
},
{
tabName: "settings",
Expand Down Expand Up @@ -86,6 +88,7 @@ export default function SubNavBar({
.filter(Boolean)
: isSnapshots
? snapshotsConfig
.filter((link) => !link.hide)
.map((link, idx) => {
const generatedMenuItem = (
<li
Expand Down Expand Up @@ -124,6 +127,7 @@ export default function SubNavBar({
link.displayRule({
app: app || {},
isHelmManaged,
isEmbeddedCluster,
isIdentityServiceSupported:
app.isAppIdentityServiceSupported,
isVeleroInstalled,
Expand Down
1 change: 1 addition & 0 deletions web/src/components/snapshots/AppSnapshots.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -781,6 +781,7 @@ class AppSnapshots extends Component {
)}
{deleteSnapshotModal && (
<DeleteSnapshotModal
featureName="snapshot"
deleteSnapshotModal={deleteSnapshotModal}
toggleConfirmDeleteModal={this.toggleConfirmDeleteModal}
handleDeleteSnapshot={this.handleDeleteSnapshot}
Expand Down
35 changes: 20 additions & 15 deletions web/src/components/snapshots/GettingStartedSnapshots.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Icon from "@components/Icon";
import { Utilities } from "@src/utilities/utilities";
import { Link } from "react-router-dom";

function navigateToConfiguration(props) {
Expand All @@ -12,31 +13,35 @@ export default function GettingStartedSnapshots(props) {
isApp,
app,
startManualSnapshot,
isEmbeddedCluster,
} = props;

let featureName = "snapshot";
if (isEmbeddedCluster) {
featureName = "backup";
}

return (
<div className="flex flex-column card-item GettingStartedSnapshots--wrapper alignItems--center">
<Icon icon="snapshot-getstarted" size={50} />
<p className="u-fontSize--jumbo2 u-fontWeight--bold u-lineHeight--more u-textColor--secondary u-marginTop--20">
{" "}
{isVeleroInstalled
? "No snapshots yet"
: "Get started with Snapshots"}{" "}
? `No ${featureName}s yet`
: `Get started with ${Utilities.toTitleCase(featureName)}s`}{" "}
</p>
{isApp ? (
<p className="u-marginTop--10 u-fontSize--normal u-lineHeight--more u-fontWeight--medium u-textColor--bodyCopy">
There have been no snapshots made for {app?.name} yet. You can
manually trigger snapshots or you can set up automatic snapshots to be
made on a custom schedule.{" "}
There have been no {featureName}s made for {app?.name} yet. You can
manually trigger {featureName}s or you can set up automatic{" "}
{featureName}s to be made on a custom schedule.{" "}
</p>
) : isVeleroInstalled ? (
<p className="u-marginTop--10 u-fontSize--normal u-lineHeight--more u-fontWeight--medium u-textColor--bodyCopy">
Now that Velero is configured, you can start making snapshots. You can{" "}
<Link to="/snapshots/settings" className="link u-fontSize--normal">
create a schedule{" "}
</Link>
for automatic snapshots or you can trigger one manually whenever you’d
like.
Create a schedule
</Link>{" "}
for automatic {featureName}s or take a {featureName} manually.
</p>
) : (
<p className="u-marginTop--10 u-fontSize--normal u-lineHeight--more u-fontWeight--medium u-textColor--bodyCopy">
Expand All @@ -50,7 +55,7 @@ export default function GettingStartedSnapshots(props) {
Velero
</a>{" "}
installed in the cluster and configured to connect with the cloud
provider you want to send your backups to
provider you want to send your {featureName}s to
</p>
)}
<div className="flex justifyContent--cenyer u-marginTop--20">
Expand All @@ -65,8 +70,8 @@ export default function GettingStartedSnapshots(props) {
>
{" "}
{isVeleroInstalled
? "Start a snapshot"
: "Configure snapshot settings"}
? `Start a ${featureName}`
: `Configure ${featureName} settings`}
</button>
) : (
<button
Expand All @@ -79,8 +84,8 @@ export default function GettingStartedSnapshots(props) {
>
{" "}
{isVeleroInstalled
? "Start a snapshot"
: "Configure snapshot settings"}
? `Start a ${featureName}`
: `Configure ${featureName} settings`}
</button>
)}
</div>
Expand Down
Loading

0 comments on commit 63cecc4

Please sign in to comment.