diff --git a/api/server/handlers/datastore/update.go b/api/server/handlers/datastore/update.go
index 7a2a335387..7fc92d1455 100644
--- a/api/server/handlers/datastore/update.go
+++ b/api/server/handlers/datastore/update.go
@@ -190,6 +190,9 @@ func (h *UpdateDatastoreHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
case "NEON":
datastoreProto.Kind = porterv1.EnumDatastoreKind_ENUM_DATASTORE_KIND_NEON
datastoreProto.KindValues = &porterv1.ManagedDatastore_NeonKind{}
+ case "UPSTASH":
+ datastoreProto.Kind = porterv1.EnumDatastoreKind_ENUM_DATASTORE_KIND_UPSTASH
+ datastoreProto.KindValues = &porterv1.ManagedDatastore_UpstashKind{}
default:
err = telemetry.Error(ctx, span, nil, "invalid datastore type")
h.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
diff --git a/api/server/handlers/neon_integration/list.go b/api/server/handlers/neon_integration/list.go
index e250e6e587..f6f1dddc32 100644
--- a/api/server/handlers/neon_integration/list.go
+++ b/api/server/handlers/neon_integration/list.go
@@ -13,7 +13,7 @@ import (
"github.com/porter-dev/porter/internal/telemetry"
)
-// ListNeonIntegrationsHandler is a struct for listing all noen integrations for a given project
+// ListNeonIntegrationsHandler is a struct for listing all neon integrations for a given project
type ListNeonIntegrationsHandler struct {
handlers.PorterHandlerReadWriter
}
diff --git a/api/server/handlers/oauth_callback/upstash.go b/api/server/handlers/oauth_callback/upstash.go
index 97622b9df9..3907eb9efc 100644
--- a/api/server/handlers/oauth_callback/upstash.go
+++ b/api/server/handlers/oauth_callback/upstash.go
@@ -111,6 +111,7 @@ func (p *OAuthCallbackUpstashHandler) ServeHTTP(w http.ResponseWriter, r *http.R
oauthInt := integrations.UpstashIntegration{
SharedOAuthModel: integrations.SharedOAuthModel{
+ ClientID: []byte(p.Config().UpstashConf.ClientID),
AccessToken: []byte(token.AccessToken),
RefreshToken: []byte(token.RefreshToken),
Expiry: token.Expiry,
diff --git a/api/server/handlers/upstash_integration/list.go b/api/server/handlers/upstash_integration/list.go
new file mode 100644
index 0000000000..20c4f79ad8
--- /dev/null
+++ b/api/server/handlers/upstash_integration/list.go
@@ -0,0 +1,69 @@
+package upstash_integration
+
+import (
+ "net/http"
+ "time"
+
+ "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"
+)
+
+// ListUpstashIntegrationsHandler is a struct for listing all upstash integrations for a given project
+type ListUpstashIntegrationsHandler struct {
+ handlers.PorterHandlerReadWriter
+}
+
+// NewListUpstashIntegrationsHandler constructs a ListUpstashIntegrationsHandler
+func NewListUpstashIntegrationsHandler(
+ config *config.Config,
+ decoderValidator shared.RequestDecoderValidator,
+ writer shared.ResultWriter,
+) *ListUpstashIntegrationsHandler {
+ return &ListUpstashIntegrationsHandler{
+ PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+ }
+}
+
+// UpstashIntegration describes a upstash integration
+type UpstashIntegration struct {
+ CreatedAt time.Time `json:"created_at"`
+}
+
+// ListUpstashIntegrationsResponse describes the list upstash integrations response body
+type ListUpstashIntegrationsResponse struct {
+ // Integrations is a list of upstash integrations
+ Integrations []UpstashIntegration `json:"integrations"`
+}
+
+// ServeHTTP returns a list of upstash integrations associated with the specified project
+func (h *ListUpstashIntegrationsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ ctx, span := telemetry.NewSpan(r.Context(), "serve-list-upstash-integrations")
+ defer span.End()
+
+ project, _ := ctx.Value(types.ProjectScope).(*models.Project)
+
+ resp := ListUpstashIntegrationsResponse{}
+ integrationList := make([]UpstashIntegration, 0)
+
+ integrations, err := h.Repo().UpstashIntegration().Integrations(ctx, project.ID)
+ if err != nil {
+ err := telemetry.Error(ctx, span, err, "error getting datastores")
+ h.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+ return
+ }
+
+ for _, int := range integrations {
+ integrationList = append(integrationList, UpstashIntegration{
+ CreatedAt: int.CreatedAt,
+ })
+ }
+
+ resp.Integrations = integrationList
+
+ h.WriteResult(w, r, resp)
+}
diff --git a/api/server/router/project.go b/api/server/router/project.go
index 2b30b03103..5a922f33bb 100644
--- a/api/server/router/project.go
+++ b/api/server/router/project.go
@@ -5,6 +5,7 @@ import (
"github.com/porter-dev/porter/api/server/handlers/cloud_provider"
"github.com/porter-dev/porter/api/server/handlers/neon_integration"
+ "github.com/porter-dev/porter/api/server/handlers/upstash_integration"
"github.com/porter-dev/porter/api/server/handlers/deployment_target"
@@ -2049,5 +2050,32 @@ func getProjectRoutes(
Router: r,
})
+ // GET /api/projects/{project_id}/upstash-integrations -> apiContract.NewListUpstashIntegrationsHandler
+ listUpstashIntegrationsEndpoint := factory.NewAPIEndpoint(
+ &types.APIRequestMetadata{
+ Verb: types.APIVerbGet,
+ Method: types.HTTPVerbGet,
+ Path: &types.Path{
+ Parent: basePath,
+ RelativePath: relPath + "/upstash-integrations",
+ },
+ Scopes: []types.PermissionScope{
+ types.UserScope,
+ types.ProjectScope,
+ },
+ },
+ )
+
+ listUpstashIntegrationsHandler := upstash_integration.NewListUpstashIntegrationsHandler(
+ config,
+ factory.GetDecoderValidator(),
+ factory.GetResultWriter(),
+ )
+ routes = append(routes, &router.Route{
+ Endpoint: listUpstashIntegrationsEndpoint,
+ Handler: listUpstashIntegrationsHandler,
+ Router: r,
+ })
+
return routes, newPath
}
diff --git a/dashboard/src/assets/upstash.svg b/dashboard/src/assets/upstash.svg
new file mode 100644
index 0000000000..e928467552
--- /dev/null
+++ b/dashboard/src/assets/upstash.svg
@@ -0,0 +1,15 @@
+
diff --git a/dashboard/src/lib/databases/types.ts b/dashboard/src/lib/databases/types.ts
index 0d49a168a3..84fb81aa13 100644
--- a/dashboard/src/lib/databases/types.ts
+++ b/dashboard/src/lib/databases/types.ts
@@ -37,6 +37,7 @@ const datastoreTypeValidator = z.enum([
"MANAGED_REDIS",
"MANAGED_POSTGRES",
"NEON",
+ "UPSTASH",
]);
const datastoreEngineValidator = z.enum([
"UNKNOWN",
@@ -114,6 +115,10 @@ export const DATASTORE_TYPE_NEON: DatastoreType = {
name: "NEON" as const,
displayName: "Neon",
};
+export const DATASTORE_TYPE_UPSTASH: DatastoreType = {
+ name: "UPSTASH" as const,
+ displayName: "Upstash",
+};
export type DatastoreState = {
state: z.infer["status"];
@@ -334,6 +339,10 @@ const neonValidator = z.object({
type: z.literal("neon"),
});
+const upstashValidator = z.object({
+ type: z.literal("upstash"),
+});
+
export const dbFormValidator = z.object({
name: z
.string()
@@ -355,6 +364,7 @@ export const dbFormValidator = z.object({
managedRedisConfigValidator,
managedPostgresConfigValidator,
neonValidator,
+ upstashValidator,
]),
clusterId: z.number(),
});
diff --git a/dashboard/src/lib/hooks/useDatastore.ts b/dashboard/src/lib/hooks/useDatastore.ts
index 9031ca5a37..2401aab029 100644
--- a/dashboard/src/lib/hooks/useDatastore.ts
+++ b/dashboard/src/lib/hooks/useDatastore.ts
@@ -21,7 +21,13 @@ type DatastoreHook = {
};
type CreateDatastoreInput = {
name: string;
- type: "RDS" | "ELASTICACHE" | "MANAGED-POSTGRES" | "MANAGED-REDIS" | "NEON";
+ type:
+ | "RDS"
+ | "ELASTICACHE"
+ | "MANAGED-POSTGRES"
+ | "MANAGED-REDIS"
+ | "NEON"
+ | "UPSTASH";
engine: "POSTGRES" | "AURORA-POSTGRES" | "REDIS";
values: object;
};
@@ -147,6 +153,19 @@ const clientDbToCreateInput = (values: DbFormData): CreateDatastoreInput => {
engine: "POSTGRES",
})
)
+ .with(
+ { config: { type: "upstash" } },
+ (values): CreateDatastoreInput => ({
+ name: values.name,
+ values: {
+ config: {
+ name: values.name,
+ },
+ },
+ type: "UPSTASH",
+ engine: "REDIS",
+ })
+ )
.exhaustive();
};
diff --git a/dashboard/src/lib/hooks/useUpstash.ts b/dashboard/src/lib/hooks/useUpstash.ts
new file mode 100644
index 0000000000..5945a323cd
--- /dev/null
+++ b/dashboard/src/lib/hooks/useUpstash.ts
@@ -0,0 +1,41 @@
+import { z } from "zod";
+
+import {
+ upstashIntegrationValidator,
+ type ClientUpstashIntegration,
+} from "lib/upstash/types";
+
+import api from "shared/api";
+
+type TUseUpstash = {
+ getUpstashIntegrations: ({
+ projectId,
+ }: {
+ projectId: number;
+ }) => Promise;
+};
+export const useUpstash = (): TUseUpstash => {
+ const getUpstashIntegrations = async ({
+ projectId,
+ }: {
+ projectId: number;
+ }): Promise => {
+ const response = await api.getUpstashIntegrations(
+ "",
+ {},
+ {
+ projectId,
+ }
+ );
+
+ const results = await z
+ .object({ integrations: z.array(upstashIntegrationValidator) })
+ .parseAsync(response.data);
+
+ return results.integrations;
+ };
+
+ return {
+ getUpstashIntegrations,
+ };
+};
diff --git a/dashboard/src/lib/upstash/types.ts b/dashboard/src/lib/upstash/types.ts
new file mode 100644
index 0000000000..6aa578ec27
--- /dev/null
+++ b/dashboard/src/lib/upstash/types.ts
@@ -0,0 +1,8 @@
+import { z } from "zod";
+
+export const upstashIntegrationValidator = z.object({
+ created_at: z.string(),
+});
+export type ClientUpstashIntegration = z.infer<
+ typeof upstashIntegrationValidator
+>;
diff --git a/dashboard/src/main/home/database-dashboard/DatastoreFormContextProvider.tsx b/dashboard/src/main/home/database-dashboard/DatastoreFormContextProvider.tsx
index d2cba34e90..34f3838a3f 100644
--- a/dashboard/src/main/home/database-dashboard/DatastoreFormContextProvider.tsx
+++ b/dashboard/src/main/home/database-dashboard/DatastoreFormContextProvider.tsx
@@ -12,10 +12,14 @@ import { useDatastoreList } from "lib/hooks/useDatabaseList";
import { useDatastore } from "lib/hooks/useDatastore";
import { useIntercom } from "lib/hooks/useIntercom";
import { useNeon } from "lib/hooks/useNeon";
+import { useUpstash } from "lib/hooks/useUpstash";
import { Context } from "shared/Context";
-import NeonIntegrationModal from "./shared/NeonIntegrationModal";
+import {
+ NeonIntegrationModal,
+ UpstashIntegrationModal,
+} from "./shared/NeonIntegrationModal";
// todo(ianedwards): refactor button to use more predictable state
export type UpdateDatastoreButtonProps = {
@@ -53,8 +57,11 @@ const DatastoreFormContextProvider: React.FC<
const [updateDatastoreError, setUpdateDatastoreError] = useState("");
const { getNeonIntegrations } = useNeon();
+ const { getUpstashIntegrations } = useUpstash();
const [showNeonIntegrationModal, setShowNeonIntegrationModal] =
useState(false);
+ const [showUpstashIntegrationModal, setShowUpstashIntegrationModal] =
+ useState(false);
const { showIntercomWithMessage } = useIntercom();
@@ -117,6 +124,16 @@ const DatastoreFormContextProvider: React.FC<
return;
}
}
+ if (data.config.type === "upstash") {
+ const integrations = await getUpstashIntegrations({
+ projectId: currentProject.id,
+ });
+ if (integrations.length === 0) {
+ setShowUpstashIntegrationModal(true);
+ return;
+ }
+ }
+
await createDatastore(data);
history.push(`/datastores/${data.name}`);
} catch (err) {
@@ -154,6 +171,13 @@ const DatastoreFormContextProvider: React.FC<
}}
/>
)}
+ {showUpstashIntegrationModal && (
+ {
+ setShowUpstashIntegrationModal(false);
+ }}
+ />
+ )}
);
};
diff --git a/dashboard/src/main/home/database-dashboard/DatastoreProvisioningIndicator.tsx b/dashboard/src/main/home/database-dashboard/DatastoreProvisioningIndicator.tsx
index 30c8783ef8..6e114a8dea 100644
--- a/dashboard/src/main/home/database-dashboard/DatastoreProvisioningIndicator.tsx
+++ b/dashboard/src/main/home/database-dashboard/DatastoreProvisioningIndicator.tsx
@@ -2,7 +2,10 @@ import React, { useMemo } from "react";
import StatusBar from "components/porter/StatusBar";
-import { DATASTORE_TEMPLATE_NEON } from "./constants";
+import {
+ DATASTORE_TEMPLATE_NEON,
+ DATASTORE_TEMPLATE_UPSTASH,
+} from "./constants";
import { useDatastoreContext } from "./DatabaseContextProvider";
const DatastoreProvisioningIndicator: React.FC = () => {
@@ -40,9 +43,13 @@ const DatastoreProvisioningIndicator: React.FC = () => {
return { percentCompleted, title, titleDescriptor, isCreating };
}, [datastore]);
+ // TODO: refactor this so the template has the setup/deletion time
const subtitle = useMemo(() => {
return `${isCreating ? "Setup" : "Deletion"} can take up to ${
- datastore.template === DATASTORE_TEMPLATE_NEON ? 5 : 20
+ datastore.template === DATASTORE_TEMPLATE_NEON ||
+ datastore.template === DATASTORE_TEMPLATE_UPSTASH
+ ? 5
+ : 20
} minutes. You can close this window and come back later.`;
}, [datastore]);
diff --git a/dashboard/src/main/home/database-dashboard/constants.ts b/dashboard/src/main/home/database-dashboard/constants.ts
index 6aed92c2e8..e0584a9976 100644
--- a/dashboard/src/main/home/database-dashboard/constants.ts
+++ b/dashboard/src/main/home/database-dashboard/constants.ts
@@ -15,6 +15,7 @@ import {
DATASTORE_TYPE_MANAGED_REDIS,
DATASTORE_TYPE_NEON,
DATASTORE_TYPE_RDS,
+ DATASTORE_TYPE_UPSTASH,
type DatastoreEngine,
type DatastoreTemplate,
} from "lib/databases/types";
@@ -25,6 +26,7 @@ import infra from "assets/cluster.svg";
import neon from "assets/neon.svg";
import postgresql from "assets/postgresql.svg";
import redis from "assets/redis.svg";
+import upstash from "assets/upstash.svg";
import ConfigurationTab from "./tabs/ConfigurationTab";
import ConnectTab from "./tabs/ConnectTab";
@@ -418,6 +420,41 @@ export const DATASTORE_TEMPLATE_NEON: DatastoreTemplate = Object.freeze({
],
});
+export const DATASTORE_TEMPLATE_UPSTASH: DatastoreTemplate = Object.freeze({
+ name: "Upstash",
+ displayName: "Upstash",
+ highLevelType: DATASTORE_ENGINE_REDIS,
+ type: DATASTORE_TYPE_UPSTASH,
+ engine: DATASTORE_ENGINE_REDIS,
+ supportedEngineVersions: [],
+ icon: upstash as string,
+ description:
+ "A fully managed serverless Postgres. Neon separates storage and compute to offer autoscaling, branching, and bottomless storage.",
+ disabled: true,
+ instanceTiers: [],
+ creationStateProgression: [
+ DATASTORE_STATE_CREATING,
+ DATASTORE_STATE_AVAILABLE,
+ ],
+ deletionStateProgression: [
+ DATASTORE_STATE_AWAITING_DELETION,
+ DATASTORE_STATE_DELETING_RECORD,
+ DATASTORE_STATE_DELETED,
+ ],
+ tabs: [
+ {
+ name: "connectivity",
+ displayName: "Connectivity",
+ component: PublicDatastoreConnectTab,
+ },
+ {
+ name: "settings",
+ displayName: "Settings",
+ component: SettingsTab,
+ },
+ ],
+});
+
export const SUPPORTED_DATASTORE_TEMPLATES: DatastoreTemplate[] = [
DATASTORE_TEMPLATE_AWS_RDS,
DATASTORE_TEMPLATE_AWS_AURORA,
@@ -425,4 +462,5 @@ export const SUPPORTED_DATASTORE_TEMPLATES: DatastoreTemplate[] = [
DATASTORE_TEMPLATE_MANAGED_POSTGRES,
DATASTORE_TEMPLATE_MANAGED_REDIS,
DATASTORE_TEMPLATE_NEON,
+ DATASTORE_TEMPLATE_UPSTASH,
];
diff --git a/dashboard/src/main/home/database-dashboard/forms/SandboxDatastoreForm.tsx b/dashboard/src/main/home/database-dashboard/forms/SandboxDatastoreForm.tsx
index 70590cf62d..36ae1b1dd3 100644
--- a/dashboard/src/main/home/database-dashboard/forms/SandboxDatastoreForm.tsx
+++ b/dashboard/src/main/home/database-dashboard/forms/SandboxDatastoreForm.tsx
@@ -24,6 +24,7 @@ import {
DATASTORE_ENGINE_POSTGRES,
DATASTORE_ENGINE_REDIS,
DATASTORE_TEMPLATE_NEON,
+ DATASTORE_TEMPLATE_UPSTASH,
SUPPORTED_DATASTORE_TEMPLATES,
} from "../constants";
import { useDatastoreFormContext } from "../DatastoreFormContextProvider";
@@ -48,21 +49,14 @@ const SandboxDatastoreForm: React.FC = () => {
const { updateDatastoreButtonProps } = useDatastoreFormContext();
const availableEngines: BlockSelectOption[] = useMemo(() => {
- return [
- DATASTORE_ENGINE_POSTGRES,
- {
- ...DATASTORE_ENGINE_REDIS,
- disabledOpts: {
- tooltipText: "Coming soon!",
- },
- },
- ];
+ return [DATASTORE_ENGINE_POSTGRES, DATASTORE_ENGINE_REDIS];
}, [watchClusterId]);
const availableHostTypes: BlockSelectOption[] = useMemo(() => {
- const options = [DATASTORE_TEMPLATE_NEON].filter(
- (t) => t.highLevelType.name === watchEngine
- );
+ const options = [
+ DATASTORE_TEMPLATE_NEON,
+ DATASTORE_TEMPLATE_UPSTASH,
+ ].filter((t) => t.highLevelType.name === watchEngine);
return options;
}, [watchEngine]);
diff --git a/dashboard/src/main/home/database-dashboard/shared/ConnectionInfo.tsx b/dashboard/src/main/home/database-dashboard/shared/ConnectionInfo.tsx
index 750da78703..0e64e6d1bb 100644
--- a/dashboard/src/main/home/database-dashboard/shared/ConnectionInfo.tsx
+++ b/dashboard/src/main/home/database-dashboard/shared/ConnectionInfo.tsx
@@ -13,6 +13,7 @@ import {
DATASTORE_TYPE_MANAGED_REDIS,
DATASTORE_TYPE_NEON,
DATASTORE_TYPE_RDS,
+ DATASTORE_TYPE_UPSTASH,
type DatastoreConnectionInfo,
type DatastoreTemplate,
} from "lib/databases/types";
@@ -39,6 +40,11 @@ const ConnectionInfo: React.FC = ({ connectionInfo, template }) => {
() =>
`rediss://:${connectionInfo.password}@${connectionInfo.host}:${connectionInfo.port}/0?ssl_cert_reqs=CERT_REQUIRED`
)
+ .with(
+ { type: DATASTORE_TYPE_UPSTASH },
+ () =>
+ `rediss://default:${connectionInfo.password}@${connectionInfo.host}:${connectionInfo.port}/0`
+ )
.with(
{ type: DATASTORE_TYPE_MANAGED_REDIS },
() =>
diff --git a/dashboard/src/main/home/database-dashboard/shared/NeonIntegrationModal.tsx b/dashboard/src/main/home/database-dashboard/shared/NeonIntegrationModal.tsx
index 3c0118efb8..ec5199401d 100644
--- a/dashboard/src/main/home/database-dashboard/shared/NeonIntegrationModal.tsx
+++ b/dashboard/src/main/home/database-dashboard/shared/NeonIntegrationModal.tsx
@@ -7,6 +7,7 @@ import Spacer from "components/porter/Spacer";
import Text from "components/porter/Text";
import { useAuthWindow } from "lib/hooks/useAuthWindow";
import { useNeon } from "lib/hooks/useNeon";
+import { useUpstash } from "lib/hooks/useUpstash";
import { useDatastoreFormContext } from "../DatastoreFormContextProvider";
@@ -14,7 +15,7 @@ type Props = {
onClose: () => void;
};
-const NeonIntegrationModal: React.FC = ({ onClose }) => {
+export const NeonIntegrationModal: React.FC = ({ onClose }) => {
const { projectId } = useDatastoreFormContext();
const { getNeonIntegrations } = useNeon();
const { openAuthWindow } = useAuthWindow({
@@ -57,4 +58,45 @@ const NeonIntegrationModal: React.FC = ({ onClose }) => {
);
};
-export default NeonIntegrationModal;
+export const UpstashIntegrationModal: React.FC = ({ onClose }) => {
+ const { projectId } = useDatastoreFormContext();
+ const { getUpstashIntegrations } = useUpstash();
+ const { openAuthWindow } = useAuthWindow({
+ authUrl: `/api/projects/${projectId}/oauth/upstash`,
+ });
+
+ const upstashIntegrationsResp = useQuery(
+ ["getUpstashIntegrations", projectId],
+ async () => {
+ const integrations = await getUpstashIntegrations({
+ projectId,
+ });
+ return integrations;
+ },
+ {
+ enabled: !!projectId,
+ refetchInterval: 1000,
+ }
+ );
+ useEffect(() => {
+ if (
+ upstashIntegrationsResp.isSuccess &&
+ upstashIntegrationsResp.data.length > 0
+ ) {
+ onClose();
+ }
+ }, [upstashIntegrationsResp]);
+
+ return (
+
+ Integrate Upstash
+
+
+ To continue, you must authenticate with Upstash.{" "}
+
+ Authorize Porter to create Upstash datastores on your behalf
+
+
+
+ );
+};
diff --git a/dashboard/src/shared/api.tsx b/dashboard/src/shared/api.tsx
index a8f19ccc25..da73c5a746 100644
--- a/dashboard/src/shared/api.tsx
+++ b/dashboard/src/shared/api.tsx
@@ -2068,6 +2068,13 @@ const getNeonIntegrations = baseApi<{}, { projectId: number }>(
}
);
+const getUpstashIntegrations = baseApi<{}, { projectId: number }>(
+ "GET",
+ ({ projectId }) => {
+ return `/api/projects/${projectId}/upstash-integrations`;
+ }
+);
+
const getRevisions = baseApi<
{},
{ id: number; cluster_id: number; namespace: string; name: string }
@@ -2889,7 +2896,13 @@ const getDatastoreCredential = baseApi<
const updateDatastore = baseApi<
{
name: string;
- type: "RDS" | "ELASTICACHE" | "MANAGED-POSTGRES" | "MANAGED-REDIS" | "NEON";
+ type:
+ | "RDS"
+ | "ELASTICACHE"
+ | "MANAGED-POSTGRES"
+ | "MANAGED-REDIS"
+ | "NEON"
+ | "UPSTASH";
engine: "POSTGRES" | "AURORA-POSTGRES" | "REDIS";
values: any;
@@ -3895,6 +3908,7 @@ export default {
getRepoIntegrations,
getSlackIntegrations,
getNeonIntegrations,
+ getUpstashIntegrations,
getRepos,
getRevisions,
getTemplateInfo,
diff --git a/go.mod b/go.mod
index 73ab45ba82..4124f28a99 100644
--- a/go.mod
+++ b/go.mod
@@ -2,6 +2,9 @@ module github.com/porter-dev/porter
go 1.20
+// replace api-contracts with local
+replace github.com/porter-dev/api-contracts => ../api-contracts
+
require (
cloud.google.com/go v0.110.2 // indirect
github.com/AlecAivazis/survey/v2 v2.2.9
diff --git a/internal/repository/gorm/upstash.go b/internal/repository/gorm/upstash.go
index 2e39b5b67a..34b3b04192 100644
--- a/internal/repository/gorm/upstash.go
+++ b/internal/repository/gorm/upstash.go
@@ -42,6 +42,31 @@ func (repo *UpstashIntegrationRepository) Insert(
return created, nil
}
+// Integrations returns all upstash integrations for a given project
+func (repo *UpstashIntegrationRepository) Integrations(
+ ctx context.Context, projectID uint,
+) ([]ints.UpstashIntegration, error) {
+ ctx, span := telemetry.NewSpan(ctx, "gorm-list-upstash-integrations")
+ defer span.End()
+
+ var integrations []ints.UpstashIntegration
+
+ if err := repo.db.Where("project_id = ?", projectID).Find(&integrations).Error; err != nil {
+ return integrations, telemetry.Error(ctx, span, err, "failed to list upstash integrations")
+ }
+
+ for i, integration := range integrations {
+ decrypted, err := repo.DecryptUpstashIntegration(integration, repo.key)
+ if err != nil {
+ return integrations, telemetry.Error(ctx, span, err, "failed to decrypt")
+ }
+
+ integrations[i] = decrypted
+ }
+
+ return integrations, nil
+}
+
// EncryptUpstashIntegration will encrypt the upstash integration data before
// writing to the DB
func (repo *UpstashIntegrationRepository) EncryptUpstashIntegration(
@@ -88,3 +113,50 @@ func (repo *UpstashIntegrationRepository) EncryptUpstashIntegration(
return encrypted, nil
}
+
+// DecryptUpstashIntegration will decrypt the upstash integration data before
+// returning it from the DB
+func (repo *UpstashIntegrationRepository) DecryptUpstashIntegration(
+ upstashInt ints.UpstashIntegration,
+ key *[32]byte,
+) (ints.UpstashIntegration, error) {
+ decrypted := upstashInt
+
+ if len(decrypted.ClientID) > 0 {
+ plaintext, err := encryption.Decrypt(decrypted.ClientID, key)
+ if err != nil {
+ return decrypted, err
+ }
+
+ decrypted.ClientID = plaintext
+ }
+
+ if len(decrypted.AccessToken) > 0 {
+ plaintext, err := encryption.Decrypt(decrypted.AccessToken, key)
+ if err != nil {
+ return decrypted, err
+ }
+
+ decrypted.AccessToken = plaintext
+ }
+
+ if len(decrypted.RefreshToken) > 0 {
+ plaintext, err := encryption.Decrypt(decrypted.RefreshToken, key)
+ if err != nil {
+ return decrypted, err
+ }
+
+ decrypted.RefreshToken = plaintext
+ }
+
+ if len(decrypted.DeveloperApiKey) > 0 {
+ plaintext, err := encryption.Decrypt(decrypted.DeveloperApiKey, key)
+ if err != nil {
+ return decrypted, err
+ }
+
+ decrypted.DeveloperApiKey = plaintext
+ }
+
+ return decrypted, nil
+}
diff --git a/internal/repository/upstash.go b/internal/repository/upstash.go
index 18b4629579..56fc00ef16 100644
--- a/internal/repository/upstash.go
+++ b/internal/repository/upstash.go
@@ -10,4 +10,6 @@ import (
type UpstashIntegrationRepository interface {
// Insert creates a new upstash integration
Insert(ctx context.Context, upstashInt ints.UpstashIntegration) (ints.UpstashIntegration, error)
+ // Integrations returns all upstash integrations belonging to a project
+ Integrations(ctx context.Context, projectID uint) ([]ints.UpstashIntegration, error)
}