Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 55 additions & 52 deletions admin/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,42 +20,44 @@ import (
)

type Options struct {
DatabaseDriver string
DatabaseDSN string
DatabaseEncryptionKeyring string
ExternalURL string
FrontendURL string
ProvisionerSetJSON string
ProvisionerMaxConcurrency int
DefaultProvisioner string
Version version.Version
MetricsProjectOrg string
MetricsProjectName string
AutoscalerCron string
ScaleDownConstraint int
DatabaseDriver string
DatabaseDSN string
DatabaseEncryptionKeyring string
ExternalURL string
FrontendURL string
ProvisionerSetJSON string
ProvisionerMaxConcurrency int
DefaultProvisioner string
Version version.Version
MetricsProjectOrg string
MetricsProjectName string
AutoscalerCron string
ScaleDownConstraint int
EmbeddedAnalyticsServiceToken string
}

type Service struct {
DB database.DB
Jobs jobs.Client
URLs *URLs
ProvisionerSet map[string]provisioner.Provisioner
ProvisionerMaxConcurrency int
Email *email.Client
Github Github
AI drivers.AIService
Assets *storage.BucketHandle
Used *usedFlusher
Logger *zap.Logger
opts *Options
issuer *auth.Issuer
authCache *lru.Cache
Version version.Version
MetricsProjectID string
AutoscalerCron string
ScaleDownConstraint int
Biller billing.Biller
PaymentProvider payment.Provider
DB database.DB
Jobs jobs.Client
URLs *URLs
ProvisionerSet map[string]provisioner.Provisioner
ProvisionerMaxConcurrency int
Email *email.Client
Github Github
AI drivers.AIService
Assets *storage.BucketHandle
Used *usedFlusher
Logger *zap.Logger
opts *Options
issuer *auth.Issuer
authCache *lru.Cache
Version version.Version
MetricsProjectID string
AutoscalerCron string
ScaleDownConstraint int
Biller billing.Biller
PaymentProvider payment.Provider
EmbeddedAnalyticsServiceToken string
}

func New(ctx context.Context, opts *Options, logger *zap.Logger, issuer *auth.Issuer, emailClient *email.Client, github Github, aiService drivers.AIService, assets *storage.BucketHandle, biller billing.Biller, p payment.Provider) (*Service, error) {
Expand Down Expand Up @@ -121,25 +123,26 @@ func New(ctx context.Context, opts *Options, logger *zap.Logger, issuer *auth.Is
}

return &Service{
DB: db,
URLs: urls,
ProvisionerSet: provSet,
ProvisionerMaxConcurrency: opts.ProvisionerMaxConcurrency,
Email: emailClient,
Github: github,
AI: aiService,
Assets: assets,
Used: newUsedFlusher(logger, db),
Logger: logger,
opts: opts,
issuer: issuer,
authCache: authCache,
Version: opts.Version,
MetricsProjectID: metricsProjectID,
AutoscalerCron: opts.AutoscalerCron,
ScaleDownConstraint: opts.ScaleDownConstraint,
Biller: biller,
PaymentProvider: p,
DB: db,
URLs: urls,
ProvisionerSet: provSet,
ProvisionerMaxConcurrency: opts.ProvisionerMaxConcurrency,
Email: emailClient,
Github: github,
AI: aiService,
Assets: assets,
Used: newUsedFlusher(logger, db),
Logger: logger,
opts: opts,
issuer: issuer,
authCache: authCache,
Version: opts.Version,
MetricsProjectID: metricsProjectID,
AutoscalerCron: opts.AutoscalerCron,
ScaleDownConstraint: opts.ScaleDownConstraint,
Biller: biller,
PaymentProvider: p,
EmbeddedAnalyticsServiceToken: opts.EmbeddedAnalyticsServiceToken,
}, nil
}

Expand Down
117 changes: 117 additions & 0 deletions admin/server/billing.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package server

import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"math"
"net/http"
"strings"
"time"

Expand Down Expand Up @@ -877,6 +881,65 @@ func (s *Server) ListOrganizationBillingIssues(ctx context.Context, req *adminv1
}, nil
}

func (s *Server) GetEmbeddedAnalytics(ctx context.Context, req *adminv1.GetEmbeddedAnalyticsRequest) (*adminv1.GetEmbeddedAnalyticsResponse, error) {
observability.AddRequestAttributes(ctx, attribute.String("args.org", req.Org))

org, err := s.admin.DB.FindOrganizationByName(ctx, req.Org)
if err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}

claims := auth.GetClaims(ctx)
if !claims.OrganizationPermissions(ctx, org.ID).ManageOrg {
return nil, status.Error(codes.PermissionDenied, "not allowed to view org analytics")
}

if s.admin.EmbeddedAnalyticsServiceToken == "" {
return nil, status.Error(codes.Unavailable, "embedded analytics is not configured")
}

iframeURL, err := s.fetchEmbeddedAnalyticsIframeURL(ctx, "")
if err != nil {
return nil, status.Error(codes.Internal, fmt.Sprintf("failed to fetch embedded analytics: %v", err))
}

return &adminv1.GetEmbeddedAnalyticsResponse{
IframeUrl: iframeURL,
}, nil
}

func (s *Server) GetProjectEmbeddedAnalytics(ctx context.Context, req *adminv1.GetProjectEmbeddedAnalyticsRequest) (*adminv1.GetProjectEmbeddedAnalyticsResponse, error) {
observability.AddRequestAttributes(ctx, attribute.String("args.org", req.Org), attribute.String("args.project", req.Project))

org, err := s.admin.DB.FindOrganizationByName(ctx, req.Org)
if err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}

_, err = s.admin.DB.FindProjectByName(ctx, req.Org, req.Project)
if err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}

claims := auth.GetClaims(ctx)
if !claims.OrganizationPermissions(ctx, org.ID).ManageOrg {
return nil, status.Error(codes.PermissionDenied, "not allowed to view project analytics")
}

if s.admin.EmbeddedAnalyticsServiceToken == "" {
return nil, status.Error(codes.Unavailable, "embedded analytics is not configured")
}

iframeURL, err := s.fetchEmbeddedAnalyticsIframeURL(ctx, "d337de05-8fba-4cc0-a62d-bc6f3eb36db2")
if err != nil {
return nil, status.Error(codes.Internal, fmt.Sprintf("failed to fetch embedded analytics: %v", err))
}

return &adminv1.GetProjectEmbeddedAnalyticsResponse{
IframeUrl: iframeURL,
}, nil
}

func (s *Server) SudoDeleteOrganizationBillingIssue(ctx context.Context, req *adminv1.SudoDeleteOrganizationBillingIssueRequest) (*adminv1.SudoDeleteOrganizationBillingIssueResponse, error) {
observability.AddRequestAttributes(ctx, attribute.String("args.org", req.Org), attribute.String("args.type", req.Type.String()))

Expand Down Expand Up @@ -1275,3 +1338,57 @@ func biggerOfInt64(ptr *int64, def int64) int64 {

return def
}

func (s *Server) fetchEmbeddedAnalyticsIframeURL(ctx context.Context, projectID string) (string, error) {
type iframeRequest struct {
Resource string `json:"resource"`
Type string `json:"type"`
Attributes map[string]any `json:"attributes"`
}

type iframeResponse struct {
IframeURL string `json:"iframeSrc"`
}

reqBody := iframeRequest{
Resource: "top_level_metrics",
Type: "canvas",
Attributes: map[string]any{
"organization_id": "654b91fa-d39a-46d2-8d6f-33d2fb55cbde",
"project_id": projectID,
},
}

body, err := json.Marshal(reqBody)
if err != nil {
return "", fmt.Errorf("failed to marshal request: %w", err)
}

url := "https://api.rilldata.com/v1/organizations/embedded-analytics/projects/embedded-analytics/iframe"
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
if err != nil {
return "", fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
token := s.admin.EmbeddedAnalyticsServiceToken
s.logger.Info("embedded analytics request", zap.String("token_prefix", token[:min(20, len(token))]), zap.String("token_len", fmt.Sprintf("%d", len(token))), zap.String("body", string(body)))
req.Header.Set("Authorization", "Bearer "+token)

resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", fmt.Errorf("failed to call iframe API: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
respBody, _ := io.ReadAll(resp.Body)
return "", fmt.Errorf("iframe API returned status %d: %s", resp.StatusCode, string(respBody))
}

var iframeResp iframeResponse
if err := json.NewDecoder(resp.Body).Decode(&iframeResp); err != nil {
return "", fmt.Errorf("failed to decode iframe response: %w", err)
}

return iframeResp.IframeURL, nil
}
28 changes: 15 additions & 13 deletions cli/cmd/admin/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ type Config struct {
StripeAPIKey string `split_words:"true"`
StripeWebhookSecret string `split_words:"true"`
PylonIdentitySecret string `split_words:"true"`
EmbeddedAnalyticsServiceToken string `split_words:"true"`
}

// StartCmd starts an admin server. It only allows configuration using environment variables.
Expand Down Expand Up @@ -318,19 +319,20 @@ func StartCmd(ch *cmdutil.Helper) *cobra.Command {

// Init admin service
admOpts := &admin.Options{
DatabaseDriver: conf.DatabaseDriver,
DatabaseDSN: conf.DatabaseURL,
DatabaseEncryptionKeyring: conf.DatabaseEncryptionKeyring,
ExternalURL: conf.ExternalGRPCURL, // NOTE: using gRPC url
FrontendURL: conf.FrontendURL,
ProvisionerSetJSON: conf.ProvisionerSetJSON,
ProvisionerMaxConcurrency: conf.ProvisionerMaxConcurrency,
DefaultProvisioner: conf.DefaultProvisioner,
Version: ch.Version,
MetricsProjectOrg: metricsProjectOrg,
MetricsProjectName: metricsProjectName,
AutoscalerCron: conf.AutoscalerCron,
ScaleDownConstraint: conf.ScaleDownConstraint,
DatabaseDriver: conf.DatabaseDriver,
DatabaseDSN: conf.DatabaseURL,
DatabaseEncryptionKeyring: conf.DatabaseEncryptionKeyring,
ExternalURL: conf.ExternalGRPCURL, // NOTE: using gRPC url
FrontendURL: conf.FrontendURL,
ProvisionerSetJSON: conf.ProvisionerSetJSON,
ProvisionerMaxConcurrency: conf.ProvisionerMaxConcurrency,
DefaultProvisioner: conf.DefaultProvisioner,
Version: ch.Version,
MetricsProjectOrg: metricsProjectOrg,
MetricsProjectName: metricsProjectName,
AutoscalerCron: conf.AutoscalerCron,
ScaleDownConstraint: conf.ScaleDownConstraint,
EmbeddedAnalyticsServiceToken: conf.EmbeddedAnalyticsServiceToken,
}
adm, err := admin.New(cmd.Context(), admOpts, logger, issuer, emailClient, gh, aiService, assetsBucket, biller, p)
if err != nil {
Expand Down
50 changes: 50 additions & 0 deletions proto/gen/rill/admin/v1/admin.swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -711,6 +711,24 @@ paths:
estimatedSizeBytes:
type: string
format: int64
/v1/orgs/{org}/embedded-analytics:
get:
summary: GetEmbeddedAnalytics returns an iframe URL for the embedded analytics dashboard
operationId: AdminService_GetEmbeddedAnalytics
responses:
"200":
description: A successful response.
schema:
$ref: '#/definitions/v1GetEmbeddedAnalyticsResponse'
default:
description: An unexpected error response.
schema:
$ref: '#/definitions/rpcStatus'
parameters:
- name: org
in: path
required: true
type: string
/v1/orgs/{org}/invites:
get:
summary: ListOrganizationInvites lists all the org invites
Expand Down Expand Up @@ -1537,6 +1555,28 @@ paths:
description: |-
Whether the deployment is editable and the edited changes are persisted back to the git repo.
Can't be set for `prod` deployments.
/v1/orgs/{org}/projects/{project}/embedded-analytics:
get:
summary: GetProjectEmbeddedAnalytics returns an iframe URL for the project-level embedded analytics dashboard
operationId: AdminService_GetProjectEmbeddedAnalytics
responses:
"200":
description: A successful response.
schema:
$ref: '#/definitions/v1GetProjectEmbeddedAnalyticsResponse'
default:
description: An unexpected error response.
schema:
$ref: '#/definitions/rpcStatus'
parameters:
- name: org
in: path
required: true
type: string
- name: project
in: path
required: true
type: string
/v1/orgs/{org}/projects/{project}/hibernate:
post:
summary: HibernateProject hibernates a project by tearing down its deployments.
Expand Down Expand Up @@ -5137,6 +5177,11 @@ definitions:
ttlSeconds:
type: integer
format: int64
v1GetEmbeddedAnalyticsResponse:
type: object
properties:
iframeUrl:
type: string
v1GetGithubRepoStatusResponse:
type: object
properties:
Expand Down Expand Up @@ -5214,6 +5259,11 @@ definitions:
properties:
project:
$ref: '#/definitions/v1Project'
v1GetProjectEmbeddedAnalyticsResponse:
type: object
properties:
iframeUrl:
type: string
v1GetProjectMemberUserResponse:
type: object
properties:
Expand Down
Loading
Loading