Skip to content
Merged
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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM golang:1.19.9-alpine3.18 AS build-env
FROM golang:1.24.6-alpine AS build-env
RUN apk add --no-cache git gcc musl-dev
RUN apk add --update make
RUN go install github.com/google/wire/cmd/wire@latest
Expand Down
26 changes: 14 additions & 12 deletions Wire.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,25 +21,24 @@ package main

import (
"github.com/devtron-labs/central-api/api"
"github.com/devtron-labs/central-api/api/currency"
util "github.com/devtron-labs/central-api/client"
"github.com/devtron-labs/central-api/internal/logger"
"github.com/devtron-labs/central-api/pkg"
currencyPkg "github.com/devtron-labs/central-api/pkg/currency"
blob_storage "github.com/devtron-labs/common-lib/blob-storage"
"github.com/devtron-labs/common-lib/utils"
"github.com/google/wire"
)

func InitializeApp() (*App, error) {
wire.Build(
utils.NewSugardLogger,
logger.NewSugardLogger,
//sql.PgSqlWireSet,
//releaseNote.NewReleaseNoteRepositoryImpl,
//wire.Bind(new(releaseNote.ReleaseNoteRepository), new(*releaseNote.ReleaseNoteRepositoryImpl)),
blob_storage.NewBlobStorageServiceImpl,
NewApp,
api.NewMuxRouter,
util.NewGitHubClient,
util.NewGoogleSheetsClient,
//logger.NewHttpClient,
api.NewRestHandlerImpl,
wire.Bind(new(api.RestHandler), new(*api.RestHandlerImpl)),
Expand All @@ -53,14 +52,17 @@ func InitializeApp() (*App, error) {
pkg.NewCiBuildMetadataServiceImpl,
wire.Bind(new(pkg.CiBuildMetadataService), new(*pkg.CiBuildMetadataServiceImpl)),

// Currency service dependencies
currencyPkg.NewCurrencyConfig,
currencyPkg.NewServiceImpl,
wire.Bind(new(currencyPkg.Service), new(*currencyPkg.ServiceImpl)),
currency.NewCurrencyRestHandlerImpl,
wire.Bind(new(currency.CurrencyRestHandler), new(*currency.CurrencyRestHandlerImpl)),
currency.NewRouter,
wire.Bind(new(currency.Router), new(*currency.RouterImpl)),
// S3 Upload Service
pkg.NewS3UploadServiceImpl,
wire.Bind(new(pkg.S3UploadService), new(*pkg.S3UploadServiceImpl)),

// Google Sheets Service
pkg.NewGoogleSheetsServiceImpl,
wire.Bind(new(pkg.GoogleSheetsService), new(*pkg.GoogleSheetsServiceImpl)),

// Feedback Service
pkg.NewFeedbackServiceImpl,
wire.Bind(new(pkg.FeedbackService), new(*pkg.FeedbackServiceImpl)),
)
return &App{}, nil
}
54 changes: 53 additions & 1 deletion api/RestHandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"net/http"
"strconv"
"strings"
"time"
)

type RestHandler interface {
Expand All @@ -39,16 +40,19 @@ type RestHandler interface {
GetModuleByName(w http.ResponseWriter, r *http.Request)
GetDockerfileTemplateMetadata(w http.ResponseWriter, r *http.Request)
GetBuildpackMetadata(w http.ResponseWriter, r *http.Request)
SubmitFeedback(w http.ResponseWriter, r *http.Request)
}

func NewRestHandlerImpl(logger *zap.SugaredLogger, releaseNoteService pkg.ReleaseNoteService,
webhookSecretValidator pkg.WebhookSecretValidator, client *util.GitHubClient, ciBuildMetadataService pkg.CiBuildMetadataService) *RestHandlerImpl {
webhookSecretValidator pkg.WebhookSecretValidator, client *util.GitHubClient,
ciBuildMetadataService pkg.CiBuildMetadataService, feedbackService pkg.FeedbackService) *RestHandlerImpl {
return &RestHandlerImpl{
logger: logger,
releaseNoteService: releaseNoteService,
webhookSecretValidator: webhookSecretValidator,
client: client,
ciBuildMetadataService: ciBuildMetadataService,
feedbackService: feedbackService,
}
}

Expand All @@ -58,6 +62,7 @@ type RestHandlerImpl struct {
webhookSecretValidator pkg.WebhookSecretValidator
client *util.GitHubClient
ciBuildMetadataService pkg.CiBuildMetadataService
feedbackService pkg.FeedbackService
}

func (impl *RestHandlerImpl) GetModules(w http.ResponseWriter, r *http.Request) {
Expand Down Expand Up @@ -249,3 +254,50 @@ func isVersionNewer(v1, v2 string) bool {
// Compare using semver
return ver1.GreaterThan(ver2)
}

// SubmitFeedback handles the feedback submission endpoint
func (impl *RestHandlerImpl) SubmitFeedback(w http.ResponseWriter, r *http.Request) {
impl.logger.Info("received feedback submission request")
setupResponse(&w, r)

// Read request body
body, err := ioutil.ReadAll(r.Body)
if err != nil {
impl.logger.Errorw("error reading request body", "err", err)
impl.WriteJsonResp(w, err, "Failed to read request body", http.StatusBadRequest)
return
}
defer r.Body.Close()

// Parse request directly into FeedbackData
var feedbackData common.FeedbackData
err = json.Unmarshal(body, &feedbackData)
if err != nil {
impl.logger.Errorw("error unmarshalling feedback request", "err", err)
impl.WriteJsonResp(w, err, "Invalid request format", http.StatusBadRequest)
return
}

// Set submitted time if not provided
if feedbackData.SubmittedAt.IsZero() {
feedbackData.SubmittedAt = time.Now().UTC()
}

// Submit feedback (this will upload to S3 and add to Google Sheets)
err = impl.feedbackService.SubmitFeedback(&feedbackData)
if err != nil {
impl.logger.Errorw("error submitting feedback", "err", err, "ucid", feedbackData.UCID)
impl.WriteJsonResp(w, err, "Failed to submit feedback", http.StatusInternalServerError)
return
}

impl.logger.Infow("successfully submitted feedback", "ucid", feedbackData.UCID, "threadName", feedbackData.ThreadName)

// Return success response
response := map[string]interface{}{
"message": "Feedback submitted successfully",
"ucid": feedbackData.UCID,
"s3Url": feedbackData.FullConversationURL,
}
impl.WriteJsonResp(w, nil, response, http.StatusOK)
}
3 changes: 3 additions & 0 deletions api/Router.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,7 @@ func (r MuxRouter) Init() {
currencyRouter := r.Router.PathPrefix("/currency").Subrouter()
// Initialize currency routes
r.currencyRouter.InitCurrencyRoutes(currencyRouter)

// athena-ai-chat feedback router
r.Router.Path("/athena-feedback").HandlerFunc(r.restHandler.SubmitFeedback).Methods("POST")
}
13 changes: 13 additions & 0 deletions client/BlobConfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,19 @@ type BlobConfigVariables struct {
AzureBlobContainerName string `env:"AZURE_BLOB_CONTAINER_NAME"`
GcpBucketName string `env:"GCP_BUCKET_NAME"`
GcpCredentialFileJsonData string `env:"GCP_CREDENTIAL_FILE_JSON_DATA"`

// Feedback Storage Configuration
FeedbackStorageType blob_storage.BlobStorageType `env:"FEEDBACK_STORAGE_TYPE" envDefault:"S3"` // S3, GCP, AZURE
FeedbackS3AccessKey string `env:"FEEDBACK_S3_ACCESS_KEY"`
FeedbackS3Passkey string `env:"FEEDBACK_S3_PASS_KEY"`
FeedbackS3BucketName string `env:"FEEDBACK_S3_BUCKET_NAME"`
FeedbackS3Region string `env:"FEEDBACK_S3_REGION" envDefault:"us-east-1"`
FeedbackGcpBucketName string `env:"FEEDBACK_GCP_BUCKET_NAME"`
FeedbackGcpCredentialFileJsonData string `env:"FEEDBACK_GCP_CREDENTIAL_FILE_JSON_DATA"` // Can use same as Google Sheets service account
FeedbackAzureEnabled bool `env:"FEEDBACK_AZURE_ENABLED" envDefault:"false"`
FeedbackAzureAccountName string `env:"FEEDBACK_AZURE_ACCOUNT_NAME"`
FeedbackAzureAccountKey string `env:"FEEDBACK_AZURE_ACCOUNT_KEY"`
FeedbackAzureBlobContainerName string `env:"FEEDBACK_AZURE_BLOB_CONTAINER_NAME"`
}

func NewBlobConfig(logger *zap.SugaredLogger) (*BlobConfigVariables, error) {
Expand Down
125 changes: 125 additions & 0 deletions client/GoogleSheetsClient.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
* Copyright (c) 2020-2024. Devtron Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package util

import (
"context"
"encoding/json"
"github.com/caarlos0/env"
"go.uber.org/zap"
"golang.org/x/oauth2/google"
"google.golang.org/api/option"
"google.golang.org/api/sheets/v4"
)

// GoogleCloudConfig holds configuration for Google Cloud services (Sheets, Storage, etc.)
type GoogleCloudConfig struct {
ServiceAccountJSON string `env:"FEEDBACK_GCP_CREDENTIAL_FILE_JSON_DATA" envDefault:""`
SpreadsheetID string `env:"GOOGLE_SPREADSHEET_ID" envDefault:""`
}

// GoogleSheetsClient provides access to Google Sheets API and service account credentials
// The same service account can be used for Google Cloud Storage
type GoogleSheetsClient struct {
SheetsService *sheets.Service
Config *GoogleCloudConfig
}

/* #nosec */
func NewGoogleSheetsClient(logger *zap.SugaredLogger, blobConfig *BlobConfigVariables) (*GoogleSheetsClient, error) {
cfg := &GoogleCloudConfig{}
err := env.Parse(cfg)
if err != nil {
logger.Errorw("error parsing google cloud config", "err", err)
return &GoogleSheetsClient{}, err
}

// Fallback: If GOOGLE_SERVICE_ACCOUNT_JSON is not set, try using FEEDBACK_GCP_CREDENTIAL_FILE_JSON_DATA
serviceAccountJSON := cfg.ServiceAccountJSON
if serviceAccountJSON == "" && blobConfig != nil && blobConfig.FeedbackGcpCredentialFileJsonData != "" {
serviceAccountJSON = blobConfig.FeedbackGcpCredentialFileJsonData
logger.Info("Using FEEDBACK_GCP_CREDENTIAL_FILE_JSON_DATA for Google Sheets authentication")
}

// If service account JSON is not provided, return empty client
if serviceAccountJSON == "" {
logger.Warn("Google service account JSON not provided (neither GOOGLE_SERVICE_ACCOUNT_JSON nor FEEDBACK_GCP_CREDENTIAL_FILE_JSON_DATA), Google Sheets client will not be initialized")
return &GoogleSheetsClient{
SheetsService: nil,
Config: cfg,
}, nil
}

// Store the actual service account JSON being used
cfg.ServiceAccountJSON = serviceAccountJSON

ctx := context.Background()

// Parse the service account JSON
credentials, err := google.CredentialsFromJSON(ctx, []byte(serviceAccountJSON), sheets.SpreadsheetsScope)
if err != nil {
logger.Errorw("error creating credentials from service account JSON", "err", err)
return nil, err
}

// Create the sheets service
sheetsService, err := sheets.NewService(ctx, option.WithCredentials(credentials))
if err != nil {
logger.Errorw("error creating sheets service", "err", err)
return nil, err
}

logger.Info("Google Sheets client initialized successfully")

return &GoogleSheetsClient{
SheetsService: sheetsService,
Config: cfg,
}, nil
}

// IsConfigured returns true if the Google Sheets client is properly configured
func (c *GoogleSheetsClient) IsConfigured() bool {
if c == nil {
return false
}
return c.SheetsService != nil && c.Config != nil && c.Config.SpreadsheetID != ""
}

// GetSpreadsheetID returns the configured spreadsheet ID
func (c *GoogleSheetsClient) GetSpreadsheetID() string {
if c == nil || c.Config == nil {
return ""
}
return c.Config.SpreadsheetID
}

// GetServiceAccountJSON returns the service account JSON for use with other Google Cloud services
func (c *GoogleSheetsClient) GetServiceAccountJSON() string {
if c == nil || c.Config == nil {
return ""
}
return c.Config.ServiceAccountJSON
}

// MarshalServiceAccountJSON is a helper to convert service account key to JSON string
func MarshalServiceAccountJSON(serviceAccountKey map[string]interface{}) (string, error) {
jsonBytes, err := json.Marshal(serviceAccountKey)
if err != nil {
return "", err
}
return string(jsonBytes), nil
}
13 changes: 13 additions & 0 deletions common/bean.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,16 @@ type GroupVersionKind struct {
type ResourceIdentifier struct {
Labels map[string]string `json:"labels"`
}

// FeedbackData represents the data structure for feedback submissions
type FeedbackData struct {
UCID string `json:"ucid"`
ThreadName string `json:"threadName"`
UserEmail string `json:"userEmail"`
Reasons []string `json:"reasons"`
AdditionalDetails string `json:"additionalDetails"`
ConversationText string `json:"conversationText"`
IsCompressed bool `json:"isCompressed"`
SubmittedAt time.Time `json:"submittedAt"`
FullConversationURL string `json:"-"` // Internal field, not serialized to JSON
}
Loading
Loading