Skip to content

Commit

Permalink
Database view (#4065)
Browse files Browse the repository at this point in the history
Co-authored-by: jose-fully-ported <[email protected]>
Co-authored-by: Jose Diaz-Gonzalez <[email protected]>
Co-authored-by: Feroze Mohideen <[email protected]>
  • Loading branch information
4 people authored Jan 3, 2024
1 parent adbf21d commit e0b0a1a
Show file tree
Hide file tree
Showing 19 changed files with 1,385 additions and 363 deletions.
20 changes: 18 additions & 2 deletions api/server/handlers/cloud_provider/list_aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,26 @@ func (c *ListAwsAccountsHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
return
}

res.Accounts = append(res.Accounts, AwsAccount{
account := AwsAccount{
CloudProviderID: targetArn.AccountID,
ProjectID: uint(link.ProjectID),
})
}
if contains(res.Accounts, account) {
continue
}

res.Accounts = append(res.Accounts, account)
}
c.WriteResult(w, r, res)
}

// contains will check if the list of AwsAccounts contains the specified account
// TODO: replace this with an upgrade to Go 1.21 in favor of slices.Contains()
func contains(s []AwsAccount, e AwsAccount) bool {
for _, a := range s {
if a == e {
return true
}
}
return false
}
119 changes: 119 additions & 0 deletions api/server/handlers/datastore/delete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package datastore

import (
"context"
"net/http"

"github.com/porter-dev/porter/api/server/authz"
"github.com/porter-dev/porter/api/server/handlers"
"github.com/porter-dev/porter/api/server/shared"
"github.com/porter-dev/porter/api/server/shared/apierrors"
"github.com/porter-dev/porter/api/server/shared/config"
"github.com/porter-dev/porter/api/types"
"github.com/porter-dev/porter/internal/models"
"github.com/porter-dev/porter/internal/telemetry"
)

// DeleteRequest describes an inbound datastore deletion request
type DeleteRequest struct {
Type string `json:"type" form:"required"`
Name string `json:"name" form:"required"`
}

// DeleteDatastoreHandler is a struct for handling datastore deletion requests
type DeleteDatastoreHandler struct {
handlers.PorterHandlerReadWriter
authz.KubernetesAgentGetter
}

// NewDeleteDatastoreHandler constructs a datastore DeleteDatastoreHandler
func NewDeleteDatastoreHandler(
config *config.Config,
decoderValidator shared.RequestDecoderValidator,
writer shared.ResultWriter,
) *DeleteDatastoreHandler {
return &DeleteDatastoreHandler{
PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
KubernetesAgentGetter: authz.NewOutOfClusterAgentGetter(config),
}
}

func (h *DeleteDatastoreHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx, span := telemetry.NewSpan(r.Context(), "serve-datastore-delete")
defer span.End()
project, _ := ctx.Value(types.ProjectScope).(*models.Project)

request := &StatusRequest{}
if ok := h.DecodeAndValidate(w, r, request); !ok {
err := telemetry.Error(ctx, span, nil, "error decoding request")
h.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
return
}

telemetry.WithAttributes(span,
telemetry.AttributeKV{Key: "datastore-name", Value: request.Name},
telemetry.AttributeKV{Key: "datastore-type", Value: request.Type},
)

cluster, err := h.getClusterForDatastore(ctx, r, project.ID, request.Name)
if err != nil {
err = telemetry.Error(ctx, span, err, "unable to find datastore on any associated cluster")
h.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
return
}

telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "cluster-id", Value: cluster.ID})

helmAgent, err := h.GetHelmAgent(ctx, r, cluster, "ack-system")
if err != nil {
err := telemetry.Error(ctx, span, err, "unable to get helm client for cluster")
h.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
return
}

_, err = helmAgent.GetRelease(ctx, request.Name, 0, false)
if err != nil {
err := telemetry.Error(ctx, span, err, "unable to get helm release")
h.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
return
}

_, err = helmAgent.UninstallChart(ctx, request.Name)
if err != nil {
telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "cluster-id", Value: cluster.ID})
err := telemetry.Error(ctx, span, err, "unable to uninstall chart")
h.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
return
}

// if the release was deleted by helm without error, mark it as accepted
w.WriteHeader(http.StatusAccepted)
}

func (h *DeleteDatastoreHandler) getClusterForDatastore(ctx context.Context, r *http.Request, projectID uint, datastoreName string) (*models.Cluster, error) {
ctx, span := telemetry.NewSpan(ctx, "get-cluster-for-datastore")

if r == nil {
return nil, telemetry.Error(ctx, span, nil, "missing http request object")
}

clusters, err := h.Repo().Cluster().ListClustersByProjectID(projectID)
if err != nil {
return nil, telemetry.Error(ctx, span, err, "unable to get project clusters")
}

for _, cluster := range clusters {
helmAgent, err := h.GetHelmAgent(ctx, r, cluster, "ack-system")
if err != nil {
telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "cluster-id", Value: cluster.ID})
return nil, telemetry.Error(ctx, span, err, "unable to get helm client for cluster")
}

_, err = helmAgent.GetRelease(ctx, datastoreName, 0, false)
if err == nil {
return cluster, nil
}
}

return nil, telemetry.Error(ctx, span, nil, "unable to find datastore on any associated cluster")
}
2 changes: 1 addition & 1 deletion api/server/handlers/datastore/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ type ListDatastoresHandler struct {
authz.KubernetesAgentGetter
}

// NewListDatastoresHandler constructs a datastore ListHandler
// NewListDatastoresHandler constructs a datastore ListDatastoresHandler
func NewListDatastoresHandler(
config *config.Config,
decoderValidator shared.RequestDecoderValidator,
Expand Down
32 changes: 30 additions & 2 deletions api/server/router/datastore.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func getDatastoreRoutes(
}
routes := make([]*router.Route, 0)

// GET /api/projects/{project_id}/cloud-providers/{cloud_provider_type}/{cloud_provider_id}/datastores -> cloud_provider.NewListHandler
// GET /api/projects/{project_id}/cloud-providers/{cloud_provider_type}/{cloud_provider_id}/datastores -> cloud_provider.NewListDatastoresHandler
listEndpoint := factory.NewAPIEndpoint(
&types.APIRequestMetadata{
Verb: types.APIVerbGet,
Expand Down Expand Up @@ -86,7 +86,35 @@ func getDatastoreRoutes(
Router: r,
})

// GET /api/projects/{project_id}/cloud-providers/{cloud_provider_type}/{cloud_provider_id}/datastores/{datastore_type}/{datastore_name} -> cloud_provider.NewListHandler
// DELETE /api/projects/{project_id}/cloud-providers/{cloud_provider_type}/{cloud_provider_id}/datastores -> cloud_provider.NewDeleteDatastoreHandler
deleteEndpoint := factory.NewAPIEndpoint(
&types.APIRequestMetadata{
Verb: types.APIVerbDelete,
Method: types.HTTPVerbDelete,
Path: &types.Path{
Parent: basePath,
RelativePath: fmt.Sprintf("%s/{%s}/{%s}/datastores", relPath, types.URLParamCloudProviderType, types.URLParamCloudProviderID),
},
Scopes: []types.PermissionScope{
types.UserScope,
types.ProjectScope,
},
},
)

deleteHandler := datastore.NewDeleteDatastoreHandler(
config,
factory.GetDecoderValidator(),
factory.GetResultWriter(),
)

routes = append(routes, &router.Route{
Endpoint: deleteEndpoint,
Handler: deleteHandler,
Router: r,
})

// GET /api/projects/{project_id}/cloud-providers/{cloud_provider_type}/{cloud_provider_id}/datastores/{datastore_type}/{datastore_name} -> cloud_provider.NewListDatastoresHandler
getEndpoint := factory.NewAPIEndpoint(
&types.APIRequestMetadata{
Verb: types.APIVerbGet,
Expand Down
86 changes: 47 additions & 39 deletions dashboard/src/main/home/Home.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import React, { useEffect, useState, useContext, useRef } from "react";
import React, { useContext, useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { Route, RouteComponentProps, Switch, withRouter } from "react-router";
import styled, { ThemeProvider } from "styled-components";
import { createPortal } from "react-dom";

import api from "shared/api";
import midnight from "shared/themes/midnight";
import standard from "shared/themes/standard";
import { Context } from "shared/Context";
import { PorterUrl, pushFiltered, pushQueryParams } from "shared/routing";
import { ClusterType, ProjectType, ProjectListType } from "shared/types";
import midnight from "shared/themes/midnight";
import standard from "shared/themes/standard";
import { ClusterType, ProjectListType, ProjectType } from "shared/types";

import ConfirmOverlay from "components/ConfirmOverlay";
import Loading from "components/Loading";
Expand All @@ -17,37 +17,39 @@ import Dashboard from "./dashboard/Dashboard";
import Integrations from "./integrations/Integrations";
import LaunchWrapper from "./launch/LaunchWrapper";

import AddOnDashboard from "./add-on-dashboard/AddOnDashboard";
import AppDashboard from "./app-dashboard/AppDashboard";
import CreateDatabase from "./database-dashboard/CreateDatabase";
import DatabaseDashboard from "./database-dashboard/DatabaseDashboard";
import Navbar from "./navbar/Navbar";
import ProjectSettings from "./project-settings/ProjectSettings";
import Sidebar from "./sidebar/Sidebar";
import AppDashboard from "./app-dashboard/AppDashboard";
import AddOnDashboard from "./add-on-dashboard/AddOnDashboard";
import DatabaseDashboard from "./database-dashboard/DatabaseDashboard";
import CreateDatabase from "./database-dashboard/CreateDatabase";

import { fakeGuardedRoute } from "shared/auth/RouteGuard";
import { withAuth, WithAuthProps } from "shared/auth/AuthorizationHoc";
import discordLogo from "../../assets/discord.svg";
import Onboarding from "./onboarding/Onboarding";
import ModalHandler from "./ModalHandler";
import { NewProjectFC } from "./new-project/NewProject";
import InfrastructureRouter from "./infrastructure/InfrastructureRouter";
import { overrideInfraTabEnabled } from "utils/infrastructure";
import NoClusterPlaceHolder from "components/NoClusterPlaceHolder";
import NewAddOnFlow from "./add-on-dashboard/NewAddOnFlow";
import Button from "components/porter/Button";
import Modal from "components/porter/Modal";
import Text from "components/porter/Text";
import Spacer from "components/porter/Spacer";
import Button from "components/porter/Button";
import NewAppFlow from "./app-dashboard/new-app-flow/NewAppFlow";
import ExpandedApp from "./app-dashboard/expanded-app/ExpandedApp";
import CreateApp from "./app-dashboard/create-app/CreateApp";
import Text from "components/porter/Text";
import { withAuth, WithAuthProps } from "shared/auth/AuthorizationHoc";
import { fakeGuardedRoute } from "shared/auth/RouteGuard";
import ClusterResourcesProvider from "shared/ClusterResourcesContext";
import DeploymentTargetProvider from "shared/DeploymentTargetContext";
import { overrideInfraTabEnabled } from "utils/infrastructure";
import discordLogo from "../../assets/discord.svg";
import NewAddOnFlow from "./add-on-dashboard/NewAddOnFlow";
import AppView from "./app-dashboard/app-view/AppView";
import Apps from "./app-dashboard/apps/Apps";
import DeploymentTargetProvider from "shared/DeploymentTargetContext";
import CreateApp from "./app-dashboard/create-app/CreateApp";
import ExpandedApp from "./app-dashboard/expanded-app/ExpandedApp";
import NewAppFlow from "./app-dashboard/new-app-flow/NewAppFlow";
import PreviewEnvs from "./cluster-dashboard/preview-environments/v2/PreviewEnvs";
import SetupApp from "./cluster-dashboard/preview-environments/v2/setup-app/SetupApp";
import ClusterResourcesProvider from "shared/ClusterResourcesContext";
import DatabaseView from "./database-dashboard/DatabaseView";
import InfrastructureRouter from "./infrastructure/InfrastructureRouter";
import ModalHandler from "./ModalHandler";
import { NewProjectFC } from "./new-project/NewProject";
import Onboarding from "./onboarding/Onboarding";


// Guarded components
const GuardedProjectSettings = fakeGuardedRoute("settings", "", [
Expand Down Expand Up @@ -198,7 +200,7 @@ const Home: React.FC<Props> = (props) => {
} else {
setHasFinishedOnboarding(true);
}
} catch (error) {}
} catch (error) { }
};

useEffect(() => {
Expand Down Expand Up @@ -460,8 +462,14 @@ const Home: React.FC<Props> = (props) => {
<Route path="/databases/new">
<CreateDatabase />
</Route>
<Route path="/databases/:projectId/:cloudProviderName/:cloudProviderId/:datastoreName/:tab">
<DatabaseView />
</Route>
<Route path="/databases/:projectId/:cloudProviderName/:cloudProviderId/:datastoreName">
<DatabaseView />
</Route>
<Route path="/databases">
<DatabaseDashboard />
<DatabaseDashboard projectId={currentProject?.id} />
</Route>

<Route path="/addons/new">
Expand All @@ -486,17 +494,17 @@ const Home: React.FC<Props> = (props) => {
overrideInfraTabEnabled({
projectID: currentProject?.id,
})) && (
<Route
path="/infrastructure"
render={() => {
return (
<DashboardWrapper>
<InfrastructureRouter />
</DashboardWrapper>
);
}}
/>
)}
<Route
path="/infrastructure"
render={() => {
return (
<DashboardWrapper>
<InfrastructureRouter />
</DashboardWrapper>
);
}}
/>
)}
<Route
path="/dashboard"
render={() => {
Expand Down Expand Up @@ -553,7 +561,7 @@ const Home: React.FC<Props> = (props) => {
render={() => <GuardedProjectSettings />}
/>
{currentProject?.validate_apply_v2 &&
currentProject.preview_envs_enabled ? (
currentProject.preview_envs_enabled ? (
<>
<Route exact path="/preview-environments/configure">
<SetupApp />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import EnvEditorModal from "main/home/modals/EnvEditorModal";
import Modal from "main/home/modals/Modal";
import React, { useEffect, useState } from "react";
import styled from "styled-components";
import Modal from "main/home/modals/Modal";
import EnvEditorModal from "main/home/modals/EnvEditorModal";

import upload from "assets/upload.svg";
import { MultiLineInput } from "components/porter-form/field-components/KeyValueArray";
import { dotenv_parse } from "shared/string_utils";

export type KeyValueType = {
Expand Down Expand Up @@ -348,7 +347,7 @@ const Label = styled.div`

const StyledInputArray = styled.div`
margin-bottom: 15px;
margin-top: 22px;
margin-top: 10px;
`;

export const MultiLineInputer = styled.textarea<InputProps>`
Expand Down
Loading

0 comments on commit e0b0a1a

Please sign in to comment.