From e603a4893339baa8d5c28666b5f189430039e5e8 Mon Sep 17 00:00:00 2001 From: PiyushSingh-ZS Date: Wed, 11 Dec 2024 16:53:41 +0530 Subject: [PATCH 1/7] 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 2/7] 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 3/7] 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 4/7] 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 66f37764475da22f2942e4987a788414b9871345 Mon Sep 17 00:00:00 2001 From: PiyushSingh-ZS Date: Tue, 17 Dec 2024 01:30:29 +0530 Subject: [PATCH 5/7] add environment api --- applications/handler/handler.go | 18 ++ applications/service/interface.go | 1 + applications/service/mock_interface.go | 15 ++ applications/service/service.go | 79 +++++- applications/service/service_test.go | 125 +++++++++- applications/store/interface.go | 2 + applications/store/mock_interface.go | 30 +++ applications/store/models.go | 55 ++++- applications/store/query.go | 8 +- applications/store/store.go | 28 +++ applications/store/store_test.go | 80 ++++++ deploymentspace/cluster/service/service.go | 80 ++++++ .../cluster/service/service_test.go | 215 ++++++++++++++++ deploymentspace/cluster/store/interface.go | 39 +++ .../cluster/store/mock_interface.go | 81 ++++++ deploymentspace/cluster/store/models.go | 51 ++++ deploymentspace/cluster/store/query.go | 11 + deploymentspace/cluster/store/store.go | 54 ++++ deploymentspace/cluster/store/store_test.go | 164 +++++++++++++ deploymentspace/handler/handler.go | 83 +++++++ deploymentspace/handler/handler_test.go | 122 ++++++++++ deploymentspace/interface.go | 44 ++++ deploymentspace/mock_interface.go | 80 ++++++ deploymentspace/service/interface.go | 35 +++ deploymentspace/service/mock_interface.go | 66 +++++ deploymentspace/service/models.go | 63 +++++ deploymentspace/service/service.go | 144 +++++++++++ deploymentspace/service/service_test.go | 230 ++++++++++++++++++ deploymentspace/store/interface.go | 33 +++ deploymentspace/store/mock_interface.go | 66 +++++ deploymentspace/store/models.go | 77 ++++++ deploymentspace/store/query.go | 10 + deploymentspace/store/store.go | 48 ++++ deploymentspace/store/store_test.go | 153 ++++++++++++ environments/handler/handler.go | 162 ++++++++++++ environments/service/interface.go | 44 ++++ environments/service/mock_interface.go | 81 ++++++ environments/service/models.go | 6 + environments/service/service.go | 134 ++++++++++ environments/store/interface.go | 65 +++++ environments/store/mock_interface.go | 95 ++++++++ environments/store/models.go | 31 +++ environments/store/query.go | 15 ++ environments/store/store.go | 142 +++++++++++ environments/store/store_test.go | 149 ++++++++++++ go.mod | 47 +++- go.sum | 97 ++++++-- main.go | 37 ++- .../20241211121308_createEnvironmentTable.go | 33 +++ ...241211121841_createDeploymentSpaceTable.go | 31 +++ .../20241212162207_createClusterTable.go | 35 +++ migrations/all.go | 3 + 52 files changed, 3554 insertions(+), 43 deletions(-) create mode 100644 deploymentspace/cluster/service/service.go create mode 100644 deploymentspace/cluster/service/service_test.go create mode 100644 deploymentspace/cluster/store/interface.go create mode 100644 deploymentspace/cluster/store/mock_interface.go create mode 100644 deploymentspace/cluster/store/models.go create mode 100644 deploymentspace/cluster/store/query.go create mode 100644 deploymentspace/cluster/store/store.go create mode 100644 deploymentspace/cluster/store/store_test.go create mode 100644 deploymentspace/handler/handler.go create mode 100644 deploymentspace/handler/handler_test.go create mode 100644 deploymentspace/interface.go create mode 100644 deploymentspace/mock_interface.go create mode 100644 deploymentspace/service/interface.go create mode 100644 deploymentspace/service/mock_interface.go create mode 100644 deploymentspace/service/models.go create mode 100644 deploymentspace/service/service.go create mode 100644 deploymentspace/service/service_test.go create mode 100644 deploymentspace/store/interface.go create mode 100644 deploymentspace/store/mock_interface.go create mode 100644 deploymentspace/store/models.go create mode 100644 deploymentspace/store/query.go create mode 100644 deploymentspace/store/store.go create mode 100644 deploymentspace/store/store_test.go create mode 100644 environments/handler/handler.go create mode 100644 environments/service/interface.go create mode 100644 environments/service/mock_interface.go create mode 100644 environments/service/models.go create mode 100644 environments/service/service.go create mode 100644 environments/store/interface.go create mode 100644 environments/store/mock_interface.go create mode 100644 environments/store/models.go create mode 100644 environments/store/query.go create mode 100644 environments/store/store.go create mode 100644 environments/store/store_test.go create mode 100755 migrations/20241211121308_createEnvironmentTable.go create mode 100755 migrations/20241211121841_createDeploymentSpaceTable.go create mode 100755 migrations/20241212162207_createClusterTable.go diff --git a/applications/handler/handler.go b/applications/handler/handler.go index 01903dc..1bc36e3 100644 --- a/applications/handler/handler.go +++ b/applications/handler/handler.go @@ -1,6 +1,7 @@ package handler import ( + "strconv" "strings" "github.com/zopdev/zop-api/applications/service" @@ -49,6 +50,23 @@ func (h *Handler) ListApplications(ctx *gofr.Context) (interface{}, error) { return applications, nil } +func (h *Handler) GetApplication(ctx *gofr.Context) (interface{}, error) { + id := ctx.PathParam("id") + id = strings.TrimSpace(id) + + applicationID, err := strconv.Atoi(id) + if err != nil { + return nil, http.ErrorInvalidParam{Params: []string{"id"}} + } + + res, err := h.service.GetApplication(ctx, applicationID) + if err != nil { + return nil, err + } + + return res, nil +} + func validateApplication(application *store.Application) error { application.Name = strings.TrimSpace(application.Name) diff --git a/applications/service/interface.go b/applications/service/interface.go index a801ce7..2d06db1 100644 --- a/applications/service/interface.go +++ b/applications/service/interface.go @@ -8,4 +8,5 @@ import ( type ApplicationService interface { AddApplication(ctx *gofr.Context, application *store.Application) (*store.Application, error) FetchAllApplications(ctx *gofr.Context) ([]store.Application, error) + GetApplication(ctx *gofr.Context, id int) (*store.Application, error) } diff --git a/applications/service/mock_interface.go b/applications/service/mock_interface.go index 8808b66..eab3f3f 100644 --- a/applications/service/mock_interface.go +++ b/applications/service/mock_interface.go @@ -64,3 +64,18 @@ func (mr *MockApplicationServiceMockRecorder) FetchAllApplications(ctx interface mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchAllApplications", reflect.TypeOf((*MockApplicationService)(nil).FetchAllApplications), ctx) } + +// GetApplication mocks base method. +func (m *MockApplicationService) GetApplication(ctx *gofr.Context, id int) (*store.Application, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetApplication", ctx, id) + ret0, _ := ret[0].(*store.Application) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetApplication indicates an expected call of GetApplication. +func (mr *MockApplicationServiceMockRecorder) GetApplication(ctx, id interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetApplication", reflect.TypeOf((*MockApplicationService)(nil).GetApplication), ctx, id) +} diff --git a/applications/service/service.go b/applications/service/service.go index 49460c2..4031f15 100644 --- a/applications/service/service.go +++ b/applications/service/service.go @@ -1,9 +1,11 @@ package service import ( + "database/sql" + "encoding/json" "errors" - "database/sql" + "github.com/zopdev/zop-api/environments/service" "github.com/zopdev/zop-api/applications/store" "gofr.dev/pkg/gofr" @@ -11,11 +13,12 @@ import ( ) type Service struct { - store store.ApplicationStore + store store.ApplicationStore + environmentService service.EnvironmentService } -func New(str store.ApplicationStore) ApplicationService { - return &Service{store: str} +func New(str store.ApplicationStore, envSvc service.EnvironmentService) ApplicationService { + return &Service{store: str, environmentService: envSvc} } func (s *Service) AddApplication(ctx *gofr.Context, application *store.Application) (*store.Application, error) { @@ -30,14 +33,80 @@ func (s *Service) AddApplication(ctx *gofr.Context, application *store.Applicati return nil, http.ErrorEntityAlreadyExist{} } + environments := application.Environments + application, err = s.store.InsertApplication(ctx, application) if err != nil { return nil, err } + if len(environments) == 0 { + environments = append(environments, store.Environment{ + Name: "default", + Level: 1, + }) + } + + for i := range environments { + environments[i].ApplicationID = application.ID + + environment, err := s.store.InsertEnvironment(ctx, &environments[i]) + if err != nil { + return nil, err + } + + environments[i] = *environment + } + return application, nil } func (s *Service) FetchAllApplications(ctx *gofr.Context) ([]store.Application, error) { - return s.store.GetALLApplications(ctx) + applications, err := s.store.GetALLApplications(ctx) + if err != nil { + return nil, err + } + + for i := range applications { + environments, err := s.environmentService.FetchAll(ctx, int(applications[i].ID)) + if err != nil { + return nil, err + } + + bytes, err := json.Marshal(environments) + if err != nil { + return nil, err + } + + err = json.Unmarshal(bytes, &applications[i].Environments) + if err != nil { + return nil, err + } + } + + return applications, nil +} + +func (s *Service) GetApplication(ctx *gofr.Context, id int) (*store.Application, error) { + application, err := s.store.GetApplicationByID(ctx, id) + if err != nil { + return nil, err + } + + environments, err := s.environmentService.FetchAll(ctx, id) + if err != nil { + return nil, err + } + + bytes, err := json.Marshal(environments) + if err != nil { + return nil, err + } + + err = json.Unmarshal(bytes, &application.Environments) + if err != nil { + return nil, err + } + + return application, nil } diff --git a/applications/service/service_test.go b/applications/service/service_test.go index c5f7e4f..20568e9 100644 --- a/applications/service/service_test.go +++ b/applications/service/service_test.go @@ -10,7 +10,10 @@ import ( "go.uber.org/mock/gomock" "gofr.dev/pkg/gofr" + envStore "github.com/zopdev/zop-api/environments/store" + "github.com/zopdev/zop-api/applications/store" + "github.com/zopdev/zop-api/environments/service" "gofr.dev/pkg/gofr/http" ) @@ -23,6 +26,7 @@ func TestService_AddApplication(t *testing.T) { defer ctrl.Finish() mockStore := store.NewMockApplicationStore(ctrl) + mockEvironmentService := service.NewMockEnvironmentService(ctrl) ctx := &gofr.Context{} application := &store.Application{ @@ -44,6 +48,9 @@ func TestService_AddApplication(t *testing.T) { mockStore.EXPECT(). InsertApplication(ctx, application). Return(application, nil) + mockStore.EXPECT(). + InsertEnvironment(ctx, gomock.Any()). + Return(&store.Environment{ID: 1}, nil).Times(1) }, input: application, expectedError: nil, @@ -81,14 +88,30 @@ func TestService_AddApplication(t *testing.T) { input: application, expectedError: errTest, }, + { + name: "error inserting environment", + mockBehavior: func() { + mockStore.EXPECT(). + GetApplicationByName(ctx, "Test Application"). + Return(nil, sql.ErrNoRows) + mockStore.EXPECT(). + InsertApplication(ctx, application). + Return(application, nil) + mockStore.EXPECT(). + InsertEnvironment(ctx, gomock.Any()). + Return(nil, errTest).Times(1) + }, + 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) + appService := New(mockStore, mockEvironmentService) + _, err := appService.AddApplication(ctx, tc.input) if tc.expectedError != nil { require.Error(t, err) @@ -105,6 +128,8 @@ func TestService_FetchAllApplications(t *testing.T) { defer ctrl.Finish() mockStore := store.NewMockApplicationStore(ctrl) + mockEvironmentService := service.NewMockEnvironmentService(ctrl) + ctx := &gofr.Context{} expectedApplications := []store.Application{ @@ -126,6 +151,9 @@ func TestService_FetchAllApplications(t *testing.T) { mockStore.EXPECT(). GetALLApplications(ctx). Return(expectedApplications, nil) + mockEvironmentService.EXPECT(). + FetchAll(ctx, 1). + Return([]envStore.Environment{{ID: 1, Name: "default", Level: 1}}, nil) }, expectedError: nil, }, @@ -138,14 +166,26 @@ func TestService_FetchAllApplications(t *testing.T) { }, expectedError: errTest, }, + { + name: "error fetching environments for application", + mockBehavior: func() { + mockStore.EXPECT(). + GetALLApplications(ctx). + Return(expectedApplications, nil) + mockEvironmentService.EXPECT(). + FetchAll(ctx, 1). + 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) + appService := New(mockStore, mockEvironmentService) + applications, err := appService.FetchAllApplications(ctx) if tc.expectedError != nil { require.Error(t, err) @@ -157,3 +197,80 @@ func TestService_FetchAllApplications(t *testing.T) { }) } } + +func TestService_GetApplication(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockStore := store.NewMockApplicationStore(ctrl) + mockEvironmentService := service.NewMockEnvironmentService(ctrl) + + ctx := &gofr.Context{} + + expectedApplication := store.Application{ + ID: 1, + Name: "Test Application", + CreatedAt: "2023-12-11T00:00:00Z", + } + + testCases := []struct { + name string + mockBehavior func() + expectedError error + expectedApp *store.Application + }{ + { + name: "success", + mockBehavior: func() { + mockStore.EXPECT(). + GetApplicationByID(ctx, 1). + Return(&expectedApplication, nil) + mockEvironmentService.EXPECT(). + FetchAll(ctx, 1). + Return([]envStore.Environment{{ID: 1, Name: "default", Level: 1}}, nil) + }, + expectedError: nil, + expectedApp: &expectedApplication, + }, + { + name: "error fetching application by ID", + mockBehavior: func() { + mockStore.EXPECT(). + GetApplicationByID(ctx, 1). + Return(nil, errTest) + }, + expectedError: errTest, + expectedApp: nil, + }, + { + name: "error fetching environments for application", + mockBehavior: func() { + mockStore.EXPECT(). + GetApplicationByID(ctx, 1). + Return(&expectedApplication, nil) + mockEvironmentService.EXPECT(). + FetchAll(ctx, 1). + Return(nil, errTest) + }, + expectedError: errTest, + expectedApp: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tc.mockBehavior() + + appService := New(mockStore, mockEvironmentService) + application, err := appService.GetApplication(ctx, 1) + + if tc.expectedError != nil { + require.Error(t, err) + require.Equal(t, tc.expectedError, err) + } else { + require.NoError(t, err) + require.Equal(t, tc.expectedApp, application) + } + }) + } +} diff --git a/applications/store/interface.go b/applications/store/interface.go index d212eca..ee288e7 100644 --- a/applications/store/interface.go +++ b/applications/store/interface.go @@ -6,4 +6,6 @@ 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) + GetApplicationByID(ctx *gofr.Context, id int) (*Application, error) + InsertEnvironment(ctx *gofr.Context, environment *Environment) (*Environment, error) } diff --git a/applications/store/mock_interface.go b/applications/store/mock_interface.go index d52d186..c9ec826 100644 --- a/applications/store/mock_interface.go +++ b/applications/store/mock_interface.go @@ -49,6 +49,21 @@ func (mr *MockApplicationStoreMockRecorder) GetALLApplications(ctx interface{}) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetALLApplications", reflect.TypeOf((*MockApplicationStore)(nil).GetALLApplications), ctx) } +// GetApplicationByID mocks base method. +func (m *MockApplicationStore) GetApplicationByID(ctx *gofr.Context, id int) (*Application, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetApplicationByID", ctx, id) + ret0, _ := ret[0].(*Application) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetApplicationByID indicates an expected call of GetApplicationByID. +func (mr *MockApplicationStoreMockRecorder) GetApplicationByID(ctx, id interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetApplicationByID", reflect.TypeOf((*MockApplicationStore)(nil).GetApplicationByID), ctx, id) +} + // GetApplicationByName mocks base method. func (m *MockApplicationStore) GetApplicationByName(ctx *gofr.Context, name string) (*Application, error) { m.ctrl.T.Helper() @@ -78,3 +93,18 @@ func (mr *MockApplicationStoreMockRecorder) InsertApplication(ctx, application i mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertApplication", reflect.TypeOf((*MockApplicationStore)(nil).InsertApplication), ctx, application) } + +// InsertEnvironment mocks base method. +func (m *MockApplicationStore) InsertEnvironment(ctx *gofr.Context, environment *Environment) (*Environment, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "InsertEnvironment", ctx, environment) + ret0, _ := ret[0].(*Environment) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// InsertEnvironment indicates an expected call of InsertEnvironment. +func (mr *MockApplicationStoreMockRecorder) InsertEnvironment(ctx, environment interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertEnvironment", reflect.TypeOf((*MockApplicationStore)(nil).InsertEnvironment), ctx, environment) +} diff --git a/applications/store/models.go b/applications/store/models.go index 149b8d2..07c40ce 100644 --- a/applications/store/models.go +++ b/applications/store/models.go @@ -1,15 +1,62 @@ +/* +Package store provides types for managing applications and their environments. +It defines the structure for an `Application` and its associated `Environment`. + +Applications represent a logical grouping of resources, while Environments +represent specific configurations or stages (e.g., development, staging, production) +within an Application. +*/ package store +// Application represents an application with a unique ID, name, and associated environments. +// It also includes timestamps for tracking the creation, update, and optional deletion of the application. type Application struct { - ID int64 `json:"ID"` + // ID is the unique identifier of the application. + ID int64 `json:"id"` + + // Name is the name of the application. + Name string `json:"name"` + + // Environments is a list of environments associated with the application. + Environments []Environment `json:"environments"` + + // CreatedAt is the timestamp of when the application was created. + CreatedAt string `json:"createdAt"` + + // UpdatedAt is the timestamp of the last update to the application. + UpdatedAt string `json:"updatedAt"` + + // DeletedAt is the timestamp of when the application was deleted, if applicable. + DeletedAt string `json:"deletedAt,omitempty"` +} + +// Environment represents a specific environment within an application. +// Environments have unique identifiers, a name, a hierarchical level, and a reference to their parent application. +// Each environment also contains deployment-specific details. +type Environment struct { + // ID is the unique identifier of the environment. + ID int64 `json:"id"` + + // Name is the name of the environment. Name string `json:"name"` - // CreatedAt is the timestamp of when the cloud account was created. + // Level represents the hierarchical level of the environment. + // For example, 1 for development, 2 for staging, and 3 for production. + Level int `json:"level"` + + // ApplicationID is the ID of the application to which the environment belongs. + ApplicationID int64 `json:"applicationID"` + + // DeploymentSpace contains configuration details specific to the deployment in this environment. + // The type `any` allows for flexibility in defining deployment-specific data. + DeploymentSpace any `json:"deploymentSpace"` + + // CreatedAt is the timestamp of when the environment was created. CreatedAt string `json:"createdAt"` - // UpdatedAt is the timestamp of the last update to the cloud account. + // UpdatedAt is the timestamp of the last update to the environment. UpdatedAt string `json:"updatedAt"` - // DeletedAt is the timestamp of when the cloud account was deleted, if applicable. + // DeletedAt is the timestamp of when the environment was deleted, if applicable. DeletedAt string `json:"deletedAt,omitempty"` } diff --git a/applications/store/query.go b/applications/store/query.go index 4cd8326..cd11d84 100644 --- a/applications/store/query.go +++ b/applications/store/query.go @@ -1,7 +1,9 @@ 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;" + 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;" + INSERTENVIRONMENTQUERY = "INSERT INTO environment (name,level,application_id) VALUES ( ?,?, ?);" + GETBYIDQUERY = "SELECT id, name, created_at, updated_at FROM application WHERE id = ? and deleted_at IS NULL;" ) diff --git a/applications/store/store.go b/applications/store/store.go index 3e03bd5..bc971bb 100644 --- a/applications/store/store.go +++ b/applications/store/store.go @@ -65,3 +65,31 @@ func (*Store) GetApplicationByName(ctx *gofr.Context, name string) (*Application return &application, nil } + +func (*Store) GetApplicationByID(ctx *gofr.Context, id int) (*Application, error) { + row := ctx.SQL.QueryRowContext(ctx, GETBYIDQUERY, id) + 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 +} + +func (*Store) InsertEnvironment(ctx *gofr.Context, environment *Environment) (*Environment, error) { + res, err := ctx.SQL.ExecContext(ctx, INSERTENVIRONMENTQUERY, environment.Name, environment.Level, environment.ApplicationID) + if err != nil { + return nil, err + } + + environment.ID, _ = res.LastInsertId() + + return environment, nil +} diff --git a/applications/store/store_test.go b/applications/store/store_test.go index 0b243fe..47661e2 100644 --- a/applications/store/store_test.go +++ b/applications/store/store_test.go @@ -202,3 +202,83 @@ func TestGetApplicationByName(t *testing.T) { }) } } + +func TestGetApplicationByID(t *testing.T) { + mockContainer, mock := container.NewMockContainer(t) + ctx := &gofr.Context{ + Context: context.Background(), + Request: nil, + Container: mockContainer, + } + + testCases := []struct { + name string + appID int + mockBehavior func() + expectedError bool + expectedNil bool + expectedID int64 + }{ + { + name: "success", + appID: 1, + mockBehavior: func() { + mockRow := sqlmock.NewRows([]string{"id", "name", "created_at", "updated_at"}). + AddRow(1, "Test Application", time.Now(), time.Now()) + mock.SQL.ExpectQuery(GETBYIDQUERY). + WithArgs(1). + WillReturnRows(mockRow) + }, + expectedError: false, + expectedNil: false, + expectedID: 1, + }, + { + name: "failure on query execution", + appID: 1, + mockBehavior: func() { + mock.SQL.ExpectQuery(GETBYIDQUERY). + WithArgs(1). + WillReturnError(sql.ErrConnDone) + }, + expectedError: true, + expectedNil: true, + expectedID: 0, + }, + { + name: "no rows found", + appID: 1, + mockBehavior: func() { + mock.SQL.ExpectQuery(GETBYIDQUERY). + WithArgs(1). + WillReturnRows(sqlmock.NewRows(nil)) + }, + expectedError: true, + expectedNil: true, + expectedID: 0, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tc.mockBehavior() + + store := New() + application, err := store.GetApplicationByID(ctx, tc.appID) + + 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.expectedID, application.ID) + } + } + }) + } +} diff --git a/deploymentspace/cluster/service/service.go b/deploymentspace/cluster/service/service.go new file mode 100644 index 0000000..e95ded9 --- /dev/null +++ b/deploymentspace/cluster/service/service.go @@ -0,0 +1,80 @@ +package service + +import ( + "database/sql" + "encoding/json" + "errors" + + "gofr.dev/pkg/gofr" + "gofr.dev/pkg/gofr/http" + + "github.com/zopdev/zop-api/deploymentspace" + "github.com/zopdev/zop-api/deploymentspace/cluster/store" +) + +type Service struct { + store store.ClusterStore +} + +func New(str store.ClusterStore) deploymentspace.DeploymentEntity { + return &Service{ + store: str, + } +} + +func (s *Service) FetchByDeploymentSpaceID(ctx *gofr.Context, id int) (interface{}, error) { + cluster, err := s.store.GetByDeploymentSpaceID(ctx, id) + if err != nil { + return nil, err + } + + return cluster, nil +} + +func (s *Service) Add(ctx *gofr.Context, data any) (interface{}, error) { + bytes, err := json.Marshal(data) + if err != nil { + return nil, err + } + + cluster := store.Cluster{} + + err = json.Unmarshal(bytes, &cluster) + if err != nil { + return nil, err + } + + resp, err := s.store.Insert(ctx, &cluster) + if err != nil { + return nil, err + } + + return resp, nil +} + +func (s *Service) DuplicateCheck(ctx *gofr.Context, data any) (interface{}, error) { + bytes, err := json.Marshal(data) + if err != nil { + return nil, err + } + + cluster := store.Cluster{} + + err = json.Unmarshal(bytes, &cluster) + if err != nil { + return nil, err + } + + resp, err := s.store.GetByCluster(ctx, &cluster) + if err != nil { + if !errors.Is(err, sql.ErrNoRows) { + return nil, err + } + } + + if resp != nil { + return nil, http.ErrorEntityAlreadyExist{} + } + + return nil, nil +} diff --git a/deploymentspace/cluster/service/service_test.go b/deploymentspace/cluster/service/service_test.go new file mode 100644 index 0000000..88d00c6 --- /dev/null +++ b/deploymentspace/cluster/service/service_test.go @@ -0,0 +1,215 @@ +package service_test + +import ( + "database/sql" + "errors" + "testing" + + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + + "github.com/zopdev/zop-api/deploymentspace/cluster/service" + "github.com/zopdev/zop-api/deploymentspace/cluster/store" + "gofr.dev/pkg/gofr" + "gofr.dev/pkg/gofr/http" +) + +var errTest = errors.New("service error") + +func TestService_Add(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockStore := store.NewMockClusterStore(ctrl) + ctx := &gofr.Context{} + + // Mock input data + cluster := &store.Cluster{ + DeploymentSpaceID: 1, + Provider: "aws", + ProviderID: "provider-123", + } + + testCases := []struct { + name string + mockBehavior func() + input interface{} + expectedError error + }{ + { + name: "success", + mockBehavior: func() { + mockStore.EXPECT(). + Insert(ctx, gomock.Any()). + Return(cluster, nil) + }, + input: cluster, + expectedError: nil, + }, + { + name: "error in Insert", + mockBehavior: func() { + mockStore.EXPECT(). + Insert(ctx, gomock.Any()). + Return(nil, errTest) + }, + input: cluster, + expectedError: errTest, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tc.mockBehavior() + + svc := service.New(mockStore) + _, err := svc.Add(ctx, tc.input) + + if tc.expectedError != nil { + require.Error(t, err) + require.Equal(t, tc.expectedError, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestService_FetchByDeploymentSpaceID(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockStore := store.NewMockClusterStore(ctrl) + ctx := &gofr.Context{} + + expectedCluster := &store.Cluster{ + ID: 1, + DeploymentSpaceID: 1, + Identifier: "cluster-1", + Provider: "aws", + ProviderID: "provider-123", + } + + testCases := []struct { + name string + mockBehavior func() + inputID int + expectedError error + expectedCluster *store.Cluster + }{ + { + name: "success", + mockBehavior: func() { + mockStore.EXPECT(). + GetByDeploymentSpaceID(ctx, 1). + Return(expectedCluster, nil) + }, + inputID: 1, + expectedError: nil, + expectedCluster: expectedCluster, + }, + { + name: "store layer error", + mockBehavior: func() { + mockStore.EXPECT(). + GetByDeploymentSpaceID(ctx, 1). + Return(nil, errTest) + }, + inputID: 1, + expectedError: errTest, + expectedCluster: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tc.mockBehavior() + + svc := service.New(mockStore) + result, err := svc.FetchByDeploymentSpaceID(ctx, tc.inputID) + + if tc.expectedError != nil { + require.Error(t, err) + require.Equal(t, tc.expectedError, err) + } else { + require.NoError(t, err) + require.Equal(t, tc.expectedCluster, result) + } + }) + } +} + +func TestService_DuplicateCheck(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockStore := store.NewMockClusterStore(ctrl) + ctx := &gofr.Context{} + + mockCluster := &store.Cluster{ + ID: 1, + DeploymentSpaceID: 1, + Provider: "aws", + ProviderID: "provider-123", + } + + testCases := []struct { + name string + mockBehavior func() + input any + expectedError error + expectedResp interface{} + }{ + { + name: "success - no duplicate found", + mockBehavior: func() { + mockStore.EXPECT(). + GetByCluster(ctx, mockCluster). + Return(nil, sql.ErrNoRows) + }, + input: mockCluster, + expectedError: nil, + expectedResp: nil, + }, + { + name: "error during GetByCluster", + mockBehavior: func() { + mockStore.EXPECT(). + GetByCluster(ctx, mockCluster). + Return(nil, errTest) + }, + input: mockCluster, + expectedError: errTest, + expectedResp: nil, + }, + { + name: "duplicate cluster found", + mockBehavior: func() { + mockStore.EXPECT(). + GetByCluster(ctx, mockCluster). + Return(mockCluster, nil) + }, + input: mockCluster, + expectedError: http.ErrorEntityAlreadyExist{}, + expectedResp: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tc.mockBehavior() + + svc := service.New(mockStore) + resp, err := svc.DuplicateCheck(ctx, tc.input) + + if tc.expectedError != nil { + require.Error(t, err) + require.IsType(t, tc.expectedError, err) + } else { + require.NoError(t, err) + } + + require.Equal(t, tc.expectedResp, resp) + }) + } +} diff --git a/deploymentspace/cluster/store/interface.go b/deploymentspace/cluster/store/interface.go new file mode 100644 index 0000000..4d2cd6f --- /dev/null +++ b/deploymentspace/cluster/store/interface.go @@ -0,0 +1,39 @@ +package store + +import "gofr.dev/pkg/gofr" + +// ClusterStore defines methods for interacting with cluster data in the storage system. +type ClusterStore interface { + // Insert inserts a new cluster into the storage. + // + // Parameters: + // ctx - The request context. + // cluster - The Cluster object to be inserted. + // + // Returns: + // *Cluster - The inserted cluster with generated fields. + // error - Any error encountered during insertion. + Insert(ctx *gofr.Context, cluster *Cluster) (*Cluster, error) + + // GetByDeploymentSpaceID retrieves a cluster by its deploymentSpaceID. + // + // Parameters: + // ctx - The request context. + // deploymentSpaceID - The deployment space ID. + // + // Returns: + // *Cluster - The cluster associated with the ID, or nil if not found. + // error - Any error encountered during retrieval. + GetByDeploymentSpaceID(ctx *gofr.Context, deploymentSpaceID int) (*Cluster, error) + + // GetByCluster retrieves a cluster by its cluster configs. + // + // Parameters: + // ctx - The request context. + // cluster - cluster details + // + // Returns: + // *Cluster - The cluster associated with the ID, or nil if not found. + // error - Any error encountered during retrieval. + GetByCluster(ctx *gofr.Context, cluster *Cluster) (*Cluster, error) +} diff --git a/deploymentspace/cluster/store/mock_interface.go b/deploymentspace/cluster/store/mock_interface.go new file mode 100644 index 0000000..de5cb22 --- /dev/null +++ b/deploymentspace/cluster/store/mock_interface.go @@ -0,0 +1,81 @@ +// 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" +) + +// MockClusterStore is a mock of ClusterStore interface. +type MockClusterStore struct { + ctrl *gomock.Controller + recorder *MockClusterStoreMockRecorder +} + +// MockClusterStoreMockRecorder is the mock recorder for MockClusterStore. +type MockClusterStoreMockRecorder struct { + mock *MockClusterStore +} + +// NewMockClusterStore creates a new mock instance. +func NewMockClusterStore(ctrl *gomock.Controller) *MockClusterStore { + mock := &MockClusterStore{ctrl: ctrl} + mock.recorder = &MockClusterStoreMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockClusterStore) EXPECT() *MockClusterStoreMockRecorder { + return m.recorder +} + +// GetByCluster mocks base method. +func (m *MockClusterStore) GetByCluster(ctx *gofr.Context, cluster *Cluster) (*Cluster, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetByCluster", ctx, cluster) + ret0, _ := ret[0].(*Cluster) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetByCluster indicates an expected call of GetByCluster. +func (mr *MockClusterStoreMockRecorder) GetByCluster(ctx, cluster interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByCluster", reflect.TypeOf((*MockClusterStore)(nil).GetByCluster), ctx, cluster) +} + +// GetByDeploymentSpaceID mocks base method. +func (m *MockClusterStore) GetByDeploymentSpaceID(ctx *gofr.Context, deploymentSpaceID int) (*Cluster, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetByDeploymentSpaceID", ctx, deploymentSpaceID) + ret0, _ := ret[0].(*Cluster) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetByDeploymentSpaceID indicates an expected call of GetByDeploymentSpaceID. +func (mr *MockClusterStoreMockRecorder) GetByDeploymentSpaceID(ctx, deploymentSpaceID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByDeploymentSpaceID", reflect.TypeOf((*MockClusterStore)(nil).GetByDeploymentSpaceID), ctx, deploymentSpaceID) +} + +// Insert mocks base method. +func (m *MockClusterStore) Insert(ctx *gofr.Context, cluster *Cluster) (*Cluster, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Insert", ctx, cluster) + ret0, _ := ret[0].(*Cluster) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Insert indicates an expected call of Insert. +func (mr *MockClusterStoreMockRecorder) Insert(ctx, cluster interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Insert", reflect.TypeOf((*MockClusterStore)(nil).Insert), ctx, cluster) +} diff --git a/deploymentspace/cluster/store/models.go b/deploymentspace/cluster/store/models.go new file mode 100644 index 0000000..a6c5d95 --- /dev/null +++ b/deploymentspace/cluster/store/models.go @@ -0,0 +1,51 @@ +/* +Package store provides types for managing clusters and their associated namespaces. +It defines the structure for a `Cluster`, which represents a logical unit of computing resources, +and the `Namespace` within the cluster. +*/ +package store + +// Cluster represents a computing cluster with a unique identifier, deployment space, and configuration details. +// It includes information about the cluster's name, region, provider, and timestamps for tracking creation, update, +// and optional deletion. Each cluster is associated with a `Namespace`. +type Cluster struct { + // DeploymentSpaceID is the unique identifier of the deployment space to which the cluster belongs. + DeploymentSpaceID int64 `json:"deploymentSpaceId"` + + // ID is the unique identifier of the cluster. + ID int64 `json:"id"` + + // Identifier is a unique identifier for the cluster, typically provided by the cloud provider. + Identifier string `json:"identifier"` + + // Name is the name of the cluster. + Name string `json:"name"` + + // Region is the geographical region where the cluster is deployed. + Region string `json:"region"` + + // Provider is the cloud provider hosting the cluster (e.g., AWS, GCP, Azure). + Provider string `json:"provider"` + + // ProviderID is the unique identifier of the cluster from the cloud provider's perspective. + ProviderID string `json:"providerId"` + + // CreatedAt is the timestamp of when the cluster was created. + CreatedAt string `json:"createdAt"` + + // UpdatedAt is the timestamp of the last update to the cluster. + UpdatedAt string `json:"updatedAt"` + + // DeletedAt is the timestamp of when the cluster was deleted, if applicable. + DeletedAt string `json:"deletedAt,omitempty"` + + // Namespace represents the namespace associated with the cluster. + Namespace Namespace `json:"namespace"` +} + +// Namespace represents a logical partition within a cluster. +// It typically defines an isolated environment for resources within the cluster. +type Namespace struct { + // Name is the name of the namespace. + Name string `json:"name"` +} diff --git a/deploymentspace/cluster/store/query.go b/deploymentspace/cluster/store/query.go new file mode 100644 index 0000000..a3f3ef3 --- /dev/null +++ b/deploymentspace/cluster/store/query.go @@ -0,0 +1,11 @@ +package store + +const ( + INSERTQUERY = "INSERT INTO cluster (deployment_space_id,cluster_id,name, region,provider_id," + + "provider,namespace) VALUES ( ?, ?, ?, ?, ?, ?, ?);" + GETQUERY = "SELECT id, deployment_space_id, cluster_id, name, region, provider_id, provider, " + + "namespace, created_at, updated_at FROM cluster WHERE deployment_space_id = ? and deleted_at IS NULL;" + GETBYCLUSTER = "SELECT id, deployment_space_id, cluster_id, name, region, provider_id, provider, " + + "namespace, created_at, updated_at FROM cluster WHERE provider = ? and name = ? and region = ? and provider_id = ?" + + " and namespace = ? and deleted_at IS NULL;" +) diff --git a/deploymentspace/cluster/store/store.go b/deploymentspace/cluster/store/store.go new file mode 100644 index 0000000..2d94113 --- /dev/null +++ b/deploymentspace/cluster/store/store.go @@ -0,0 +1,54 @@ +package store + +import "gofr.dev/pkg/gofr" + +// Store implements the ClusterStore interface for managing cluster data in the storage system. +type Store struct{} + +// New creates and returns a new instance of Store that implements ClusterStore. +func New() ClusterStore { + return &Store{} +} + +// Insert inserts a new cluster into the storage and returns the inserted cluster with its generated ID. +func (*Store) Insert(ctx *gofr.Context, cluster *Cluster) (*Cluster, error) { + res, err := ctx.SQL.ExecContext(ctx, INSERTQUERY, cluster.DeploymentSpaceID, cluster.Identifier, + cluster.Name, cluster.Region, cluster.ProviderID, cluster.Provider, cluster.Namespace.Name) + if err != nil { + return nil, err + } + + cluster.ID, err = res.LastInsertId() + if err != nil { + return nil, err + } + + return cluster, nil +} + +// GetByDeploymentSpaceID retrieves a cluster by its associated deployment space ID. +func (*Store) GetByDeploymentSpaceID(ctx *gofr.Context, deploymentSpaceID int) (*Cluster, error) { + cluster := Cluster{} + + err := ctx.SQL.QueryRowContext(ctx, GETQUERY, deploymentSpaceID).Scan(&cluster.ID, &cluster.DeploymentSpaceID, &cluster.Identifier, + &cluster.Name, &cluster.Region, &cluster.ProviderID, &cluster.Provider, &cluster.Namespace.Name, &cluster.CreatedAt, &cluster.UpdatedAt) + if err != nil { + return nil, err + } + + return &cluster, nil +} + +// GetByCluster retrieves a cluster by its associated deployment space ID. +func (*Store) GetByCluster(ctx *gofr.Context, cluster *Cluster) (*Cluster, error) { + err := ctx.SQL.QueryRowContext(ctx, GETBYCLUSTER, cluster.Provider, cluster.Name, + cluster.Region, cluster.ProviderID, cluster.Namespace.Name). + Scan(&cluster.ID, &cluster.DeploymentSpaceID, &cluster.Identifier, &cluster.Name, + &cluster.Region, &cluster.ProviderID, &cluster.Provider, &cluster.Namespace.Name, + &cluster.CreatedAt, &cluster.UpdatedAt) + if err != nil { + return nil, err + } + + return cluster, nil +} diff --git a/deploymentspace/cluster/store/store_test.go b/deploymentspace/cluster/store/store_test.go new file mode 100644 index 0000000..2483186 --- /dev/null +++ b/deploymentspace/cluster/store/store_test.go @@ -0,0 +1,164 @@ +package store + +import ( + "context" + "database/sql" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/require" + "gofr.dev/pkg/gofr" + "gofr.dev/pkg/gofr/container" +) + +func TestInsertCluster(t *testing.T) { + mockContainer, mock := container.NewMockContainer(t) + ctx := &gofr.Context{ + Context: context.Background(), + Request: nil, + Container: mockContainer, + } + + cluster := &Cluster{ + DeploymentSpaceID: 1, + Identifier: "test-id", + Name: "Test Cluster", + Region: "us-central1", + ProviderID: "123", + Provider: "gcp", + Namespace: Namespace{Name: "test-namespace"}, + } + + testCases := []struct { + name string + cluster *Cluster + expectedError bool + mockBehavior func() + }{ + { + name: "success", + cluster: cluster, + expectedError: false, + mockBehavior: func() { + mock.SQL.ExpectExec(INSERTQUERY). + WithArgs(cluster.DeploymentSpaceID, cluster.Identifier, cluster.Name, cluster.Region, + cluster.ProviderID, cluster.Provider, cluster.Namespace.Name). + WillReturnResult(sqlmock.NewResult(1, 1)) + }, + }, + { + name: "error inserting cluster", + cluster: cluster, + expectedError: true, + mockBehavior: func() { + mock.SQL.ExpectExec(INSERTQUERY). + WithArgs(cluster.DeploymentSpaceID, cluster.Identifier, cluster.Name, cluster.Region, + cluster.ProviderID, cluster.Provider, cluster.Namespace.Name). + WillReturnError(sql.ErrConnDone) + }, + }, + } + + // Iterate through test cases + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tc.mockBehavior() + + store := New() + result, err := store.Insert(ctx, tc.cluster) + + if tc.expectedError { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, int64(1), result.ID) + } + }) + } +} + +func TestGetClusterByDeploymentSpaceID(t *testing.T) { + mockContainer, mock := container.NewMockContainer(t) + ctx := &gofr.Context{ + Context: context.Background(), + Request: nil, + Container: mockContainer, + } + + expectedCluster := &Cluster{ + ID: 1, + DeploymentSpaceID: 1, + Identifier: "test-id", + Name: "Test Cluster", + Region: "us-central1", + ProviderID: "123", + Provider: "gcp", + Namespace: Namespace{Name: "test-namespace"}, + CreatedAt: "2023-12-11T00:00:00Z", + UpdatedAt: "2023-12-11T00:00:00Z", + } + + testCases := []struct { + name string + deploymentSpaceID int + mockBehavior func() + expectedError bool + expectedCluster *Cluster + }{ + { + name: "success", + deploymentSpaceID: 1, + expectedError: false, + mockBehavior: func() { + mockRow := sqlmock.NewRows([]string{"id", "deployment_space_id", "identifier", "name", "region", "provider_id", + "provider", "namespace", "created_at", "updated_at"}). + AddRow(1, 1, "test-id", "Test Cluster", "us-central1", 123, "gcp", "test-namespace", + "2023-12-11T00:00:00Z", "2023-12-11T00:00:00Z") + mock.SQL.ExpectQuery(GETQUERY). + WithArgs(1). + WillReturnRows(mockRow) + }, + expectedCluster: expectedCluster, + }, + { + name: "no cluster found", + deploymentSpaceID: 1, + expectedError: true, + mockBehavior: func() { + mock.SQL.ExpectQuery(GETQUERY). + WithArgs(1). + WillReturnRows(sqlmock.NewRows(nil)) // No rows returned + }, + expectedCluster: nil, + }, + { + name: "error on query execution", + deploymentSpaceID: 1, + expectedError: true, + mockBehavior: func() { + mock.SQL.ExpectQuery(GETQUERY). + WithArgs(1). + WillReturnError(sql.ErrConnDone) + }, + expectedCluster: nil, + }, + } + + // Iterate through test cases + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tc.mockBehavior() + + store := New() + result, err := store.GetByDeploymentSpaceID(ctx, tc.deploymentSpaceID) + + if tc.expectedError { + require.Error(t, err) + require.Nil(t, result) + } else { + require.NoError(t, err) + require.Equal(t, tc.expectedCluster, result) + } + }) + } +} diff --git a/deploymentspace/handler/handler.go b/deploymentspace/handler/handler.go new file mode 100644 index 0000000..5118100 --- /dev/null +++ b/deploymentspace/handler/handler.go @@ -0,0 +1,83 @@ +// Package handler provides HTTP handlers for managing deployment spaces. +// It acts as a layer connecting the HTTP interface to the service layer +// for deployment space operations. +package handler + +import ( + "strconv" + "strings" + + "gofr.dev/pkg/gofr" + "gofr.dev/pkg/gofr/http" + + "github.com/zopdev/zop-api/deploymentspace/service" +) + +// Handler is responsible for handling HTTP requests related to deployment spaces. +// It utilizes the DeploymentSpaceService to perform business logic operations. +type Handler struct { + service service.DeploymentSpaceService +} + +// New initializes a new Handler with the provided DeploymentSpaceService. +// +// Parameters: +// - svc: An instance of DeploymentSpaceService to handle deployment space operations. +// +// Returns: +// - An initialized Handler instance. +func New(svc service.DeploymentSpaceService) Handler { + return Handler{ + service: svc, + } +} + +// Add handles HTTP POST requests to add a new deployment space. +func (h *Handler) Add(ctx *gofr.Context) (interface{}, error) { + id := ctx.PathParam("id") + id = strings.TrimSpace(id) + + environmentID, err := strconv.Atoi(id) + if err != nil { + ctx.Logger.Error(err, "failed to convert environment id to int") + + return nil, http.ErrorInvalidParam{Params: []string{"id"}} + } + + deploymentSpace := service.DeploymentSpace{} + + err = ctx.Bind(&deploymentSpace) + if err != nil { + return nil, err + } + + err = validate(&deploymentSpace) + if err != nil { + return nil, err + } + + resp, err := h.service.Add(ctx, &deploymentSpace, environmentID) + if err != nil { + return nil, err + } + + return resp, nil +} + +func validate(deploymentSpace *service.DeploymentSpace) error { + params := []string{} + + if deploymentSpace.CloudAccount.ID == 0 { + params = append(params, "cloudAccount ID") + } + + if deploymentSpace.Type.Name == "" { + params = append(params, "type") + } + + if len(params) > 0 { + return http.ErrorMissingParam{Params: params} + } + + return nil +} diff --git a/deploymentspace/handler/handler_test.go b/deploymentspace/handler/handler_test.go new file mode 100644 index 0000000..e1718ac --- /dev/null +++ b/deploymentspace/handler/handler_test.go @@ -0,0 +1,122 @@ +package handler + +import ( + "context" + "encoding/json" + "errors" + netHTTP "net/http" + "strings" + "testing" + + "github.com/gorilla/mux" + + "github.com/stretchr/testify/require" + + "go.uber.org/mock/gomock" + + "github.com/zopdev/zop-api/deploymentspace/service" + "gofr.dev/pkg/gofr" + "gofr.dev/pkg/gofr/http" +) + +var ( + errTest = errors.New("service error") + errJSON = errors.New("invalid character 'i' looking for beginning of value") +) + +func TestHandler_Add(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockService := service.NewMockDeploymentSpaceService(ctrl) + handler := New(mockService) + + deploymentSpace := service.DeploymentSpace{ + CloudAccount: service.CloudAccount{ + ID: 1, + Name: "test-cloud-account", + }, + Type: service.Type{ + Name: "test-type", + }, + } + + bytes, err := json.Marshal(&deploymentSpace) + if err != nil { + return + } + + testCases := []struct { + name string + pathParam string + requestBody string + mockBehavior func() + expectedStatus int + expectedError error + }{ + { + name: "success", + pathParam: "123", + requestBody: string(bytes), + mockBehavior: func() { + mockService.EXPECT(). + Add(gomock.Any(), &deploymentSpace, 123). + Return(&service.DeploymentSpace{ + Type: service.Type{Name: "test-type"}, + CloudAccount: service.CloudAccount{ + Name: "test-cloud-account", + ID: 1, + }, + }, nil) + }, + expectedStatus: netHTTP.StatusOK, + expectedError: nil, + }, + { + name: "error binding request body", + pathParam: "123", + requestBody: `invalid-json`, + mockBehavior: func() {}, + expectedStatus: netHTTP.StatusBadRequest, + expectedError: errJSON, + }, + { + name: "service layer error", + pathParam: "123", + requestBody: string(bytes), + mockBehavior: func() { + mockService.EXPECT(). + Add(gomock.Any(), &deploymentSpace, 123). + 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, _ := netHTTP.NewRequestWithContext(context.Background(), netHTTP.MethodPost, "/add/{id}", strings.NewReader(tc.requestBody)) + req = mux.SetURLVars(req, map[string]string{"id": tc.pathParam}) + req.Header.Set("Content-Type", "application/json") + + // Add path parameter to the request + ctx := &gofr.Context{ + Context: context.Background(), + Request: http.NewRequest(req), + } + + _, err := handler.Add(ctx) + + if tc.expectedError != nil { + require.Error(t, err) + require.Equal(t, tc.expectedError.Error(), err.Error()) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/deploymentspace/interface.go b/deploymentspace/interface.go new file mode 100644 index 0000000..be92886 --- /dev/null +++ b/deploymentspace/interface.go @@ -0,0 +1,44 @@ +/* +Package deploymentspace provides an interface for interacting with deployment spaces. +It defines methods for fetching deployment space details by ID and adding new resources to the deployment space. +*/ +package deploymentspace + +import ( + "gofr.dev/pkg/gofr" +) + +// DeploymentEntity defines the interface for managing deployment spaces. +// It provides methods to fetch details of a deployment space by its ID and add resources to the deployment space. +type DeploymentEntity interface { + // FetchByDeploymentSpaceID retrieves a deployment space by its unique identifier. + // It returns the resource details as an interface{}, or an error if the operation fails. + // + // Parameters: + // ctx - The context of the request. + // id - The unique identifier of the deployment space. + // + // Returns: + // An interface{} containing the deployment space resource details, or an error if fetching the resource fails. + FetchByDeploymentSpaceID(ctx *gofr.Context, id int) (interface{}, error) + + // Add adds a new resource to the deployment space. + // It returns the newly added resource details as an interface{}, or an error if the operation fails. + // + // Parameters: + // ctx - The context of the request. + // resource - The resource to be added to the deployment space. + // + // Returns: + // An interface{} containing the newly added resource details, or an error if the addition fails. + Add(ctx *gofr.Context, resource any) (interface{}, error) + + // DuplicateCheck check if deploymentspace is already configure with any environment + // Parameters: + // ctx - The context of the request. + // resource - The resource to be added to the deployment space. + // + // Returns: + // An interface{} containing the resource details, or an error if the duplicate is found + DuplicateCheck(ctx *gofr.Context, data any) (interface{}, error) +} diff --git a/deploymentspace/mock_interface.go b/deploymentspace/mock_interface.go new file mode 100644 index 0000000..ef6a19c --- /dev/null +++ b/deploymentspace/mock_interface.go @@ -0,0 +1,80 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: interface.go + +// Package deploymentspace is a generated GoMock package. +package deploymentspace + +import ( + reflect "reflect" + + gomock "go.uber.org/mock/gomock" + gofr "gofr.dev/pkg/gofr" +) + +// MockDeploymentEntity is a mock of DeploymentEntity interface. +type MockDeploymentEntity struct { + ctrl *gomock.Controller + recorder *MockDeploymentEntityMockRecorder +} + +// MockDeploymentEntityMockRecorder is the mock recorder for MockDeploymentEntity. +type MockDeploymentEntityMockRecorder struct { + mock *MockDeploymentEntity +} + +// NewMockDeploymentEntity creates a new mock instance. +func NewMockDeploymentEntity(ctrl *gomock.Controller) *MockDeploymentEntity { + mock := &MockDeploymentEntity{ctrl: ctrl} + mock.recorder = &MockDeploymentEntityMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockDeploymentEntity) EXPECT() *MockDeploymentEntityMockRecorder { + return m.recorder +} + +// Add mocks base method. +func (m *MockDeploymentEntity) Add(ctx *gofr.Context, resource any) (interface{}, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Add", ctx, resource) + ret0, _ := ret[0].(interface{}) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Add indicates an expected call of Add. +func (mr *MockDeploymentEntityMockRecorder) Add(ctx, resource interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Add", reflect.TypeOf((*MockDeploymentEntity)(nil).Add), ctx, resource) +} + +// DuplicateCheck mocks base method. +func (m *MockDeploymentEntity) DuplicateCheck(ctx *gofr.Context, data any) (interface{}, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DuplicateCheck", ctx, data) + ret0, _ := ret[0].(interface{}) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DuplicateCheck indicates an expected call of DuplicateCheck. +func (mr *MockDeploymentEntityMockRecorder) DuplicateCheck(ctx, data interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DuplicateCheck", reflect.TypeOf((*MockDeploymentEntity)(nil).DuplicateCheck), ctx, data) +} + +// FetchByDeploymentSpaceID mocks base method. +func (m *MockDeploymentEntity) FetchByDeploymentSpaceID(ctx *gofr.Context, id int) (interface{}, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FetchByDeploymentSpaceID", ctx, id) + ret0, _ := ret[0].(interface{}) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FetchByDeploymentSpaceID indicates an expected call of FetchByDeploymentSpaceID. +func (mr *MockDeploymentEntityMockRecorder) FetchByDeploymentSpaceID(ctx, id interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchByDeploymentSpaceID", reflect.TypeOf((*MockDeploymentEntity)(nil).FetchByDeploymentSpaceID), ctx, id) +} diff --git a/deploymentspace/service/interface.go b/deploymentspace/service/interface.go new file mode 100644 index 0000000..53899ec --- /dev/null +++ b/deploymentspace/service/interface.go @@ -0,0 +1,35 @@ +package service + +import ( + "gofr.dev/pkg/gofr" +) + +// DeploymentSpaceService is an interface that provides methods for managing deployment spaces. +// It includes methods to add a deployment space and fetch deployment space details by environment ID. +type DeploymentSpaceService interface { + // Add adds a new deployment space to the system. + // It accepts the context, a DeploymentSpace object containing the deployment details, and an environment ID. + // The method returns the created DeploymentSpace and any error encountered during the operation. + // + // Parameters: + // ctx - The GoFR context that carries request-specific data like SQL connections. + // deploymentSpace - The deployment space object to be added. + // environmentID - The ID of the environment in which the deployment space will be created. + // + // Returns: + // *DeploymentSpace - The newly added deployment space with updated details (including ID). + // error - Any error that occurs during the add operation. + Add(ctx *gofr.Context, deploymentSpace *DeploymentSpace, environmentID int) (*DeploymentSpace, error) + + // Fetch fetches the deployment space details for a given environment ID. + // It returns a DeploymentSpaceResp object, which includes the deployment space and the associated cluster. + // + // Parameters: + // ctx - The GoFR context that carries request-specific data like SQL connections. + // environmentID - The ID of the environment for which the deployment space details are to be fetched. + // + // Returns: + // *DeploymentSpaceResp - The deployment space response object that includes the deployment space and cluster. + // error - Any error encountered during the fetch operation. + Fetch(ctx *gofr.Context, environmentID int) (*DeploymentSpaceResp, error) +} diff --git a/deploymentspace/service/mock_interface.go b/deploymentspace/service/mock_interface.go new file mode 100644 index 0000000..401cd7c --- /dev/null +++ b/deploymentspace/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" + + gomock "go.uber.org/mock/gomock" + + gofr "gofr.dev/pkg/gofr" +) + +// MockDeploymentSpaceService is a mock of DeploymentSpaceService interface. +type MockDeploymentSpaceService struct { + ctrl *gomock.Controller + recorder *MockDeploymentSpaceServiceMockRecorder +} + +// MockDeploymentSpaceServiceMockRecorder is the mock recorder for MockDeploymentSpaceService. +type MockDeploymentSpaceServiceMockRecorder struct { + mock *MockDeploymentSpaceService +} + +// NewMockDeploymentSpaceService creates a new mock instance. +func NewMockDeploymentSpaceService(ctrl *gomock.Controller) *MockDeploymentSpaceService { + mock := &MockDeploymentSpaceService{ctrl: ctrl} + mock.recorder = &MockDeploymentSpaceServiceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockDeploymentSpaceService) EXPECT() *MockDeploymentSpaceServiceMockRecorder { + return m.recorder +} + +// Add mocks base method. +func (m *MockDeploymentSpaceService) Add(ctx *gofr.Context, deploymentSpace *DeploymentSpace, environmentID int) (*DeploymentSpace, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Add", ctx, deploymentSpace, environmentID) + ret0, _ := ret[0].(*DeploymentSpace) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Add indicates an expected call of Add. +func (mr *MockDeploymentSpaceServiceMockRecorder) Add(ctx, deploymentSpace, environmentID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Add", reflect.TypeOf((*MockDeploymentSpaceService)(nil).Add), ctx, deploymentSpace, environmentID) +} + +// Fetch mocks base method. +func (m *MockDeploymentSpaceService) Fetch(ctx *gofr.Context, environmentID int) (*DeploymentSpaceResp, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Fetch", ctx, environmentID) + ret0, _ := ret[0].(*DeploymentSpaceResp) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Fetch indicates an expected call of Fetch. +func (mr *MockDeploymentSpaceServiceMockRecorder) Fetch(ctx, environmentID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Fetch", reflect.TypeOf((*MockDeploymentSpaceService)(nil).Fetch), ctx, environmentID) +} diff --git a/deploymentspace/service/models.go b/deploymentspace/service/models.go new file mode 100644 index 0000000..19bd6e2 --- /dev/null +++ b/deploymentspace/service/models.go @@ -0,0 +1,63 @@ +package service + +import "github.com/zopdev/zop-api/deploymentspace/store" + +// DeploymentSpaceResp represents the response structure for a deployment space. +// It contains the details of the deployment space and the associated cluster. +type DeploymentSpaceResp struct { + // DeploymentSpace is the deployment space object containing details like CloudAccount, Type, and DeploymentSpace. + DeploymentSpace *store.DeploymentSpace `json:"deploymentSpace"` + + // Cluster is the associated cluster within the deployment space. + Cluster *store.Cluster `json:"cluster"` +} + +// DeploymentSpace represents a deployment space in the service layer, including details about the cloud account, +// type, and deployment space itself. +type DeploymentSpace struct { + // CloudAccount represents the cloud account associated with the deployment space. + CloudAccount CloudAccount `json:"cloudAccount"` + + // Type represents the type of the deployment space (e.g., production, staging). + Type Type `json:"type"` + + // DeploymentSpace represents the deployment space object itself. + // It may include the actual deployment space model or relevant data about the space. + DeploymentSpace interface{} `json:"deploymentSpace"` +} + +// Type represents the type of the deployment space, such as the environment or the specific function the space serves. +type Type struct { + // Name is the name of the deployment space type (e.g., production, staging). + Name string `json:"name"` +} + +// 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"` + + // 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"` + + // 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"` + + // 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/deploymentspace/service/service.go b/deploymentspace/service/service.go new file mode 100644 index 0000000..e8daf91 --- /dev/null +++ b/deploymentspace/service/service.go @@ -0,0 +1,144 @@ +/* +Package service provides the implementation of the DeploymentSpaceService interface. +It manages the addition and retrieval of deployment spaces, including their associated clusters and cloud account details. +The service interacts with underlying data stores and cluster management components to fulfill requests. +*/ +package service + +import ( + "database/sql" + "encoding/json" + "errors" + + "github.com/zopdev/zop-api/deploymentspace" + "github.com/zopdev/zop-api/deploymentspace/store" + + clusterStore "github.com/zopdev/zop-api/deploymentspace/cluster/store" + + "gofr.dev/pkg/gofr" + "gofr.dev/pkg/gofr/http" +) + +// Service implements the DeploymentSpaceService interface. +// It uses a combination of deployment space and cluster stores to manage deployment space operations. +type Service struct { + store store.DeploymentSpaceStore + clusterService deploymentspace.DeploymentEntity +} + +// New initializes a new instance of Service with the provided deployment space store and cluster service. +// +// Parameters: +// +// str - The deployment space store used for data persistence. +// clusterSvc - The cluster service used for managing clusters. +// +// Returns: +// +// DeploymentSpaceService - An instance of the DeploymentSpaceService interface. +func New(str store.DeploymentSpaceStore, clusterSvc deploymentspace.DeploymentEntity) DeploymentSpaceService { + return &Service{store: str, clusterService: clusterSvc} +} + +// Add adds a new deployment space along with its associated cluster to the system. +// +// Parameters: +// +// ctx - The GoFR context that carries request-specific data. +// deploymentSpace - The DeploymentEntity object containing cloud account, type, and deployment details. +// environmentID - The ID of the environment where the deployment space is being created. +// +// Returns: +// +// *DeploymentEntity - The newly created deployment space with updated details (including ID and cluster response). +// error - Any error encountered during the add operation. +func (s *Service) Add(ctx *gofr.Context, deploymentSpace *DeploymentSpace, environmentID int) (*DeploymentSpace, error) { + if deploymentSpace.DeploymentSpace == nil { + return nil, http.ErrorInvalidParam{Params: []string{"body"}} + } + + dpSpace := store.DeploymentSpace{ + CloudAccountID: deploymentSpace.CloudAccount.ID, + EnvironmentID: int64(environmentID), + Type: deploymentSpace.Type.Name, + } + + cl := clusterStore.Cluster{} + + bytes, err := json.Marshal(deploymentSpace.DeploymentSpace) + if err != nil { + return nil, err + } + + err = json.Unmarshal(bytes, &cl) + if err != nil { + return nil, err + } + + cl.Provider = deploymentSpace.CloudAccount.Provider + cl.ProviderID = deploymentSpace.CloudAccount.ProviderID + + _, err = s.clusterService.DuplicateCheck(ctx, &cl) + if err != nil { + return nil, err + } + + ds, err := s.store.Insert(ctx, &dpSpace) + if err != nil { + return nil, err + } + + cl.DeploymentSpaceID = ds.ID + + clResp, err := s.clusterService.Add(ctx, cl) + if err != nil { + return nil, err + } + + deploymentSpace.DeploymentSpace = ds + deploymentSpace.DeploymentSpace = clResp + + return deploymentSpace, nil +} + +// Fetch retrieves a deployment space and its associated cluster details by environment ID. +// +// Parameters: +// +// ctx - The GoFR context that carries request-specific data. +// environmentID - The ID of the environment for which the deployment space is being fetched. +// +// Returns: +// +// *DeploymentSpaceResp - The deployment space response containing the deployment space and cluster details. +// error - Any error encountered during the fetch operation. +func (s *Service) Fetch(ctx *gofr.Context, environmentID int) (*DeploymentSpaceResp, error) { + deploymentSpace, err := s.store.GetByEnvironmentID(ctx, environmentID) + if err != nil { + return nil, err + } + + resp, err := s.clusterService.FetchByDeploymentSpaceID(ctx, int(deploymentSpace.ID)) + if err != nil { + if !errors.Is(err, sql.ErrNoRows) { + return nil, err + } + } + + bytes, err := json.Marshal(resp) + if err != nil { + return nil, err + } + + cluster := store.Cluster{} + + err = json.Unmarshal(bytes, &cluster) + if err != nil { + return nil, err + } + + return &DeploymentSpaceResp{ + DeploymentSpace: deploymentSpace, + Cluster: &cluster, + }, nil +} diff --git a/deploymentspace/service/service_test.go b/deploymentspace/service/service_test.go new file mode 100644 index 0000000..72edff7 --- /dev/null +++ b/deploymentspace/service/service_test.go @@ -0,0 +1,230 @@ +package service_test + +import ( + "database/sql" + "errors" + "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/deploymentspace" + clusterStore "github.com/zopdev/zop-api/deploymentspace/cluster/store" + "github.com/zopdev/zop-api/deploymentspace/service" + "github.com/zopdev/zop-api/deploymentspace/store" +) + +var errTest = errors.New("service error") + +//nolint:funlen // test function +func TestService_AddDeploymentSpace(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockStore := store.NewMockDeploymentSpaceStore(ctrl) + mockClusterService := deploymentspace.NewMockDeploymentEntity(ctrl) + + ctx := &gofr.Context{} + + deploymentSpace := &service.DeploymentSpace{ + CloudAccount: service.CloudAccount{ + ID: 1, + Provider: "aws", + ProviderID: "provider-123", + }, + Type: service.Type{Name: "test-type"}, + DeploymentSpace: map[string]interface{}{ + "key": "value", + }, + } + + mockCluster := clusterStore.Cluster{ + DeploymentSpaceID: 1, + Provider: "aws", + ProviderID: "provider-123", + } + + mockDeploymentSpace := &store.DeploymentSpace{ID: 1} + + testCases := []struct { + name string + mockBehavior func() + input *service.DeploymentSpace + envID int + expectedError error + }{ + { + name: "success", + mockBehavior: func() { + mockClusterService.EXPECT(). + DuplicateCheck(ctx, gomock.Any()). + Return(nil, nil) // No duplicate found + mockStore.EXPECT(). + Insert(ctx, gomock.Any()). + Return(mockDeploymentSpace, nil) + mockClusterService.EXPECT(). + Add(ctx, gomock.Any()). + Return(&mockCluster, nil) + }, + input: deploymentSpace, + envID: 1, + expectedError: nil, + }, + { + name: "duplicate cluster error", + mockBehavior: func() { + mockClusterService.EXPECT(). + DuplicateCheck(ctx, gomock.Any()). + Return(nil, http.ErrorEntityAlreadyExist{}) // Duplicate found + }, + input: deploymentSpace, + envID: 1, + expectedError: http.ErrorEntityAlreadyExist{}, + }, + { + name: "store layer error", + mockBehavior: func() { + mockClusterService.EXPECT(). + DuplicateCheck(ctx, gomock.Any()). + Return(nil, nil) // No duplicate found + mockStore.EXPECT(). + Insert(ctx, gomock.Any()). + Return(nil, errTest) + }, + input: deploymentSpace, + envID: 1, + expectedError: errTest, + }, + { + name: "cluster service error", + mockBehavior: func() { + mockClusterService.EXPECT(). + DuplicateCheck(ctx, gomock.Any()). + Return(nil, nil) // No duplicate found + mockStore.EXPECT(). + Insert(ctx, gomock.Any()). + Return(mockDeploymentSpace, nil) + mockClusterService.EXPECT(). + Add(ctx, gomock.Any()). + Return(nil, errTest) + }, + input: deploymentSpace, + envID: 1, + expectedError: errTest, + }, + { + name: "invalid request body", + mockBehavior: func() {}, + input: &service.DeploymentSpace{ + CloudAccount: service.CloudAccount{}, + Type: service.Type{}, + DeploymentSpace: nil, // Invalid DeploymentEntity + }, + envID: 1, + expectedError: http.ErrorInvalidParam{Params: []string{"body"}}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tc.mockBehavior() + + svc := service.New(mockStore, mockClusterService) + _, err := svc.Add(ctx, tc.input, tc.envID) + + if tc.expectedError != nil { + require.Error(t, err) + require.Equal(t, tc.expectedError, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestService_FetchDeploymentSpace(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockStore := store.NewMockDeploymentSpaceStore(ctrl) + mockClusterService := deploymentspace.NewMockDeploymentEntity(ctrl) + + ctx := &gofr.Context{} + + mockDeploymentSpace := &store.DeploymentSpace{ + ID: 1, + CloudAccountID: 1, + EnvironmentID: 1, + Type: "test-type", + } + + mockCluster := clusterStore.Cluster{ + ID: 1, + DeploymentSpaceID: 1, + Name: "test-cluster", + } + + testCases := []struct { + name string + mockBehavior func() + envID int + expectedError error + }{ + { + name: "success", + mockBehavior: func() { + mockStore.EXPECT(). + GetByEnvironmentID(ctx, 1). + Return(mockDeploymentSpace, nil) + mockClusterService.EXPECT(). + FetchByDeploymentSpaceID(ctx, 1). + Return(mockCluster, nil) + }, + envID: 1, + expectedError: nil, + }, + { + name: "store layer error", + mockBehavior: func() { + mockStore.EXPECT(). + GetByEnvironmentID(ctx, 1). + Return(nil, errTest) + }, + envID: 1, + expectedError: errTest, + }, + { + name: "no cluster found", + mockBehavior: func() { + mockStore.EXPECT(). + GetByEnvironmentID(ctx, 1). + Return(mockDeploymentSpace, nil) + mockClusterService.EXPECT(). + FetchByDeploymentSpaceID(ctx, 1). + Return(nil, sql.ErrNoRows) + }, + envID: 1, + expectedError: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tc.mockBehavior() + + svc := service.New(mockStore, mockClusterService) + resp, err := svc.Fetch(ctx, tc.envID) + + if tc.expectedError != nil { + require.Error(t, err) + require.Equal(t, tc.expectedError, err) + } else { + require.NoError(t, err) + require.NotNil(t, resp) + } + }) + } +} diff --git a/deploymentspace/store/interface.go b/deploymentspace/store/interface.go new file mode 100644 index 0000000..a567727 --- /dev/null +++ b/deploymentspace/store/interface.go @@ -0,0 +1,33 @@ +/* +Package store provides an interface for interacting with deployment spaces in a store. +It defines methods for inserting a new deployment space and fetching a deployment space by its associated environment ID. +*/ +package store + +import "gofr.dev/pkg/gofr" + +// DeploymentSpaceStore defines the interface for managing deployment spaces in a data store. +// It provides methods for inserting new deployment spaces and retrieving deployment spaces by environment ID. +type DeploymentSpaceStore interface { + // Insert adds a new deployment space to the store. + // It returns the inserted deployment space or an error if the operation fails. + // + // Parameters: + // ctx - The context of the request. + // deploymentSpace - The deployment space to be inserted. + // + // Returns: + // The inserted deployment space or an error if the operation fails. + Insert(ctx *gofr.Context, deploymentSpace *DeploymentSpace) (*DeploymentSpace, error) + + // GetByEnvironmentID retrieves a deployment space associated with the given environment ID. + // It returns the deployment space or an error if the operation fails. + // + // Parameters: + // ctx - The context of the request. + // environmentID - The unique identifier of the environment. + // + // Returns: + // The deployment space associated with the environment ID or an error if fetching the resource fails. + GetByEnvironmentID(ctx *gofr.Context, environmentID int) (*DeploymentSpace, error) +} diff --git a/deploymentspace/store/mock_interface.go b/deploymentspace/store/mock_interface.go new file mode 100644 index 0000000..3798e39 --- /dev/null +++ b/deploymentspace/store/mock_interface.go @@ -0,0 +1,66 @@ +// 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" +) + +// MockDeploymentSpaceStore is a mock of DeploymentSpaceStore interface. +type MockDeploymentSpaceStore struct { + ctrl *gomock.Controller + recorder *MockDeploymentSpaceStoreMockRecorder +} + +// MockDeploymentSpaceStoreMockRecorder is the mock recorder for MockDeploymentSpaceStore. +type MockDeploymentSpaceStoreMockRecorder struct { + mock *MockDeploymentSpaceStore +} + +// NewMockDeploymentSpaceStore creates a new mock instance. +func NewMockDeploymentSpaceStore(ctrl *gomock.Controller) *MockDeploymentSpaceStore { + mock := &MockDeploymentSpaceStore{ctrl: ctrl} + mock.recorder = &MockDeploymentSpaceStoreMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockDeploymentSpaceStore) EXPECT() *MockDeploymentSpaceStoreMockRecorder { + return m.recorder +} + +// GetByEnvironmentID mocks base method. +func (m *MockDeploymentSpaceStore) GetByEnvironmentID(ctx *gofr.Context, environmentID int) (*DeploymentSpace, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetByEnvironmentID", ctx, environmentID) + ret0, _ := ret[0].(*DeploymentSpace) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetByEnvironmentID indicates an expected call of GetByEnvironmentID. +func (mr *MockDeploymentSpaceStoreMockRecorder) GetByEnvironmentID(ctx, environmentID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByEnvironmentID", reflect.TypeOf((*MockDeploymentSpaceStore)(nil).GetByEnvironmentID), ctx, environmentID) +} + +// Insert mocks base method. +func (m *MockDeploymentSpaceStore) Insert(ctx *gofr.Context, deploymentSpace *DeploymentSpace) (*DeploymentSpace, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Insert", ctx, deploymentSpace) + ret0, _ := ret[0].(*DeploymentSpace) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Insert indicates an expected call of Insert. +func (mr *MockDeploymentSpaceStoreMockRecorder) Insert(ctx, deploymentSpace interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Insert", reflect.TypeOf((*MockDeploymentSpaceStore)(nil).Insert), ctx, deploymentSpace) +} diff --git a/deploymentspace/store/models.go b/deploymentspace/store/models.go new file mode 100644 index 0000000..456cbb4 --- /dev/null +++ b/deploymentspace/store/models.go @@ -0,0 +1,77 @@ +package store + +// DeploymentSpace represents a logical unit in a cloud environment. +// It contains information about a deployment space, including associated cloud account details, +// the environment ID, and timestamps for tracking the creation, update, and optional deletion of the deployment space. +type DeploymentSpace struct { + // ID is the unique identifier for the deployment space. + ID int64 `json:"id"` + + // CloudAccountID is the ID of the cloud account associated with the deployment space. + CloudAccountID int64 `json:"cloudAccountId"` + + // EnvironmentID is the ID of the environment to which the deployment space belongs. + EnvironmentID int64 `json:"environmentId"` + + // CloudAccountName is the name of the cloud account associated with the deployment space. + CloudAccountName string `json:"cloudAccountName"` + + // Type is the type of the deployment space (e.g., production, staging). + Type string `json:"type"` + + // CreatedAt is the timestamp of when the deployment space was created. + CreatedAt string `json:"createdAt"` + + // UpdatedAt is the timestamp of the last update to the deployment space. + UpdatedAt string `json:"updatedAt"` + + // DeletedAt is the timestamp of when the deployment space was deleted, if applicable. + // This field is omitted if the deployment space is not deleted. + DeletedAt string `json:"deletedAt,omitempty"` +} + +// Cluster represents a cluster within a deployment space. +// A cluster is a set of computing resources within a specific region and provider. +// It includes the cluster's identifier, name, and associated metadata. +type Cluster struct { + // DeploymentSpaceID is the unique identifier of the deployment space to which the cluster belongs. + DeploymentSpaceID int64 `json:"deploymentSpaceId"` + + // ID is the unique identifier of the cluster. + ID int64 `json:"id"` + + // Identifier is a unique identifier for the cluster, typically provided by the cloud provider. + Identifier string `json:"identifier"` + + // Name is the name of the cluster. + Name string `json:"name"` + + // Region is the geographical region where the cluster is deployed. + Region string `json:"region"` + + // Provider is the cloud provider hosting the cluster (e.g., AWS, GCP, Azure). + Provider string `json:"provider"` + + // ProviderID is the unique identifier of the cluster from the cloud provider's perspective. + ProviderID string `json:"providerId"` + + // CreatedAt is the timestamp of when the cluster was created. + CreatedAt string `json:"createdAt"` + + // UpdatedAt is the timestamp of the last update to the cluster. + UpdatedAt string `json:"updatedAt"` + + // DeletedAt is the timestamp of when the cluster was deleted, if applicable. + // This field is omitted if the cluster is not deleted. + DeletedAt string `json:"deletedAt,omitempty"` + + // Namespace represents the logical partition within the cluster. + Namespace Namespace `json:"namespace"` +} + +// Namespace represents a logical partition within a cluster. +// A namespace allows for isolating resources within the same cluster. +type Namespace struct { + // Name is the name of the namespace. + Name string `json:"Name"` +} diff --git a/deploymentspace/store/query.go b/deploymentspace/store/query.go new file mode 100644 index 0000000..98ca520 --- /dev/null +++ b/deploymentspace/store/query.go @@ -0,0 +1,10 @@ +package store + +const ( + INSERTQUERY = "INSERT INTO deployment_space (cloud_account_id, environment_id, type) VALUES ( ?, ?, ?);" + GETQUERYBYENVID = `SELECT ds.id, ds.cloud_account_id, ds.environment_id, ds.type, ds.created_at, + ds.updated_at, ca.Name + FROM deployment_space ds + JOIN cloud_account ca ON ds.cloud_account_id = ca.id + WHERE ds.environment_id = ? AND ds.deleted_at IS NULL;` +) diff --git a/deploymentspace/store/store.go b/deploymentspace/store/store.go new file mode 100644 index 0000000..01c905c --- /dev/null +++ b/deploymentspace/store/store.go @@ -0,0 +1,48 @@ +/* +Package store provides an implementation of the DeploymentSpaceStore interface for managing deployment spaces. +The Store struct implements methods to insert a new deployment space and retrieve a deployment space by its environment ID. +*/ +package store + +import "gofr.dev/pkg/gofr" + +type Store struct{} + +// New creates and returns a new instance of Store, which implements the DeploymentSpaceStore interface. +func New() DeploymentSpaceStore { + return &Store{} +} + +// Insert inserts a new deployment space into the data store. +// It takes a context and a pointer to the deployment space to be inserted. +// Upon successful insertion, it returns the newly inserted deployment space, including its generated ID. +// If there is an error during insertion, it returns nil and the error. +func (*Store) Insert(ctx *gofr.Context, deploymentSpace *DeploymentSpace) (*DeploymentSpace, error) { + res, err := ctx.SQL.ExecContext(ctx, INSERTQUERY, deploymentSpace.CloudAccountID, deploymentSpace.EnvironmentID, deploymentSpace.Type) + if err != nil { + return nil, err + } + + deploymentSpace.ID, err = res.LastInsertId() + if err != nil { + return nil, err + } + + return deploymentSpace, nil +} + +// GetByEnvironmentID retrieves a deployment space based on the given environment ID. +// It queries the data store using the environment ID and populates the provided DeploymentSpace object with the retrieved data. +// If successful, it returns a pointer to the populated DeploymentSpace. If there is an error, it returns nil and the error. +func (*Store) GetByEnvironmentID(ctx *gofr.Context, environmentID int) (*DeploymentSpace, error) { + deploymentSpace := DeploymentSpace{} + + err := ctx.SQL.QueryRowContext(ctx, GETQUERYBYENVID, environmentID).Scan(&deploymentSpace.ID, + &deploymentSpace.CloudAccountID, &deploymentSpace.EnvironmentID, &deploymentSpace.Type, + &deploymentSpace.CreatedAt, &deploymentSpace.UpdatedAt, &deploymentSpace.CloudAccountName) + if err != nil { + return nil, err + } + + return &deploymentSpace, nil +} diff --git a/deploymentspace/store/store_test.go b/deploymentspace/store/store_test.go new file mode 100644 index 0000000..c3e9d89 --- /dev/null +++ b/deploymentspace/store/store_test.go @@ -0,0 +1,153 @@ +package store + +import ( + "context" + "database/sql" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/require" + "gofr.dev/pkg/gofr" + "gofr.dev/pkg/gofr/container" +) + +func TestInsertDeploymentSpace(t *testing.T) { + mockContainer, mock := container.NewMockContainer(t) + ctx := &gofr.Context{ + Context: context.Background(), + Request: nil, + Container: mockContainer, + } + + deploymentSpace := &DeploymentSpace{ + CloudAccountID: 1, + EnvironmentID: 1, + Type: "test-type", + } + + testCases := []struct { + name string + deploymentSpace *DeploymentSpace + expectedError bool + mockBehavior func() + }{ + { + name: "success", + deploymentSpace: deploymentSpace, + expectedError: false, + mockBehavior: func() { + mock.SQL.ExpectExec(INSERTQUERY). + WithArgs(deploymentSpace.CloudAccountID, deploymentSpace.EnvironmentID, deploymentSpace.Type). + WillReturnResult(sqlmock.NewResult(123, 1)) + }, + }, + { + name: "error inserting deployment space", + deploymentSpace: deploymentSpace, + expectedError: true, + mockBehavior: func() { + mock.SQL.ExpectExec(INSERTQUERY). + WithArgs(deploymentSpace.CloudAccountID, deploymentSpace.EnvironmentID, deploymentSpace.Type). + WillReturnError(sql.ErrConnDone) + }, + }, + } + + // Iterate through test cases + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tc.mockBehavior() + + store := New() + result, err := store.Insert(ctx, tc.deploymentSpace) + + if tc.expectedError { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, int64(123), result.ID) + } + }) + } +} + +func TestGetDeploymentSpaceByEnvID(t *testing.T) { + mockContainer, mock := container.NewMockContainer(t) + ctx := &gofr.Context{ + Context: context.Background(), + Request: nil, + Container: mockContainer, + } + + expectedDeploymentSpace := &DeploymentSpace{ + ID: 1, + CloudAccountID: 1, + EnvironmentID: 1, + Type: "test-type", + CloudAccountName: "Test Cloud Account", + CreatedAt: "2023-12-11T00:00:00Z", + UpdatedAt: "2023-12-11T00:00:00Z", + } + + testCases := []struct { + name string + environmentID int + mockBehavior func() + expectedError bool + expectedSpace *DeploymentSpace + }{ + { + name: "success", + environmentID: 1, + expectedError: false, + mockBehavior: func() { + mockRow := sqlmock.NewRows([]string{"id", "cloud_account_id", "environment_id", "type", "created_at", "updated_at", + "cloud_account_name"}). + AddRow(1, 1, 1, "test-type", "2023-12-11T00:00:00Z", "2023-12-11T00:00:00Z", "Test Cloud Account") + mock.SQL.ExpectQuery(GETQUERYBYENVID). + WithArgs(1). + WillReturnRows(mockRow) + }, + expectedSpace: expectedDeploymentSpace, + }, + { + name: "no deployment space found", + environmentID: 1, + expectedError: true, + mockBehavior: func() { + mock.SQL.ExpectQuery(GETQUERYBYENVID). + WithArgs(1). + WillReturnRows(sqlmock.NewRows(nil)) + }, + expectedSpace: nil, + }, + { + name: "error on query execution", + environmentID: 1, + expectedError: true, + mockBehavior: func() { + mock.SQL.ExpectQuery(GETQUERYBYENVID). + WithArgs(1). + WillReturnError(sql.ErrConnDone) + }, + expectedSpace: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tc.mockBehavior() + + store := New() + result, err := store.GetByEnvironmentID(ctx, tc.environmentID) + + if tc.expectedError { + require.Error(t, err) + require.Nil(t, result) + } else { + require.NoError(t, err) + require.Equal(t, tc.expectedSpace, result) + } + }) + } +} diff --git a/environments/handler/handler.go b/environments/handler/handler.go new file mode 100644 index 0000000..05e5cfb --- /dev/null +++ b/environments/handler/handler.go @@ -0,0 +1,162 @@ +package handler + +import ( + "strconv" + "strings" + + "github.com/zopdev/zop-api/environments/service" + "github.com/zopdev/zop-api/environments/store" + "gofr.dev/pkg/gofr" + "gofr.dev/pkg/gofr/http" +) + +// Handler is responsible for handling HTTP requests related to environments. +// It interacts with the EnvironmentService to perform operations on environments. +type Handler struct { + service service.EnvironmentService +} + +// New creates a new instance of Handler. +// +// Parameters: +// - svc: The EnvironmentService implementation for handling business logic. +// +// Returns: +// - *Handler: A new handler instance. +func New(svc service.EnvironmentService) *Handler { + return &Handler{ + service: svc, + } +} + +// Add handles the HTTP request to add a new environment. +// +// The method extracts the application ID from the request's path parameter, validates the input, +// and delegates the creation of the environment to the service layer. +// +// Parameters: +// - ctx: The HTTP context, which includes the request and response data. +// +// Returns: +// - interface{}: The newly created environment record. +// - error: An error if the operation fails. +func (h *Handler) Add(ctx *gofr.Context) (interface{}, error) { + id := ctx.PathParam("id") + id = strings.TrimSpace(id) + + applicationID, err := strconv.ParseInt(id, 10, 64) + if err != nil { + return nil, err + } + + environment := store.Environment{} + + err = ctx.Bind(&environment) + if err != nil { + return nil, err + } + + environment.ApplicationID = applicationID + + err = validateEnvironment(&environment) + if err != nil { + return nil, err + } + + res, err := h.service.Add(ctx, &environment) + if err != nil { + return nil, err + } + + return res, nil +} + +// List handles the HTTP request to retrieve all environments for a specific application. +// +// The method extracts the application ID from the request's path parameter and +// delegates the fetching of environments to the service layer. +// +// Parameters: +// - ctx: The HTTP context, which includes the request and response data. +// +// Returns: +// - interface{}: A slice of environments associated with the application. +// - error: An error if the operation fails. +func (h *Handler) List(ctx *gofr.Context) (interface{}, error) { + id := ctx.PathParam("id") + id = strings.TrimSpace(id) + + applicationID, err := strconv.Atoi(id) + if err != nil { + return nil, err + } + + res, err := h.service.FetchAll(ctx, applicationID) + if err != nil { + return nil, err + } + + return res, nil +} + +// Update handles the HTTP request to update multiple environments. +// +// The method binds the input from the request body, validates the data, and +// delegates the update operation to the service layer. +// +// Parameters: +// - ctx: The HTTP context, which includes the request and response data. +// +// Returns: +// - interface{}: A slice of updated environment records. +// - error: An error if the operation fails. +func (h *Handler) Update(ctx *gofr.Context) (interface{}, error) { + environments := []store.Environment{} + + err := ctx.Bind(&environments) + if err != nil { + return nil, err + } + + for i := range environments { + err = validateEnvironment(&environments[i]) + if err != nil { + return nil, err + } + } + + res, err := h.service.Update(ctx, environments) + if err != nil { + return nil, err + } + + return res, nil +} + +// validateEnvironment validates the input for an environment. +// +// The method checks if required fields are present and trims extra spaces. +// +// Parameters: +// - environment: A pointer to the Environment struct to be validated. +// +// Returns: +// - error: An error if validation fails, specifying the missing fields. +func validateEnvironment(environment *store.Environment) error { + environment.Name = strings.TrimSpace(environment.Name) + params := []string{} + + if environment.Name == "" { + params = append(params, "name") + } + + if environment.ApplicationID == 0 { + params = append(params, "application_id") + } + + if len(params) > 0 { + return http.ErrorInvalidParam{Params: params} + } + + return nil +} diff --git a/environments/service/interface.go b/environments/service/interface.go new file mode 100644 index 0000000..9ed57f2 --- /dev/null +++ b/environments/service/interface.go @@ -0,0 +1,44 @@ +package service + +import ( + "github.com/zopdev/zop-api/environments/store" + "gofr.dev/pkg/gofr" +) + +// EnvironmentService defines the business logic layer for managing environments. +// It provides methods to fetch, add, and update environments. +type EnvironmentService interface { + + // FetchAll retrieves all environments associated with a specific application. + // + // Parameters: + // - ctx: The request context, which includes logging and request-scoped values. + // - applicationID: The unique identifier of the application whose environments need to be fetched. + // + // Returns: + // - []store.Environment: A slice of environments linked to the application. + // - error: An error if the operation fails. + FetchAll(ctx *gofr.Context, applicationID int) ([]store.Environment, error) + + // Add creates a new environment and stores it in the datastore. + // + // Parameters: + // - ctx: The request context, which includes logging and request-scoped values. + // - environment: A pointer to the Environment struct containing the details to be added. + // + // Returns: + // - *store.Environment: A pointer to the newly created environment record, including its ID. + // - error: An error if the operation fails. + Add(ctx *gofr.Context, environment *store.Environment) (*store.Environment, error) + + // Update modifies multiple environment records in the datastore. + // + // Parameters: + // - ctx: The request context, which includes logging and request-scoped values. + // - environments: A slice of Environment structs containing the updated details. + // + // Returns: + // - []store.Environment: A slice of the updated environment records. + // - error: An error if the operation fails. + Update(ctx *gofr.Context, environments []store.Environment) ([]store.Environment, error) +} diff --git a/environments/service/mock_interface.go b/environments/service/mock_interface.go new file mode 100644 index 0000000..aa5c9e4 --- /dev/null +++ b/environments/service/mock_interface.go @@ -0,0 +1,81 @@ +// 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/environments/store" + gomock "go.uber.org/mock/gomock" + gofr "gofr.dev/pkg/gofr" +) + +// MockEnvironmentService is a mock of EnvironmentService interface. +type MockEnvironmentService struct { + ctrl *gomock.Controller + recorder *MockEnvironmentServiceMockRecorder +} + +// MockEnvironmentServiceMockRecorder is the mock recorder for MockEnvironmentService. +type MockEnvironmentServiceMockRecorder struct { + mock *MockEnvironmentService +} + +// NewMockEnvironmentService creates a new mock instance. +func NewMockEnvironmentService(ctrl *gomock.Controller) *MockEnvironmentService { + mock := &MockEnvironmentService{ctrl: ctrl} + mock.recorder = &MockEnvironmentServiceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockEnvironmentService) EXPECT() *MockEnvironmentServiceMockRecorder { + return m.recorder +} + +// Add mocks base method. +func (m *MockEnvironmentService) Add(ctx *gofr.Context, environment *store.Environment) (*store.Environment, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Add", ctx, environment) + ret0, _ := ret[0].(*store.Environment) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Add indicates an expected call of Add. +func (mr *MockEnvironmentServiceMockRecorder) Add(ctx, environment interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Add", reflect.TypeOf((*MockEnvironmentService)(nil).Add), ctx, environment) +} + +// FetchAll mocks base method. +func (m *MockEnvironmentService) FetchAll(ctx *gofr.Context, applicationID int) ([]store.Environment, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FetchAll", ctx, applicationID) + ret0, _ := ret[0].([]store.Environment) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FetchAll indicates an expected call of FetchAll. +func (mr *MockEnvironmentServiceMockRecorder) FetchAll(ctx, applicationID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchAll", reflect.TypeOf((*MockEnvironmentService)(nil).FetchAll), ctx, applicationID) +} + +// Update mocks base method. +func (m *MockEnvironmentService) Update(ctx *gofr.Context, environments []store.Environment) ([]store.Environment, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Update", ctx, environments) + ret0, _ := ret[0].([]store.Environment) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Update indicates an expected call of Update. +func (mr *MockEnvironmentServiceMockRecorder) Update(ctx, environments interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockEnvironmentService)(nil).Update), ctx, environments) +} diff --git a/environments/service/models.go b/environments/service/models.go new file mode 100644 index 0000000..ee5ef7d --- /dev/null +++ b/environments/service/models.go @@ -0,0 +1,6 @@ +package service + +type DeploymentSpaceResponse struct { + Name string `json:"name"` + Next *DeploymentSpaceResponse `json:"next,omitempty"` +} diff --git a/environments/service/service.go b/environments/service/service.go new file mode 100644 index 0000000..b73155a --- /dev/null +++ b/environments/service/service.go @@ -0,0 +1,134 @@ +package service + +import ( + "database/sql" + "errors" + + "gofr.dev/pkg/gofr" + "gofr.dev/pkg/gofr/http" + + "github.com/zopdev/zop-api/deploymentspace/service" + "github.com/zopdev/zop-api/environments/store" +) + +type Service struct { + store store.EnvironmentStore + deploymentSpaceService service.DeploymentSpaceService +} + +// New creates a new instance of the EnvironmentService. +// +// Parameters: +// - enStore: The EnvironmentStore implementation for managing datastore operations. +// - deploySvc: The DeploymentSpaceService implementation for handling deployment space logic. +// +// Returns: +// - EnvironmentService: A new instance of the service layer. +func New(enStore store.EnvironmentStore, deploySvc service.DeploymentSpaceService) EnvironmentService { + return &Service{store: enStore, deploymentSpaceService: deploySvc} +} + +// Add adds a new environment to the datastore. +// +// The method first checks if an environment with the same name already exists for the application. +// If no such environment exists, it creates a new record. +// +// Parameters: +// - ctx: The request context, which includes logging and request-scoped values. +// - environment: A pointer to the Environment struct containing the details to be added. +// +// Returns: +// - *store.Environment: A pointer to the newly created environment record, including its ID. +// - error: +// - An error if the operation fails. +// - http.ErrorEntityAlreadyExist if the environment already exists. +func (s *Service) Add(ctx *gofr.Context, environment *store.Environment) (*store.Environment, error) { + tempEnvironment, err := s.store.GetByName(ctx, int(environment.ApplicationID), environment.Name) + if err != nil { + if !errors.Is(err, sql.ErrNoRows) && err != nil { + return nil, err + } + } + + if tempEnvironment != nil { + return nil, http.ErrorEntityAlreadyExist{} + } + + maxLevel, err := s.store.GetMaxLevel(ctx, int(environment.ApplicationID)) + if err != nil { + return nil, err + } + + environment.Level = maxLevel + 1 + + return s.store.Insert(ctx, environment) +} + +// FetchAll retrieves all environments for a specific application and populates their deployment spaces. +// +// The method fetches environments from the datastore and augments them with deployment space details +// retrieved from the DeploymentSpaceService. +// +// Parameters: +// - ctx: The request context, which includes logging and request-scoped values. +// - applicationID: The unique identifier of the application whose environments need to be fetched. +// +// Returns: +// - []store.Environment: A slice of environments, each augmented with deployment space details. +// - error: An error if the operation fails. + +func (s *Service) FetchAll(ctx *gofr.Context, applicationID int) ([]store.Environment, error) { + environments, err := s.store.GetALL(ctx, applicationID) + if err != nil { + return nil, err + } + + for i := range environments { + deploymentSpace, err := s.deploymentSpaceService.Fetch(ctx, int(environments[i].ID)) + if !errors.Is(err, sql.ErrNoRows) && err != nil { + return nil, err + } + + if deploymentSpace != nil { + deploymentSpaceResp := DeploymentSpaceResponse{ + Name: deploymentSpace.DeploymentSpace.CloudAccountName, + Next: &DeploymentSpaceResponse{ + Name: deploymentSpace.DeploymentSpace.Type, + Next: &DeploymentSpaceResponse{ + Name: deploymentSpace.Cluster.Name, + Next: &DeploymentSpaceResponse{ + Name: deploymentSpace.Cluster.Namespace.Name, + }, + }, + }, + } + environments[i].DeploymentSpace = deploymentSpaceResp + } + } + + return environments, nil +} + +// Update modifies existing environment records in the datastore. +// +// The method iterates through the list of environments, updates each one, and returns the updated records. +// +// Parameters: +// - ctx: The request context, which includes logging and request-scoped values. +// - environments: A slice of Environment structs containing the updated details. +// +// Returns: +// - []store.Environment: A slice of the updated environment records. +// - error: An error if the operation fails. +func (s *Service) Update(ctx *gofr.Context, environments []store.Environment) ([]store.Environment, error) { + for i := range environments { + env, err := s.store.Update(ctx, &environments[i]) + if err != nil { + return nil, err + } + + environments[i] = *env + } + + return environments, nil +} diff --git a/environments/store/interface.go b/environments/store/interface.go new file mode 100644 index 0000000..217694c --- /dev/null +++ b/environments/store/interface.go @@ -0,0 +1,65 @@ +package store + +import "gofr.dev/pkg/gofr" + +// EnvironmentStore provides an abstraction for managing environment-related operations +// in the datastore. It defines methods to insert, fetch, and update environments. +type EnvironmentStore interface { + + // Insert adds a new environment to the datastore. + // + // Parameters: + // - ctx: The context for the request, which includes logging and request-scoped values. + // - environment: A pointer to the Environment struct containing the details of the environment to insert. + // + // Returns: + // - *Environment: A pointer to the inserted environment record. + // - error: An error if the operation fails. + Insert(ctx *gofr.Context, environment *Environment) (*Environment, error) + + // GetALL fetches all environments for a specific application from the datastore. + // + // Parameters: + // - ctx: The context for the request, which includes logging and request-scoped values. + // - applicationID: The unique identifier of the application whose environments need to be fetched. + // + // Returns: + // - []Environment: A slice of environments associated with the application. + // - error: An error if the operation fails. + GetALL(ctx *gofr.Context, applicationID int) ([]Environment, error) + + // GetByName fetches a specific environment by its name for a given application. + // + // Parameters: + // - ctx: The context for the request, which includes logging and request-scoped values. + // - applicationID: The unique identifier of the application to which the environment belongs. + // - name: The name of the environment to fetch. + // + // Returns: + // - *Environment: A pointer to the environment matching the given name. + // - error: An error if the operation fails or no environment is found. + GetByName(ctx *gofr.Context, applicationID int, name string) (*Environment, error) + + // Update updates the details of an existing environment in the datastore. + // + // Parameters: + // - ctx: The context for the request, which includes logging and request-scoped values. + // - environment: A pointer to the Environment struct containing the updated details. + // + // Returns: + // - *Environment: A pointer to the updated environment record. + // - error: An error if the operation fails. + Update(ctx *gofr.Context, environment *Environment) (*Environment, error) + + // GetMaxLevel retrieves the maximum environment `level` for a specified application from the `environment` table. + // It considers only active records where `deleted_at IS NULL`. + // + // Parameters: + // - ctx (*gofr.Context): Context for request handling, logging, and tracing. + // - applicationID (int): The unique identifier for the application. + // + // Returns: + // - int: The highest environment level associated with the given application ID. + // - error: An error if the query fails or no matching data exists. + GetMaxLevel(ctx *gofr.Context, applicationID int) (int, error) +} diff --git a/environments/store/mock_interface.go b/environments/store/mock_interface.go new file mode 100644 index 0000000..d7cd106 --- /dev/null +++ b/environments/store/mock_interface.go @@ -0,0 +1,95 @@ +// 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" +) + +// MockEnvironmentStore is a mock of EnvironmentStore interface. +type MockEnvironmentStore struct { + ctrl *gomock.Controller + recorder *MockEnvironmentStoreMockRecorder +} + +// MockEnvironmentStoreMockRecorder is the mock recorder for MockEnvironmentStore. +type MockEnvironmentStoreMockRecorder struct { + mock *MockEnvironmentStore +} + +// NewMockEnvironmentStore creates a new mock instance. +func NewMockEnvironmentStore(ctrl *gomock.Controller) *MockEnvironmentStore { + mock := &MockEnvironmentStore{ctrl: ctrl} + mock.recorder = &MockEnvironmentStoreMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockEnvironmentStore) EXPECT() *MockEnvironmentStoreMockRecorder { + return m.recorder +} + +// GetALL mocks base method. +func (m *MockEnvironmentStore) GetALL(ctx *gofr.Context, applicationID int) ([]Environment, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetALL", ctx, applicationID) + ret0, _ := ret[0].([]Environment) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetALL indicates an expected call of GetALL. +func (mr *MockEnvironmentStoreMockRecorder) GetALL(ctx, applicationID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetALL", reflect.TypeOf((*MockEnvironmentStore)(nil).GetALL), ctx, applicationID) +} + +// GetByName mocks base method. +func (m *MockEnvironmentStore) GetByName(ctx *gofr.Context, applicationID int, name string) (*Environment, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetByName", ctx, applicationID, name) + ret0, _ := ret[0].(*Environment) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetByName indicates an expected call of GetByName. +func (mr *MockEnvironmentStoreMockRecorder) GetByName(ctx, applicationID, name interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByName", reflect.TypeOf((*MockEnvironmentStore)(nil).GetByName), ctx, applicationID, name) +} + +// Insert mocks base method. +func (m *MockEnvironmentStore) Insert(ctx *gofr.Context, environment *Environment) (*Environment, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Insert", ctx, environment) + ret0, _ := ret[0].(*Environment) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Insert indicates an expected call of Insert. +func (mr *MockEnvironmentStoreMockRecorder) Insert(ctx, environment interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Insert", reflect.TypeOf((*MockEnvironmentStore)(nil).Insert), ctx, environment) +} + +// Update mocks base method. +func (m *MockEnvironmentStore) Update(ctx *gofr.Context, environment *Environment) (*Environment, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Update", ctx, environment) + ret0, _ := ret[0].(*Environment) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Update indicates an expected call of Update. +func (mr *MockEnvironmentStoreMockRecorder) Update(ctx, environment interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockEnvironmentStore)(nil).Update), ctx, environment) +} diff --git a/environments/store/models.go b/environments/store/models.go new file mode 100644 index 0000000..9a71254 --- /dev/null +++ b/environments/store/models.go @@ -0,0 +1,31 @@ +package store + +// Environment represents the configuration and metadata for a specific application environment. +// It includes information about the environment's name, level, associated application, and timestamps. +type Environment struct { + + // ID is the unique identifier of the environment. + ID int64 `json:"id"` + + // ApplicationID is the unique identifier of the application to which this environment belongs. + ApplicationID int64 `json:"applicationId"` + + // Name is the name of the environment (e.g., "development", "staging", "production"). + Name string `json:"name"` + + // CreatedAt is the timestamp indicating when the environment was created. + CreatedAt string `json:"createdAt"` + + // UpdatedAt is the timestamp of the last update made to the environment. + UpdatedAt string `json:"updatedAt"` + + // DeletedAt is the timestamp indicating when the environment was deleted, if applicable. + // This field is optional and may be omitted if the environment is active. + DeletedAt string `json:"deletedAt,omitempty"` + + // Level represents the environment's hierarchical level or priority, such as an integer scale. + Level int `json:"level"` + + // DeploymentSpace contains any additional deployment-related configuration or metadata. + DeploymentSpace any `json:"deploymentSpace"` +} diff --git a/environments/store/query.go b/environments/store/query.go new file mode 100644 index 0000000..cc49f2a --- /dev/null +++ b/environments/store/query.go @@ -0,0 +1,15 @@ +package store + +const ( + INSERTQUERY = "INSERT INTO environment (name,level,application_id) VALUES ( ?,?, ?);" + GETALLQUERY = "SELECT id, name,level,application_id, created_at, updated_at FROM environment WHERE application_id = ? " + + "and deleted_at IS NULL;" + GETBYNAMEQUERY = "SELECT id, name,level,application_id, created_at, updated_at FROM environment WHERE name = ? " + + "and application_id = ? and deleted_at IS NULL;" + UPDATEQUERY = "UPDATE environment SET name = ?, level = ?, updated_at = UTC_TIMESTAMP() WHERE id = ?;" + GETMAXLEVEL = ` + SELECT MAX(level) AS highest_level + FROM environment + WHERE application_id = ? AND deleted_at IS NULL; +` +) diff --git a/environments/store/store.go b/environments/store/store.go new file mode 100644 index 0000000..376f8a2 --- /dev/null +++ b/environments/store/store.go @@ -0,0 +1,142 @@ +package store + +import ( + "time" + + "gofr.dev/pkg/gofr" +) + +type Store struct { +} + +// New creates and returns a new instance of EnvironmentStore. +// +// Returns: +// - EnvironmentStore: A new instance of the store implementation. +func New() EnvironmentStore { + return &Store{} +} + +// Insert inserts a new environment record into the datastore. +// +// Parameters: +// - ctx: The request context, which includes database connection and logging. +// - environment: A pointer to the Environment struct containing the details to be inserted. +// +// Returns: +// - *Environment: A pointer to the newly inserted environment record, including the generated ID. +// - error: An error if the operation fails. +func (*Store) Insert(ctx *gofr.Context, environment *Environment) (*Environment, error) { + res, err := ctx.SQL.ExecContext(ctx, INSERTQUERY, environment.Name, environment.Level, environment.ApplicationID) + if err != nil { + return nil, err + } + + environment.ID, _ = res.LastInsertId() + + environment.CreatedAt = time.Now().UTC().Format(time.RFC3339) + + return environment, nil +} + +// GetALL retrieves all environments associated with a specific application. +// +// Parameters: +// - ctx: The request context, which includes database connection and logging. +// - applicationID: The unique identifier of the application whose environments need to be fetched. +// +// Returns: +// - []Environment: A slice of environments associated with the specified application. +// - error: An error if the operation fails or the query returns no results. +func (*Store) GetALL(ctx *gofr.Context, applicationID int) ([]Environment, error) { + environments := []Environment{} + + rows, err := ctx.SQL.QueryContext(ctx, GETALLQUERY, applicationID) + if err != nil { + return nil, err + } + + if rows.Err() != nil { + return nil, rows.Err() + } + + for rows.Next() { + var environment Environment + + err := rows.Scan(&environment.ID, &environment.Name, &environment.Level, &environment.ApplicationID, + &environment.CreatedAt, &environment.UpdatedAt) + if err != nil { + return nil, err + } + + environments = append(environments, environment) + } + + return environments, nil +} + +// GetByName retrieves a specific environment by its name for a given application. +// +// Parameters: +// - ctx: The request context, which includes database connection and logging. +// - applicationID: The unique identifier of the application to which the environment belongs. +// - name: The name of the environment to retrieve. +// +// Returns: +// - *Environment: A pointer to the environment matching the specified name. +// - error: An error if the operation fails or no environment is found. +func (*Store) GetByName(ctx *gofr.Context, applicationID int, name string) (*Environment, error) { + row := ctx.SQL.QueryRowContext(ctx, GETBYNAMEQUERY, name, applicationID) + if row.Err() != nil { + return nil, row.Err() + } + + var environment Environment + + err := row.Scan(&environment.ID, &environment.Name, &environment.Level, &environment.ApplicationID, + &environment.CreatedAt, &environment.UpdatedAt) + if err != nil { + return nil, err + } + + return &environment, nil +} + +// Update modifies an existing environment record in the datastore. +// +// Parameters: +// - ctx: The request context, which includes database connection and logging. +// - environment: A pointer to the Environment struct containing the updated details. +// +// Returns: +// - *Environment: A pointer to the updated environment record. +// - error: An error if the operation fails. +func (*Store) Update(ctx *gofr.Context, environment *Environment) (*Environment, error) { + _, err := ctx.SQL.ExecContext(ctx, UPDATEQUERY, environment.Name, environment.Level, environment.ID) + if err != nil { + return nil, err + } + + return environment, nil +} + +// GetMaxLevel retrieves the maximum environment `level` for a specified application from the `environment` table. +// It considers only active records where `deleted_at IS NULL`. +// +// Parameters: +// - ctx (*gofr.Context): Context for request handling, logging, and tracing. +// - applicationID (int): The unique identifier for the application. +// +// Returns: +// - int: The highest environment level associated with the given application ID. +// - error: An error if the query fails or no matching data exists. +func (*Store) GetMaxLevel(ctx *gofr.Context, applicationID int) (int, error) { + var maxLevel int + + err := ctx.SQL.QueryRowContext(ctx, GETMAXLEVEL, applicationID).Scan(&maxLevel) + if err != nil { + return 0, err + } + + return maxLevel, nil +} diff --git a/environments/store/store_test.go b/environments/store/store_test.go new file mode 100644 index 0000000..f1fbf1e --- /dev/null +++ b/environments/store/store_test.go @@ -0,0 +1,149 @@ +package store + +import ( + "context" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/require" + + "gofr.dev/pkg/gofr" + "gofr.dev/pkg/gofr/container" +) + +func TestStore_Insert(t *testing.T) { + mockContainer, mock := container.NewMockContainer(t) + ctx := &gofr.Context{ + Context: context.Background(), + Request: nil, + Container: mockContainer, + } + + store := &Store{} + + environment := &Environment{ + Name: "Test Environment", + Level: 1, + ApplicationID: 1, + } + + mock.SQL.ExpectExec(INSERTQUERY). + WithArgs(environment.Name, environment.Level, environment.ApplicationID). + WillReturnResult(sqlmock.NewResult(1, 1)) + + res, err := store.Insert(ctx, environment) + require.NoError(t, err) + require.NotNil(t, res) + require.Equal(t, int64(1), res.ID) + + require.NoError(t, mock.SQL.ExpectationsWereMet()) +} + +func TestStore_GetALL(t *testing.T) { + mockContainer, mock := container.NewMockContainer(t) + ctx := &gofr.Context{ + Context: context.Background(), + Request: nil, + Container: mockContainer, + } + + store := &Store{} + + applicationID := 1 + environments := []Environment{ + { + ID: 1, + Name: "Test Environment 1", + Level: 2, + ApplicationID: int64(applicationID), + CreatedAt: time.Now().String(), + UpdatedAt: time.Now().String(), + }, + { + ID: 2, + Name: "Test Environment 2", + Level: 1, + ApplicationID: int64(applicationID), + CreatedAt: time.Now().String(), + UpdatedAt: time.Now().String(), + }, + } + + rows := sqlmock.NewRows([]string{"id", "name", "level", "application_id", "created_at", "updated_at"}). + AddRow(environments[0].ID, environments[0].Name, environments[0].Level, + environments[0].ApplicationID, environments[0].CreatedAt, environments[0].UpdatedAt). + AddRow(environments[1].ID, environments[1].Name, environments[1].Level, + environments[1].ApplicationID, environments[1].CreatedAt, environments[1].UpdatedAt) + + mock.SQL.ExpectQuery(GETALLQUERY).WithArgs(applicationID).WillReturnRows(rows) + + res, err := store.GetALL(ctx, applicationID) + require.NoError(t, err) + require.Len(t, res, 2) + + require.NoError(t, mock.SQL.ExpectationsWereMet()) +} + +func TestStore_GetByName(t *testing.T) { + mockContainer, mock := container.NewMockContainer(t) + ctx := &gofr.Context{ + Context: context.Background(), + Request: nil, + Container: mockContainer, + } + + store := &Store{} + + applicationID := 1 + name := "Test Environment" + environment := &Environment{ + ID: 1, + Name: name, + Level: 1, + ApplicationID: int64(applicationID), + CreatedAt: time.Now().String(), + UpdatedAt: time.Now().String(), + } + + row := sqlmock.NewRows([]string{"id", "name", "level", "application_id", "created_at", "updated_at"}). + AddRow(environment.ID, environment.Name, environment.Level, environment.ApplicationID, environment.CreatedAt, environment.UpdatedAt) + + mock.SQL.ExpectQuery(GETBYNAMEQUERY).WithArgs(name, applicationID).WillReturnRows(row) + + res, err := store.GetByName(ctx, applicationID, name) + require.NoError(t, err) + require.NotNil(t, res) + require.Equal(t, name, res.Name) + + require.NoError(t, mock.SQL.ExpectationsWereMet()) +} + +func TestStore_Update(t *testing.T) { + mockContainer, mock := container.NewMockContainer(t) + ctx := &gofr.Context{ + Context: context.Background(), + Request: nil, + Container: mockContainer, + } + + store := &Store{} + + environment := &Environment{ + ID: 1, + Name: "Updated Environment", + Level: 2, + ApplicationID: 1, + } + + mock.SQL.ExpectExec(UPDATEQUERY). + WithArgs(environment.Name, environment.Level, environment.ID). + WillReturnResult(sqlmock.NewResult(0, 1)) + + res, err := store.Update(ctx, environment) + require.NoError(t, err) + require.NotNil(t, res) + require.Equal(t, "Updated Environment", res.Name) + + require.NoError(t, mock.SQL.ExpectationsWereMet()) +} diff --git a/go.mod b/go.mod index 2ee88f9..d96e8df 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,15 @@ 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 + k8s.io/apimachinery v0.31.3 + k8s.io/client-go v0.31.3 ) require ( @@ -21,17 +26,26 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/eclipse/paho.mqtt.golang v1.5.0 // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.22.4 // indirect github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v5 v5.2.1 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/gofuzz v1.2.0 // indirect github.com/google/s2a-go v0.1.8 // indirect github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect @@ -41,16 +55,22 @@ require ( github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect + github.com/imdario/mergo v0.3.6 // indirect github.com/joho/godotenv v1.5.1 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.17.9 // indirect github.com/lib/pq v1.10.9 // indirect + github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect github.com/openzipkin/zipkin-go v0.4.3 // indirect github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.20.5 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.59.1 // indirect @@ -60,6 +80,8 @@ require ( github.com/redis/go-redis/v9 v9.7.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/segmentio/kafka-go v0.4.47 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/x448/float16 v0.8.4 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.56.0 // indirect @@ -74,21 +96,25 @@ 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/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/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/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 google.golang.org/grpc v1.67.1 // indirect google.golang.org/protobuf v1.35.2 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/api v0.31.3 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect + k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect modernc.org/libc v1.55.3 // indirect modernc.org/mathutil v1.6.0 // indirect @@ -96,4 +122,7 @@ require ( modernc.org/sqlite v1.34.1 // indirect modernc.org/strutil v1.2.0 // indirect modernc.org/token v1.1.0 // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/go.sum b/go.sum index 06fb406..3333e06 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= @@ -40,21 +42,27 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/eclipse/paho.mqtt.golang v1.5.0 h1:EH+bUVJNgttidWFkLLVKaQPGmkTUfQQqjOsyvMGvD6o= github.com/eclipse/paho.mqtt.golang v1.5.0/go.mod h1:du/2qNQVqJf/Sqs4MEL77kR8QTqANF7XU7Fk0aOTAgk= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -62,9 +70,19 @@ github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= +github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= @@ -86,16 +104,22 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= -github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af h1:kmjWCqn2qkEml422C2Rrd27c3VGxi6a/6HNq8QmHRKM= +github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM= github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -115,8 +139,14 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0 h1:ad0vkEBuk23VJzZR9nkLVG0YAoN github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0/go.mod h1:igFoXX2ELCW06bol23DWPB5BEWfZISOzSP5K2sbLea0= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= +github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= @@ -125,6 +155,7 @@ github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2 github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -135,12 +166,23 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA= +github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= +github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= +github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/openzipkin/zipkin-go v0.4.3 h1:9EGwpqkgnwdEIJ+Od7QVSEIH+ocmm5nPat0G7sjsSdg= github.com/openzipkin/zipkin-go v0.4.3/go.mod h1:M9wCJZFWCo2RiY+o1eBCEMe0Dp2S5LDHcMZmk3RmK7c= @@ -150,8 +192,9 @@ github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFu github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= @@ -174,6 +217,8 @@ github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWN github.com/segmentio/kafka-go v0.4.47 h1:IqziR4pA3vrZq7YdRxaT3w1/5fvIH5qpCwstUanQQB0= github.com/segmentio/kafka-go v0.4.47/go.mod h1:HjF6XbOKh0Pjlkr5GVZxt6CsjjwnmhVOfURM5KMd8qg= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= @@ -189,6 +234,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= @@ -245,8 +292,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= @@ -284,8 +331,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= @@ -300,15 +347,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= @@ -316,8 +363,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= @@ -374,14 +421,30 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +k8s.io/api v0.31.3 h1:umzm5o8lFbdN/hIXbrK9oRpOproJO62CV1zqxXrLgk8= +k8s.io/api v0.31.3/go.mod h1:UJrkIp9pnMOI9K2nlL6vwpxRzzEX5sWgn8kGQe92kCE= +k8s.io/apimachinery v0.31.3 h1:6l0WhcYgasZ/wk9ktLq5vLaoXJJr5ts6lkaQzgeYPq4= +k8s.io/apimachinery v0.31.3/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= +k8s.io/client-go v0.31.3 h1:CAlZuM+PH2cm+86LOBemaJI/lQ5linJ6UFxKX/SoG+4= +k8s.io/client-go v0.31.3/go.mod h1:2CgjPUTpv3fE5dNygAr2NcM8nhHzXvxB8KL5gYc3kJs= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= +k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= +k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A= +k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ= modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y= @@ -408,3 +471,9 @@ modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/main.go b/main.go index 87e691a..34fca94 100644 --- a/main.go +++ b/main.go @@ -1,14 +1,25 @@ package main import ( + 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/cloudaccounts/handler" "github.com/zopdev/zop-api/cloudaccounts/service" "github.com/zopdev/zop-api/cloudaccounts/store" + clStore "github.com/zopdev/zop-api/deploymentspace/cluster/store" "github.com/zopdev/zop-api/provider/gcp" - appHandler "github.com/zopdev/zop-api/applications/handler" - appService "github.com/zopdev/zop-api/applications/service" - appStore "github.com/zopdev/zop-api/applications/store" + envHandler "github.com/zopdev/zop-api/environments/handler" + envService "github.com/zopdev/zop-api/environments/service" + envStore "github.com/zopdev/zop-api/environments/store" + + deployHandler "github.com/zopdev/zop-api/deploymentspace/handler" + deployService "github.com/zopdev/zop-api/deploymentspace/service" + deployStore "github.com/zopdev/zop-api/deploymentspace/store" + + clService "github.com/zopdev/zop-api/deploymentspace/cluster/service" + "github.com/zopdev/zop-api/migrations" "gofr.dev/pkg/gofr" ) @@ -24,8 +35,19 @@ func main() { cloudAccountService := service.New(cloudAccountStore, gkeSvc) cloudAccountHandler := handler.New(cloudAccountService) + deploymentStore := deployStore.New() + clusterStore := clStore.New() + clusterService := clService.New(clusterStore) + deploymentService := deployService.New(deploymentStore, clusterService) + + deploymentHandler := deployHandler.New(deploymentService) + + environmentStore := envStore.New() + environmentService := envService.New(environmentStore, deploymentService) + envrionmentHandler := envHandler.New(environmentService) + applicationStore := appStore.New() - applicationService := appService.New(applicationStore) + applicationService := appService.New(applicationStore, environmentService) applicationHandler := appHandler.New(applicationService) app.POST("/cloud-accounts", cloudAccountHandler.AddCloudAccount) @@ -36,6 +58,13 @@ func main() { app.POST("/applications", applicationHandler.AddApplication) app.GET("/applications", applicationHandler.ListApplications) + app.GET("/applications/{id}", applicationHandler.GetApplication) + + app.POST("/applications/{id}/environments", envrionmentHandler.Add) + app.PATCH("/environments", envrionmentHandler.Update) + app.GET("/applications/{id}/environments", envrionmentHandler.List) + + app.POST("/environments/{id}/deploymentspace", deploymentHandler.Add) app.Run() } diff --git a/migrations/20241211121308_createEnvironmentTable.go b/migrations/20241211121308_createEnvironmentTable.go new file mode 100755 index 0000000..853c6ec --- /dev/null +++ b/migrations/20241211121308_createEnvironmentTable.go @@ -0,0 +1,33 @@ +package migrations + +import ( + "fmt" + + "gofr.dev/pkg/gofr/migration" +) + +func createEnvironmentTable() migration.Migrate { + return migration.Migrate{ + UP: func(d migration.Datasource) error { + const query = ` + CREATE TABLE IF NOT EXISTS environment ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name VARCHAR(255) NOT NULL, + level INTEGER NOT NULL, + application_id INTEGER 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 { + fmt.Println(err) + return err + } + + return nil + }, + } +} diff --git a/migrations/20241211121841_createDeploymentSpaceTable.go b/migrations/20241211121841_createDeploymentSpaceTable.go new file mode 100755 index 0000000..ec633c1 --- /dev/null +++ b/migrations/20241211121841_createDeploymentSpaceTable.go @@ -0,0 +1,31 @@ +package migrations + +import ( + "gofr.dev/pkg/gofr/migration" +) + +func createDeploymentSpaceTable() migration.Migrate { + return migration.Migrate{ + UP: func(d migration.Datasource) error { + const query = ` + CREATE TABLE if not exists deployment_space ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + type VARCHAR(255) NOT NULL, + environment_id INTEGER NOT NULL, + cloud_account_id INTEGER NOT NULL, + details TEXT, + 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/20241212162207_createClusterTable.go b/migrations/20241212162207_createClusterTable.go new file mode 100755 index 0000000..9d27a07 --- /dev/null +++ b/migrations/20241212162207_createClusterTable.go @@ -0,0 +1,35 @@ +package migrations + +import ( + "gofr.dev/pkg/gofr/migration" +) + +func createClusterTable() migration.Migrate { + return migration.Migrate{ + UP: func(d migration.Datasource) error { + const query = ` + CREATE TABLE if not exists cluster ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + deployment_space_id INTEGER NOT NULL, + cluster_id INTEGER, + name VARCHAR(255) NOT NULL, + region VARCHAR(255) NOT NULL, + provider_id VARCHAR(255) NOT NULL, + provider VARCHAR(255) NOT NULL, + namespace VARCHAR(255), + details TEXT, + 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 867fb6d..d9b3881 100644 --- a/migrations/all.go +++ b/migrations/all.go @@ -10,5 +10,8 @@ func All() map[int64]migration.Migrate { 20241209162239: createCloudAccountTable(), 20241211121223: createAPPlicationTable(), + 20241211121308: createEnvironmentTable(), + 20241211121841: createDeploymentSpaceTable(), + 20241212162207: createClusterTable(), } } From 699b267bcb4bfe3addd6f6e9d66fe9d1c13c2699 Mon Sep 17 00:00:00 2001 From: PiyushSingh-ZS Date: Tue, 17 Dec 2024 13:24:47 +0530 Subject: [PATCH 6/7] check if environment is already configure with deployment space --- deploymentspace/cluster/service/service.go | 10 ++++++---- deploymentspace/cluster/service/service_test.go | 8 +++++--- deploymentspace/service/service.go | 15 +++++++++++++++ deploymentspace/service/service_test.go | 12 ++++++++++++ 4 files changed, 38 insertions(+), 7 deletions(-) diff --git a/deploymentspace/cluster/service/service.go b/deploymentspace/cluster/service/service.go index e95ded9..163467f 100644 --- a/deploymentspace/cluster/service/service.go +++ b/deploymentspace/cluster/service/service.go @@ -5,11 +5,9 @@ import ( "encoding/json" "errors" - "gofr.dev/pkg/gofr" - "gofr.dev/pkg/gofr/http" - "github.com/zopdev/zop-api/deploymentspace" "github.com/zopdev/zop-api/deploymentspace/cluster/store" + "gofr.dev/pkg/gofr" ) type Service struct { @@ -22,6 +20,10 @@ func New(str store.ClusterStore) deploymentspace.DeploymentEntity { } } +var ( + errNamespaceAlreadyInUSe = errors.New("namespace already in use") +) + func (s *Service) FetchByDeploymentSpaceID(ctx *gofr.Context, id int) (interface{}, error) { cluster, err := s.store.GetByDeploymentSpaceID(ctx, id) if err != nil { @@ -73,7 +75,7 @@ func (s *Service) DuplicateCheck(ctx *gofr.Context, data any) (interface{}, erro } if resp != nil { - return nil, http.ErrorEntityAlreadyExist{} + return nil, errNamespaceAlreadyInUSe } return nil, nil diff --git a/deploymentspace/cluster/service/service_test.go b/deploymentspace/cluster/service/service_test.go index 88d00c6..1207c98 100644 --- a/deploymentspace/cluster/service/service_test.go +++ b/deploymentspace/cluster/service/service_test.go @@ -11,10 +11,12 @@ import ( "github.com/zopdev/zop-api/deploymentspace/cluster/service" "github.com/zopdev/zop-api/deploymentspace/cluster/store" "gofr.dev/pkg/gofr" - "gofr.dev/pkg/gofr/http" ) -var errTest = errors.New("service error") +var ( + errNamespaceAlreadyInUSe = errors.New("namespace already in use") + errTest = errors.New("service error") +) func TestService_Add(t *testing.T) { ctrl := gomock.NewController(t) @@ -190,7 +192,7 @@ func TestService_DuplicateCheck(t *testing.T) { Return(mockCluster, nil) }, input: mockCluster, - expectedError: http.ErrorEntityAlreadyExist{}, + expectedError: errNamespaceAlreadyInUSe, expectedResp: nil, }, } diff --git a/deploymentspace/service/service.go b/deploymentspace/service/service.go index e8daf91..90a7939 100644 --- a/deploymentspace/service/service.go +++ b/deploymentspace/service/service.go @@ -19,6 +19,10 @@ import ( "gofr.dev/pkg/gofr/http" ) +var ( + errDeploymentSpaceAlreadyConfigured = errors.New("deployment space already exists") +) + // Service implements the DeploymentSpaceService interface. // It uses a combination of deployment space and cluster stores to manage deployment space operations. type Service struct { @@ -57,6 +61,17 @@ func (s *Service) Add(ctx *gofr.Context, deploymentSpace *DeploymentSpace, envir return nil, http.ErrorInvalidParam{Params: []string{"body"}} } + tempDeploymentSpace, err := s.store.GetByEnvironmentID(ctx, environmentID) + if err != nil { + if !errors.Is(err, sql.ErrNoRows) { + return nil, err + } + } + + if tempDeploymentSpace != nil { + return nil, errDeploymentSpaceAlreadyConfigured + } + dpSpace := store.DeploymentSpace{ CloudAccountID: deploymentSpace.CloudAccount.ID, EnvironmentID: int64(environmentID), diff --git a/deploymentspace/service/service_test.go b/deploymentspace/service/service_test.go index 72edff7..4213dfe 100644 --- a/deploymentspace/service/service_test.go +++ b/deploymentspace/service/service_test.go @@ -59,6 +59,9 @@ func TestService_AddDeploymentSpace(t *testing.T) { { name: "success", mockBehavior: func() { + mockStore.EXPECT(). + GetByEnvironmentID(ctx, gomock.Any()). + Return(nil, nil) mockClusterService.EXPECT(). DuplicateCheck(ctx, gomock.Any()). Return(nil, nil) // No duplicate found @@ -76,6 +79,9 @@ func TestService_AddDeploymentSpace(t *testing.T) { { name: "duplicate cluster error", mockBehavior: func() { + mockStore.EXPECT(). + GetByEnvironmentID(ctx, gomock.Any()). + Return(nil, nil) mockClusterService.EXPECT(). DuplicateCheck(ctx, gomock.Any()). Return(nil, http.ErrorEntityAlreadyExist{}) // Duplicate found @@ -87,6 +93,9 @@ func TestService_AddDeploymentSpace(t *testing.T) { { name: "store layer error", mockBehavior: func() { + mockStore.EXPECT(). + GetByEnvironmentID(ctx, gomock.Any()). + Return(nil, nil) mockClusterService.EXPECT(). DuplicateCheck(ctx, gomock.Any()). Return(nil, nil) // No duplicate found @@ -101,6 +110,9 @@ func TestService_AddDeploymentSpace(t *testing.T) { { name: "cluster service error", mockBehavior: func() { + mockStore.EXPECT(). + GetByEnvironmentID(ctx, gomock.Any()). + Return(nil, nil) mockClusterService.EXPECT(). DuplicateCheck(ctx, gomock.Any()). Return(nil, nil) // No duplicate found From c099d9ac120173a3722da7b66513bac1e688c2b1 Mon Sep 17 00:00:00 2001 From: PiyushSingh-ZS Date: Wed, 18 Dec 2024 12:57:13 +0530 Subject: [PATCH 7/7] add godoc --- deploymentspace/cluster/service/service.go | 38 ++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/deploymentspace/cluster/service/service.go b/deploymentspace/cluster/service/service.go index 163467f..e7a9090 100644 --- a/deploymentspace/cluster/service/service.go +++ b/deploymentspace/cluster/service/service.go @@ -1,3 +1,5 @@ +// Package service provides the business logic for managing clusters in a deployment space. + package service import ( @@ -10,10 +12,12 @@ import ( "gofr.dev/pkg/gofr" ) +// Service implements the deploymentspace.DeploymentEntity interface to manage clusters. type Service struct { store store.ClusterStore } +// New initializes and returns a new Service with the provided ClusterStore. func New(str store.ClusterStore) deploymentspace.DeploymentEntity { return &Service{ store: str, @@ -21,9 +25,21 @@ func New(str store.ClusterStore) deploymentspace.DeploymentEntity { } var ( + // errNamespaceAlreadyInUse indicates that the namespace is already associated with another environment. errNamespaceAlreadyInUSe = errors.New("namespace already in use") ) +// FetchByDeploymentSpaceID retrieves a cluster by its deployment space ID. +// +// Parameters: +// +// ctx - The GoFR context carrying request-specific data. +// id - The ID of the deployment space whose cluster is being fetched. +// +// Returns: +// +// interface{} - The retrieved cluster details. +// error - Any error encountered during the fetch operation. func (s *Service) FetchByDeploymentSpaceID(ctx *gofr.Context, id int) (interface{}, error) { cluster, err := s.store.GetByDeploymentSpaceID(ctx, id) if err != nil { @@ -33,6 +49,17 @@ func (s *Service) FetchByDeploymentSpaceID(ctx *gofr.Context, id int) (interface return cluster, nil } +// Add adds a new cluster to the storage. +// +// Parameters: +// +// ctx - The GoFR context carrying request-specific data. +// data - The cluster details to be added in a serializable format. +// +// Returns: +// +// interface{} - The added cluster details. +// error - Any error encountered during the add operation. func (s *Service) Add(ctx *gofr.Context, data any) (interface{}, error) { bytes, err := json.Marshal(data) if err != nil { @@ -54,6 +81,17 @@ func (s *Service) Add(ctx *gofr.Context, data any) (interface{}, error) { return resp, nil } +// DuplicateCheck checks if a cluster with the same details already exists. +// +// Parameters: +// +// ctx - The GoFR context carrying request-specific data. +// data - The cluster details to check for duplication in a serializable format. +// +// Returns: +// +// interface{} - nil if no duplicate is found, otherwise an error indicating the namespace is already in use. +// error - Any other error encountered during the check operation. func (s *Service) DuplicateCheck(ctx *gofr.Context, data any) (interface{}, error) { bytes, err := json.Marshal(data) if err != nil {