Skip to content

Commit

Permalink
boilerplate addon dashboard code (#4526)
Browse files Browse the repository at this point in the history
  • Loading branch information
Feroze Mohideen authored Apr 29, 2024
1 parent 171bf19 commit 41362b6
Show file tree
Hide file tree
Showing 49 changed files with 5,191 additions and 612 deletions.
76 changes: 76 additions & 0 deletions api/server/handlers/addons/delete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package addons

import (
"net/http"

"connectrpc.com/connect"
"github.com/google/uuid"
porterv1 "github.com/porter-dev/api-contracts/generated/go/porter/v1"
"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/server/shared/requestutils"
"github.com/porter-dev/porter/api/types"
"github.com/porter-dev/porter/internal/models"
"github.com/porter-dev/porter/internal/telemetry"
)

// DeleteAddonHandler handles requests to the /addons/delete endpoint
type DeleteAddonHandler struct {
handlers.PorterHandlerReadWriter
}

// NewDeleteAddonHandler returns a new DeleteAddonHandler
func NewDeleteAddonHandler(
config *config.Config,
decoderValidator shared.RequestDecoderValidator,
writer shared.ResultWriter,
) *DeleteAddonHandler {
return &DeleteAddonHandler{
PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
}
}

func (c *DeleteAddonHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx, span := telemetry.NewSpan(r.Context(), "serve-delete-addon")
defer span.End()

project, _ := ctx.Value(types.ProjectScope).(*models.Project)
deploymentTarget, _ := ctx.Value(types.DeploymentTargetScope).(types.DeploymentTarget)

addonName, reqErr := requestutils.GetURLParamString(r, types.URLParamAddonName)
if reqErr != nil {
err := telemetry.Error(ctx, span, reqErr, "error parsing addon name")
c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
return
}

var deploymentTargetIdentifier *porterv1.DeploymentTargetIdentifier
if deploymentTarget.ID != uuid.Nil {
deploymentTargetIdentifier = &porterv1.DeploymentTargetIdentifier{
Id: deploymentTarget.ID.String(),
}
}

if addonName == "" {
err := telemetry.Error(ctx, span, nil, "no addon name provided")
c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
return
}

deleteAddonRequest := connect.NewRequest(&porterv1.DeleteAddonRequest{
ProjectId: int64(project.ID),
DeploymentTargetIdentifier: deploymentTargetIdentifier,
AddonName: addonName,
})

_, err := c.Config().ClusterControlPlaneClient.DeleteAddon(ctx, deleteAddonRequest)
if err != nil {
err = telemetry.Error(ctx, span, err, "error deleting addon")
c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
return
}

c.WriteResult(w, r, "")
}
104 changes: 104 additions & 0 deletions api/server/handlers/addons/get.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package addons

import (
"encoding/base64"
"net/http"

"connectrpc.com/connect"
"github.com/google/uuid"
"github.com/porter-dev/api-contracts/generated/go/helpers"
porterv1 "github.com/porter-dev/api-contracts/generated/go/porter/v1"
"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/server/shared/requestutils"
"github.com/porter-dev/porter/api/types"
"github.com/porter-dev/porter/internal/models"
"github.com/porter-dev/porter/internal/telemetry"
)

// AddonHandler handles requests to the /addons/{addon_name} endpoint
type AddonHandler struct {
handlers.PorterHandlerReadWriter
}

// NewAddonHandler returns a new AddonHandler
func NewAddonHandler(
config *config.Config,
decoderValidator shared.RequestDecoderValidator,
writer shared.ResultWriter,
) *AddonHandler {
return &AddonHandler{
PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
}
}

// AddonResponse represents the response from the /addons/{addon_name} endpoints
type AddonResponse struct {
Addon string `json:"addon"`
}

func (c *AddonHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx, span := telemetry.NewSpan(r.Context(), "serve-get-addon")
defer span.End()

project, _ := ctx.Value(types.ProjectScope).(*models.Project)
deploymentTarget, _ := ctx.Value(types.DeploymentTargetScope).(types.DeploymentTarget)

addonName, reqErr := requestutils.GetURLParamString(r, types.URLParamAddonName)
if reqErr != nil {
err := telemetry.Error(ctx, span, reqErr, "error parsing addon name")
c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
return
}

telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "addon-name", Value: addonName})

var deploymentTargetIdentifier *porterv1.DeploymentTargetIdentifier
if deploymentTarget.ID != uuid.Nil {
deploymentTargetIdentifier = &porterv1.DeploymentTargetIdentifier{
Id: deploymentTarget.ID.String(),
}
}

if addonName == "" {
err := telemetry.Error(ctx, span, nil, "no addon name provided")
c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
return
}

addonRequest := connect.NewRequest(&porterv1.AddonRequest{
ProjectId: int64(project.ID),
DeploymentTargetIdentifier: deploymentTargetIdentifier,
AddonName: addonName,
})

resp, err := c.Config().ClusterControlPlaneClient.Addon(ctx, addonRequest)
if err != nil {
err = telemetry.Error(ctx, span, err, "error getting addon")
c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
return
}

if resp == nil || resp.Msg == nil {
err = telemetry.Error(ctx, span, nil, "addon response is nil")
c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
return
}

by, err := helpers.MarshalContractObject(ctx, resp.Msg.Addon)
if err != nil {
err = telemetry.Error(ctx, span, err, "error marshaling addon")
c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
return
}

encoded := base64.StdEncoding.EncodeToString(by)

res := &AddonResponse{
Addon: encoded,
}

c.WriteResult(w, r, res)
}
28 changes: 6 additions & 22 deletions api/server/handlers/addons/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"net/http"

"connectrpc.com/connect"
"github.com/google/uuid"
"github.com/porter-dev/api-contracts/generated/go/helpers"
porterv1 "github.com/porter-dev/api-contracts/generated/go/porter/v1"
"github.com/porter-dev/porter/api/server/handlers"
Expand All @@ -16,7 +17,7 @@ import (
"github.com/porter-dev/porter/internal/telemetry"
)

// LatestAddonsHandler handles requests to the /addons/latest endpoint
// LatestAddonsHandler handles requests to the /addons endpoint
type LatestAddonsHandler struct {
handlers.PorterHandlerReadWriter
}
Expand All @@ -32,12 +33,7 @@ func NewLatestAddonsHandler(
}
}

// LatestAddonsRequest represents the request for the /addons/latest endpoint
type LatestAddonsRequest struct {
DeploymentTargetID string `schema:"deployment_target_id"`
}

// LatestAddonsResponse represents the response from the /addons/latest endpoint
// LatestAddonsResponse represents the response from the /addons endpoint
type LatestAddonsResponse struct {
Base64Addons []string `json:"base64_addons"`
}
Expand All @@ -47,29 +43,17 @@ func (c *LatestAddonsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
defer span.End()

project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)

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

telemetry.WithAttributes(span,
telemetry.AttributeKV{Key: "deployment-target-id", Value: request.DeploymentTargetID},
)
deploymentTarget, _ := ctx.Value(types.DeploymentTargetScope).(types.DeploymentTarget)

var deploymentTargetIdentifier *porterv1.DeploymentTargetIdentifier
if request.DeploymentTargetID != "" {
if deploymentTarget.ID != uuid.Nil {
deploymentTargetIdentifier = &porterv1.DeploymentTargetIdentifier{
Id: request.DeploymentTargetID,
Id: deploymentTarget.ID.String(),
}
}

latestAddonsReq := connect.NewRequest(&porterv1.LatestAddonsRequest{
ProjectId: int64(project.ID),
ClusterId: int64(cluster.ID),
DeploymentTargetIdentifier: deploymentTargetIdentifier,
})

Expand Down
107 changes: 107 additions & 0 deletions api/server/handlers/addons/tailscale_services.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package addons

import (
"fmt"
"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"
)

// TailscaleServicesHandler handles requests to the /addons/tailscale-services endpoint
type TailscaleServicesHandler struct {
handlers.PorterHandlerReadWriter
authz.KubernetesAgentGetter
}

// NewTailscaleServicesHandler returns a new TailscaleServicesHandler
func NewTailscaleServicesHandler(
config *config.Config,
decoderValidator shared.RequestDecoderValidator,
writer shared.ResultWriter,
) *TailscaleServicesHandler {
return &TailscaleServicesHandler{
PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
KubernetesAgentGetter: authz.NewOutOfClusterAgentGetter(config),
}
}

// TailscaleServicesResponse represents the response from the /addons/tailscale-services endpoints
type TailscaleServicesResponse struct {
Services []TailscaleService `json:"services"`
}

// TailscaleService represents a Tailscale service
type TailscaleService struct {
Name string `json:"name"`
IP string `json:"ip"`
Port int `json:"port"`
}

// ServeHTTP returns all services that can be accessed through Tailscale
// TODO: move this logic to CCP
func (c *TailscaleServicesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx, span := telemetry.NewSpan(r.Context(), "serve-get-tailscale-services")
defer span.End()

project, _ := ctx.Value(types.ProjectScope).(*models.Project)
deploymentTarget, _ := ctx.Value(types.DeploymentTargetScope).(types.DeploymentTarget)

telemetry.WithAttributes(span,
telemetry.AttributeKV{Key: "namespace", Value: deploymentTarget.Namespace},
telemetry.AttributeKV{Key: "cluster-id", Value: deploymentTarget.ClusterID},
)

cluster, err := c.Repo().Cluster().ReadCluster(project.ID, deploymentTarget.ClusterID)
if err != nil {
err = telemetry.Error(ctx, span, err, "error reading cluster")
c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
return
}

agent, err := c.GetAgent(r, cluster, deploymentTarget.Namespace)
if err != nil {
err = telemetry.Error(ctx, span, err, "error getting agent")
c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
return
}

svcList, err := agent.ListServices(ctx, deploymentTarget.Namespace, "porter.run/tailscale-svc=true")
if err != nil {
err = telemetry.Error(ctx, span, err, "error listing services")
c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
return
}

var services []TailscaleService
for _, svc := range svcList.Items {
var port int
if len(svc.Spec.Ports) > 0 {
port = int(svc.Spec.Ports[0].Port)
}
service := TailscaleService{
Name: svc.Name,
IP: svc.Spec.ClusterIP,
Port: port,
}
if appName, ok := svc.Labels["porter.run/app-name"]; ok {
if serviceName, ok := svc.Labels["porter.run/service-name"]; ok {
service.Name = fmt.Sprintf("%s (%s)", serviceName, appName)
}
}

services = append(services, service)
}

resp := TailscaleServicesResponse{
Services: services,
}

c.WriteResult(w, r, resp)
}
Loading

0 comments on commit 41362b6

Please sign in to comment.