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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion internal/handlers/v1alpha1/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ package v1alpha1
import (
"context"
"encoding/json"
"fmt"

v1alpha1 "github.com/kubev2v/migration-planner/api/v1alpha1/agent"
agentServer "github.com/kubev2v/migration-planner/internal/api/server/agent"
server "github.com/kubev2v/migration-planner/internal/api/server/agent"
apiMappers "github.com/kubev2v/migration-planner/internal/handlers/v1alpha1/mappers"
"github.com/kubev2v/migration-planner/internal/handlers/validator"
"github.com/kubev2v/migration-planner/internal/service"
Expand Down Expand Up @@ -54,7 +56,12 @@ func (h *AgentHandler) UpdateSourceInventory(ctx context.Context, request agentS
}
}

return agentServer.UpdateSourceInventory200JSONResponse(apiMappers.SourceToApi(*updatedSource)), nil
response, err := apiMappers.SourceToApi(*updatedSource)
if err != nil {
return server.UpdateSourceInventory500JSONResponse{Message: fmt.Sprintf("failed to map source to api: %v", err)}, nil
}

return agentServer.UpdateSourceInventory200JSONResponse(response), nil
}

// UpdateAgentStatus updates or creates a new agent resource
Expand Down
38 changes: 31 additions & 7 deletions internal/handlers/v1alpha1/mappers/outbound.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import (
"github.com/kubev2v/migration-planner/api/v1alpha1"
api "github.com/kubev2v/migration-planner/api/v1alpha1"
"github.com/kubev2v/migration-planner/internal/store/model"
"github.com/kubev2v/migration-planner/internal/util"
)

func SourceToApi(s model.Source) api.Source {
func SourceToApi(s model.Source) (api.Source, error) {
source := api.Source{
Id: s.ID,
Inventory: nil,
Expand All @@ -21,9 +22,28 @@ func SourceToApi(s model.Source) api.Source {
}

if len(s.Inventory) > 0 {
i := v1alpha1.Inventory{}
_ = json.Unmarshal(s.Inventory, &i)
source.Inventory = &i
v := util.GetInventoryVersion(s.Inventory)
switch v {
case model.SnapshotVersionV1:
i := v1alpha1.InventoryData{}
if err := json.Unmarshal(s.Inventory, &i); err != nil {
return api.Source{}, fmt.Errorf("failed to unmarshal v1 inventory: %w", err)
}
if i.Vcenter == nil {
return api.Source{}, fmt.Errorf("v1 inventory missing vcenter data")
}
source.Inventory = &v1alpha1.Inventory{
Vcenter: &i,
VcenterId: i.Vcenter.Id,
Clusters: map[string]api.InventoryData{},
}
default:
v2 := v1alpha1.Inventory{}
if err := json.Unmarshal(s.Inventory, &v2); err != nil {
return api.Source{}, fmt.Errorf("failed to unmarshal v2 inventory: %w", err)
}
source.Inventory = &v2
}
}

if len(s.Labels) > 0 {
Expand Down Expand Up @@ -78,7 +98,7 @@ func SourceToApi(s model.Source) api.Source {
// while other agents in up-to-date states exists.
// Which one should be presented in the API response?
if len(s.Agents) == 0 {
return source
return source, nil
}

slices.SortFunc(s.Agents, func(a model.Agent, b model.Agent) int {
Expand All @@ -93,14 +113,18 @@ func SourceToApi(s model.Source) api.Source {
agent := AgentToApi(s.Agents[0])
source.Agent = &agent

return source
return source, nil
}

func SourceListToApi(sources ...model.SourceList) api.SourceList {
sourceList := []api.Source{}
for _, source := range sources {
for _, s := range source {
sourceList = append(sourceList, SourceToApi(s))
apiSource, err := SourceToApi(s)
if err != nil {
continue
}
sourceList = append(sourceList, apiSource)
}
}
return sourceList
Expand Down
28 changes: 24 additions & 4 deletions internal/handlers/v1alpha1/source.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,12 @@ func (s *ServiceHandler) CreateSource(ctx context.Context, request server.Create
return server.CreateSource500JSONResponse{Message: fmt.Sprintf("failed to create source: %v", err)}, nil
}

return server.CreateSource201JSONResponse(mappers.SourceToApi(source)), nil
response, err := mappers.SourceToApi(source)
if err != nil {
return server.CreateSource500JSONResponse{Message: fmt.Sprintf("failed to map source to api: %v", err)}, nil
}

return server.CreateSource201JSONResponse(response), nil
}

// (DELETE /api/v1/sources)
Expand Down Expand Up @@ -126,7 +131,12 @@ func (s *ServiceHandler) GetSource(ctx context.Context, request server.GetSource
return server.GetSource403JSONResponse{Message: message}, nil
}

return server.GetSource200JSONResponse(mappers.SourceToApi(*source)), nil
response, err := mappers.SourceToApi(*source)
if err != nil {
return server.GetSource500JSONResponse{Message: fmt.Sprintf("failed to map source to api: %v", err)}, nil
}

return server.GetSource200JSONResponse(response), nil
}

// (PUT /api/v1/sources/{id})
Expand Down Expand Up @@ -168,7 +178,12 @@ func (s *ServiceHandler) UpdateSource(ctx context.Context, request server.Update
}
}

return server.UpdateSource200JSONResponse(mappers.SourceToApi(*updatedSource)), nil
response, err := mappers.SourceToApi(*updatedSource)
if err != nil {
return server.UpdateSource500JSONResponse{Message: fmt.Sprintf("failed to map source to api: %v", err)}, nil
}

return server.UpdateSource200JSONResponse(response), nil
}

// (PUT /api/v1/sources/{id}/inventory)
Expand Down Expand Up @@ -213,7 +228,12 @@ func (s *ServiceHandler) UpdateInventory(ctx context.Context, request server.Upd
}
}

return server.UpdateInventory200JSONResponse(mappers.SourceToApi(updatedSource)), nil
response, err := mappers.SourceToApi(updatedSource)
if err != nil {
return server.UpdateInventory500JSONResponse{Message: fmt.Sprintf("failed to map source to api: %v", err)}, nil
}

return server.UpdateInventory200JSONResponse(response), nil
}

// (HEAD /api/v1/sources/{id}/image)
Expand Down
79 changes: 78 additions & 1 deletion internal/handlers/v1alpha1/source_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,84 @@ var _ = Describe("source handler", Ordered, func() {
Expect(reflect.TypeOf(resp).String()).To(Equal(reflect.TypeOf(server.GetSource403JSONResponse{}).String()))
})

It("successfully converts V1 inventory to V2 format", func() {
sourceID := uuid.New()
agentID := uuid.New()

// V1 inventory format: InventoryData without vcenter_id at root level
v1InventoryJSON := `{"vms":{"total":100,"totalMigratable":80},"infra":{"totalHosts":10,"totalClusters":2},"vcenter":{"id":"vcenter-123","name":"test-vcenter"}}`

insertSourceWithInventoryStm := "INSERT INTO sources (id, name, username, org_id, inventory) VALUES ('%s', 'source_name', '%s', '%s', '%s');"
tx := gormdb.Exec(fmt.Sprintf(insertSourceWithInventoryStm, sourceID, "admin", "admin", v1InventoryJSON))
Expect(tx.Error).To(BeNil())
tx = gormdb.Exec(fmt.Sprintf(insertAgentStm, agentID, "not-connected", "status-info-1", "cred_url-1", sourceID))
Expect(tx.Error).To(BeNil())

user := auth.User{
Username: "admin",
Organization: "admin",
EmailDomain: "admin.example.com",
}
ctx := auth.NewTokenContext(context.TODO(), user)

srv := handlers.NewServiceHandler(service.NewSourceService(s, nil), service.NewAssessmentService(s, nil), nil)
resp, err := srv.GetSource(ctx, server.GetSourceRequestObject{Id: sourceID})
Expect(err).To(BeNil())
Expect(reflect.TypeOf(resp).String()).To(Equal(reflect.TypeOf(server.GetSource200JSONResponse{}).String()))

source := resp.(server.GetSource200JSONResponse)
Expect(source.Inventory).ToNot(BeNil())
// V2 format should have vcenter_id at root level
Expect(source.Inventory.VcenterId).To(Equal("vcenter-123"))
// V2 format should have vcenter data in Vcenter field
Expect(source.Inventory.Vcenter).ToNot(BeNil())
Expect(source.Inventory.Vcenter.Vms.Total).To(Equal(100))
Expect(source.Inventory.Vcenter.Vms.TotalMigratable).To(Equal(80))
Expect(source.Inventory.Vcenter.Infra.TotalHosts).To(Equal(10))
Expect(source.Inventory.Vcenter.Infra.TotalClusters).NotTo(BeNil())
Expect(*source.Inventory.Vcenter.Infra.TotalClusters).To(Equal(2))
// V2 format should have empty clusters map
Expect(source.Inventory.Clusters).To(BeEmpty())
})

It("successfully returns V2 inventory as-is", func() {
sourceID := uuid.New()
agentID := uuid.New()

// V2 inventory format: has vcenter_id at root level
v2InventoryJSON := `{"vcenter_id":"vcenter-456","vcenter":{"vms":{"total":200,"totalMigratable":150},"infra":{"totalHosts":20,"totalClusters":5}},"clusters":{"cluster-1":{"vms":{"total":50},"infra":{}}}}`

insertSourceWithInventoryStm := "INSERT INTO sources (id, name, username, org_id, inventory) VALUES ('%s', 'source_name', '%s', '%s', '%s');"
tx := gormdb.Exec(fmt.Sprintf(insertSourceWithInventoryStm, sourceID, "admin", "admin", v2InventoryJSON))
Expect(tx.Error).To(BeNil())
tx = gormdb.Exec(fmt.Sprintf(insertAgentStm, agentID, "not-connected", "status-info-1", "cred_url-1", sourceID))
Expect(tx.Error).To(BeNil())

user := auth.User{
Username: "admin",
Organization: "admin",
EmailDomain: "admin.example.com",
}
ctx := auth.NewTokenContext(context.TODO(), user)

srv := handlers.NewServiceHandler(service.NewSourceService(s, nil), service.NewAssessmentService(s, nil), nil)
resp, err := srv.GetSource(ctx, server.GetSourceRequestObject{Id: sourceID})
Expect(err).To(BeNil())
Expect(reflect.TypeOf(resp).String()).To(Equal(reflect.TypeOf(server.GetSource200JSONResponse{}).String()))

source := resp.(server.GetSource200JSONResponse)
Expect(source.Inventory).ToNot(BeNil())
Expect(source.Inventory.VcenterId).To(Equal("vcenter-456"))
Expect(source.Inventory.Vcenter).ToNot(BeNil())
Expect(source.Inventory.Vcenter.Vms.Total).To(Equal(200))
Expect(source.Inventory.Vcenter.Vms.TotalMigratable).To(Equal(150))
Expect(source.Inventory.Vcenter.Infra.TotalHosts).To(Equal(20))
Expect(source.Inventory.Vcenter.Infra.TotalClusters).NotTo(BeNil())
Expect(*source.Inventory.Vcenter.Infra.TotalClusters).To(Equal(5))
Expect(source.Inventory.Clusters).To(HaveLen(1))
Expect(source.Inventory.Clusters["cluster-1"].Vms.Total).To(Equal(50))
})

AfterEach(func() {
gormdb.Exec("DELETE from labels;")
gormdb.Exec("DELETE FROM agents;")
Expand Down Expand Up @@ -555,7 +633,6 @@ var _ = Describe("source handler", Ordered, func() {
tx = gormdb.Raw("SELECT COUNT(*) FROM SOURCES;").Scan(&count)
Expect(tx.Error).To(BeNil())
Expect(count).To(Equal(0))

})

It("successfully deletes a source", func() {
Expand Down
64 changes: 64 additions & 0 deletions internal/service/assessment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,70 @@ var _ = Describe("assessment service", Ordered, func() {
Expect(assessment).To(BeNil())
Expect(err.Error()).To(ContainSubstring("record not found"))
})

It("successfully creates assessment from source with V1 inventory and stores correct version", func() {
// Create a source with V1 inventory (no vcenter_id at root level)
sourceID := uuid.New()
v1InventoryJSON := `{"vms":{"total":100,"totalMigratable":80},"infra":{"totalHosts":10,"totalClusters":2},"vcenter":{"id":"vcenter-123","name":"test-vcenter"}}`

tx := gormdb.Exec(fmt.Sprintf(insertSourceStm, sourceID, "test-source", "user1", "org1", v1InventoryJSON))
Expect(tx.Error).To(BeNil())

testAssessmentID := uuid.New()
createForm := mappers.AssessmentCreateForm{
ID: testAssessmentID,
Name: "V1 Assessment",
OrgID: "org1",
Username: "user1",
Source: service.SourceTypeAgent,
SourceID: &sourceID,
}

assessment, err := svc.CreateAssessment(context.TODO(), createForm)

Expect(err).To(BeNil())
Expect(assessment).ToNot(BeNil())
Expect(assessment.ID).To(Equal(testAssessmentID))
Expect(assessment.Snapshots).To(HaveLen(1))

// Verify the snapshot was stored with V1 version
var snapshotVersion int
tx = gormdb.Raw("SELECT version FROM snapshots WHERE assessment_id = ?", testAssessmentID).Scan(&snapshotVersion)
Expect(tx.Error).To(BeNil())
Expect(snapshotVersion).To(Equal(1)) // V1
})

It("successfully creates assessment from source with V2 inventory and stores correct version", func() {
// Create a source with V2 inventory (has vcenter_id at root level)
sourceID := uuid.New()
v2InventoryJSON := `{"vcenter_id":"vcenter-456","vcenter":{"vms":{"total":200,"totalMigratable":150},"infra":{"totalHosts":20,"totalClusters":5}},"clusters":{}}`

tx := gormdb.Exec(fmt.Sprintf(insertSourceStm, sourceID, "test-source", "user1", "org1", v2InventoryJSON))
Expect(tx.Error).To(BeNil())

testAssessmentID := uuid.New()
createForm := mappers.AssessmentCreateForm{
ID: testAssessmentID,
Name: "V2 Assessment",
OrgID: "org1",
Username: "user1",
Source: service.SourceTypeAgent,
SourceID: &sourceID,
}

assessment, err := svc.CreateAssessment(context.TODO(), createForm)

Expect(err).To(BeNil())
Expect(assessment).ToNot(BeNil())
Expect(assessment.ID).To(Equal(testAssessmentID))
Expect(assessment.Snapshots).To(HaveLen(1))

// Verify the snapshot was stored with V2 version
var snapshotVersion int
tx = gormdb.Raw("SELECT version FROM snapshots WHERE assessment_id = ?", testAssessmentID).Scan(&snapshotVersion)
Expect(tx.Error).To(BeNil())
Expect(snapshotVersion).To(Equal(2)) // V2
})
})

AfterEach(func() {
Expand Down
9 changes: 5 additions & 4 deletions internal/store/assessment.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import (
"time"

"github.com/google/uuid"
"github.com/kubev2v/migration-planner/internal/store/model"
"gorm.io/gorm"
"gorm.io/gorm/clause"

"github.com/kubev2v/migration-planner/internal/store/model"
"github.com/kubev2v/migration-planner/internal/util"
)

type Assessment interface {
Expand Down Expand Up @@ -78,7 +80,7 @@ func (a *AssessmentStore) Create(ctx context.Context, assessment model.Assessmen
snapshot := model.Snapshot{
AssessmentID: assessment.ID,
Inventory: inventory,
Version: model.SnapshotVersionV2,
Version: uint(util.GetInventoryVersion(inventory)),
}

if err := a.getDB(ctx).Create(&snapshot).Error; err != nil {
Expand All @@ -105,11 +107,10 @@ func (a *AssessmentStore) Update(ctx context.Context, assessmentID uuid.UUID, na
}

if inventory != nil {
// Create a new snapshot
snapshot := model.Snapshot{
AssessmentID: assessmentID,
Inventory: inventory,
Version: model.SnapshotVersionV2,
Version: uint(util.GetInventoryVersion(inventory)),
}

if err := a.getDB(ctx).Create(&snapshot).Error; err != nil {
Expand Down
15 changes: 15 additions & 0 deletions internal/util/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import (
"os"
"strconv"
"time"

"github.com/kubev2v/migration-planner/api/v1alpha1"
"github.com/kubev2v/migration-planner/internal/store/model"
)

type StringerWithError func() (string, error)
Expand Down Expand Up @@ -141,3 +144,15 @@ func GBToTB[T ~int | ~int64 | ~float64](gb T) float64 {
func MBToGB[T ~int | ~int32 | ~float64](mb T) int {
return int(math.Round(float64(mb) / 1024.0))
}

// Unmarshal does not return error when v1 inventory is unmarshal into a v2 struct.
// The only way to differentiate the version is to check the internal structure.
func GetInventoryVersion(inventory []byte) int {
i := v1alpha1.Inventory{}
_ = json.Unmarshal(inventory, &i)

if i.VcenterId == "" {
return model.SnapshotVersionV1
}
return model.SnapshotVersionV2
}