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/cloudaccounts/service/models.go b/cloudaccounts/service/models.go index ae6de43..fbcbbd4 100644 --- a/cloudaccounts/service/models.go +++ b/cloudaccounts/service/models.go @@ -14,7 +14,6 @@ type gcpCredentials struct { UniverseDomain string `json:"universe_domain"` } -// DeploymentSpaceOptions contains options to setup deployment space. type DeploymentSpaceOptions struct { Name string `json:"name"` Path string `json:"path"` diff --git a/cloudaccounts/service/service.go b/cloudaccounts/service/service.go index ea1fe29..552816f 100644 --- a/cloudaccounts/service/service.go +++ b/cloudaccounts/service/service.go @@ -142,7 +142,8 @@ func (s *Service) ListNamespaces(ctx *gofr.Context, id int, clusterName, cluster func (*Service) FetchDeploymentSpaceOptions(_ *gofr.Context, id int) ([]DeploymentSpaceOptions, error) { options := []DeploymentSpaceOptions{ - {Name: "gke", + { + Name: "gke", Path: fmt.Sprintf("/cloud-accounts/%v/deployment-space/clusters", id), Type: "type"}, } diff --git a/deploymentspace/cluster/service/service.go b/deploymentspace/cluster/service/service.go new file mode 100644 index 0000000..e7a9090 --- /dev/null +++ b/deploymentspace/cluster/service/service.go @@ -0,0 +1,120 @@ +// Package service provides the business logic for managing clusters in a deployment space. + +package service + +import ( + "database/sql" + "encoding/json" + "errors" + + "github.com/zopdev/zop-api/deploymentspace" + "github.com/zopdev/zop-api/deploymentspace/cluster/store" + "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, + } +} + +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 { + return nil, err + } + + 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 { + 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 +} + +// 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 { + 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, errNamespaceAlreadyInUSe + } + + 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..1207c98 --- /dev/null +++ b/deploymentspace/cluster/service/service_test.go @@ -0,0 +1,217 @@ +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" +) + +var ( + errNamespaceAlreadyInUSe = errors.New("namespace already in use") + 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: errNamespaceAlreadyInUSe, + 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..90a7939 --- /dev/null +++ b/deploymentspace/service/service.go @@ -0,0 +1,159 @@ +/* +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" +) + +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 { + 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"}} + } + + 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), + 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..4213dfe --- /dev/null +++ b/deploymentspace/service/service_test.go @@ -0,0 +1,242 @@ +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() { + mockStore.EXPECT(). + GetByEnvironmentID(ctx, gomock.Any()). + Return(nil, nil) + 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() { + mockStore.EXPECT(). + GetByEnvironmentID(ctx, gomock.Any()). + Return(nil, nil) + 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() { + mockStore.EXPECT(). + GetByEnvironmentID(ctx, gomock.Any()). + Return(nil, nil) + 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() { + mockStore.EXPECT(). + GetByEnvironmentID(ctx, gomock.Any()). + Return(nil, nil) + 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 a3abf93..015a562 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.22.8 require ( cloud.google.com/go/container v1.41.0 github.com/DATA-DOG/go-sqlmock v1.5.2 + github.com/gorilla/mux v1.8.1 github.com/stretchr/testify v1.10.0 go.uber.org/mock v0.5.0 gofr.dev v1.28.0 @@ -39,7 +40,6 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect github.com/googleapis/gax-go/v2 v2.14.0 // indirect - github.com/gorilla/mux v1.8.1 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0 // indirect 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(), } } diff --git a/provider/models.go b/provider/models.go index 3e8b2ae..cfed9ba 100644 --- a/provider/models.go +++ b/provider/models.go @@ -13,7 +13,7 @@ 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. + // Next contains pagination information for retrieving the next set of resources. Next Next `json:"next"` Metadata Metadata `json:"metadata"` @@ -21,20 +21,18 @@ type ClusterResponse struct { type Metadata struct { Name string `json:"name"` - // Next contains pagination information for retrieving the next set of resources. - Next Next `json:"next"` } // Next provides pagination details for fetching additional data. -// It contains the name, path, and parameters required to get the next page of results. +// It contains the name, path, and parameters required to get the next set of results. type Next struct { - // Name is the name of the next page. + // Name is the name of the next . Name string `json:"name"` - // Path is the URL path to the next page of results. + // Path is the URL path to the next set of results. Path string `json:"path"` - // Params holds the parameters required to fetch the next page. + // Params holds the parameters required to fetch the next set. Params map[string]string `json:"params"` }