From da588d6fbca1eab6f5d5d886a6e7f7ed3a8fdc51 Mon Sep 17 00:00:00 2001 From: Cosmin Tupangiu Date: Mon, 25 Nov 2024 09:24:56 +0100 Subject: [PATCH] agent/multi-source: Implementation of multi-source model in the agent This commit is a breaking-change. It implementing the multi-source model into agent. The agent is nolonger bound to a specific sourceID. The sourceID is computed from vCenter ID. The agents sends requests to two endpoints: `/agents/status` and `/sources/status`. The agent endpoint is used to send the status of the agent which is actually the status of the source in the old model. The `source` endpoint is used to send the inventory of the source only when the inventory is up-to-date. The flow is: - agent starts up and is waiting for credentials. It sends a request to /agents endpoint with the new state. - user enters the credentials and if agent obtained the inventory, it sends it to /source endpoint. Signed-off-by: Cosmin Tupangiu --- api/v1alpha1/agent/openapi.yaml | 1 - api/v1alpha1/agent/spec.gen.go | 44 ++-- api/v1alpha1/openapi.yaml | 4 +- api/v1alpha1/spec.gen.go | 42 ++-- api/v1alpha1/types.gen.go | 19 +- cmd/planner-agent/main.go | 8 +- cmd/planner/main.go | 1 - internal/agent/agent.go | 45 ++++- internal/agent/agent_test.go | 107 ++++++++++ internal/agent/client/client.go | 5 +- internal/agent/client/planner.go | 22 +- internal/agent/client/zz_generated_planner.go | 189 ++++++++++++++++++ internal/agent/health_test.go | 7 +- internal/agent/inventory.go | 62 ++---- internal/agent/inventory_test.go | 47 +++++ internal/agent/rest.go | 10 +- internal/agent/server.go | 4 +- internal/agent/status.go | 83 ++++++++ internal/agent/status_test.go | 131 ++++++++++++ internal/cli/create.go | 85 -------- 20 files changed, 701 insertions(+), 215 deletions(-) create mode 100644 internal/agent/agent_test.go create mode 100644 internal/agent/client/zz_generated_planner.go create mode 100644 internal/agent/inventory_test.go create mode 100644 internal/agent/status.go create mode 100644 internal/agent/status_test.go delete mode 100644 internal/cli/create.go diff --git a/api/v1alpha1/agent/openapi.yaml b/api/v1alpha1/agent/openapi.yaml index 767963c..55c89c1 100644 --- a/api/v1alpha1/agent/openapi.yaml +++ b/api/v1alpha1/agent/openapi.yaml @@ -130,7 +130,6 @@ components: required: - inventory - agentId - AgentStatusUpdate: type: object properties: diff --git a/api/v1alpha1/agent/spec.gen.go b/api/v1alpha1/agent/spec.gen.go index faa848f..b711e82 100644 --- a/api/v1alpha1/agent/spec.gen.go +++ b/api/v1alpha1/agent/spec.gen.go @@ -19,28 +19,28 @@ import ( // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/api/v1alpha1/openapi.yaml b/api/v1alpha1/openapi.yaml index 2208cac..eae37c4 100644 --- a/api/v1alpha1/openapi.yaml +++ b/api/v1alpha1/openapi.yaml @@ -292,8 +292,6 @@ components: type: string inventory: $ref: '#/components/schemas/Inventory' - credentialUrl: - type: string createdAt: type: string format: date-time @@ -328,7 +326,7 @@ components: type: array items: $ref: '#/components/schemas/Source' - + SourceAgentItem: type: object properties: diff --git a/api/v1alpha1/spec.gen.go b/api/v1alpha1/spec.gen.go index 8282b3f..11b1c1e 100644 --- a/api/v1alpha1/spec.gen.go +++ b/api/v1alpha1/spec.gen.go @@ -19,7 +19,7 @@ import ( var swaggerSpec = []string{ "H4sIAAAAAAAC/+xaX3PbNhL/Khj0Zu6FEp1cbuZGb4qbNJo2iSdu3YfED2tyRaImARZYSuPL6LvfACAl", - "SgQlOk1c58ZvFgHsn9/+9g+QfOaJKislUZLhs8/cJDmW4P6cZyjJ/lFpVaEmge4zGKMSAYSp/UV3FfIZ", + "SgQl2k3c5MZvFgHsn9/+9g+QfOaJKislUZLhs8/cJDmW4P6cZyjJ/lFpVaEmge4zGKMSAYSp/UV3FfIZ", "v1GqQJB8E/FEo12cu6NLpUsgPuMpEE5IlMij9owhLWTWHElRkoDiN110pO52pFjgPYWKNCjJqFonuBhY", "JKDaeYmyLvnsI5eKJomSEhPrcMTXIEjIbLJUerIz2/CIo9ZK84hnQDlagRMhhV2cCLlCSUrf8YjX1YTU", "xBrOW1smmZLIr6MhcxZyqYLW1lV6X6RXqI1QMiBuE3GNf9ZC27h+tOht4dgz5DBa3YB3TYq6RNkp3rmp", @@ -28,26 +28,26 @@ var swaggerSpec = []string{ "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/api/v1alpha1/types.gen.go b/api/v1alpha1/types.gen.go index 0f98afa..ea88529 100644 --- a/api/v1alpha1/types.gen.go +++ b/api/v1alpha1/types.gen.go @@ -96,16 +96,15 @@ type MigrationIssues = []struct { // Source defines model for Source. type Source struct { - Agents *[]SourceAgentItem `json:"agents,omitempty"` - CreatedAt time.Time `json:"createdAt"` - CredentialUrl *string `json:"credentialUrl,omitempty"` - Id openapi_types.UUID `json:"id"` - Inventory *Inventory `json:"inventory,omitempty"` - Name string `json:"name"` - SshKey *string `json:"sshKey,omitempty"` - Status SourceStatus `json:"status"` - StatusInfo string `json:"statusInfo"` - UpdatedAt time.Time `json:"updatedAt"` + Agents *[]SourceAgentItem `json:"agents,omitempty"` + CreatedAt time.Time `json:"createdAt"` + Id openapi_types.UUID `json:"id"` + Inventory *Inventory `json:"inventory,omitempty"` + Name string `json:"name"` + SshKey *string `json:"sshKey,omitempty"` + Status SourceStatus `json:"status"` + StatusInfo string `json:"statusInfo"` + UpdatedAt time.Time `json:"updatedAt"` } // SourceStatus defines model for Source.Status. diff --git a/cmd/planner-agent/main.go b/cmd/planner-agent/main.go index 582f905..9420514 100644 --- a/cmd/planner-agent/main.go +++ b/cmd/planner-agent/main.go @@ -6,10 +6,15 @@ import ( "fmt" "os" + "github.com/google/uuid" "github.com/kubev2v/migration-planner/internal/agent" "github.com/kubev2v/migration-planner/pkg/log" ) +var ( + agentID string +) + func main() { command := NewAgentCommand() if err := command.Execute(); err != nil { @@ -30,6 +35,7 @@ func NewAgentCommand() *agentCmd { } flag.StringVar(&a.configFile, "config", agent.DefaultConfigFile, "Path to the agent's configuration file.") + flag.StringVar(&agentID, "id", os.Getenv("AGENT_ID"), "ID of the agent") flag.Usage = func() { fmt.Fprintf(flag.CommandLine.Output(), "Usage of %s:\n", os.Args[0]) @@ -52,7 +58,7 @@ func NewAgentCommand() *agentCmd { } func (a *agentCmd) Execute() error { - agentInstance := agent.New(a.log, a.config) + agentInstance := agent.New(uuid.MustParse(agentID), a.log, a.config) if err := agentInstance.Run(context.Background()); err != nil { a.log.Fatalf("running device agent: %v", err) } diff --git a/cmd/planner/main.go b/cmd/planner/main.go index 2274026..19dba59 100644 --- a/cmd/planner/main.go +++ b/cmd/planner/main.go @@ -24,7 +24,6 @@ func NewPlannerCtlCommand() *cobra.Command { }, } cmd.AddCommand(cli.NewCmdGet()) - cmd.AddCommand(cli.NewCmdCreate()) cmd.AddCommand(cli.NewCmdDelete()) cmd.AddCommand(cli.NewCmdVersion()) diff --git a/internal/agent/agent.go b/internal/agent/agent.go index d1bd3f9..d44b69e 100644 --- a/internal/agent/agent.go +++ b/internal/agent/agent.go @@ -2,6 +2,7 @@ package agent import ( "context" + "errors" "fmt" "net" "net/url" @@ -10,6 +11,8 @@ import ( "syscall" "time" + "github.com/google/uuid" + api "github.com/kubev2v/migration-planner/api/v1alpha1" "github.com/kubev2v/migration-planner/internal/agent/client" "github.com/kubev2v/migration-planner/pkg/log" "github.com/lthibault/jitterbug" @@ -28,11 +31,12 @@ const ( var version string // New creates a new agent. -func New(log *log.PrefixLogger, config *Config) *Agent { +func New(id uuid.UUID, log *log.PrefixLogger, config *Config) *Agent { return &Agent{ config: config, log: log, healtCheckStopCh: make(chan chan any), + id: id, } } @@ -42,6 +46,7 @@ type Agent struct { server *Server healtCheckStopCh chan chan any credUrl string + id uuid.UUID } func (a *Agent) GetLogPrefix() string { @@ -67,7 +72,10 @@ func (a *Agent) Run(ctx context.Context) error { ctx, cancel := context.WithCancel(ctx) a.start(ctx, client) - <-sig + select { + case <-sig: + case <-ctx.Done(): + } a.log.Info("stopping agent...") @@ -91,9 +99,12 @@ func (a *Agent) Stop() { } func (a *Agent) start(ctx context.Context, plannerClient client.Planner) { + inventoryUpdater := NewInventoryUpdater(a.log, a.id, plannerClient) + statusUpdater := NewStatusUpdater(a.log, a.id, version, a.credUrl, a.config, plannerClient) + // start server a.server = NewServer(defaultAgentPort, a.config.DataDir, a.config.WwwDir) - go a.server.Start(a.log) + go a.server.Start(a.log, statusUpdater) // get the credentials url a.initializeCredentialUrl() @@ -115,9 +126,15 @@ func (a *Agent) start(ctx context.Context, plannerClient client.Planner) { collector := NewCollector(a.log, a.config.DataDir) collector.collect(ctx) - inventoryUpdater := NewInventoryUpdater(a.log, a.config, a.credUrl, plannerClient) updateTicker := jitterbug.New(time.Duration(a.config.UpdateInterval.Duration), &jitterbug.Norm{Stdev: 30 * time.Millisecond, Mean: 0}) + /* + Main loop + The status of agent is always computed even if we don't have connectivity with the backend. + If we're connected to the backend, the agent sends its status and if the status is UpToDate, + it sends the inventory. + In case of "source gone", it stops everything and break from the loop. + */ go func() { for { select { @@ -126,16 +143,30 @@ func (a *Agent) start(ctx context.Context, plannerClient client.Planner) { case <-updateTicker.C: } + // calculate status regardless if we have connectivity withe the backend. + status, statusInfo, inventory := statusUpdater.CalculateStatus() + // check for health. Send requests only if we have connectivity if healthChecker.State() == HealthCheckStateConsoleUnreachable { continue } - // set the status - inventoryUpdater.UpdateServiceWithInventory(ctx) + if err := statusUpdater.UpdateStatus(ctx, status, statusInfo); err != nil { + if errors.Is(err, client.ErrSourceGone) { + a.log.Info("Source is gone..Stop sending requests") + // stop the server and the healthchecker + a.Stop() + break + } + a.log.Errorf("unable to update agent status: %s", err) + continue // skip inventory update if we cannot update agent's state. + } + + if status == api.AgentStatusUpToDate { + inventoryUpdater.UpdateServiceWithInventory(ctx, api.SourceStatusUpToDate, "Inventory collected with success", inventory) + } } }() - } func (a *Agent) initializeCredentialUrl() { diff --git a/internal/agent/agent_test.go b/internal/agent/agent_test.go new file mode 100644 index 0000000..31a08b8 --- /dev/null +++ b/internal/agent/agent_test.go @@ -0,0 +1,107 @@ +package agent_test + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "os" + "strings" + "time" + + "github.com/google/uuid" + agentapi "github.com/kubev2v/migration-planner/api/v1alpha1/agent" + "github.com/kubev2v/migration-planner/internal/agent" + "github.com/kubev2v/migration-planner/internal/agent/client" + "github.com/kubev2v/migration-planner/internal/util" + "github.com/kubev2v/migration-planner/pkg/log" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Agent", func() { + var ( + // test http server used to get the cred url + testHttpServer *httptest.Server + agentTmpFolder string + agentID uuid.UUID + logger *log.PrefixLogger + endpointsCalled map[string]any + ) + + BeforeEach(func() { + logger = log.NewPrefixLogger("") + agentID, _ = uuid.NewUUID() + endpointsCalled = make(map[string]any) + + testHttpServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.HasPrefix(r.URL.String(), "/api/v1/agents") { + // save the response + body, err := io.ReadAll(r.Body) + Expect(err).To(BeNil()) + + status := agentapi.AgentStatusUpdate{} + + err = json.Unmarshal(body, &status) + Expect(err).To(BeNil()) + endpointsCalled[r.URL.String()] = status + w.WriteHeader(http.StatusOK) + return + } + + endpointsCalled[r.URL.String()] = true + w.WriteHeader(http.StatusOK) + })) + var err error + agentTmpFolder, err = os.MkdirTemp("", "agent-data-folder") + Expect(err).To(BeNil()) + }) + + AfterEach(func() { + testHttpServer.Close() + os.RemoveAll(agentTmpFolder) + }) + + Context("Agent", func() { + It("agents starts successfully -- status waiting-for-credentials", func() { + updateInterval, _ := time.ParseDuration("5s") + config := agent.Config{ + PlannerService: agent.PlannerService{Config: *client.NewDefault()}, + DataDir: agentTmpFolder, + ConfigDir: agentTmpFolder, + UpdateInterval: util.Duration{Duration: updateInterval}, + HealthCheckInterval: 10, + } + config.PlannerService.Service.Server = testHttpServer.URL + + a := agent.New(agentID, logger, &config) + ctx, cancel := context.WithTimeout(context.TODO(), 30*time.Second) + go func() { + err := a.Run(ctx) + Expect(err).To(BeNil()) + }() + + <-time.After(30 * time.Second) + cancel() + + select { + case <-ctx.Done(): + Expect(ctx.Err().Error()).To(Equal("context canceled")) + case <-time.After(20 * time.Second): + Fail("agent did not returned when context was canceled") + } + + // We should have calles to /health and /agents endpoint + status, found := endpointsCalled[fmt.Sprintf("/api/v1/agents/%s/status", agentID)] + Expect(found).To(BeTrue()) + Expect(status.(agentapi.AgentStatusUpdate).CredentialUrl).NotTo(BeEmpty()) + Expect(status.(agentapi.AgentStatusUpdate).Status).To(Equal("waiting-for-credentials")) + + _, found = endpointsCalled["/health"] + Expect(found).To(BeTrue()) + }) + }) + +}) diff --git a/internal/agent/client/client.go b/internal/agent/client/client.go index fc228a2..5a9a29c 100644 --- a/internal/agent/client/client.go +++ b/internal/agent/client/client.go @@ -34,9 +34,12 @@ func NewDefault() *Config { return baseclient.NewDefault() } +//go:generate moq -out zz_generated_planner.go . Planner // Planner is the client interface for migration planning. type Planner interface { - UpdateSourceStatus(ctx context.Context, id uuid.UUID, params api.SourceStatusUpdate, rcb ...client.RequestEditorFn) error + UpdateSourceStatus(ctx context.Context, id uuid.UUID, params api.SourceStatusUpdate) error // Health is checking the connectivity with console.redhat.com by making requests to /health endpoint. Health(ctx context.Context) error + // UpdateAgentStatus updates the agent status. + UpdateAgentStatus(ctx context.Context, id uuid.UUID, params api.AgentStatusUpdate) error } diff --git a/internal/agent/client/planner.go b/internal/agent/client/planner.go index e640801..ae68f70 100644 --- a/internal/agent/client/planner.go +++ b/internal/agent/client/planner.go @@ -15,6 +15,7 @@ var _ Planner = (*planner)(nil) var ( ErrEmptyResponse = errors.New("empty response") + ErrSourceGone = errors.New("source is gone") ) func NewPlanner(client *client.ClientWithResponses) Planner { @@ -27,8 +28,8 @@ type planner struct { client *client.ClientWithResponses } -func (p *planner) UpdateSourceStatus(ctx context.Context, id uuid.UUID, params api.SourceStatusUpdate, rcb ...client.RequestEditorFn) error { - resp, err := p.client.ReplaceSourceStatusWithResponse(ctx, id, params, rcb...) +func (p *planner) UpdateSourceStatus(ctx context.Context, id uuid.UUID, params api.SourceStatusUpdate) error { + resp, err := p.client.ReplaceSourceStatusWithResponse(ctx, id, params) if err != nil { return err } @@ -55,3 +56,20 @@ func (p *planner) Health(ctx context.Context) error { } return nil } + +func (p *planner) UpdateAgentStatus(ctx context.Context, id uuid.UUID, params api.AgentStatusUpdate) error { + resp, err := p.client.UpdateAgentStatusWithResponse(ctx, id, params) + if err != nil { + return err + } + if resp.HTTPResponse != nil { + defer resp.HTTPResponse.Body.Close() + } + if resp.StatusCode() == http.StatusGone { + return ErrSourceGone + } + if resp.StatusCode() != http.StatusOK && resp.StatusCode() != http.StatusCreated { + return fmt.Errorf("update agent status failed with status: %s", resp.Status()) + } + return nil +} diff --git a/internal/agent/client/zz_generated_planner.go b/internal/agent/client/zz_generated_planner.go new file mode 100644 index 0000000..19fff97 --- /dev/null +++ b/internal/agent/client/zz_generated_planner.go @@ -0,0 +1,189 @@ +// Code generated by moq; DO NOT EDIT. +// github.com/matryer/moq + +package client + +import ( + "context" + "github.com/google/uuid" + api "github.com/kubev2v/migration-planner/api/v1alpha1/agent" + "sync" +) + +// Ensure, that PlannerMock does implement Planner. +// If this is not the case, regenerate this file with moq. +var _ Planner = &PlannerMock{} + +// PlannerMock is a mock implementation of Planner. +// +// func TestSomethingThatUsesPlanner(t *testing.T) { +// +// // make and configure a mocked Planner +// mockedPlanner := &PlannerMock{ +// HealthFunc: func(ctx context.Context) error { +// panic("mock out the Health method") +// }, +// UpdateAgentStatusFunc: func(ctx context.Context, id uuid.UUID, params api.AgentStatusUpdate) error { +// panic("mock out the UpdateAgentStatus method") +// }, +// UpdateSourceStatusFunc: func(ctx context.Context, id uuid.UUID, params api.SourceStatusUpdate) error { +// panic("mock out the UpdateSourceStatus method") +// }, +// } +// +// // use mockedPlanner in code that requires Planner +// // and then make assertions. +// +// } +type PlannerMock struct { + // HealthFunc mocks the Health method. + HealthFunc func(ctx context.Context) error + + // UpdateAgentStatusFunc mocks the UpdateAgentStatus method. + UpdateAgentStatusFunc func(ctx context.Context, id uuid.UUID, params api.AgentStatusUpdate) error + + // UpdateSourceStatusFunc mocks the UpdateSourceStatus method. + UpdateSourceStatusFunc func(ctx context.Context, id uuid.UUID, params api.SourceStatusUpdate) error + + // calls tracks calls to the methods. + calls struct { + // Health holds details about calls to the Health method. + Health []struct { + // Ctx is the ctx argument value. + Ctx context.Context + } + // UpdateAgentStatus holds details about calls to the UpdateAgentStatus method. + UpdateAgentStatus []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // ID is the id argument value. + ID uuid.UUID + // Params is the params argument value. + Params api.AgentStatusUpdate + } + // UpdateSourceStatus holds details about calls to the UpdateSourceStatus method. + UpdateSourceStatus []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // ID is the id argument value. + ID uuid.UUID + // Params is the params argument value. + Params api.SourceStatusUpdate + } + } + lockHealth sync.RWMutex + lockUpdateAgentStatus sync.RWMutex + lockUpdateSourceStatus sync.RWMutex +} + +// Health calls HealthFunc. +func (mock *PlannerMock) Health(ctx context.Context) error { + if mock.HealthFunc == nil { + panic("PlannerMock.HealthFunc: method is nil but Planner.Health was just called") + } + callInfo := struct { + Ctx context.Context + }{ + Ctx: ctx, + } + mock.lockHealth.Lock() + mock.calls.Health = append(mock.calls.Health, callInfo) + mock.lockHealth.Unlock() + return mock.HealthFunc(ctx) +} + +// HealthCalls gets all the calls that were made to Health. +// Check the length with: +// +// len(mockedPlanner.HealthCalls()) +func (mock *PlannerMock) HealthCalls() []struct { + Ctx context.Context +} { + var calls []struct { + Ctx context.Context + } + mock.lockHealth.RLock() + calls = mock.calls.Health + mock.lockHealth.RUnlock() + return calls +} + +// UpdateAgentStatus calls UpdateAgentStatusFunc. +func (mock *PlannerMock) UpdateAgentStatus(ctx context.Context, id uuid.UUID, params api.AgentStatusUpdate) error { + if mock.UpdateAgentStatusFunc == nil { + panic("PlannerMock.UpdateAgentStatusFunc: method is nil but Planner.UpdateAgentStatus was just called") + } + callInfo := struct { + Ctx context.Context + ID uuid.UUID + Params api.AgentStatusUpdate + }{ + Ctx: ctx, + ID: id, + Params: params, + } + mock.lockUpdateAgentStatus.Lock() + mock.calls.UpdateAgentStatus = append(mock.calls.UpdateAgentStatus, callInfo) + mock.lockUpdateAgentStatus.Unlock() + return mock.UpdateAgentStatusFunc(ctx, id, params) +} + +// UpdateAgentStatusCalls gets all the calls that were made to UpdateAgentStatus. +// Check the length with: +// +// len(mockedPlanner.UpdateAgentStatusCalls()) +func (mock *PlannerMock) UpdateAgentStatusCalls() []struct { + Ctx context.Context + ID uuid.UUID + Params api.AgentStatusUpdate +} { + var calls []struct { + Ctx context.Context + ID uuid.UUID + Params api.AgentStatusUpdate + } + mock.lockUpdateAgentStatus.RLock() + calls = mock.calls.UpdateAgentStatus + mock.lockUpdateAgentStatus.RUnlock() + return calls +} + +// UpdateSourceStatus calls UpdateSourceStatusFunc. +func (mock *PlannerMock) UpdateSourceStatus(ctx context.Context, id uuid.UUID, params api.SourceStatusUpdate) error { + if mock.UpdateSourceStatusFunc == nil { + panic("PlannerMock.UpdateSourceStatusFunc: method is nil but Planner.UpdateSourceStatus was just called") + } + callInfo := struct { + Ctx context.Context + ID uuid.UUID + Params api.SourceStatusUpdate + }{ + Ctx: ctx, + ID: id, + Params: params, + } + mock.lockUpdateSourceStatus.Lock() + mock.calls.UpdateSourceStatus = append(mock.calls.UpdateSourceStatus, callInfo) + mock.lockUpdateSourceStatus.Unlock() + return mock.UpdateSourceStatusFunc(ctx, id, params) +} + +// UpdateSourceStatusCalls gets all the calls that were made to UpdateSourceStatus. +// Check the length with: +// +// len(mockedPlanner.UpdateSourceStatusCalls()) +func (mock *PlannerMock) UpdateSourceStatusCalls() []struct { + Ctx context.Context + ID uuid.UUID + Params api.SourceStatusUpdate +} { + var calls []struct { + Ctx context.Context + ID uuid.UUID + Params api.SourceStatusUpdate + } + mock.lockUpdateSourceStatus.RLock() + calls = mock.calls.UpdateSourceStatus + mock.lockUpdateSourceStatus.RUnlock() + return calls +} diff --git a/internal/agent/health_test.go b/internal/agent/health_test.go index e92fee8..fd55580 100644 --- a/internal/agent/health_test.go +++ b/internal/agent/health_test.go @@ -10,7 +10,6 @@ import ( "github.com/google/uuid" api "github.com/kubev2v/migration-planner/api/v1alpha1/agent" - client "github.com/kubev2v/migration-planner/internal/api/client/agent" "github.com/kubev2v/migration-planner/pkg/log" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -232,6 +231,10 @@ func (c *agentTestClient) Health(ctx context.Context) error { return nil } -func (c *agentTestClient) UpdateSourceStatus(ctx context.Context, id uuid.UUID, params api.SourceStatusUpdate, rcb ...client.RequestEditorFn) error { +func (c *agentTestClient) UpdateSourceStatus(ctx context.Context, id uuid.UUID, params api.SourceStatusUpdate) error { + return nil +} + +func (c *agentTestClient) UpdateAgentStatus(ctx context.Context, id uuid.UUID, params api.AgentStatusUpdate) error { return nil } diff --git a/internal/agent/inventory.go b/internal/agent/inventory.go index bf7299d..840798b 100644 --- a/internal/agent/inventory.go +++ b/internal/agent/inventory.go @@ -4,22 +4,19 @@ import ( "bytes" "context" "encoding/json" - "fmt" - "path/filepath" "github.com/google/uuid" api "github.com/kubev2v/migration-planner/api/v1alpha1" agentapi "github.com/kubev2v/migration-planner/api/v1alpha1/agent" "github.com/kubev2v/migration-planner/internal/agent/client" - "github.com/kubev2v/migration-planner/internal/agent/fileio" "github.com/kubev2v/migration-planner/pkg/log" ) type InventoryUpdater struct { log *log.PrefixLogger - config *Config + sourceID uuid.UUID client client.Planner - credUrl string + agentID uuid.UUID prevStatus []byte } @@ -28,29 +25,22 @@ type InventoryData struct { Error string `json:"error"` } -func NewInventoryUpdater(log *log.PrefixLogger, config *Config, credUrl string, client client.Planner) *InventoryUpdater { - return &InventoryUpdater{ +func NewInventoryUpdater(log *log.PrefixLogger, agentID uuid.UUID, client client.Planner) *InventoryUpdater { + updater := &InventoryUpdater{ log: log, - config: config, client: client, + agentID: agentID, prevStatus: []byte{}, - credUrl: credUrl, } + return updater } -func (u *InventoryUpdater) UpdateServiceWithInventory(ctx context.Context) { - status, statusInfo, inventory := calculateStatus(u.config.DataDir) - u.updateSourceStatus(ctx, status, statusInfo, inventory) -} - -func (u *InventoryUpdater) updateSourceStatus(ctx context.Context, status api.SourceStatus, statusInfo string, inventory *api.Inventory) { +func (u *InventoryUpdater) UpdateServiceWithInventory(ctx context.Context, status api.SourceStatus, statusInfo string, inventory *api.Inventory) { update := agentapi.SourceStatusUpdate{ - Status: string(status), - StatusInfo: statusInfo, - Inventory: inventory, - CredentialUrl: u.credUrl, - // TODO: when moving to AgentStatusUpdate put this: - //Version: version, + Status: string(status), + StatusInfo: statusInfo, + Inventory: *inventory, + AgentId: u.agentID, } newContents, err := json.Marshal(update) @@ -63,7 +53,7 @@ func (u *InventoryUpdater) updateSourceStatus(ctx context.Context, status api.So } u.log.Debugf("Updating status to %s: %s", string(status), statusInfo) - err = u.client.UpdateSourceStatus(ctx, uuid.MustParse(u.config.SourceID), update) + err = u.client.UpdateSourceStatus(ctx, uuid.MustParse(inventory.Vcenter.Id), update) if err != nil { u.log.Errorf("failed updating status: %v", err) return @@ -71,31 +61,3 @@ func (u *InventoryUpdater) updateSourceStatus(ctx context.Context, status api.So u.prevStatus = newContents } - -func calculateStatus(dataDir string) (api.SourceStatus, string, *api.Inventory) { - inventoryFilePath := filepath.Join(dataDir, InventoryFile) - credentialsFilePath := filepath.Join(dataDir, CredentialsFile) - reader := fileio.NewReader() - - err := reader.CheckPathExists(credentialsFilePath) - if err != nil { - return api.SourceStatusWaitingForCredentials, "No credentials provided", nil - } - err = reader.CheckPathExists(inventoryFilePath) - if err != nil { - return api.SourceStatusGatheringInitialInventory, "Inventory not yet collected", nil - } - inventoryData, err := reader.ReadFile(inventoryFilePath) - if err != nil { - return api.SourceStatusError, fmt.Sprintf("Failed reading inventory file: %v", err), nil - } - var inventory InventoryData - err = json.Unmarshal(inventoryData, &inventory) - if err != nil { - return api.SourceStatusError, fmt.Sprintf("Invalid inventory file: %v", err), nil - } - if len(inventory.Error) > 0 { - return api.SourceStatusError, inventory.Error, &inventory.Inventory - } - return api.SourceStatusUpToDate, "Inventory successfully collected", &inventory.Inventory -} diff --git a/internal/agent/inventory_test.go b/internal/agent/inventory_test.go new file mode 100644 index 0000000..01e506e --- /dev/null +++ b/internal/agent/inventory_test.go @@ -0,0 +1,47 @@ +package agent_test + +import ( + "context" + + "github.com/google/uuid" + api "github.com/kubev2v/migration-planner/api/v1alpha1" + v1alpha1 "github.com/kubev2v/migration-planner/api/v1alpha1/agent" + "github.com/kubev2v/migration-planner/internal/agent" + "github.com/kubev2v/migration-planner/internal/agent/client" + "github.com/kubev2v/migration-planner/pkg/log" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Inventory", func() { + var ( + agentID uuid.UUID + sourceID uuid.UUID + ) + BeforeEach(func() { + agentID, _ = uuid.NewUUID() + sourceID, _ = uuid.NewUUID() + }) + + Context("update inventory", func() { + It("successfully updates inventory", func() { + client := client.PlannerMock{ + UpdateSourceStatusFunc: func(ctx context.Context, id uuid.UUID, params v1alpha1.SourceStatusUpdate) error { + Expect(id).To(Equal(sourceID)) + Expect(params.AgentId).To(Equal(agentID)) + Expect(params.Status).To(Equal("up-to-date")) + Expect(params.StatusInfo).To(Equal("status_info")) + Expect(params.Inventory).ToNot(BeNil()) + return nil + + }, + } + + inventory := &api.Inventory{ + Vms: api.VMs{Total: 2}, + } + inventoryUpdater := agent.NewInventoryUpdater(log.NewPrefixLogger(""), agentID, &client) + inventoryUpdater.UpdateServiceWithInventory(context.TODO(), api.SourceStatusUpToDate, "status_info", inventory) + }) + }) +}) diff --git a/internal/agent/rest.go b/internal/agent/rest.go index bee4dc5..6659b7f 100644 --- a/internal/agent/rest.go +++ b/internal/agent/rest.go @@ -26,12 +26,13 @@ const ( CredentialsFile = "credentials.json" ) -func RegisterApi(router *chi.Mux, log *log.PrefixLogger, dataDir string) { +func RegisterApi(router *chi.Mux, log *log.PrefixLogger, statusUpdater *StatusUpdater, dataDir string) { router.Get("/api/v1/version", func(w http.ResponseWriter, r *http.Request) { _ = render.Render(w, r, VersionReply{Version: version}) }) router.Get("/api/v1/status", func(w http.ResponseWriter, r *http.Request) { - statusHandler(dataDir, w, r) + status, statusInfo, _ := statusUpdater.CalculateStatus() + _ = render.Render(w, r, StatusReply{Status: string(status), StatusInfo: statusInfo}) }) router.Put("/api/v1/credentials", func(w http.ResponseWriter, r *http.Request) { credentialHandler(log, dataDir, w, r) @@ -55,11 +56,6 @@ func (v VersionReply) Render(w http.ResponseWriter, r *http.Request) error { return nil } -func statusHandler(dataDir string, w http.ResponseWriter, r *http.Request) { - status, statusInfo, _ := calculateStatus(dataDir) - _ = render.Render(w, r, StatusReply{Status: string(status), StatusInfo: statusInfo}) -} - type Credentials struct { URL string `json:"url"` Username string `json:"username"` diff --git a/internal/agent/server.go b/internal/agent/server.go index 5401ebe..e9fd52c 100644 --- a/internal/agent/server.go +++ b/internal/agent/server.go @@ -33,13 +33,13 @@ func NewServer(port int, dataFolder, wwwFolder string) *Server { } } -func (s *Server) Start(log *log.PrefixLogger) { +func (s *Server) Start(log *log.PrefixLogger, statusUpdater *StatusUpdater) { router := chi.NewRouter() router.Use(middleware.RequestID) router.Use(middleware.Logger) RegisterFileServer(router, log, s.wwwFolder) - RegisterApi(router, log, s.dataFolder) + RegisterApi(router, log, statusUpdater, s.dataFolder) s.restServer = &http.Server{Addr: fmt.Sprintf("0.0.0.0:%d", s.port), Handler: router} diff --git a/internal/agent/status.go b/internal/agent/status.go new file mode 100644 index 0000000..5a2fa25 --- /dev/null +++ b/internal/agent/status.go @@ -0,0 +1,83 @@ +package agent + +import ( + "context" + "encoding/json" + "fmt" + "path/filepath" + "time" + + "github.com/google/uuid" + api "github.com/kubev2v/migration-planner/api/v1alpha1" + agentapi "github.com/kubev2v/migration-planner/api/v1alpha1/agent" + "github.com/kubev2v/migration-planner/internal/agent/client" + "github.com/kubev2v/migration-planner/internal/agent/fileio" + "github.com/kubev2v/migration-planner/pkg/log" +) + +const ( + defaultUpdateStatusTimeout = 5 * time.Second +) + +type StatusUpdater struct { + agentID uuid.UUID + log *log.PrefixLogger + version string + config *Config + client client.Planner + credUrl string +} + +func NewStatusUpdater(log *log.PrefixLogger, agentID uuid.UUID, version, credUrl string, config *Config, client client.Planner) *StatusUpdater { + return &StatusUpdater{ + log: log, + client: client, + config: config, + agentID: agentID, + credUrl: credUrl, + version: version, + } +} + +func (s *StatusUpdater) UpdateStatus(ctx context.Context, status api.AgentStatus, statusInfo string) error { + ctx, cancel := context.WithTimeout(ctx, defaultUpdateStatusTimeout*time.Second) + defer cancel() + + bodyParameters := agentapi.AgentStatusUpdate{ + Id: s.agentID.String(), + Status: string(status), + StatusInfo: statusInfo, + CredentialUrl: s.credUrl, + Version: s.version, + } + + return s.client.UpdateAgentStatus(ctx, s.agentID, bodyParameters) +} + +func (s *StatusUpdater) CalculateStatus() (api.AgentStatus, string, *api.Inventory) { + inventoryFilePath := filepath.Join(s.config.DataDir, InventoryFile) + credentialsFilePath := filepath.Join(s.config.DataDir, CredentialsFile) + reader := fileio.NewReader() + + err := reader.CheckPathExists(credentialsFilePath) + if err != nil { + return api.AgentStatusWaitingForCredentials, "No credentials provided", nil + } + err = reader.CheckPathExists(inventoryFilePath) + if err != nil { + return api.AgentStatusGatheringInitialInventory, "Inventory not yet collected", nil + } + inventoryData, err := reader.ReadFile(inventoryFilePath) + if err != nil { + return api.AgentStatusError, fmt.Sprintf("Failed reading inventory file: %v", err), nil + } + var inventory InventoryData + err = json.Unmarshal(inventoryData, &inventory) + if err != nil { + return api.AgentStatusError, fmt.Sprintf("Invalid inventory file: %v", err), nil + } + if len(inventory.Error) > 0 { + return api.AgentStatusError, inventory.Error, &inventory.Inventory + } + return api.AgentStatusUpToDate, "Inventory successfully collected", &inventory.Inventory +} diff --git a/internal/agent/status_test.go b/internal/agent/status_test.go new file mode 100644 index 0000000..78b282c --- /dev/null +++ b/internal/agent/status_test.go @@ -0,0 +1,131 @@ +package agent_test + +import ( + "context" + "os" + "path" + + "github.com/google/uuid" + api "github.com/kubev2v/migration-planner/api/v1alpha1" + v1alpha1 "github.com/kubev2v/migration-planner/api/v1alpha1/agent" + "github.com/kubev2v/migration-planner/internal/agent" + "github.com/kubev2v/migration-planner/internal/agent/client" + "github.com/kubev2v/migration-planner/pkg/log" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +const ( + noCredentialsProvidedStatusInfo = "No credentials provided" + inventoryNotYetCollectedStatusInfo = "Inventory not yet collected" + inventoryCollectedStatusInfo = "Inventory successfully collected" +) + +var _ = Describe("Status", func() { + var agentID uuid.UUID + BeforeEach(func() { + agentID, _ = uuid.NewUUID() + }) + + Context("update status", func() { + It("successfully updates status", func() { + client := client.PlannerMock{ + UpdateAgentStatusFunc: func(ctx context.Context, id uuid.UUID, params v1alpha1.AgentStatusUpdate) error { + Expect(id).To(Equal(agentID)) + Expect(params.Version).To(Equal("best_version")) + Expect(params.Status).To(Equal("up-to-date")) + Expect(params.CredentialUrl).To(Equal("www-cred-url")) + Expect(params.StatusInfo).To(Equal("status_info")) + return nil + }, + } + + statusUpdater := agent.NewStatusUpdater(log.NewPrefixLogger(""), agentID, "best_version", "www-cred-url", &agent.Config{}, &client) + Expect(statusUpdater.UpdateStatus(context.TODO(), api.AgentStatusUpToDate, "status_info")) + }) + }) + + Context("compute status", func() { + var ( + dataTmpFolder string + plannerClient *client.PlannerMock + ) + BeforeEach(func() { + var err error + dataTmpFolder, err = os.MkdirTemp("", "agent-data-folder") + Expect(err).To(BeNil()) + plannerClient = &client.PlannerMock{} + }) + AfterEach(func() { + os.RemoveAll(dataTmpFolder) + }) + + It("compute status returns Waiting for credentials", func() { + statusUpdater := agent.NewStatusUpdater(log.NewPrefixLogger(""), + agentID, + "best_version", + "www-cred-url", + &agent.Config{ + DataDir: dataTmpFolder, + }, + plannerClient, + ) + + status, status_info, inventory := statusUpdater.CalculateStatus() + Expect(status).To(Equal(api.AgentStatusWaitingForCredentials)) + Expect(status_info).To(Equal(noCredentialsProvidedStatusInfo)) + Expect(inventory).To(BeNil()) + }) + + It("compute status returns GatheringInitialInventory", func() { + // create credentials.json + creds, err := os.Create(path.Join(dataTmpFolder, "credentials.json")) + Expect(err).To(BeNil()) + creds.Close() + + statusUpdater := agent.NewStatusUpdater(log.NewPrefixLogger(""), + agentID, + "best_version", + "www-cred-url", + &agent.Config{ + DataDir: dataTmpFolder, + }, + plannerClient, + ) + + status, status_info, inventory := statusUpdater.CalculateStatus() + Expect(status).To(Equal(api.AgentStatusGatheringInitialInventory)) + Expect(status_info).To(Equal(inventoryNotYetCollectedStatusInfo)) + Expect(inventory).To(BeNil()) + }) + + It("compute status returns InventoryUptoDate", func() { + // create credentials.json + creds, err := os.Create(path.Join(dataTmpFolder, "credentials.json")) + Expect(err).To(BeNil()) + creds.Close() + + inventoryFile, err := os.Create(path.Join(dataTmpFolder, "inventory.json")) + Expect(err).To(BeNil()) + + _, err = inventoryFile.Write([]byte("{\"inventory\": {}, \"error\": \"\"}")) + Expect(err).To(BeNil()) + + statusUpdater := agent.NewStatusUpdater(log.NewPrefixLogger(""), + agentID, + "best_version", + "www-cred-url", + &agent.Config{ + DataDir: dataTmpFolder, + }, + plannerClient, + ) + + status, status_info, inventory := statusUpdater.CalculateStatus() + Expect(status).To(Equal(api.AgentStatusUpToDate)) + Expect(status_info).To(Equal(inventoryCollectedStatusInfo)) + Expect(inventory).ToNot(BeNil()) + }) + }) + +}) diff --git a/internal/cli/create.go b/internal/cli/create.go deleted file mode 100644 index f126ac6..0000000 --- a/internal/cli/create.go +++ /dev/null @@ -1,85 +0,0 @@ -package cli - -import ( - "context" - "fmt" - "net/http" - - api "github.com/kubev2v/migration-planner/api/v1alpha1" - "github.com/kubev2v/migration-planner/internal/client" - "github.com/spf13/cobra" - "github.com/spf13/pflag" -) - -type CreateOptions struct { - GlobalOptions -} - -func DefaultCreateOptions() *CreateOptions { - return &CreateOptions{ - GlobalOptions: DefaultGlobalOptions(), - } -} - -func NewCmdCreate() *cobra.Command { - o := DefaultCreateOptions() - cmd := &cobra.Command{ - Use: "create TYPE NAME", - Short: "Create a resource.", - Args: cobra.ExactArgs(2), - RunE: func(cmd *cobra.Command, args []string) error { - if err := o.Complete(cmd, args); err != nil { - return err - } - if err := o.Validate(args); err != nil { - return err - } - return o.Run(cmd.Context(), args) - }, - SilenceUsage: true, - } - o.Bind(cmd.Flags()) - return cmd -} - -func (o *CreateOptions) Bind(fs *pflag.FlagSet) { - o.GlobalOptions.Bind(fs) -} - -func (o *CreateOptions) Complete(cmd *cobra.Command, args []string) error { - if err := o.GlobalOptions.Complete(cmd, args); err != nil { - return err - } - - return nil -} - -func (o *CreateOptions) Validate(args []string) error { - if err := o.GlobalOptions.Validate(args); err != nil { - return err - } - - _, _, err := parseAndValidateKindId(args[0]) - if err != nil { - return err - } - - return nil -} - -func (o *CreateOptions) Run(ctx context.Context, args []string) error { - c, err := client.NewFromConfigFile(o.ConfigFilePath) - if err != nil { - return fmt.Errorf("creating client: %w", err) - } - - body := api.SourceCreate{Name: args[1]} - response, err := c.CreateSource(ctx, body) - if err != nil { - return fmt.Errorf("creating source: %w, http response: %+v", err, response) - } - if response.StatusCode != http.StatusCreated { - return fmt.Errorf("creating source: %+v", response) - } - return nil -}