Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

disaster recovery UI for embedded cluster #4570

Merged
merged 14 commits into from
Apr 25, 2024
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",
})
}
sgalsaleh marked this conversation as resolved.
Show resolved Hide resolved

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
Loading