From e603a4893339baa8d5c28666b5f189430039e5e8 Mon Sep 17 00:00:00 2001 From: PiyushSingh-ZS Date: Wed, 11 Dec 2024 16:53:41 +0530 Subject: [PATCH 01/11] add applications post and list apis --- applications/handler/handler.go | 65 ++++++++ applications/handler/handler_test.go | 147 ++++++++++++++++++ applications/service/interface.go | 11 ++ applications/service/mock_interface.go | 66 ++++++++ applications/service/service.go | 43 ++++++ applications/service/service_test.go | 159 +++++++++++++++++++ applications/store/interface.go | 9 ++ applications/store/mock_interface.go | 80 ++++++++++ applications/store/models.go | 15 ++ applications/store/query.go | 7 + applications/store/store.go | 67 ++++++++ applications/store/store_test.go | 204 +++++++++++++++++++++++++ environments/store/models.go | 0 main.go | 11 ++ 14 files changed, 884 insertions(+) create mode 100644 applications/handler/handler.go create mode 100644 applications/handler/handler_test.go create mode 100644 applications/service/interface.go create mode 100644 applications/service/mock_interface.go create mode 100644 applications/service/service.go create mode 100644 applications/service/service_test.go create mode 100644 applications/store/interface.go create mode 100644 applications/store/mock_interface.go create mode 100644 applications/store/models.go create mode 100644 applications/store/query.go create mode 100644 applications/store/store.go create mode 100644 applications/store/store_test.go create mode 100644 environments/store/models.go diff --git a/applications/handler/handler.go b/applications/handler/handler.go new file mode 100644 index 0000000..01903dc --- /dev/null +++ b/applications/handler/handler.go @@ -0,0 +1,65 @@ +package handler + +import ( + "strings" + + "github.com/zopdev/zop-api/applications/service" + "github.com/zopdev/zop-api/applications/store" + + "gofr.dev/pkg/gofr" + "gofr.dev/pkg/gofr/http" +) + +type Handler struct { + service service.ApplicationService +} + +func New(svc service.ApplicationService) *Handler { + return &Handler{service: svc} +} + +func (h *Handler) AddApplication(ctx *gofr.Context) (interface{}, error) { + application := store.Application{} + + err := ctx.Bind(&application) + if err != nil { + ctx.Logger.Error(err) + return nil, http.ErrorInvalidParam{Params: []string{"body"}} + } + + err = validateApplication(&application) + if err != nil { + return nil, err + } + + res, err := h.service.AddApplication(ctx, &application) + if err != nil { + return nil, err + } + + return res, nil +} + +func (h *Handler) ListApplications(ctx *gofr.Context) (interface{}, error) { + applications, err := h.service.FetchAllApplications(ctx) + if err != nil { + return nil, err + } + + return applications, nil +} + +func validateApplication(application *store.Application) error { + application.Name = strings.TrimSpace(application.Name) + + params := []string{} + if application.Name == "" { + params = append(params, "name") + } + + if len(params) > 0 { + return http.ErrorInvalidParam{Params: params} + } + + return nil +} diff --git a/applications/handler/handler_test.go b/applications/handler/handler_test.go new file mode 100644 index 0000000..b4c0967 --- /dev/null +++ b/applications/handler/handler_test.go @@ -0,0 +1,147 @@ +package handler + +import ( + "context" + "errors" + netHTTP "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + "gofr.dev/pkg/gofr" + "gofr.dev/pkg/gofr/http" + + "github.com/zopdev/zop-api/applications/service" + "github.com/zopdev/zop-api/applications/store" +) + +var ( + errTest = errors.New("service error") +) + +func TestHandler_AddApplication(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockService := service.NewMockApplicationService(ctrl) + handler := New(mockService) + + testCases := []struct { + name string + requestBody string + mockBehavior func() + expectedStatus int + expectedError error + }{ + { + name: "success", + requestBody: `{"name":"Test Application"}`, + mockBehavior: func() { + mockService.EXPECT(). + AddApplication(gomock.Any(), gomock.Any()). + Return(&store.Application{Name: "Test Application"}, nil) + }, + expectedStatus: netHTTP.StatusOK, + expectedError: nil, + }, + { + name: "missing name", + requestBody: `{}`, + mockBehavior: func() {}, + expectedStatus: netHTTP.StatusBadRequest, + expectedError: http.ErrorInvalidParam{Params: []string{"name"}}, + }, + { + name: "service error", + requestBody: `{"name":"Test Application"}`, + mockBehavior: func() { + mockService.EXPECT(). + AddApplication(gomock.Any(), gomock.Any()). + Return(nil, errTest) + }, + expectedStatus: netHTTP.StatusInternalServerError, + expectedError: errTest, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tc.mockBehavior() + + // Prepare HTTP request + req := httptest.NewRequest(netHTTP.MethodPost, "/add", strings.NewReader(tc.requestBody)) + req.Header.Set("Content-Type", "application/json") + + ctx := &gofr.Context{Context: context.Background(), Request: http.NewRequest(req)} + + _, err := handler.AddApplication(ctx) + + if tc.expectedError != nil { + require.Error(t, err) + require.Equal(t, tc.expectedError, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestHandler_ListApplications(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockService := service.NewMockApplicationService(ctrl) + handler := New(mockService) + + testCases := []struct { + name string + mockBehavior func() + expectedStatus int + expectedError error + }{ + { + name: "success", + mockBehavior: func() { + mockService.EXPECT(). + FetchAllApplications(gomock.Any()). + Return([]store.Application{ + {Name: "Test Application"}, + }, nil) + }, + expectedStatus: netHTTP.StatusOK, + expectedError: nil, + }, + { + name: "service error", + mockBehavior: func() { + mockService.EXPECT(). + FetchAllApplications(gomock.Any()). + Return(nil, errTest) + }, + expectedStatus: netHTTP.StatusInternalServerError, + expectedError: errTest, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tc.mockBehavior() + + // Prepare HTTP request + req := httptest.NewRequest(netHTTP.MethodGet, "/list", netHTTP.NoBody) + + ctx := &gofr.Context{Context: context.Background(), Request: http.NewRequest(req)} + + _, err := handler.ListApplications(ctx) + + if tc.expectedError != nil { + require.Error(t, err) + require.Equal(t, tc.expectedError, err) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/applications/service/interface.go b/applications/service/interface.go new file mode 100644 index 0000000..a801ce7 --- /dev/null +++ b/applications/service/interface.go @@ -0,0 +1,11 @@ +package service + +import ( + "github.com/zopdev/zop-api/applications/store" + "gofr.dev/pkg/gofr" +) + +type ApplicationService interface { + AddApplication(ctx *gofr.Context, application *store.Application) (*store.Application, error) + FetchAllApplications(ctx *gofr.Context) ([]store.Application, error) +} diff --git a/applications/service/mock_interface.go b/applications/service/mock_interface.go new file mode 100644 index 0000000..8808b66 --- /dev/null +++ b/applications/service/mock_interface.go @@ -0,0 +1,66 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: interface.go + +// Package service is a generated GoMock package. +package service + +import ( + reflect "reflect" + + store "github.com/zopdev/zop-api/applications/store" + gomock "go.uber.org/mock/gomock" + gofr "gofr.dev/pkg/gofr" +) + +// MockApplicationService is a mock of ApplicationService interface. +type MockApplicationService struct { + ctrl *gomock.Controller + recorder *MockApplicationServiceMockRecorder +} + +// MockApplicationServiceMockRecorder is the mock recorder for MockApplicationService. +type MockApplicationServiceMockRecorder struct { + mock *MockApplicationService +} + +// NewMockApplicationService creates a new mock instance. +func NewMockApplicationService(ctrl *gomock.Controller) *MockApplicationService { + mock := &MockApplicationService{ctrl: ctrl} + mock.recorder = &MockApplicationServiceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockApplicationService) EXPECT() *MockApplicationServiceMockRecorder { + return m.recorder +} + +// AddApplication mocks base method. +func (m *MockApplicationService) AddApplication(ctx *gofr.Context, application *store.Application) (*store.Application, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddApplication", ctx, application) + ret0, _ := ret[0].(*store.Application) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// AddApplication indicates an expected call of AddApplication. +func (mr *MockApplicationServiceMockRecorder) AddApplication(ctx, application interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddApplication", reflect.TypeOf((*MockApplicationService)(nil).AddApplication), ctx, application) +} + +// FetchAllApplications mocks base method. +func (m *MockApplicationService) FetchAllApplications(ctx *gofr.Context) ([]store.Application, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FetchAllApplications", ctx) + ret0, _ := ret[0].([]store.Application) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FetchAllApplications indicates an expected call of FetchAllApplications. +func (mr *MockApplicationServiceMockRecorder) FetchAllApplications(ctx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchAllApplications", reflect.TypeOf((*MockApplicationService)(nil).FetchAllApplications), ctx) +} diff --git a/applications/service/service.go b/applications/service/service.go new file mode 100644 index 0000000..49460c2 --- /dev/null +++ b/applications/service/service.go @@ -0,0 +1,43 @@ +package service + +import ( + "errors" + + "database/sql" + + "github.com/zopdev/zop-api/applications/store" + "gofr.dev/pkg/gofr" + "gofr.dev/pkg/gofr/http" +) + +type Service struct { + store store.ApplicationStore +} + +func New(str store.ApplicationStore) ApplicationService { + return &Service{store: str} +} + +func (s *Service) AddApplication(ctx *gofr.Context, application *store.Application) (*store.Application, error) { + tempApplication, err := s.store.GetApplicationByName(ctx, application.Name) + if err != nil { + if !errors.Is(err, sql.ErrNoRows) { + return nil, err + } + } + + if tempApplication != nil { + return nil, http.ErrorEntityAlreadyExist{} + } + + application, err = s.store.InsertApplication(ctx, application) + if err != nil { + return nil, err + } + + return application, nil +} + +func (s *Service) FetchAllApplications(ctx *gofr.Context) ([]store.Application, error) { + return s.store.GetALLApplications(ctx) +} diff --git a/applications/service/service_test.go b/applications/service/service_test.go new file mode 100644 index 0000000..c5f7e4f --- /dev/null +++ b/applications/service/service_test.go @@ -0,0 +1,159 @@ +package service + +import ( + "database/sql" + "errors" + "testing" + + "github.com/stretchr/testify/require" + + "go.uber.org/mock/gomock" + "gofr.dev/pkg/gofr" + + "github.com/zopdev/zop-api/applications/store" + "gofr.dev/pkg/gofr/http" +) + +var ( + errTest = errors.New("service error") +) + +func TestService_AddApplication(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockStore := store.NewMockApplicationStore(ctrl) + ctx := &gofr.Context{} + + application := &store.Application{ + Name: "Test Application", + } + + testCases := []struct { + name string + mockBehavior func() + input *store.Application + expectedError error + }{ + { + name: "success", + mockBehavior: func() { + mockStore.EXPECT(). + GetApplicationByName(ctx, "Test Application"). + Return(nil, sql.ErrNoRows) + mockStore.EXPECT(). + InsertApplication(ctx, application). + Return(application, nil) + }, + input: application, + expectedError: nil, + }, + { + name: "application already exists", + mockBehavior: func() { + mockStore.EXPECT(). + GetApplicationByName(ctx, "Test Application"). + Return(application, nil) + }, + input: application, + expectedError: http.ErrorEntityAlreadyExist{}, + }, + { + name: "error fetching application by name", + mockBehavior: func() { + mockStore.EXPECT(). + GetApplicationByName(ctx, "Test Application"). + Return(nil, errTest) + }, + input: application, + expectedError: errTest, + }, + { + name: "error inserting application", + mockBehavior: func() { + mockStore.EXPECT(). + GetApplicationByName(ctx, "Test Application"). + Return(nil, sql.ErrNoRows) + mockStore.EXPECT(). + InsertApplication(ctx, application). + Return(nil, errTest) + }, + input: application, + expectedError: errTest, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tc.mockBehavior() + + service := New(mockStore) + _, err := service.AddApplication(ctx, tc.input) + + if tc.expectedError != nil { + require.Error(t, err) + require.Equal(t, tc.expectedError.Error(), err.Error()) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestService_FetchAllApplications(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockStore := store.NewMockApplicationStore(ctrl) + ctx := &gofr.Context{} + + expectedApplications := []store.Application{ + { + ID: 1, + Name: "Test Application", + CreatedAt: "2023-12-11T00:00:00Z", + }, + } + + testCases := []struct { + name string + mockBehavior func() + expectedError error + }{ + { + name: "success", + mockBehavior: func() { + mockStore.EXPECT(). + GetALLApplications(ctx). + Return(expectedApplications, nil) + }, + expectedError: nil, + }, + { + name: "error fetching applications", + mockBehavior: func() { + mockStore.EXPECT(). + GetALLApplications(ctx). + Return(nil, errTest) + }, + expectedError: errTest, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tc.mockBehavior() + + service := New(mockStore) + applications, err := service.FetchAllApplications(ctx) + + if tc.expectedError != nil { + require.Error(t, err) + require.Equal(t, tc.expectedError, err) + } else { + require.NoError(t, err) + require.Equal(t, expectedApplications, applications) + } + }) + } +} diff --git a/applications/store/interface.go b/applications/store/interface.go new file mode 100644 index 0000000..d212eca --- /dev/null +++ b/applications/store/interface.go @@ -0,0 +1,9 @@ +package store + +import "gofr.dev/pkg/gofr" + +type ApplicationStore interface { + InsertApplication(ctx *gofr.Context, application *Application) (*Application, error) + GetALLApplications(ctx *gofr.Context) ([]Application, error) + GetApplicationByName(ctx *gofr.Context, name string) (*Application, error) +} diff --git a/applications/store/mock_interface.go b/applications/store/mock_interface.go new file mode 100644 index 0000000..d52d186 --- /dev/null +++ b/applications/store/mock_interface.go @@ -0,0 +1,80 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: interface.go + +// Package store is a generated GoMock package. +package store + +import ( + reflect "reflect" + + gomock "go.uber.org/mock/gomock" + gofr "gofr.dev/pkg/gofr" +) + +// MockApplicationStore is a mock of ApplicationStore interface. +type MockApplicationStore struct { + ctrl *gomock.Controller + recorder *MockApplicationStoreMockRecorder +} + +// MockApplicationStoreMockRecorder is the mock recorder for MockApplicationStore. +type MockApplicationStoreMockRecorder struct { + mock *MockApplicationStore +} + +// NewMockApplicationStore creates a new mock instance. +func NewMockApplicationStore(ctrl *gomock.Controller) *MockApplicationStore { + mock := &MockApplicationStore{ctrl: ctrl} + mock.recorder = &MockApplicationStoreMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockApplicationStore) EXPECT() *MockApplicationStoreMockRecorder { + return m.recorder +} + +// GetALLApplications mocks base method. +func (m *MockApplicationStore) GetALLApplications(ctx *gofr.Context) ([]Application, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetALLApplications", ctx) + ret0, _ := ret[0].([]Application) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetALLApplications indicates an expected call of GetALLApplications. +func (mr *MockApplicationStoreMockRecorder) GetALLApplications(ctx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetALLApplications", reflect.TypeOf((*MockApplicationStore)(nil).GetALLApplications), ctx) +} + +// GetApplicationByName mocks base method. +func (m *MockApplicationStore) GetApplicationByName(ctx *gofr.Context, name string) (*Application, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetApplicationByName", ctx, name) + ret0, _ := ret[0].(*Application) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetApplicationByName indicates an expected call of GetApplicationByName. +func (mr *MockApplicationStoreMockRecorder) GetApplicationByName(ctx, name interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetApplicationByName", reflect.TypeOf((*MockApplicationStore)(nil).GetApplicationByName), ctx, name) +} + +// InsertApplication mocks base method. +func (m *MockApplicationStore) InsertApplication(ctx *gofr.Context, application *Application) (*Application, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "InsertApplication", ctx, application) + ret0, _ := ret[0].(*Application) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// InsertApplication indicates an expected call of InsertApplication. +func (mr *MockApplicationStoreMockRecorder) InsertApplication(ctx, application interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertApplication", reflect.TypeOf((*MockApplicationStore)(nil).InsertApplication), ctx, application) +} diff --git a/applications/store/models.go b/applications/store/models.go new file mode 100644 index 0000000..149b8d2 --- /dev/null +++ b/applications/store/models.go @@ -0,0 +1,15 @@ +package store + +type Application struct { + ID int64 `json:"ID"` + Name string `json:"name"` + + // CreatedAt is the timestamp of when the cloud account was created. + CreatedAt string `json:"createdAt"` + + // UpdatedAt is the timestamp of the last update to the cloud account. + UpdatedAt string `json:"updatedAt"` + + // DeletedAt is the timestamp of when the cloud account was deleted, if applicable. + DeletedAt string `json:"deletedAt,omitempty"` +} diff --git a/applications/store/query.go b/applications/store/query.go new file mode 100644 index 0000000..4cd8326 --- /dev/null +++ b/applications/store/query.go @@ -0,0 +1,7 @@ +package store + +const ( + INSERTQUERY = "INSERT INTO application ( name) VALUES ( ?);" + GETALLQUERY = "SELECT id, name, created_at, updated_at FROM application WHERE deleted_at IS NULL;" + GETBYNAMEQUERY = "SELECT id, name, created_at, updated_at FROM application WHERE name = ? and deleted_at IS NULL;" +) diff --git a/applications/store/store.go b/applications/store/store.go new file mode 100644 index 0000000..3e03bd5 --- /dev/null +++ b/applications/store/store.go @@ -0,0 +1,67 @@ +package store + +import ( + "time" + + "gofr.dev/pkg/gofr" +) + +type Store struct{} + +func New() ApplicationStore { + return &Store{} +} +func (*Store) InsertApplication(ctx *gofr.Context, application *Application) (*Application, error) { + res, err := ctx.SQL.ExecContext(ctx, INSERTQUERY, application.Name) + if err != nil { + return nil, err + } + + application.ID, err = res.LastInsertId() + application.CreatedAt = time.Now().UTC().Format(time.RFC3339) + + return application, err +} + +func (*Store) GetALLApplications(ctx *gofr.Context) ([]Application, error) { + rows, err := ctx.SQL.QueryContext(ctx, GETALLQUERY) + if err != nil { + return nil, err + } + + if rows.Err() != nil { + return nil, rows.Err() + } + + applications := make([]Application, 0) + + for rows.Next() { + application := Application{} + + err = rows.Scan(&application.ID, &application.Name, &application.CreatedAt, &application.UpdatedAt) + if err != nil { + return nil, err + } + + applications = append(applications, application) + } + + return applications, nil +} + +func (*Store) GetApplicationByName(ctx *gofr.Context, name string) (*Application, error) { + row := ctx.SQL.QueryRowContext(ctx, GETBYNAMEQUERY, name) + if row.Err() != nil { + return nil, row.Err() + } + + application := Application{} + + err := row.Scan(&application.ID, &application.Name, &application.CreatedAt, &application.UpdatedAt) + + if err != nil { + return nil, err + } + + return &application, nil +} diff --git a/applications/store/store_test.go b/applications/store/store_test.go new file mode 100644 index 0000000..0b243fe --- /dev/null +++ b/applications/store/store_test.go @@ -0,0 +1,204 @@ +package store + +import ( + "context" + "database/sql" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/require" + "gofr.dev/pkg/gofr" + "gofr.dev/pkg/gofr/container" +) + +func TestInsertApplication(t *testing.T) { + mockContainer, mock := container.NewMockContainer(t) + ctx := &gofr.Context{ + Context: context.Background(), + Request: nil, + Container: mockContainer, + } + + application := &Application{Name: "Test Application"} + + testCases := []struct { + name string + application *Application + expectedError bool + mockBehavior func() + }{ + { + name: "success", + application: application, + expectedError: false, + mockBehavior: func() { + mock.SQL.ExpectExec(INSERTQUERY). + WithArgs(application.Name). + WillReturnResult(sqlmock.NewResult(1, 1)) + }, + }, + { + name: "failure on query execution", + application: application, + expectedError: true, + mockBehavior: func() { + mock.SQL.ExpectExec(INSERTQUERY). + WithArgs(application.Name). + WillReturnError(sql.ErrConnDone) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tc.mockBehavior() + + store := New() + result, err := store.InsertApplication(ctx, tc.application) + + if tc.expectedError { + require.Error(t, err) + require.Nil(t, result) + } else { + require.NoError(t, err) + require.NotNil(t, result) + require.Equal(t, application.Name, result.Name) + } + }) + } +} + +func TestGetALLApplications(t *testing.T) { + mockContainer, mock := container.NewMockContainer(t) + ctx := &gofr.Context{ + Context: context.Background(), + Request: nil, + Container: mockContainer, + } + + testCases := []struct { + name string + mockBehavior func() + expectedError bool + expectedCount int + }{ + { + name: "success", + mockBehavior: func() { + mockRows := sqlmock.NewRows([]string{"id", "name", "created_at", "updated_at"}). + AddRow(1, "Test Application", time.Now(), time.Now()) + mock.SQL.ExpectQuery(GETALLQUERY). + WillReturnRows(mockRows) + }, + expectedError: false, + expectedCount: 1, + }, + { + name: "failure on query execution", + mockBehavior: func() { + mock.SQL.ExpectQuery(GETALLQUERY). + WillReturnError(sql.ErrConnDone) + }, + expectedError: true, + expectedCount: 0, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tc.mockBehavior() + + store := New() + applications, err := store.GetALLApplications(ctx) + + if tc.expectedError { + require.Error(t, err) + require.Nil(t, applications) + } else { + require.NoError(t, err) + require.Len(t, applications, tc.expectedCount) + } + }) + } +} + +func TestGetApplicationByName(t *testing.T) { + mockContainer, mock := container.NewMockContainer(t) + ctx := &gofr.Context{ + Context: context.Background(), + Request: nil, + Container: mockContainer, + } + + testCases := []struct { + name string + appName string + mockBehavior func() + expectedError bool + expectedNil bool + expectedName string + }{ + { + name: "success", + appName: "Test Application", + mockBehavior: func() { + mockRow := sqlmock.NewRows([]string{"id", "name", "created_at", "updated_at"}). + AddRow(1, "Test Application", time.Now(), time.Now()) + mock.SQL.ExpectQuery(GETBYNAMEQUERY). + WithArgs("Test Application"). + WillReturnRows(mockRow) + }, + expectedError: false, + expectedNil: false, + expectedName: "Test Application", + }, + { + name: "no rows found", + appName: "Non-existent Application", + mockBehavior: func() { + mock.SQL.ExpectQuery(GETBYNAMEQUERY). + WithArgs("Non-existent Application"). + WillReturnRows(sqlmock.NewRows(nil)) + }, + expectedError: true, + expectedNil: true, + expectedName: "", + }, + { + name: "failure on query execution", + appName: "Test Application", + mockBehavior: func() { + mock.SQL.ExpectQuery(GETBYNAMEQUERY). + WithArgs("Test Application"). + WillReturnError(sql.ErrConnDone) + }, + expectedError: true, + expectedNil: true, + expectedName: "", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tc.mockBehavior() + + store := New() + application, err := store.GetApplicationByName(ctx, tc.appName) + + if tc.expectedError { + require.Error(t, err) + require.Nil(t, application) + } else { + require.NoError(t, err) + + if tc.expectedNil { + require.Nil(t, application) + } else { + require.NotNil(t, application) + require.Equal(t, tc.expectedName, application.Name) + } + } + }) + } +} diff --git a/environments/store/models.go b/environments/store/models.go new file mode 100644 index 0000000..e69de29 diff --git a/main.go b/main.go index 4387e37..c68598b 100644 --- a/main.go +++ b/main.go @@ -4,6 +4,10 @@ import ( "github.com/zopdev/zop-api/cloudaccounts/handler" "github.com/zopdev/zop-api/cloudaccounts/service" "github.com/zopdev/zop-api/cloudaccounts/store" + + appHandler "github.com/zopdev/zop-api/applications/handler" + appService "github.com/zopdev/zop-api/applications/service" + appStore "github.com/zopdev/zop-api/applications/store" "github.com/zopdev/zop-api/migrations" "gofr.dev/pkg/gofr" ) @@ -17,8 +21,15 @@ func main() { cloudAccountService := service.New(cloudAccountStore) cloudAccountHandler := handler.New(cloudAccountService) + applicationStore := appStore.New() + applicationService := appService.New(applicationStore) + applicationHandler := appHandler.New(applicationService) + app.POST("/cloud-accounts", cloudAccountHandler.AddCloudAccount) app.GET("/cloud-accounts", cloudAccountHandler.ListCloudAccounts) + app.POST("/applications", applicationHandler.AddApplication) + app.GET("/applications", applicationHandler.ListApplications) + app.Run() } From a55fd5ab25d36afc48abc6d9f15a5740fc91a80f Mon Sep 17 00:00:00 2001 From: PiyushSingh-ZS Date: Wed, 11 Dec 2024 17:35:15 +0530 Subject: [PATCH 02/11] add create application table migration --- environments/store/models.go | 0 .../20241211121223_createAPPlicationTable.go | 28 +++++++++++++++++++ migrations/all.go | 1 + 3 files changed, 29 insertions(+) delete mode 100644 environments/store/models.go create mode 100755 migrations/20241211121223_createAPPlicationTable.go diff --git a/environments/store/models.go b/environments/store/models.go deleted file mode 100644 index e69de29..0000000 diff --git a/migrations/20241211121223_createAPPlicationTable.go b/migrations/20241211121223_createAPPlicationTable.go new file mode 100755 index 0000000..65b325c --- /dev/null +++ b/migrations/20241211121223_createAPPlicationTable.go @@ -0,0 +1,28 @@ +package migrations + +import ( + "gofr.dev/pkg/gofr/migration" +) + +func createAPPlicationTable() migration.Migrate { + return migration.Migrate{ + UP: func(d migration.Datasource) error { + const query = ` + CREATE TABLE if not exists application ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name VARCHAR(255) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP + ); + ` + + _, err := d.SQL.Exec(query) + if err != nil { + return err + } + + return nil + }, + } +} diff --git a/migrations/all.go b/migrations/all.go index 8f7b6a8..867fb6d 100644 --- a/migrations/all.go +++ b/migrations/all.go @@ -9,5 +9,6 @@ func All() map[int64]migration.Migrate { return map[int64]migration.Migrate{ 20241209162239: createCloudAccountTable(), + 20241211121223: createAPPlicationTable(), } } From 3beb46a93d54bb4818ec931db82b106ffe864720 Mon Sep 17 00:00:00 2001 From: PiyushSingh-ZS Date: Tue, 17 Dec 2024 01:25:38 +0530 Subject: [PATCH 03/11] add gcp provider --- provider/gcp/gke.go | 295 +++++++++++++++++++++++++++++++++++++ provider/gcp/models.go | 43 ++++++ provider/interface.go | 32 ++++ provider/mock_interface.go | 66 +++++++++ provider/models.go | 111 ++++++++++++++ 5 files changed, 547 insertions(+) create mode 100644 provider/gcp/gke.go create mode 100644 provider/gcp/models.go create mode 100644 provider/interface.go create mode 100644 provider/mock_interface.go create mode 100644 provider/models.go diff --git a/provider/gcp/gke.go b/provider/gcp/gke.go new file mode 100644 index 0000000..c16d5f2 --- /dev/null +++ b/provider/gcp/gke.go @@ -0,0 +1,295 @@ +// Package gcp provides an implementation of the Provider interface for interacting with +// Google Cloud Platform (GCP) resources such as GKE clusters and namespaces. +// +// It implements methods to list all clusters and namespaces in a GKE cluster using GCP credentials, +// and returns responses in the format expected by the `provider` package. +package gcp + +import ( + "crypto/tls" + "crypto/x509" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + + "cloud.google.com/go/container/apiv1/containerpb" + + "github.com/zopdev/zop-api/provider" + + "gofr.dev/pkg/gofr" + + "golang.org/x/oauth2/google" + "google.golang.org/api/option" + + container "cloud.google.com/go/container/apiv1" + + apiContainer "google.golang.org/api/container/v1" +) + +// GCP implements the provider.Provider interface for Google Cloud Platform. +type GCP struct { +} + +// New initializes and returns a new GCP provider. +func New() provider.Provider { + return &GCP{} +} + +// ListAllClusters lists all clusters available for a given cloud account in GCP. +// It uses the GCP credentials to authenticate and fetch the cluster details. +func (g *GCP) ListAllClusters(ctx *gofr.Context, cloudAccount *provider.CloudAccount, + credentials interface{}) (*provider.ClusterResponse, error) { + credBody, err := g.getCredGCP(credentials) + if err != nil { + return nil, err + } + + client, err := g.getClusterManagerClientGCP(ctx, credBody) + if err != nil { + return nil, err + } + + defer client.Close() + + req := &containerpb.ListClustersRequest{ + Parent: fmt.Sprintf("projects/%s/locations/-", cloudAccount.ProviderID), + } + + resp, err := client.ListClusters(ctx, req) + if err != nil { + return nil, err + } + + gkeClusters := make([]provider.Cluster, 0) + + for _, cl := range resp.Clusters { + gkeCluster := provider.Cluster{ + Name: cl.Name, + Identifier: cl.Id, + Region: cl.Location, + Locations: cl.Locations, + Type: "deploymentSpace", + } + + for _, nps := range cl.NodePools { + cfg := nps.GetConfig() + + nodepool := provider.NodePool{ + MachineType: cfg.MachineType, + NodeVersion: nps.Version, + CurrentNode: nps.InitialNodeCount, + NodeName: nps.Name, + } + + gkeCluster.NodePools = append(gkeCluster.NodePools, nodepool) + } + + gkeClusters = append(gkeClusters, gkeCluster) + } + + response := &provider.ClusterResponse{ + Clusters: gkeClusters, + NextPage: provider.NextPage{ + Name: "Namespace", + Path: fmt.Sprintf("/cloud-accounts/%v/deployment-space/namespaces", cloudAccount.ID), + Params: map[string]string{ + "region": "region", + "name": "name", + }, + }, + } + + return response, nil +} + +// ListNamespace fetches namespaces from the Kubernetes API for a given GKE cluster. +func (g *GCP) ListNamespace(ctx *gofr.Context, cluster *provider.Cluster, + cloudAccount *provider.CloudAccount, credentials interface{}) (interface{}, error) { + // Step 1: Get GCP credentials + credBody, err := g.getCredGCP(credentials) + if err != nil { + return nil, fmt.Errorf("failed to get credentials: %w", err) + } + + // Step 2: Get cluster information + gkeCluster, err := g.getClusterInfo(ctx, cluster, cloudAccount, credBody) + if err != nil { + return nil, fmt.Errorf("failed to get cluster info: %w", err) + } + + // Step 3: Create HTTP client with TLS configured + client, err := g.createTLSConfiguredClient(gkeCluster.MasterAuth.ClusterCaCertificate) + if err != nil { + return nil, fmt.Errorf("failed to create TLS configured client: %w", err) + } + + // Step 4: Fetch namespaces from the Kubernetes API + apiEndpoint := fmt.Sprintf("https://%s/api/v1/namespaces", gkeCluster.Endpoint) + + namespaces, err := g.fetchNamespaces(ctx, client, credBody, apiEndpoint) + if err != nil { + return nil, fmt.Errorf("failed to fetch namespaces: %w", err) + } + + return namespaces, nil +} + +// getClusterInfo retrieves detailed information about a specific GKE cluster. +func (*GCP) getClusterInfo(ctx *gofr.Context, cluster *provider.Cluster, + cloudAccount *provider.CloudAccount, credBody []byte) (*apiContainer.Cluster, error) { + // Create the GCP Container service + containerService, err := apiContainer.NewService(ctx, option.WithCredentialsJSON(credBody)) + if err != nil { + return nil, fmt.Errorf("failed to create container service: %w", err) + } + + // Construct the full cluster name + clusterFullName := fmt.Sprintf("projects/%s/locations/%s/clusters/%s", + cloudAccount.ProviderID, cluster.Region, cluster.Name) + + // Get the GCP cluster details + gkeCluster, err := containerService.Projects.Locations.Clusters.Get(clusterFullName). + Context(ctx).Do() + if err != nil { + return nil, fmt.Errorf("failed to get GCP cluster details: %w", err) + } + + return gkeCluster, nil +} + +// createTLSConfiguredClient creates an HTTP client with custom TLS configuration using the provided CA certificate. +func (*GCP) createTLSConfiguredClient(caCertificate string) (*http.Client, error) { + // Decode the Base64-encoded CA certificate + caCertBytes, err := base64.StdEncoding.DecodeString(caCertificate) + if err != nil { + return nil, fmt.Errorf("failed to decode CA certificate: %w", err) + } + + // Create a CA certificate pool + caCertPool := x509.NewCertPool() + if !caCertPool.AppendCertsFromPEM(caCertBytes) { + return nil, err + } + + //nolint:gosec //Create a custom HTTP client with the CA certificate + tlsConfig := &tls.Config{ + RootCAs: caCertPool, + } + client := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: tlsConfig, + }, + } + + return client, nil +} + +// fetchNamespaces fetches Kubernetes namespaces from the specified API endpoint using the provided HTTP client. +func (*GCP) fetchNamespaces(ctx *gofr.Context, client *http.Client, credBody []byte, + apiEndpoint string) (*provider.NamespaceResponse, error) { + // Generate a JWT token from the credentials + config, err := google.JWTConfigFromJSON(credBody, "https://www.googleapis.com/auth/cloud-platform") + if err != nil { + return nil, fmt.Errorf("failed to create JWT config: %w", err) + } + + // Create a TokenSource + tokenSource := config.TokenSource(ctx) + + // Get a token + token, err := tokenSource.Token() + if err != nil { + ctx.Logger.Errorf("failed to get token: %v", err) + return nil, err + } + + // Make a request to the Kubernetes API to list namespaces + req, err := http.NewRequestWithContext(ctx, http.MethodGet, apiEndpoint, http.NoBody) + if err != nil { + ctx.Logger.Errorf("failed to create request: %w", err) + return nil, err + } + + req.Header.Set("Authorization", "Bearer "+token.AccessToken) + + req.Header.Set("Accept", "application/json") + + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("API call failed: %w", err) + } + defer resp.Body.Close() + + // Handle unexpected status codes + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + + ctx.Logger.Errorf("API call failed with status code %d: %s", resp.StatusCode, body) + + return nil, err + } + + // Read the response body + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + + // Parse JSON response + var namespaceResponse struct { + Items []struct { + Metadata struct { + Name string `json:"name"` + } `json:"metadata"` + } `json:"items"` + } + + if err := json.Unmarshal(body, &namespaceResponse); err != nil { + return nil, fmt.Errorf("failed to parse JSON response: %w", err) + } + + // Extract namespace names + namespaces := []provider.Namespace{} + + for _, item := range namespaceResponse.Items { + namespace := provider.Namespace{ + Name: item.Metadata.Name, + Type: "deploymentSpace.namespace", + } + + namespaces = append(namespaces, namespace) + } + + return &provider.NamespaceResponse{ + Options: namespaces, + }, nil +} + +// getCredGCP extracts and marshals the credentials into the appropriate format for GCP authentication. +func (*GCP) getCredGCP(credentials any) ([]byte, error) { + var cred gcpCredentials + + credBody, err := json.Marshal(credentials) + if err != nil { + return nil, err + } + + err = json.Unmarshal(credBody, &cred) + if err != nil { + return nil, err + } + + return json.Marshal(cred) +} + +// getClusterManagerClientGCP creates a client for interacting with the GKE Cluster Manager API. +func (*GCP) getClusterManagerClientGCP(ctx *gofr.Context, credentials []byte) (*container.ClusterManagerClient, error) { + client, err := container.NewClusterManagerClient(ctx, option.WithCredentialsJSON(credentials)) + if err != nil { + return nil, err + } + + return client, nil +} diff --git a/provider/gcp/models.go b/provider/gcp/models.go new file mode 100644 index 0000000..13de87f --- /dev/null +++ b/provider/gcp/models.go @@ -0,0 +1,43 @@ +package gcp + +// gcpCredentials holds the authentication details for a Google Cloud Platform (GCP) account. +// It contains all the necessary fields to authenticate and interact with GCP resources, including +// project ID, private key, client email, and other credentials for OAuth2-based authentication. +// +// This struct is typically used for passing credentials to services that need to authenticate +// against GCP, such as the GCP provider service. + +type gcpCredentials struct { + // Type represents the type of the credentials, typically "service_account". + Type string `json:"type"` + + // ProjectID is the ID of the GCP project associated with the credentials. + ProjectID string `json:"project_id"` + + // PrivateKeyID is the identifier for the private key used in the authentication process. + PrivateKeyID string `json:"private_key_id"` + + // PrivateKey contains the private key used for authentication with the GCP service. + PrivateKey string `json:"private_key"` + + // ClientEmail is the email address associated with the GCP service account. + ClientEmail string `json:"client_email"` + + // ClientID is the identifier for the GCP service account client. + ClientID string `json:"client_id"` + + // AuthURI is the URI for the authorization server used for OAuth2 authentication. + AuthURI string `json:"auth_uri"` + + // TokenURI is the URI used to obtain the access token for authentication. + TokenURI string `json:"token_uri"` + + // AuthProviderX509CertURL is the URL of the X.509 certificate used to verify the identity of the authentication provider. + AuthProviderX509CertURL string `json:"auth_provider_x509_cert_url"` + + // ClientX509CertURL is the URL of the X.509 certificate for the client. + ClientX509CertURL string `json:"client_x509_cert_url"` + + // UniverseDomain represents the domain for the GCP service account. + UniverseDomain string `json:"universe_domain"` +} diff --git a/provider/interface.go b/provider/interface.go new file mode 100644 index 0000000..c353a3e --- /dev/null +++ b/provider/interface.go @@ -0,0 +1,32 @@ +package provider + +import ( + "gofr.dev/pkg/gofr" +) + +// Provider defines the interface for interacting with a cloud provider's resources. +// It includes methods for listing all clusters and retrieving namespaces for a given cluster. +// +// This interface can be implemented for various cloud providers such as AWS, GCP, or Azure. +// It allows users to interact with cloud infrastructure, retrieve clusters, and list namespaces. + +type Provider interface { + // ListAllClusters lists all clusters available for a given cloud account. + // + // ctx: The context for the request. + // cloudAccount: The cloud account associated with the provider (e.g., AWS, GCP, Azure). + // credentials: The authentication credentials used to access the provider's resources. + // + // Returns a ClusterResponse containing details of the available clusters, or an error if the request fails. + ListAllClusters(ctx *gofr.Context, cloudAccount *CloudAccount, credentials interface{}) (*ClusterResponse, error) + + // ListNamespace retrieves namespaces for a given cluster within a cloud account. + // + // ctx: The context for the request. + // cluster: The cluster for which to list namespaces. + // cloudAccount: The cloud account associated with the provider. + // credentials: The authentication credentials used to access the provider's resources. + // + // Returns the namespaces for the specified cluster, or an error if the request fails. + ListNamespace(ctx *gofr.Context, cluster *Cluster, cloudAccount *CloudAccount, credentials interface{}) (interface{}, error) +} diff --git a/provider/mock_interface.go b/provider/mock_interface.go new file mode 100644 index 0000000..18fc1ff --- /dev/null +++ b/provider/mock_interface.go @@ -0,0 +1,66 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: interface.go + +// Package provider is a generated GoMock package. +package provider + +import ( + reflect "reflect" + + gomock "go.uber.org/mock/gomock" + + gofr "gofr.dev/pkg/gofr" +) + +// MockProvider is a mock of Provider interface. +type MockProvider struct { + ctrl *gomock.Controller + recorder *MockProviderMockRecorder +} + +// MockProviderMockRecorder is the mock recorder for MockProvider. +type MockProviderMockRecorder struct { + mock *MockProvider +} + +// NewMockProvider creates a new mock instance. +func NewMockProvider(ctrl *gomock.Controller) *MockProvider { + mock := &MockProvider{ctrl: ctrl} + mock.recorder = &MockProviderMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockProvider) EXPECT() *MockProviderMockRecorder { + return m.recorder +} + +// ListAllClusters mocks base method. +func (m *MockProvider) ListAllClusters(ctx *gofr.Context, cloudAccount *CloudAccount, credentials interface{}) (*ClusterResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListAllClusters", ctx, cloudAccount, credentials) + ret0, _ := ret[0].(*ClusterResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListAllClusters indicates an expected call of ListAllClusters. +func (mr *MockProviderMockRecorder) ListAllClusters(ctx, cloudAccount, credentials interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAllClusters", reflect.TypeOf((*MockProvider)(nil).ListAllClusters), ctx, cloudAccount, credentials) +} + +// ListNamespace mocks base method. +func (m *MockProvider) ListNamespace(ctx *gofr.Context, cluster *Cluster, cloudAccount *CloudAccount, credentials interface{}) (interface{}, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListNamespace", ctx, cluster, cloudAccount, credentials) + ret0, _ := ret[0].(interface{}) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListNamespace indicates an expected call of ListNamespace. +func (mr *MockProviderMockRecorder) ListNamespace(ctx, cluster, cloudAccount, credentials interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListNamespace", reflect.TypeOf((*MockProvider)(nil).ListNamespace), ctx, cluster, cloudAccount, credentials) +} diff --git a/provider/models.go b/provider/models.go new file mode 100644 index 0000000..9521d64 --- /dev/null +++ b/provider/models.go @@ -0,0 +1,111 @@ +// Package provider contains types and responses for interacting with cloud providers such as AWS, GCP, and Azure. +// +// It provides data structures representing clusters, node pools, namespaces, and cloud accounts, along with their details. +// +// Example usage: +// - Retrieve cloud account details for AWS, GCP, or Azure. +// - Fetch clusters and their associated node pools and namespaces in the cloud. +package provider + +// ClusterResponse represents the response containing information about clusters. +// It includes a list of clusters and information about pagination. +type ClusterResponse struct { + // Clusters is a list of clusters available for the provider. + Clusters []Cluster `json:"options"` + + // NextPage contains pagination information for retrieving the next set of resources. + NextPage NextPage `json:"nextPage"` +} + +// NextPage provides pagination details for fetching additional data. +// It contains the name, path, and parameters required to get the next page of results. +type NextPage struct { + // Name is the name of the next page. + Name string `json:"name"` + + // Path is the URL path to the next page of results. + Path string `json:"path"` + + // Params holds the parameters required to fetch the next page. + Params map[string]string `json:"params"` +} + +// Cluster represents a cloud provider cluster, including details like its name, +// identifier, locations, region, node pools, and type. +type Cluster struct { + // Name is the name of the cluster. + Name string `json:"name"` + + // Identifier is a unique identifier for the cluster. + Identifier string `json:"identifier"` + + // Locations lists the locations available for the cluster. + Locations []string `json:"locations"` + + // to set key for sending response. + Type string `json:"type"` + + // Region specifies the region where the cluster is located. + Region string `json:"region"` + + // NodePools is a list of node pools associated with the cluster. + NodePools []NodePool `json:"nodePools"` +} + +// NodePool represents a node pool within a cluster, detailing machine type, availability zones, +// node version, current node count, and node name. +type NodePool struct { + // MachineType specifies the machine type for the node pool. + MachineType string `json:"machineType"` + + // NodeVersion indicates the version of the nodes in the pool. + NodeVersion string `json:"nodeVersion,omitempty"` + + // NodeName is the name of the node pool. + NodeName string `json:"nodeName"` + + // CurrentNode specifies the number of nodes currently in the node pool. + CurrentNode int32 `json:"currentNode"` + + // AvailabilityZones lists the availability zones where nodes in the pool are located. + AvailabilityZones []string `json:"availabilityZones"` +} + +// CloudAccount represents a cloud account, including details such as its name, +// provider, provider-specific ID, provider details, and credentials. +type CloudAccount struct { + // ID is a unique identifier for the cloud account. + ID int64 `json:"id"` + + // Name is the name of the cloud account. + Name string `json:"name"` + + // Provider is the name of the cloud service provider (e.g., AWS, GCP, Azure). + Provider string `json:"provider"` + + // ProviderID is the unique identifier for the provider account. + ProviderID string `json:"providerId"` + + // ProviderDetails contains additional details specific to the provider, + // such as API keys or other configuration settings. + ProviderDetails interface{} `json:"providerDetails"` + + // Credentials holds authentication information used to access the cloud provider. + Credentials interface{} `json:"credentials,omitempty"` +} + +// NamespaceResponse represents a response containing a list of namespaces available in a cloud provider. +// It includes an array of namespaces. +type NamespaceResponse struct { + // Options is a list of available namespaces. + Options []Namespace `json:"options"` +} + +// Namespace represents a namespace within a cloud provider. It contains the name and type of the namespace. +type Namespace struct { + // Name is the name of the namespace. + Name string `json:"name"` + + // to set key for sending response. + Type string `json:"type"` +} From c31628db13ba30a4b856eace5123b68e22b1344c Mon Sep 17 00:00:00 2001 From: PiyushSingh-ZS Date: Tue, 17 Dec 2024 01:30:03 +0530 Subject: [PATCH 04/11] add cloud account endpoints to get deployment space --- cloudaccounts/handler/handler.go | 59 +++++++++++++++++++ cloudaccounts/handler/handler_test.go | 3 +- cloudaccounts/service/interface.go | 3 + cloudaccounts/service/mock_interface.go | 46 +++++++++++++++ cloudaccounts/service/models.go | 6 ++ cloudaccounts/service/service.go | 78 ++++++++++++++++++++++++- cloudaccounts/service/service_test.go | 8 ++- cloudaccounts/store/interface.go | 2 + cloudaccounts/store/mock_interface.go | 30 ++++++++++ cloudaccounts/store/models.go | 17 +++--- cloudaccounts/store/query.go | 5 ++ cloudaccounts/store/store.go | 51 +++++++++++++++- main.go | 8 ++- 13 files changed, 298 insertions(+), 18 deletions(-) diff --git a/cloudaccounts/handler/handler.go b/cloudaccounts/handler/handler.go index c61ab0a..a3e8225 100644 --- a/cloudaccounts/handler/handler.go +++ b/cloudaccounts/handler/handler.go @@ -1,6 +1,7 @@ package handler import ( + "strconv" "strings" "gofr.dev/pkg/gofr" @@ -56,6 +57,64 @@ func (h *Handler) ListCloudAccounts(ctx *gofr.Context) (interface{}, error) { return resp, nil } +func (h *Handler) ListDeploymentSpace(ctx *gofr.Context) (interface{}, error) { + id := ctx.PathParam("id") + id = strings.TrimSpace(id) + + cloudAccountID, err := strconv.Atoi(id) + if err != nil { + return nil, http.ErrorInvalidParam{Params: []string{"id"}} + } + + res, err := h.service.FetchDeploymentSpace(ctx, cloudAccountID) + if err != nil { + return nil, err + } + + return res, nil +} + +func (h *Handler) ListNamespaces(ctx *gofr.Context) (interface{}, error) { + id := ctx.PathParam("id") + id = strings.TrimSpace(id) + + cloudAccountID, err := strconv.Atoi(id) + if err != nil { + return nil, http.ErrorInvalidParam{Params: []string{"id"}} + } + + clusterName := strings.TrimSpace(ctx.Param("name")) + clusterRegion := strings.TrimSpace(ctx.Param("region")) + + if clusterName == "" || clusterRegion == "" { + return nil, http.ErrorInvalidParam{Params: []string{"cluster"}} + } + + res, err := h.service.ListNamespaces(ctx, cloudAccountID, clusterName, clusterRegion) + if err != nil { + return nil, err + } + + return res, nil +} + +func (h *Handler) ListDeploymentSpaceOptions(ctx *gofr.Context) (interface{}, error) { + id := ctx.PathParam("id") + id = strings.TrimSpace(id) + + cloudAccountID, err := strconv.Atoi(id) + if err != nil { + return nil, http.ErrorInvalidParam{Params: []string{"id"}} + } + + res, err := h.service.FetchDeploymentSpaceOptions(ctx, cloudAccountID) + if err != nil { + return nil, err + } + + return res, nil +} + // validateCloudAccount checks the required fields and values in a CloudAccount. func validateCloudAccount(cloudAccount *store.CloudAccount) error { params := []string{} diff --git a/cloudaccounts/handler/handler_test.go b/cloudaccounts/handler/handler_test.go index 7c53cba..ed8f6d0 100644 --- a/cloudaccounts/handler/handler_test.go +++ b/cloudaccounts/handler/handler_test.go @@ -29,7 +29,6 @@ func TestHandler_AddCloudAccount(t *testing.T) { defer ctrl.Finish() mockService := service.NewMockCloudAccountService(ctrl) - handler := New(mockService) testCases := []struct { @@ -82,7 +81,7 @@ func TestHandler_AddCloudAccount(t *testing.T) { tc.mockBehavior() // Prepare HTTP request - req := httptest.NewRequest(netHTTP.MethodPost, "/add", strings.NewReader(tc.requestBody)) + req := httptest.NewRequest(netHTTP.MethodPost, "/add/{id}", strings.NewReader(tc.requestBody)) req.Header.Set("Content-Type", "application/json") ctx := &gofr.Context{Context: context.Background(), Request: http.NewRequest(req)} diff --git a/cloudaccounts/service/interface.go b/cloudaccounts/service/interface.go index c9466aa..577ca6d 100644 --- a/cloudaccounts/service/interface.go +++ b/cloudaccounts/service/interface.go @@ -9,4 +9,7 @@ import ( type CloudAccountService interface { AddCloudAccount(ctx *gofr.Context, accounts *store.CloudAccount) (*store.CloudAccount, error) FetchAllCloudAccounts(ctx *gofr.Context) ([]store.CloudAccount, error) + FetchDeploymentSpace(ctx *gofr.Context, cloudAccountID int) (interface{}, error) + ListNamespaces(ctx *gofr.Context, id int, clusterName, clusterRegion string) (interface{}, error) + FetchDeploymentSpaceOptions(ctx *gofr.Context, id int) ([]DeploymentSpaceOptions, error) } diff --git a/cloudaccounts/service/mock_interface.go b/cloudaccounts/service/mock_interface.go index ad4cfd4..c757af8 100644 --- a/cloudaccounts/service/mock_interface.go +++ b/cloudaccounts/service/mock_interface.go @@ -6,6 +6,7 @@ package service import ( reflect "reflect" + store "github.com/zopdev/zop-api/cloudaccounts/store" gomock "go.uber.org/mock/gomock" @@ -64,3 +65,48 @@ func (mr *MockCloudAccountServiceMockRecorder) FetchAllCloudAccounts(ctx interfa mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchAllCloudAccounts", reflect.TypeOf((*MockCloudAccountService)(nil).FetchAllCloudAccounts), ctx) } + +// FetchDeploymentSpace mocks base method. +func (m *MockCloudAccountService) FetchDeploymentSpace(ctx *gofr.Context, cloudAccountID int) (interface{}, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Fetch", ctx, cloudAccountID) + ret0, _ := ret[0].(interface{}) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FetchDeploymentSpace indicates an expected call of FetchDeploymentSpace. +func (mr *MockCloudAccountServiceMockRecorder) FetchDeploymentSpace(ctx, cloudAccountID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Fetch", reflect.TypeOf((*MockCloudAccountService)(nil).FetchDeploymentSpace), ctx, cloudAccountID) +} + +// FetchDeploymentSpaceOptions mocks base method. +func (m *MockCloudAccountService) FetchDeploymentSpaceOptions(ctx *gofr.Context, id int) ([]DeploymentSpaceOptions, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FetchDeploymentSpaceOptions", ctx, id) + ret0, _ := ret[0].([]DeploymentSpaceOptions) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FetchDeploymentSpaceOptions indicates an expected call of FetchDeploymentSpaceOptions. +func (mr *MockCloudAccountServiceMockRecorder) FetchDeploymentSpaceOptions(ctx, id interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchDeploymentSpaceOptions", reflect.TypeOf((*MockCloudAccountService)(nil).FetchDeploymentSpaceOptions), ctx, id) +} + +// ListNamespaces mocks base method. +func (m *MockCloudAccountService) ListNamespaces(ctx *gofr.Context, id int, clusterName, clusterRegion string) (interface{}, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListNamespaces", ctx, id, clusterName, clusterRegion) + ret0, _ := ret[0].(interface{}) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListNamespaces indicates an expected call of ListNamespaces. +func (mr *MockCloudAccountServiceMockRecorder) ListNamespaces(ctx, id, clusterName, clusterRegion interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListNamespaces", reflect.TypeOf((*MockCloudAccountService)(nil).ListNamespaces), ctx, id, clusterName, clusterRegion) +} diff --git a/cloudaccounts/service/models.go b/cloudaccounts/service/models.go index 16d8e6e..5c09550 100644 --- a/cloudaccounts/service/models.go +++ b/cloudaccounts/service/models.go @@ -13,3 +13,9 @@ type gcpCredentials struct { ClientX509CertURL string `json:"client_x509_cert_url"` UniverseDomain string `json:"universe_domain"` } + +type DeploymentSpaceOptions struct { + Name string `json:"Name"` + PATH string `json:"PATH"` + Type string `json:"Type"` +} diff --git a/cloudaccounts/service/service.go b/cloudaccounts/service/service.go index 9adb189..939c410 100644 --- a/cloudaccounts/service/service.go +++ b/cloudaccounts/service/service.go @@ -4,6 +4,7 @@ import ( "database/sql" "encoding/json" "errors" + "fmt" "strings" "time" @@ -11,15 +12,17 @@ import ( "gofr.dev/pkg/gofr/http" "github.com/zopdev/zop-api/cloudaccounts/store" + "github.com/zopdev/zop-api/provider" ) type Service struct { - store store.CloudAccountStore + store store.CloudAccountStore + deploymentSpace provider.Provider } // New creates a new CloudAccountService with the provided CloudAccountStore. -func New(clStore store.CloudAccountStore) CloudAccountService { - return &Service{store: clStore} +func New(clStore store.CloudAccountStore, deploySpace provider.Provider) CloudAccountService { + return &Service{store: clStore, deploymentSpace: deploySpace} } // AddCloudAccount adds a new cloud account to the store if it doesn't already exist. @@ -77,3 +80,72 @@ func fetchGCPProviderDetails(ctx *gofr.Context, cloudAccount *store.CloudAccount return nil } + +func (s *Service) FetchDeploymentSpace(ctx *gofr.Context, cloudAccountID int) (interface{}, error) { + cloudAccount, err := s.store.GetCloudAccountByID(ctx, cloudAccountID) + if err != nil { + return nil, err + } + + credentials, err := s.store.GetCredentials(ctx, cloudAccount.ID) + if err != nil { + return nil, err + } + + deploymentSpaceAccount := provider.CloudAccount{ + ID: cloudAccount.ID, + Name: cloudAccount.Name, + Provider: cloudAccount.Provider, + ProviderID: cloudAccount.ProviderID, + ProviderDetails: cloudAccount.ProviderDetails, + } + + clusters, err := s.deploymentSpace.ListAllClusters(ctx, &deploymentSpaceAccount, credentials) + if err != nil { + return nil, err + } + + return clusters, nil +} + +func (s *Service) ListNamespaces(ctx *gofr.Context, id int, clusterName, clusterRegion string) (interface{}, error) { + cloudAccount, err := s.store.GetCloudAccountByID(ctx, id) + if err != nil { + return nil, err + } + + credentials, err := s.store.GetCredentials(ctx, cloudAccount.ID) + if err != nil { + return nil, err + } + + deploymentSpaceAccount := provider.CloudAccount{ + ID: cloudAccount.ID, + Name: cloudAccount.Name, + Provider: cloudAccount.Provider, + ProviderID: cloudAccount.ProviderID, + ProviderDetails: cloudAccount.ProviderDetails, + } + + cluster := provider.Cluster{ + Name: clusterName, + Region: clusterRegion, + } + + res, err := s.deploymentSpace.ListNamespace(ctx, &cluster, &deploymentSpaceAccount, credentials) + if err != nil { + return nil, err + } + + return res, nil +} + +func (*Service) FetchDeploymentSpaceOptions(_ *gofr.Context, id int) ([]DeploymentSpaceOptions, error) { + options := []DeploymentSpaceOptions{ + {Name: "gke", + PATH: fmt.Sprintf("/cloud-accounts/%v/deployment-space/clusters", id), + Type: "type"}, + } + + return options, nil +} diff --git a/cloudaccounts/service/service_test.go b/cloudaccounts/service/service_test.go index b6483c1..1c6e3ae 100644 --- a/cloudaccounts/service/service_test.go +++ b/cloudaccounts/service/service_test.go @@ -7,10 +7,12 @@ import ( "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" + "gofr.dev/pkg/gofr" "gofr.dev/pkg/gofr/http" "github.com/zopdev/zop-api/cloudaccounts/store" + "github.com/zopdev/zop-api/provider" ) var ( @@ -22,6 +24,7 @@ func TestService_AddCloudAccount(t *testing.T) { defer ctrl.Finish() mockStore := store.NewMockCloudAccountStore(ctrl) + mockProvider := provider.NewMockProvider(ctrl) ctx := &gofr.Context{} @@ -100,7 +103,7 @@ func TestService_AddCloudAccount(t *testing.T) { t.Run(tc.name, func(t *testing.T) { tc.mockBehavior() - service := New(mockStore) + service := New(mockStore, mockProvider) _, err := service.AddCloudAccount(ctx, tc.input) if tc.expectedError != nil { @@ -118,6 +121,7 @@ func TestService_FetchAllCloudAccounts(t *testing.T) { defer ctrl.Finish() mockStore := store.NewMockCloudAccountStore(ctrl) + mockProvider := provider.NewMockProvider(ctrl) ctx := &gofr.Context{} @@ -160,7 +164,7 @@ func TestService_FetchAllCloudAccounts(t *testing.T) { t.Run(tc.name, func(t *testing.T) { tc.mockBehavior() - service := New(mockStore) + service := New(mockStore, mockProvider) _, err := service.FetchAllCloudAccounts(ctx) if tc.expectedError != nil { diff --git a/cloudaccounts/store/interface.go b/cloudaccounts/store/interface.go index 73939b2..fad6d12 100644 --- a/cloudaccounts/store/interface.go +++ b/cloudaccounts/store/interface.go @@ -6,4 +6,6 @@ type CloudAccountStore interface { InsertCloudAccount(ctx *gofr.Context, config *CloudAccount) (*CloudAccount, error) GetALLCloudAccounts(ctx *gofr.Context) ([]CloudAccount, error) GetCloudAccountByProvider(ctx *gofr.Context, providerType, providerID string) (*CloudAccount, error) + GetCloudAccountByID(ctx *gofr.Context, cloudAccountID int) (*CloudAccount, error) + GetCredentials(ctx *gofr.Context, cloudAccountID int64) (interface{}, error) } diff --git a/cloudaccounts/store/mock_interface.go b/cloudaccounts/store/mock_interface.go index f69e6d4..af74de4 100644 --- a/cloudaccounts/store/mock_interface.go +++ b/cloudaccounts/store/mock_interface.go @@ -49,6 +49,21 @@ func (mr *MockCloudAccountStoreMockRecorder) GetALLCloudAccounts(ctx interface{} return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetALLCloudAccounts", reflect.TypeOf((*MockCloudAccountStore)(nil).GetALLCloudAccounts), ctx) } +// GetCloudAccountByID mocks base method. +func (m *MockCloudAccountStore) GetCloudAccountByID(ctx *gofr.Context, cloudAccountID int) (*CloudAccount, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCloudAccountByID", ctx, cloudAccountID) + ret0, _ := ret[0].(*CloudAccount) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCloudAccountByID indicates an expected call of GetCloudAccountByID. +func (mr *MockCloudAccountStoreMockRecorder) GetCloudAccountByID(ctx, cloudAccountID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCloudAccountByID", reflect.TypeOf((*MockCloudAccountStore)(nil).GetCloudAccountByID), ctx, cloudAccountID) +} + // GetCloudAccountByProvider mocks base method. func (m *MockCloudAccountStore) GetCloudAccountByProvider(ctx *gofr.Context, providerType, providerID string) (*CloudAccount, error) { m.ctrl.T.Helper() @@ -64,6 +79,21 @@ func (mr *MockCloudAccountStoreMockRecorder) GetCloudAccountByProvider(ctx, prov return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCloudAccountByProvider", reflect.TypeOf((*MockCloudAccountStore)(nil).GetCloudAccountByProvider), ctx, providerType, providerID) } +// GetCredentials mocks base method. +func (m *MockCloudAccountStore) GetCredentials(ctx *gofr.Context, cloudAccountID int64) (interface{}, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCredentials", ctx, cloudAccountID) + ret0, _ := ret[0].(interface{}) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCredentials indicates an expected call of GetCredentials. +func (mr *MockCloudAccountStoreMockRecorder) GetCredentials(ctx, cloudAccountID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCredentials", reflect.TypeOf((*MockCloudAccountStore)(nil).GetCredentials), ctx, cloudAccountID) +} + // InsertCloudAccount mocks base method. func (m *MockCloudAccountStore) InsertCloudAccount(ctx *gofr.Context, config *CloudAccount) (*CloudAccount, error) { m.ctrl.T.Helper() diff --git a/cloudaccounts/store/models.go b/cloudaccounts/store/models.go index b27a434..3438807 100644 --- a/cloudaccounts/store/models.go +++ b/cloudaccounts/store/models.go @@ -2,25 +2,18 @@ package store // CloudAccount represents a cloud account with necessary attributes. type CloudAccount struct { + // ID is a unique identifier for the cloud account. + ID int64 `json:"id,omitempty"` // Name is the name of the cloud account. Name string `json:"name"` - // ID is a unique identifier for the cloud account. - ID int64 `json:"id,omitempty"` - // Provider is the name of the cloud service provider. Provider string `json:"provider"` // ProviderID is the identifier for the provider account. ProviderID string `json:"providerId"` - // ProviderDetails contains additional details specific to the provider. - ProviderDetails interface{} `json:"providerDetails"` - - // Credentials hold authentication information for access to the provider. - Credentials interface{} `json:"credentials,omitempty"` - // CreatedAt is the timestamp of when the cloud account was created. CreatedAt string `json:"createdAt"` @@ -29,4 +22,10 @@ type CloudAccount struct { // DeletedAt is the timestamp of when the cloud account was deleted, if applicable. DeletedAt string `json:"deletedAt,omitempty"` + + // ProviderDetails contains additional details specific to the provider. + ProviderDetails interface{} `json:"providerDetails"` + + // Credentials hold authentication information for access to the provider. + Credentials interface{} `json:"credentials,omitempty"` } diff --git a/cloudaccounts/store/query.go b/cloudaccounts/store/query.go index 0a4b341..924cb4c 100644 --- a/cloudaccounts/store/query.go +++ b/cloudaccounts/store/query.go @@ -7,4 +7,9 @@ const ( GETBYPROVIDERQUERY = "SELECT id, name, provider, provider_id, provider_details, created_at," + " updated_at FROM cloud_account WHERE provider = ? " + "AND provider_id = ? AND deleted_at IS NULL;" + GETBYPROVIDERIDQUERY = "SELECT id, name, provider, provider_id, provider_details, created_at," + + " updated_at FROM cloud_account WHERE " + + "id = ? AND deleted_at IS NULL;" + //nolint:gosec //query + GETCREDENTIALSQUERY = "SELECT credentials from cloud_account WHERE id = ? AND deleted_at IS NULL;" ) diff --git a/cloudaccounts/store/store.go b/cloudaccounts/store/store.go index 545996c..9a5f946 100644 --- a/cloudaccounts/store/store.go +++ b/cloudaccounts/store/store.go @@ -69,7 +69,7 @@ func (*Store) GetALLCloudAccounts(ctx *gofr.Context) ([]CloudAccount, error) { return cloudAccounts, nil } -// GetCloudAccountByProvider retrieves a cloud account by provider type and provider ID. +// GetCloudAccountByProvider retrieves a cloud account by provider type and provider Identifier. func (*Store) GetCloudAccountByProvider(ctx *gofr.Context, providerType, providerID string) (*CloudAccount, error) { row := ctx.SQL.QueryRowContext(ctx, GETBYPROVIDERQUERY, providerType, providerID) @@ -93,3 +93,52 @@ func (*Store) GetCloudAccountByProvider(ctx *gofr.Context, providerType, provide return &cloudAccount, nil } + +// GetCloudAccountByID retrieves a cloud account by id. +func (*Store) GetCloudAccountByID(ctx *gofr.Context, cloudAccountID int) (*CloudAccount, error) { + row := ctx.SQL.QueryRowContext(ctx, GETBYPROVIDERIDQUERY, cloudAccountID) + + if row.Err() != nil { + return nil, row.Err() + } + + cloudAccount := CloudAccount{} + + var providerDetails sql.NullString + + err := row.Scan(&cloudAccount.ID, &cloudAccount.Name, &cloudAccount.Provider, &cloudAccount.ProviderID, + &providerDetails, &cloudAccount.CreatedAt, &cloudAccount.UpdatedAt) + if err != nil { + return nil, err + } + + if providerDetails.Valid { + cloudAccount.ProviderDetails = providerDetails.String + } + + return &cloudAccount, nil +} + +func (*Store) GetCredentials(ctx *gofr.Context, cloudAccountID int64) (interface{}, error) { + row := ctx.SQL.QueryRowContext(ctx, GETCREDENTIALSQUERY, cloudAccountID) + + if row.Err() != nil { + return nil, row.Err() + } + + var credentials string + + err := row.Scan(&credentials) + if err != nil { + return nil, err + } + + var jsonCred map[string]string + + err = json.Unmarshal([]byte(credentials), &jsonCred) + if err != nil { + return nil, err + } + + return jsonCred, nil +} diff --git a/main.go b/main.go index c68598b..87e691a 100644 --- a/main.go +++ b/main.go @@ -4,6 +4,7 @@ import ( "github.com/zopdev/zop-api/cloudaccounts/handler" "github.com/zopdev/zop-api/cloudaccounts/service" "github.com/zopdev/zop-api/cloudaccounts/store" + "github.com/zopdev/zop-api/provider/gcp" appHandler "github.com/zopdev/zop-api/applications/handler" appService "github.com/zopdev/zop-api/applications/service" @@ -17,8 +18,10 @@ func main() { app.Migrate(migrations.All()) + gkeSvc := gcp.New() + cloudAccountStore := store.New() - cloudAccountService := service.New(cloudAccountStore) + cloudAccountService := service.New(cloudAccountStore, gkeSvc) cloudAccountHandler := handler.New(cloudAccountService) applicationStore := appStore.New() @@ -27,6 +30,9 @@ func main() { app.POST("/cloud-accounts", cloudAccountHandler.AddCloudAccount) app.GET("/cloud-accounts", cloudAccountHandler.ListCloudAccounts) + app.GET("/cloud-accounts/{id}/deployment-space/clusters", cloudAccountHandler.ListDeploymentSpace) + app.GET("/cloud-accounts/{id}/deployment-space/namespaces", cloudAccountHandler.ListNamespaces) + app.GET("/cloud-accounts/{id}/deployment-space/options", cloudAccountHandler.ListDeploymentSpaceOptions) app.POST("/applications", applicationHandler.AddApplication) app.GET("/applications", applicationHandler.ListApplications) From b45a17b4ddecdef6318a3404d8f3b2d268bcd9e3 Mon Sep 17 00:00:00 2001 From: PiyushSingh-ZS Date: Tue, 17 Dec 2024 01:32:19 +0530 Subject: [PATCH 05/11] update go mod --- go.mod | 5 +++-- go.sum | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 2ee88f9..cb59889 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,13 @@ module github.com/zopdev/zop-api go 1.22.8 require ( + cloud.google.com/go/container v1.41.0 github.com/DATA-DOG/go-sqlmock v1.5.2 github.com/stretchr/testify v1.10.0 go.uber.org/mock v0.5.0 gofr.dev v1.28.0 + golang.org/x/oauth2 v0.24.0 + google.golang.org/api v0.209.0 ) require ( @@ -76,13 +79,11 @@ require ( go.opentelemetry.io/proto/otlp v1.3.1 // indirect golang.org/x/crypto v0.29.0 // indirect golang.org/x/net v0.31.0 // indirect - golang.org/x/oauth2 v0.24.0 // indirect golang.org/x/sync v0.9.0 // indirect golang.org/x/sys v0.27.0 // indirect golang.org/x/term v0.26.0 // indirect golang.org/x/text v0.20.0 // indirect golang.org/x/time v0.8.0 // indirect - google.golang.org/api v0.209.0 // indirect google.golang.org/genproto v0.0.0-20241113202542-65e8d215514f // indirect google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20241113202542-65e8d215514f // indirect diff --git a/go.sum b/go.sum index 06fb406..8776634 100644 --- a/go.sum +++ b/go.sum @@ -7,6 +7,8 @@ cloud.google.com/go/auth/oauth2adapt v0.2.5 h1:2p29+dePqsCHPP1bqDJcKj4qxRyYCcbzK cloud.google.com/go/auth/oauth2adapt v0.2.5/go.mod h1:AlmsELtlEBnaNTL7jCj8VQFLy6mbZv0s4Q7NGBeQ5E8= cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo= cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k= +cloud.google.com/go/container v1.41.0 h1:f20+lv3PBeQKgAL7X3VeuDzvF2iYao2AVBTsuTpPk68= +cloud.google.com/go/container v1.41.0/go.mod h1:YL6lDgCUi3frIWNIFU9qrmF7/6K1EYrtspmFTyyqJ+k= cloud.google.com/go/iam v1.2.2 h1:ozUSofHUGf/F4tCNy/mu9tHLTaxZFLOUiKzjcgWHGIA= cloud.google.com/go/iam v1.2.2/go.mod h1:0Ys8ccaZHdI1dEUilwzqng/6ps2YB6vRsjIe00/+6JY= cloud.google.com/go/kms v1.20.1 h1:og29Wv59uf2FVaZlesaiDAqHFzHaoUyHI3HYp9VUHVg= From ae4037383e105c88cedc43ca00dc741a55d40cc9 Mon Sep 17 00:00:00 2001 From: PiyushSingh-ZS Date: Tue, 17 Dec 2024 01:34:07 +0530 Subject: [PATCH 06/11] update crypto package version --- go.mod | 10 +++++----- go.sum | 20 ++++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/go.mod b/go.mod index cb59889..a3abf93 100644 --- a/go.mod +++ b/go.mod @@ -77,12 +77,12 @@ require ( go.opentelemetry.io/otel/sdk/metric v1.30.0 // indirect go.opentelemetry.io/otel/trace v1.32.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect - golang.org/x/crypto v0.29.0 // indirect + golang.org/x/crypto v0.31.0 // indirect golang.org/x/net v0.31.0 // indirect - golang.org/x/sync v0.9.0 // indirect - golang.org/x/sys v0.27.0 // indirect - golang.org/x/term v0.26.0 // indirect - golang.org/x/text v0.20.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/term v0.27.0 // indirect + golang.org/x/text v0.21.0 // indirect golang.org/x/time v0.8.0 // indirect google.golang.org/genproto v0.0.0-20241113202542-65e8d215514f // indirect google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28 // indirect diff --git a/go.sum b/go.sum index 8776634..109c6dc 100644 --- a/go.sum +++ b/go.sum @@ -247,8 +247,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= -golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= -golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= @@ -286,8 +286,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= -golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -302,15 +302,15 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= -golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= -golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU= -golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= +golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -318,8 +318,8 @@ golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= -golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= From 853337a5137e72ffea7d84b23e823593dd6e8732 Mon Sep 17 00:00:00 2001 From: PiyushSingh-ZS Date: Tue, 17 Dec 2024 12:21:10 +0530 Subject: [PATCH 07/11] fix json --- cloudaccounts/service/models.go | 7 ++++--- cloudaccounts/service/service.go | 2 +- provider/gcp/gke.go | 6 ++++++ provider/models.go | 8 ++++++++ 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/cloudaccounts/service/models.go b/cloudaccounts/service/models.go index 5c09550..ae6de43 100644 --- a/cloudaccounts/service/models.go +++ b/cloudaccounts/service/models.go @@ -14,8 +14,9 @@ type gcpCredentials struct { UniverseDomain string `json:"universe_domain"` } +// DeploymentSpaceOptions contains options to setup deployment space. type DeploymentSpaceOptions struct { - Name string `json:"Name"` - PATH string `json:"PATH"` - Type string `json:"Type"` + Name string `json:"name"` + Path string `json:"path"` + Type string `json:"type"` } diff --git a/cloudaccounts/service/service.go b/cloudaccounts/service/service.go index 939c410..ea1fe29 100644 --- a/cloudaccounts/service/service.go +++ b/cloudaccounts/service/service.go @@ -143,7 +143,7 @@ func (s *Service) ListNamespaces(ctx *gofr.Context, id int, clusterName, cluster func (*Service) FetchDeploymentSpaceOptions(_ *gofr.Context, id int) ([]DeploymentSpaceOptions, error) { options := []DeploymentSpaceOptions{ {Name: "gke", - PATH: fmt.Sprintf("/cloud-accounts/%v/deployment-space/clusters", id), + Path: fmt.Sprintf("/cloud-accounts/%v/deployment-space/clusters", id), Type: "type"}, } diff --git a/provider/gcp/gke.go b/provider/gcp/gke.go index c16d5f2..c66bda2 100644 --- a/provider/gcp/gke.go +++ b/provider/gcp/gke.go @@ -99,6 +99,9 @@ func (g *GCP) ListAllClusters(ctx *gofr.Context, cloudAccount *provider.CloudAcc "name": "name", }, }, + Metadata: provider.Metadata{ + Name: "GKE Cluster", + }, } return response, nil @@ -264,6 +267,9 @@ func (*GCP) fetchNamespaces(ctx *gofr.Context, client *http.Client, credBody []b return &provider.NamespaceResponse{ Options: namespaces, + Metadata: provider.Metadata{ + Name: "namespace", + }, }, nil } diff --git a/provider/models.go b/provider/models.go index 9521d64..c214bcb 100644 --- a/provider/models.go +++ b/provider/models.go @@ -15,6 +15,12 @@ type ClusterResponse struct { // NextPage contains pagination information for retrieving the next set of resources. NextPage NextPage `json:"nextPage"` + + Metadata Metadata `json:"metadata"` +} + +type Metadata struct { + Name string `json:"name"` } // NextPage provides pagination details for fetching additional data. @@ -99,6 +105,8 @@ type CloudAccount struct { type NamespaceResponse struct { // Options is a list of available namespaces. Options []Namespace `json:"options"` + + Metadata Metadata `json:"metadata"` } // Namespace represents a namespace within a cloud provider. It contains the name and type of the namespace. From 76a0dbb41869ccf241da656191af7c165a87c0e9 Mon Sep 17 00:00:00 2001 From: PiyushSingh-ZS Date: Tue, 17 Dec 2024 12:33:14 +0530 Subject: [PATCH 08/11] fix imports format --- provider/gcp/gke.go | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/provider/gcp/gke.go b/provider/gcp/gke.go index c16d5f2..7b32c6f 100644 --- a/provider/gcp/gke.go +++ b/provider/gcp/gke.go @@ -14,18 +14,13 @@ import ( "io" "net/http" + container "cloud.google.com/go/container/apiv1" "cloud.google.com/go/container/apiv1/containerpb" - "github.com/zopdev/zop-api/provider" - "gofr.dev/pkg/gofr" - "golang.org/x/oauth2/google" - "google.golang.org/api/option" - - container "cloud.google.com/go/container/apiv1" - apiContainer "google.golang.org/api/container/v1" + "google.golang.org/api/option" ) // GCP implements the provider.Provider interface for Google Cloud Platform. From 4ef30cb967d4967efa412a691f61ce9c1f383b13 Mon Sep 17 00:00:00 2001 From: PiyushSingh-ZS Date: Tue, 17 Dec 2024 12:36:44 +0530 Subject: [PATCH 09/11] update json for nextpage --- provider/gcp/gke.go | 2 +- provider/models.go | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/provider/gcp/gke.go b/provider/gcp/gke.go index 7b32c6f..ce62f80 100644 --- a/provider/gcp/gke.go +++ b/provider/gcp/gke.go @@ -86,7 +86,7 @@ func (g *GCP) ListAllClusters(ctx *gofr.Context, cloudAccount *provider.CloudAcc response := &provider.ClusterResponse{ Clusters: gkeClusters, - NextPage: provider.NextPage{ + Next: provider.Next{ Name: "Namespace", Path: fmt.Sprintf("/cloud-accounts/%v/deployment-space/namespaces", cloudAccount.ID), Params: map[string]string{ diff --git a/provider/models.go b/provider/models.go index 9521d64..4356689 100644 --- a/provider/models.go +++ b/provider/models.go @@ -13,13 +13,13 @@ type ClusterResponse struct { // Clusters is a list of clusters available for the provider. Clusters []Cluster `json:"options"` - // NextPage contains pagination information for retrieving the next set of resources. - NextPage NextPage `json:"nextPage"` + // Next contains pagination information for retrieving the next set of resources. + Next Next `json:"next"` } -// NextPage provides pagination details for fetching additional data. +// Next provides pagination details for fetching additional data. // It contains the name, path, and parameters required to get the next page of results. -type NextPage struct { +type Next struct { // Name is the name of the next page. Name string `json:"name"` From c86507ecc99b47b7705026853b464cc3f091528e Mon Sep 17 00:00:00 2001 From: PiyushSingh-ZS Date: Tue, 17 Dec 2024 12:41:20 +0530 Subject: [PATCH 10/11] set tls min version --- provider/gcp/gke.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/provider/gcp/gke.go b/provider/gcp/gke.go index ce62f80..c059e7f 100644 --- a/provider/gcp/gke.go +++ b/provider/gcp/gke.go @@ -168,9 +168,9 @@ func (*GCP) createTLSConfiguredClient(caCertificate string) (*http.Client, error return nil, err } - //nolint:gosec //Create a custom HTTP client with the CA certificate tlsConfig := &tls.Config{ - RootCAs: caCertPool, + RootCAs: caCertPool, + MinVersion: tls.VersionTLS12, } client := &http.Client{ Transport: &http.Transport{ From 5af9a6765061d3149c94d10a3d9b3512230abee3 Mon Sep 17 00:00:00 2001 From: PiyushSingh-ZS Date: Tue, 17 Dec 2024 12:51:18 +0530 Subject: [PATCH 11/11] update models --- provider/models.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/provider/models.go b/provider/models.go index 1f16760..3e8b2ae 100644 --- a/provider/models.go +++ b/provider/models.go @@ -14,7 +14,7 @@ type ClusterResponse struct { Clusters []Cluster `json:"options"` // NextPage contains pagination information for retrieving the next set of resources. - NextPage Next `json:"nextPage"` + Next Next `json:"next"` Metadata Metadata `json:"metadata"` }