Skip to content

Commit

Permalink
Merge branch 'master' into unblock-neon-sandbox
Browse files Browse the repository at this point in the history
  • Loading branch information
Feroze Mohideen authored May 14, 2024
2 parents 199055a + 82db8f4 commit aade2b7
Show file tree
Hide file tree
Showing 33 changed files with 586 additions and 55 deletions.
42 changes: 42 additions & 0 deletions api/server/handlers/billing/ingest.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
package billing

import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"

Expand Down Expand Up @@ -80,5 +83,44 @@ func (c *IngestEventsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
return
}

// Call the ingest health endpoint
err = c.postIngestHealthEndpoint(ctx, proj.ID)
if err != nil {
err := telemetry.Error(ctx, span, err, "error calling ingest health endpoint")
c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
return
}

c.WriteResult(w, r, "")
}

func (c *IngestEventsHandler) postIngestHealthEndpoint(ctx context.Context, projectID uint) (err error) {
ctx, span := telemetry.NewSpan(ctx, "post-ingest-health-endpoint")
defer span.End()

// Call the ingest check webhook
webhookUrl := c.Config().ServerConf.IngestStatusWebhookUrl
telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "ingest-status-webhook-url", Value: webhookUrl})

if webhookUrl == "" {
return nil
}

req := struct {
ProjectID uint `json:"project_id"`
}{
ProjectID: projectID,
}

reqBody, err := json.Marshal(req)
if err != nil {
return telemetry.Error(ctx, span, err, "error marshalling ingest status webhook request")
}

client := &http.Client{}
resp, err := client.Post(webhookUrl, "application/json", bytes.NewBuffer(reqBody))
if err != nil || resp.StatusCode != http.StatusOK {
return telemetry.Error(ctx, span, err, "error sending ingest status webhook request")
}
return nil
}
3 changes: 3 additions & 0 deletions api/server/handlers/datastore/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
2 changes: 1 addition & 1 deletion api/server/handlers/neon_integration/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
22 changes: 22 additions & 0 deletions api/server/handlers/oauth_callback/upstash.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"net/url"
"time"

"github.com/golang-jwt/jwt"
"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"
Expand Down Expand Up @@ -100,6 +101,25 @@ func (p *OAuthCallbackUpstashHandler) ServeHTTP(w http.ResponseWriter, r *http.R
return
}

t, _, err := new(jwt.Parser).ParseUnverified(token.AccessToken, jwt.MapClaims{}) // safe to use because we validated the token above
if err != nil {
err = telemetry.Error(ctx, span, err, "error parsing token")
p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
return
}

var email string
if claims, ok := t.Claims.(jwt.MapClaims); ok {
if emailVal, ok := claims["https://user.io/email"].(string); ok {
email = emailVal
}
}
if email == "" {
err = telemetry.Error(ctx, span, nil, "email not found in token")
p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
return
}

// make an http call to https://api.upstash.com/apikey with authorization: bearer <access_token>
// to get the api key
apiKey, err := fetchUpstashApiKey(ctx, token.AccessToken)
Expand All @@ -111,12 +131,14 @@ 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,
},
ProjectID: projID,
DeveloperApiKey: []byte(apiKey),
UpstashEmail: email,
}

_, err = p.Repo().UpstashIntegration().Insert(ctx, oauthInt)
Expand Down
69 changes: 69 additions & 0 deletions api/server/handlers/upstash_integration/list.go
Original file line number Diff line number Diff line change
@@ -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)
}
2 changes: 1 addition & 1 deletion api/server/router/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -979,7 +979,7 @@ func getClusterRoutes(
// GET /api/projects/{project_id}/clusters/{cluster_id}/kubeconfig -> cluster.NewGetTemporaryKubeconfigHandler
getTemporaryKubeconfigEndpoint := factory.NewAPIEndpoint(
&types.APIRequestMetadata{
Verb: types.APIVerbUpdate, // we do not want users with no-write access to be able to use this
Verb: types.APIVerbGet,
Method: types.HTTPVerbGet,
Path: &types.Path{
Parent: basePath,
Expand Down
28 changes: 28 additions & 0 deletions api/server/router/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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
}
3 changes: 3 additions & 0 deletions api/server/shared/config/env/envconfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ type ServerConf struct {
PorterCloudPlanID string `env:"PORTER_CLOUD_PLAN_ID"`
PorterStandardPlanID string `env:"PORTER_STANDARD_PLAN_ID"`

// The URL of the webhook to verify ingesting events works
IngestStatusWebhookUrl string `env:"INGEST_STATUS_WEBHOOK_URL"`

// This endpoint will be passed to the porter-agent so that
// the billing manager can query Prometheus.
PrometheusUrl string `env:"PROMETHEUS_URL"`
Expand Down
1 change: 1 addition & 0 deletions api/types/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ type Project struct {
BillingEnabled bool `json:"billing_enabled"`
MetronomeEnabled bool `json:"metronome_enabled"`
InfisicalEnabled bool `json:"infisical_enabled"`
FreezeEnabled bool `json:"freeze_enabled"`
DBEnabled bool `json:"db_enabled"`
EFSEnabled bool `json:"efs_enabled"`
EnableReprovision bool `json:"enable_reprovision"`
Expand Down
15 changes: 15 additions & 0 deletions dashboard/src/assets/upstash.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions dashboard/src/lib/databases/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const datastoreTypeValidator = z.enum([
"MANAGED_REDIS",
"MANAGED_POSTGRES",
"NEON",
"UPSTASH",
]);
const datastoreEngineValidator = z.enum([
"UNKNOWN",
Expand Down Expand Up @@ -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<typeof datastoreValidator>["status"];
Expand Down Expand Up @@ -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()
Expand All @@ -355,6 +364,7 @@ export const dbFormValidator = z.object({
managedRedisConfigValidator,
managedPostgresConfigValidator,
neonValidator,
upstashValidator,
]),
clusterId: z.number(),
});
Expand Down
21 changes: 20 additions & 1 deletion dashboard/src/lib/hooks/useDatastore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand Down Expand Up @@ -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();
};

Expand Down
41 changes: 41 additions & 0 deletions dashboard/src/lib/hooks/useUpstash.ts
Original file line number Diff line number Diff line change
@@ -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<ClientUpstashIntegration[]>;
};
export const useUpstash = (): TUseUpstash => {
const getUpstashIntegrations = async ({
projectId,
}: {
projectId: number;
}): Promise<ClientUpstashIntegration[]> => {
const response = await api.getUpstashIntegrations(
"<token>",
{},
{
projectId,
}
);

const results = await z
.object({ integrations: z.array(upstashIntegrationValidator) })
.parseAsync(response.data);

return results.integrations;
};

return {
getUpstashIntegrations,
};
};
Loading

0 comments on commit aade2b7

Please sign in to comment.