Skip to content

Commit 9cabdd2

Browse files
author
Feroze Mohideen
authored
[POR-2218] CLI changes to support datastore connect (#4183)
1 parent 6b8135b commit 9cabdd2

File tree

10 files changed

+458
-30
lines changed

10 files changed

+458
-30
lines changed

api/client/datastore.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package client
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/porter-dev/porter/api/types"
8+
)
9+
10+
// CreateDatastoreProxy creates a proxy to connect to a datastore
11+
func (c *Client) CreateDatastoreProxy(
12+
ctx context.Context,
13+
projectID uint,
14+
datastoreName string,
15+
req *types.CreateDatastoreProxyRequest,
16+
) (*types.CreateDatastoreProxyResponse, error) {
17+
resp := &types.CreateDatastoreProxyResponse{}
18+
19+
err := c.postRequest(
20+
fmt.Sprintf(
21+
"/projects/%d/datastores/%s/create-proxy",
22+
projectID, datastoreName,
23+
),
24+
req,
25+
resp,
26+
)
27+
28+
return resp, err
29+
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package datastore
2+
3+
import (
4+
"net/http"
5+
6+
"connectrpc.com/connect"
7+
"github.com/google/uuid"
8+
porterv1 "github.com/porter-dev/api-contracts/generated/go/porter/v1"
9+
"github.com/porter-dev/porter/api/server/authz"
10+
"github.com/porter-dev/porter/api/server/handlers"
11+
"github.com/porter-dev/porter/api/server/shared"
12+
"github.com/porter-dev/porter/api/server/shared/apierrors"
13+
"github.com/porter-dev/porter/api/server/shared/config"
14+
"github.com/porter-dev/porter/api/server/shared/requestutils"
15+
"github.com/porter-dev/porter/api/types"
16+
"github.com/porter-dev/porter/internal/models"
17+
"github.com/porter-dev/porter/internal/telemetry"
18+
)
19+
20+
// Credential has all information about connecting to a datastore
21+
type Credential struct {
22+
Host string
23+
Port int
24+
Username string
25+
Password string
26+
DatabaseName string
27+
}
28+
29+
// CreateDatastoreProxyResponse is the response body for the create datastore proxy endpoint
30+
type CreateDatastoreProxyResponse struct {
31+
// PodName is the name of the pod that was created
32+
PodName string `json:"pod_name"`
33+
// Credential is the credential used to connect to the datastore
34+
Credential Credential `json:"credential"`
35+
// ClusterID is the ID of the cluster that the pod was created in
36+
ClusterID uint `json:"cluster_id"`
37+
// Namespace is the namespace that the pod was created in
38+
Namespace string `json:"namespace"`
39+
// Type is the type of datastore
40+
Type string `json:"type"`
41+
}
42+
43+
// CreateDatastoreProxyHandler is a handler for creating a datastore proxy pod which is used to connect to the datastore
44+
type CreateDatastoreProxyHandler struct {
45+
handlers.PorterHandlerReadWriter
46+
authz.KubernetesAgentGetter
47+
}
48+
49+
// NewCreateDatastoreProxyHandler returns a CreateDatastoreProxyHandler
50+
func NewCreateDatastoreProxyHandler(
51+
config *config.Config,
52+
decoderValidator shared.RequestDecoderValidator,
53+
writer shared.ResultWriter,
54+
) *CreateDatastoreProxyHandler {
55+
return &CreateDatastoreProxyHandler{
56+
PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
57+
KubernetesAgentGetter: authz.NewOutOfClusterAgentGetter(config),
58+
}
59+
}
60+
61+
// ServeHTTP creates a datastore proxy pod
62+
func (c *CreateDatastoreProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
63+
ctx, span := telemetry.NewSpan(r.Context(), "serve-create-datastore-proxy")
64+
defer span.End()
65+
66+
project, _ := ctx.Value(types.ProjectScope).(*models.Project)
67+
if project.ID == 0 {
68+
err := telemetry.Error(ctx, span, nil, "project not found")
69+
c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
70+
return
71+
}
72+
projectId := int64(project.ID)
73+
74+
var resp CreateDatastoreProxyResponse
75+
76+
datastoreName, reqErr := requestutils.GetURLParamString(r, types.URLParamDatastoreName)
77+
if reqErr != nil {
78+
err := telemetry.Error(ctx, span, nil, "error parsing datastore name")
79+
c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
80+
return
81+
}
82+
telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "datastore-name", Value: datastoreName})
83+
84+
datastoreRecord, err := c.Repo().Datastore().GetByProjectIDAndName(ctx, project.ID, datastoreName)
85+
if err != nil {
86+
err = telemetry.Error(ctx, span, err, "datastore record not found")
87+
c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
88+
return
89+
}
90+
91+
if datastoreRecord == nil || datastoreRecord.ID == uuid.Nil {
92+
err = telemetry.Error(ctx, span, nil, "datastore record does not exist")
93+
c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
94+
return
95+
}
96+
97+
message := porterv1.CreateDatastoreProxyRequest{
98+
ProjectId: projectId,
99+
DatastoreId: datastoreRecord.ID.String(),
100+
}
101+
req := connect.NewRequest(&message)
102+
ccpResp, err := c.Config().ClusterControlPlaneClient.CreateDatastoreProxy(ctx, req)
103+
if err != nil {
104+
err = telemetry.Error(ctx, span, err, "error creating datastore proxy")
105+
c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
106+
return
107+
}
108+
if ccpResp == nil || ccpResp.Msg == nil {
109+
err = telemetry.Error(ctx, span, nil, "error creating datastore proxy")
110+
c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
111+
return
112+
}
113+
114+
resp = CreateDatastoreProxyResponse{
115+
PodName: ccpResp.Msg.PodName,
116+
Credential: Credential{
117+
Host: ccpResp.Msg.Credential.Host,
118+
Port: int(ccpResp.Msg.Credential.Port),
119+
Username: ccpResp.Msg.Credential.Username,
120+
Password: ccpResp.Msg.Credential.Password,
121+
DatabaseName: ccpResp.Msg.Credential.DatabaseName,
122+
},
123+
ClusterID: uint(ccpResp.Msg.ClusterId),
124+
Namespace: ccpResp.Msg.Namespace,
125+
Type: datastoreRecord.Type,
126+
}
127+
128+
c.WriteResult(w, r, resp)
129+
}

api/server/handlers/datastore/list.go

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@ type Datastore struct {
6464

6565
// CreatedAtUTC is the time the datastore was created in UTC
6666
CreatedAtUTC time.Time `json:"created_at"`
67+
68+
// CloudProvider is the cloud provider associated with the datastore
69+
CloudProvider string `json:"cloud_provider"`
70+
71+
// CloudProviderCredentialIdentifier is the cloud provider credential identifier associated with the datastore
72+
CloudProviderCredentialIdentifier string `json:"cloud_provider_credential_identifier"`
6773
}
6874

6975
// ListDatastoresHandler is a struct for listing all datastores for a given project
@@ -177,24 +183,23 @@ func Datastores(ctx context.Context, inp DatastoresInput) ([]Datastore, error) {
177183
}
178184

179185
for _, datastore := range resp.Msg.Datastores {
180-
encodedDatastore := Datastore{
181-
Name: datastore.Name,
182-
Metadata: datastore.Metadata,
183-
Env: datastore.Env,
184-
}
185-
186186
datastoreRecord, err := inp.DatastoreRepository.GetByProjectIDAndName(ctx, inp.ProjectID, datastore.Name)
187187
if err != nil {
188188
telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "err-datastore-name", Value: datastore.Name})
189189
return datastores, telemetry.Error(ctx, span, err, "datastore record not found")
190190
}
191191

192-
encodedDatastore.CreatedAtUTC = datastoreRecord.CreatedAt
193-
encodedDatastore.Type = datastoreRecord.Type
194-
encodedDatastore.Engine = datastoreRecord.Engine
195-
encodedDatastore.Status = string(datastoreRecord.Status)
196-
197-
datastores = append(datastores, encodedDatastore)
192+
datastores = append(datastores, Datastore{
193+
Name: datastore.Name,
194+
Type: datastoreRecord.Type,
195+
Engine: datastoreRecord.Engine,
196+
CreatedAtUTC: datastoreRecord.CreatedAt,
197+
Status: string(datastoreRecord.Status),
198+
Metadata: datastore.Metadata,
199+
Env: datastore.Env,
200+
CloudProvider: datastoreRecord.CloudProvider,
201+
CloudProviderCredentialIdentifier: datastoreRecord.CloudProviderCredentialIdentifier,
202+
})
198203
}
199204

200205
return datastores, nil

api/server/router/project.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,35 @@ func getProjectRoutes(
527527
Router: r,
528528
})
529529

530+
// POST /api/projects/{project_id}/datastores/{datastore_name}/create-proxy -> cluster.NewCreateDatastoreProxyHandler
531+
createDatastoreProxyEndpoint := factory.NewAPIEndpoint(
532+
&types.APIRequestMetadata{
533+
Verb: types.APIVerbUpdate,
534+
Method: types.HTTPVerbPost,
535+
Path: &types.Path{
536+
Parent: basePath,
537+
RelativePath: fmt.Sprintf("%s/datastores/{%s}/create-proxy", relPath, types.URLParamDatastoreName),
538+
},
539+
Scopes: []types.PermissionScope{
540+
types.UserScope,
541+
types.ProjectScope,
542+
types.ClusterScope,
543+
},
544+
},
545+
)
546+
547+
createDatastoreProxyHandler := datastore.NewCreateDatastoreProxyHandler(
548+
config,
549+
factory.GetDecoderValidator(),
550+
factory.GetResultWriter(),
551+
)
552+
553+
routes = append(routes, &router.Route{
554+
Endpoint: createDatastoreProxyEndpoint,
555+
Handler: createDatastoreProxyHandler,
556+
Router: r,
557+
})
558+
530559
// DELETE /api/projects/{project_id}/datastores/{datastore_name} -> cloud_provider.NewDeleteDatastoreHandler
531560
deleteDatastoreEndpoint := factory.NewAPIEndpoint(
532561
&types.APIRequestMetadata{

api/types/datastore.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package types
2+
3+
// DatastoreType represents the type of the datastore
4+
type DatastoreType string
5+
6+
const (
7+
// DatastoreType_RDS is the RDS datastore type
8+
DatastoreType_RDS DatastoreType = "RDS"
9+
// DatastoreType_ElastiCache is the ElastiCache datastore type
10+
DatastoreType_ElastiCache DatastoreType = "ELASTICACHE"
11+
)
12+
13+
// CreateDatastoreProxyRequest is the request body for the create datastore proxy endpoint
14+
type CreateDatastoreProxyRequest struct{}
15+
16+
// CreateDatastoreProxyResponse is the response body for the create datastore proxy endpoint
17+
type CreateDatastoreProxyResponse struct {
18+
// PodName is the name of the pod that was created
19+
PodName string `json:"pod_name"`
20+
// Credential is the credential used to connect to the datastore
21+
Credential DatastoreCredential `json:"credential"`
22+
// ClusterID is the ID of the cluster that the pod was created in
23+
ClusterID uint `json:"cluster_id"`
24+
// Namespace is the namespace that the pod was created in
25+
Namespace string `json:"namespace"`
26+
// Type is the type of datastore
27+
Type string `json:"type"`
28+
}
29+
30+
// DatastoreCredential has all information about connecting to a datastore
31+
type DatastoreCredential struct {
32+
Host string `json:"host"`
33+
Port int `json:"port"`
34+
Username string `json:"username"`
35+
Password string `json:"password"`
36+
DatabaseName string `json:"database_name"`
37+
}

cli/cmd/commands/all.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ func RegisterCommands() (*cobra.Command, error) {
4949
rootCmd.AddCommand(registerCommand_Update(cliConf))
5050
rootCmd.AddCommand(registerCommand_Version(cliConf))
5151
rootCmd.AddCommand(registerCommand_Env(cliConf))
52+
rootCmd.AddCommand(registerCommand_Datastore(cliConf))
5253
return rootCmd, nil
5354
}
5455

0 commit comments

Comments
 (0)