From e7827b49ad2b7e95e4167d8a7c3c42e4d37152c3 Mon Sep 17 00:00:00 2001 From: Cosmin Tupangiu Date: Tue, 15 Oct 2024 15:51:55 +0200 Subject: [PATCH] Add agent data model and endpoint definition for multi sources This commit adds the data model for agent resource. The source model has been modified by adding a many2one relationship to the agent. A new endpoint `/api/v1/agents` has been added but not implemented. --- api/v1alpha1/agent/openapi.yaml | 52 ++++ api/v1alpha1/agent/spec.gen.go | 40 +-- api/v1alpha1/agent/types.gen.go | 11 + api/v1alpha1/common.go | 17 ++ api/v1alpha1/openapi.yaml | 52 ++++ api/v1alpha1/spec.gen.go | 56 ++-- api/v1alpha1/types.gen.go | 27 ++ internal/api/client/agent/client.gen.go | 154 ++++++++++ internal/api/client/client.gen.go | 110 +++++++ internal/api/server/agent/server.gen.go | 116 +++++++ internal/api/server/server.gen.go | 78 +++++ internal/service/agent.go | 11 + internal/service/agent/handler.go | 4 + internal/store/agent.go | 206 +++++++++++++ internal/store/agent_test.go | 384 ++++++++++++++++++++++++ internal/store/model/agent.go | 64 ++++ internal/store/model/source.go | 1 + internal/store/store.go | 10 + internal/store/store_suite_test.go | 13 + 19 files changed, 1360 insertions(+), 46 deletions(-) create mode 100644 internal/service/agent.go create mode 100644 internal/store/agent.go create mode 100644 internal/store/agent_test.go create mode 100644 internal/store/model/agent.go create mode 100644 internal/store/store_suite_test.go diff --git a/api/v1alpha1/agent/openapi.yaml b/api/v1alpha1/agent/openapi.yaml index a60a62e..0fe0b04 100644 --- a/api/v1alpha1/agent/openapi.yaml +++ b/api/v1alpha1/agent/openapi.yaml @@ -53,6 +53,42 @@ paths: application/json: schema: $ref: '../openapi.yaml#/components/schemas/Error' + /api/v1/agents/{id}: + put: + tags: + - agent + description: update status of the agent + operationId: updateAgentStatus + parameters: + - name: id + in: path + description: ID the agent + required: true + schema: + type: string + format: uuid + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/AgentStatusUpdate' + responses: + "200": + description: OK + "201": + description: OK + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: '../openapi.yaml#/components/schemas/Error' + "410": + description: Gone + content: + application/json: + schema: + $ref: '../openapi.yaml#/components/schemas/Error' /health: get: tags: @@ -80,3 +116,19 @@ components: - status - statusInfo - credentialUrl + AgentStatusUpdate: + type: object + properties: + id: + type: string + status: + type: string + statusInfo: + type: string + credentialUrl: + type: string + required: + - status + - statusInfo + - credentialUrl + - id diff --git a/api/v1alpha1/agent/spec.gen.go b/api/v1alpha1/agent/spec.gen.go index 1bcf98f..fed2e61 100644 --- a/api/v1alpha1/agent/spec.gen.go +++ b/api/v1alpha1/agent/spec.gen.go @@ -19,25 +19,27 @@ import ( // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", + "rP8NAAD//9nJhDNhFQAA", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/api/v1alpha1/agent/types.gen.go b/api/v1alpha1/agent/types.gen.go index 7645860..461a972 100644 --- a/api/v1alpha1/agent/types.gen.go +++ b/api/v1alpha1/agent/types.gen.go @@ -7,6 +7,14 @@ import ( externalRef0 "github.com/kubev2v/migration-planner/api/v1alpha1" ) +// AgentStatusUpdate defines model for AgentStatusUpdate. +type AgentStatusUpdate struct { + CredentialUrl string `json:"credentialUrl"` + Id string `json:"id"` + Status string `json:"status"` + StatusInfo string `json:"statusInfo"` +} + // SourceStatusUpdate defines model for SourceStatusUpdate. type SourceStatusUpdate struct { CredentialUrl string `json:"credentialUrl"` @@ -15,5 +23,8 @@ type SourceStatusUpdate struct { StatusInfo string `json:"statusInfo"` } +// UpdateAgentStatusJSONRequestBody defines body for UpdateAgentStatus for application/json ContentType. +type UpdateAgentStatusJSONRequestBody = AgentStatusUpdate + // ReplaceSourceStatusJSONRequestBody defines body for ReplaceSourceStatus for application/json ContentType. type ReplaceSourceStatusJSONRequestBody = SourceStatusUpdate diff --git a/api/v1alpha1/common.go b/api/v1alpha1/common.go index f948b9b..cd7f0f1 100644 --- a/api/v1alpha1/common.go +++ b/api/v1alpha1/common.go @@ -16,3 +16,20 @@ func StringToSourceStatus(s string) SourceStatus { return SourceStatusNotConnected } } + +func StringToAgentStatus(s string) AgentStatus { + switch s { + case string(AgentStatusError): + return AgentStatusError + case string(AgentStatusGatheringInitialInventory): + return AgentStatusGatheringInitialInventory + case string(AgentStatusUpToDate): + return AgentStatusUpToDate + case string(AgentStatusWaitingForCredentials): + return AgentStatusWaitingForCredentials + case string(AgentStatusNotConnected): + return AgentStatusNotConnected + default: + return AgentStatusNotConnected + } +} diff --git a/api/v1alpha1/openapi.yaml b/api/v1alpha1/openapi.yaml index f8e9761..41d0789 100644 --- a/api/v1alpha1/openapi.yaml +++ b/api/v1alpha1/openapi.yaml @@ -199,6 +199,25 @@ paths: application/json: schema: $ref: '#/components/schemas/Error' + /api/v1/agents: + get: + tags: + - agent + description: list agents + operationId: listAgents + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/AgentList" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' /health: get: tags: @@ -436,3 +455,36 @@ components: type: string count: type: integer + + Agent: + type: object + properties: + id: + type: string + status: + type: string + enum: [not-connected, waiting-for-credentials, error, gathering-initial-inventory, up-to-date, source-gone] + statusInfo: + type: string + credentialUrl: + type: string + sourceId: + type: string + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + required: + - id + - status + - statusInfo + - credentialUrl + - createdAt + - updatedAt + + AgentList: + type: array + items: + $ref: '#/components/schemas/Agent' diff --git a/api/v1alpha1/spec.gen.go b/api/v1alpha1/spec.gen.go index 9a1bc6c..d76cf42 100644 --- a/api/v1alpha1/spec.gen.go +++ b/api/v1alpha1/spec.gen.go @@ -18,33 +18,35 @@ import ( // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", + "AA==", } // 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 bd30601..befdb22 100644 --- a/api/v1alpha1/types.gen.go +++ b/api/v1alpha1/types.gen.go @@ -9,6 +9,16 @@ import ( openapi_types "github.com/oapi-codegen/runtime/types" ) +// Defines values for AgentStatus. +const ( + AgentStatusError AgentStatus = "error" + AgentStatusGatheringInitialInventory AgentStatus = "gathering-initial-inventory" + AgentStatusNotConnected AgentStatus = "not-connected" + AgentStatusSourceGone AgentStatus = "source-gone" + AgentStatusUpToDate AgentStatus = "up-to-date" + AgentStatusWaitingForCredentials AgentStatus = "waiting-for-credentials" +) + // Defines values for InfraNetworksType. const ( Distributed InfraNetworksType = "distributed" @@ -24,6 +34,23 @@ const ( SourceStatusWaitingForCredentials SourceStatus = "waiting-for-credentials" ) +// Agent defines model for Agent. +type Agent struct { + CreatedAt time.Time `json:"createdAt"` + CredentialUrl string `json:"credentialUrl"` + Id string `json:"id"` + SourceId *string `json:"sourceId,omitempty"` + Status AgentStatus `json:"status"` + StatusInfo string `json:"statusInfo"` + UpdatedAt time.Time `json:"updatedAt"` +} + +// AgentStatus defines model for Agent.Status. +type AgentStatus string + +// AgentList defines model for AgentList. +type AgentList = []Agent + // Error defines model for Error. type Error struct { // Message Error message diff --git a/internal/api/client/agent/client.gen.go b/internal/api/client/agent/client.gen.go index ce72f26..65ec38a 100644 --- a/internal/api/client/agent/client.gen.go +++ b/internal/api/client/agent/client.gen.go @@ -92,6 +92,11 @@ func WithRequestEditorFn(fn RequestEditorFn) ClientOption { // The interface specification for the client above. type ClientInterface interface { + // UpdateAgentStatusWithBody request with any body + UpdateAgentStatusWithBody(ctx context.Context, id openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + UpdateAgentStatus(ctx context.Context, id openapi_types.UUID, body UpdateAgentStatusJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // ReplaceSourceStatusWithBody request with any body ReplaceSourceStatusWithBody(ctx context.Context, id openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -101,6 +106,30 @@ type ClientInterface interface { Health(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) } +func (c *Client) UpdateAgentStatusWithBody(ctx context.Context, id openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewUpdateAgentStatusRequestWithBody(c.Server, id, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) UpdateAgentStatus(ctx context.Context, id openapi_types.UUID, body UpdateAgentStatusJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewUpdateAgentStatusRequest(c.Server, id, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) ReplaceSourceStatusWithBody(ctx context.Context, id openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewReplaceSourceStatusRequestWithBody(c.Server, id, contentType, body) if err != nil { @@ -137,6 +166,53 @@ func (c *Client) Health(ctx context.Context, reqEditors ...RequestEditorFn) (*ht return c.Client.Do(req) } +// NewUpdateAgentStatusRequest calls the generic UpdateAgentStatus builder with application/json body +func NewUpdateAgentStatusRequest(server string, id openapi_types.UUID, body UpdateAgentStatusJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewUpdateAgentStatusRequestWithBody(server, id, "application/json", bodyReader) +} + +// NewUpdateAgentStatusRequestWithBody generates requests for UpdateAgentStatus with any type of body +func NewUpdateAgentStatusRequestWithBody(server string, id openapi_types.UUID, contentType string, body io.Reader) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "id", runtime.ParamLocationPath, id) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/api/v1/agents/%s", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("PUT", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + // NewReplaceSourceStatusRequest calls the generic ReplaceSourceStatus builder with application/json body func NewReplaceSourceStatusRequest(server string, id openapi_types.UUID, body ReplaceSourceStatusJSONRequestBody) (*http.Request, error) { var bodyReader io.Reader @@ -254,6 +330,11 @@ func WithBaseURL(baseURL string) ClientOption { // ClientWithResponsesInterface is the interface specification for the client with responses above. type ClientWithResponsesInterface interface { + // UpdateAgentStatusWithBodyWithResponse request with any body + UpdateAgentStatusWithBodyWithResponse(ctx context.Context, id openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*UpdateAgentStatusResponse, error) + + UpdateAgentStatusWithResponse(ctx context.Context, id openapi_types.UUID, body UpdateAgentStatusJSONRequestBody, reqEditors ...RequestEditorFn) (*UpdateAgentStatusResponse, error) + // ReplaceSourceStatusWithBodyWithResponse request with any body ReplaceSourceStatusWithBodyWithResponse(ctx context.Context, id openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*ReplaceSourceStatusResponse, error) @@ -263,6 +344,29 @@ type ClientWithResponsesInterface interface { HealthWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*HealthResponse, error) } +type UpdateAgentStatusResponse struct { + Body []byte + HTTPResponse *http.Response + JSON401 *externalRef0.Error + JSON410 *externalRef0.Error +} + +// Status returns HTTPResponse.Status +func (r UpdateAgentStatusResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r UpdateAgentStatusResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type ReplaceSourceStatusResponse struct { Body []byte HTTPResponse *http.Response @@ -309,6 +413,23 @@ func (r HealthResponse) StatusCode() int { return 0 } +// UpdateAgentStatusWithBodyWithResponse request with arbitrary body returning *UpdateAgentStatusResponse +func (c *ClientWithResponses) UpdateAgentStatusWithBodyWithResponse(ctx context.Context, id openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*UpdateAgentStatusResponse, error) { + rsp, err := c.UpdateAgentStatusWithBody(ctx, id, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseUpdateAgentStatusResponse(rsp) +} + +func (c *ClientWithResponses) UpdateAgentStatusWithResponse(ctx context.Context, id openapi_types.UUID, body UpdateAgentStatusJSONRequestBody, reqEditors ...RequestEditorFn) (*UpdateAgentStatusResponse, error) { + rsp, err := c.UpdateAgentStatus(ctx, id, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseUpdateAgentStatusResponse(rsp) +} + // ReplaceSourceStatusWithBodyWithResponse request with arbitrary body returning *ReplaceSourceStatusResponse func (c *ClientWithResponses) ReplaceSourceStatusWithBodyWithResponse(ctx context.Context, id openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*ReplaceSourceStatusResponse, error) { rsp, err := c.ReplaceSourceStatusWithBody(ctx, id, contentType, body, reqEditors...) @@ -335,6 +456,39 @@ func (c *ClientWithResponses) HealthWithResponse(ctx context.Context, reqEditors return ParseHealthResponse(rsp) } +// ParseUpdateAgentStatusResponse parses an HTTP response from a UpdateAgentStatusWithResponse call +func ParseUpdateAgentStatusResponse(rsp *http.Response) (*UpdateAgentStatusResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &UpdateAgentStatusResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401: + var dest externalRef0.Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON401 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 410: + var dest externalRef0.Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON410 = &dest + + } + + return response, nil +} + // ParseReplaceSourceStatusResponse parses an HTTP response from a ReplaceSourceStatusWithResponse call func ParseReplaceSourceStatusResponse(rsp *http.Response) (*ReplaceSourceStatusResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) diff --git a/internal/api/client/client.gen.go b/internal/api/client/client.gen.go index 907371a..47bb3dd 100644 --- a/internal/api/client/client.gen.go +++ b/internal/api/client/client.gen.go @@ -91,6 +91,9 @@ func WithRequestEditorFn(fn RequestEditorFn) ClientOption { // The interface specification for the client above. type ClientInterface interface { + // ListAgents request + ListAgents(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) + // DeleteSources request DeleteSources(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -115,6 +118,18 @@ type ClientInterface interface { Health(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) } +func (c *Client) ListAgents(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewListAgentsRequest(c.Server) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) DeleteSources(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewDeleteSourcesRequest(c.Server) if err != nil { @@ -211,6 +226,33 @@ func (c *Client) Health(ctx context.Context, reqEditors ...RequestEditorFn) (*ht return c.Client.Do(req) } +// NewListAgentsRequest generates requests for ListAgents +func NewListAgentsRequest(server string) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/api/v1/agents") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + // NewDeleteSourcesRequest generates requests for DeleteSources func NewDeleteSourcesRequest(server string) (*http.Request, error) { var err error @@ -477,6 +519,9 @@ func WithBaseURL(baseURL string) ClientOption { // ClientWithResponsesInterface is the interface specification for the client with responses above. type ClientWithResponsesInterface interface { + // ListAgentsWithResponse request + ListAgentsWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*ListAgentsResponse, error) + // DeleteSourcesWithResponse request DeleteSourcesWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*DeleteSourcesResponse, error) @@ -501,6 +546,29 @@ type ClientWithResponsesInterface interface { HealthWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*HealthResponse, error) } +type ListAgentsResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *AgentList + JSON401 *Error +} + +// Status returns HTTPResponse.Status +func (r ListAgentsResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r ListAgentsResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type DeleteSourcesResponse struct { Body []byte HTTPResponse *http.Response @@ -667,6 +735,15 @@ func (r HealthResponse) StatusCode() int { return 0 } +// ListAgentsWithResponse request returning *ListAgentsResponse +func (c *ClientWithResponses) ListAgentsWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*ListAgentsResponse, error) { + rsp, err := c.ListAgents(ctx, reqEditors...) + if err != nil { + return nil, err + } + return ParseListAgentsResponse(rsp) +} + // DeleteSourcesWithResponse request returning *DeleteSourcesResponse func (c *ClientWithResponses) DeleteSourcesWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*DeleteSourcesResponse, error) { rsp, err := c.DeleteSources(ctx, reqEditors...) @@ -738,6 +815,39 @@ func (c *ClientWithResponses) HealthWithResponse(ctx context.Context, reqEditors return ParseHealthResponse(rsp) } +// ParseListAgentsResponse parses an HTTP response from a ListAgentsWithResponse call +func ParseListAgentsResponse(rsp *http.Response) (*ListAgentsResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &ListAgentsResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest AgentList + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON401 = &dest + + } + + return response, nil +} + // ParseDeleteSourcesResponse parses an HTTP response from a DeleteSourcesWithResponse call func ParseDeleteSourcesResponse(rsp *http.Response) (*DeleteSourcesResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) diff --git a/internal/api/server/agent/server.gen.go b/internal/api/server/agent/server.gen.go index 0e0d4e6..8497fed 100644 --- a/internal/api/server/agent/server.gen.go +++ b/internal/api/server/agent/server.gen.go @@ -20,6 +20,9 @@ import ( // ServerInterface represents all server handlers. type ServerInterface interface { + // (PUT /api/v1/agents/{id}) + UpdateAgentStatus(w http.ResponseWriter, r *http.Request, id openapi_types.UUID) + // (PUT /api/v1/sources/{id}/status) ReplaceSourceStatus(w http.ResponseWriter, r *http.Request, id openapi_types.UUID) @@ -31,6 +34,11 @@ type ServerInterface interface { type Unimplemented struct{} +// (PUT /api/v1/agents/{id}) +func (_ Unimplemented) UpdateAgentStatus(w http.ResponseWriter, r *http.Request, id openapi_types.UUID) { + w.WriteHeader(http.StatusNotImplemented) +} + // (PUT /api/v1/sources/{id}/status) func (_ Unimplemented) ReplaceSourceStatus(w http.ResponseWriter, r *http.Request, id openapi_types.UUID) { w.WriteHeader(http.StatusNotImplemented) @@ -50,6 +58,32 @@ type ServerInterfaceWrapper struct { type MiddlewareFunc func(http.Handler) http.Handler +// UpdateAgentStatus operation middleware +func (siw *ServerInterfaceWrapper) UpdateAgentStatus(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + var err error + + // ------------- Path parameter "id" ------------- + var id openapi_types.UUID + + err = runtime.BindStyledParameterWithOptions("simple", "id", chi.URLParam(r, "id"), &id, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "id", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.UpdateAgentStatus(w, r, id) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r.WithContext(ctx)) +} + // ReplaceSourceStatus operation middleware func (siw *ServerInterfaceWrapper) ReplaceSourceStatus(w http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -204,6 +238,9 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl ErrorHandlerFunc: options.ErrorHandlerFunc, } + r.Group(func(r chi.Router) { + r.Put(options.BaseURL+"/api/v1/agents/{id}", wrapper.UpdateAgentStatus) + }) r.Group(func(r chi.Router) { r.Put(options.BaseURL+"/api/v1/sources/{id}/status", wrapper.ReplaceSourceStatus) }) @@ -214,6 +251,49 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl return r } +type UpdateAgentStatusRequestObject struct { + Id openapi_types.UUID `json:"id"` + Body *UpdateAgentStatusJSONRequestBody +} + +type UpdateAgentStatusResponseObject interface { + VisitUpdateAgentStatusResponse(w http.ResponseWriter) error +} + +type UpdateAgentStatus200Response struct { +} + +func (response UpdateAgentStatus200Response) VisitUpdateAgentStatusResponse(w http.ResponseWriter) error { + w.WriteHeader(200) + return nil +} + +type UpdateAgentStatus201Response struct { +} + +func (response UpdateAgentStatus201Response) VisitUpdateAgentStatusResponse(w http.ResponseWriter) error { + w.WriteHeader(201) + return nil +} + +type UpdateAgentStatus401JSONResponse externalRef0.Error + +func (response UpdateAgentStatus401JSONResponse) VisitUpdateAgentStatusResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + + return json.NewEncoder(w).Encode(response) +} + +type UpdateAgentStatus410JSONResponse externalRef0.Error + +func (response UpdateAgentStatus410JSONResponse) VisitUpdateAgentStatusResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(410) + + return json.NewEncoder(w).Encode(response) +} + type ReplaceSourceStatusRequestObject struct { Id openapi_types.UUID `json:"id"` Body *ReplaceSourceStatusJSONRequestBody @@ -277,6 +357,9 @@ func (response Health200Response) VisitHealthResponse(w http.ResponseWriter) err // StrictServerInterface represents all server handlers. type StrictServerInterface interface { + // (PUT /api/v1/agents/{id}) + UpdateAgentStatus(ctx context.Context, request UpdateAgentStatusRequestObject) (UpdateAgentStatusResponseObject, error) + // (PUT /api/v1/sources/{id}/status) ReplaceSourceStatus(ctx context.Context, request ReplaceSourceStatusRequestObject) (ReplaceSourceStatusResponseObject, error) @@ -313,6 +396,39 @@ type strictHandler struct { options StrictHTTPServerOptions } +// UpdateAgentStatus operation middleware +func (sh *strictHandler) UpdateAgentStatus(w http.ResponseWriter, r *http.Request, id openapi_types.UUID) { + var request UpdateAgentStatusRequestObject + + request.Id = id + + var body UpdateAgentStatusJSONRequestBody + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + request.Body = &body + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.UpdateAgentStatus(ctx, request.(UpdateAgentStatusRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "UpdateAgentStatus") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(UpdateAgentStatusResponseObject); ok { + if err := validResponse.VisitUpdateAgentStatusResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + // ReplaceSourceStatus operation middleware func (sh *strictHandler) ReplaceSourceStatus(w http.ResponseWriter, r *http.Request, id openapi_types.UUID) { var request ReplaceSourceStatusRequestObject diff --git a/internal/api/server/server.gen.go b/internal/api/server/server.gen.go index 2eca94d..3ccdaaf 100644 --- a/internal/api/server/server.gen.go +++ b/internal/api/server/server.gen.go @@ -20,6 +20,9 @@ import ( // ServerInterface represents all server handlers. type ServerInterface interface { + // (GET /api/v1/agents) + ListAgents(w http.ResponseWriter, r *http.Request) + // (DELETE /api/v1/sources) DeleteSources(w http.ResponseWriter, r *http.Request) @@ -46,6 +49,11 @@ type ServerInterface interface { type Unimplemented struct{} +// (GET /api/v1/agents) +func (_ Unimplemented) ListAgents(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) +} + // (DELETE /api/v1/sources) func (_ Unimplemented) DeleteSources(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotImplemented) @@ -90,6 +98,21 @@ type ServerInterfaceWrapper struct { type MiddlewareFunc func(http.Handler) http.Handler +// ListAgents operation middleware +func (siw *ServerInterfaceWrapper) ListAgents(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.ListAgents(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r.WithContext(ctx)) +} + // DeleteSources operation middleware func (siw *ServerInterfaceWrapper) DeleteSources(w http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -341,6 +364,9 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl ErrorHandlerFunc: options.ErrorHandlerFunc, } + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/api/v1/agents", wrapper.ListAgents) + }) r.Group(func(r chi.Router) { r.Delete(options.BaseURL+"/api/v1/sources", wrapper.DeleteSources) }) @@ -366,6 +392,31 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl return r } +type ListAgentsRequestObject struct { +} + +type ListAgentsResponseObject interface { + VisitListAgentsResponse(w http.ResponseWriter) error +} + +type ListAgents200JSONResponse AgentList + +func (response ListAgents200JSONResponse) VisitListAgentsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type ListAgents401JSONResponse Error + +func (response ListAgents401JSONResponse) VisitListAgentsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + + return json.NewEncoder(w).Encode(response) +} + type DeleteSourcesRequestObject struct { } @@ -620,6 +671,9 @@ func (response Health200Response) VisitHealthResponse(w http.ResponseWriter) err // StrictServerInterface represents all server handlers. type StrictServerInterface interface { + // (GET /api/v1/agents) + ListAgents(ctx context.Context, request ListAgentsRequestObject) (ListAgentsResponseObject, error) + // (DELETE /api/v1/sources) DeleteSources(ctx context.Context, request DeleteSourcesRequestObject) (DeleteSourcesResponseObject, error) @@ -671,6 +725,30 @@ type strictHandler struct { options StrictHTTPServerOptions } +// ListAgents operation middleware +func (sh *strictHandler) ListAgents(w http.ResponseWriter, r *http.Request) { + var request ListAgentsRequestObject + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.ListAgents(ctx, request.(ListAgentsRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "ListAgents") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(ListAgentsResponseObject); ok { + if err := validResponse.VisitListAgentsResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + // DeleteSources operation middleware func (sh *strictHandler) DeleteSources(w http.ResponseWriter, r *http.Request) { var request DeleteSourcesRequestObject diff --git a/internal/service/agent.go b/internal/service/agent.go new file mode 100644 index 0000000..f79c29a --- /dev/null +++ b/internal/service/agent.go @@ -0,0 +1,11 @@ +package service + +import ( + "context" + + "github.com/kubev2v/migration-planner/internal/api/server" +) + +func (h *ServiceHandler) ListAgents(ctx context.Context, request server.ListAgentsRequestObject) (server.ListAgentsResponseObject, error) { + return nil, nil +} diff --git a/internal/service/agent/handler.go b/internal/service/agent/handler.go index 48a3478..a61d7a4 100644 --- a/internal/service/agent/handler.go +++ b/internal/service/agent/handler.go @@ -35,3 +35,7 @@ func (h *AgentServiceHandler) Health(ctx context.Context, request agentServer.He // NO-OP return nil, nil } + +func (h *AgentServiceHandler) UpdateAgentStatus(ctx context.Context, request agentServer.UpdateAgentStatusRequestObject) (agentServer.UpdateAgentStatusResponseObject, error) { + return nil, nil +} diff --git a/internal/store/agent.go b/internal/store/agent.go new file mode 100644 index 0000000..67906fd --- /dev/null +++ b/internal/store/agent.go @@ -0,0 +1,206 @@ +package store + +import ( + "context" + "errors" + + api "github.com/kubev2v/migration-planner/api/v1alpha1" + apiAgent "github.com/kubev2v/migration-planner/api/v1alpha1/agent" + "github.com/kubev2v/migration-planner/internal/store/model" + "github.com/sirupsen/logrus" + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +type SortOrder int + +const ( + Unsorted SortOrder = iota + SortByID + SortByUpdatedTime +) + +type Agent interface { + List(ctx context.Context, filter *AgentQueryFilter, opts *AgentQueryOptions) (*api.AgentList, error) + Get(ctx context.Context, id string) (*api.Agent, error) + Update(ctx context.Context, agentUpdate apiAgent.AgentStatusUpdate) (*api.Agent, error) + UpdateSourceID(ctx context.Context, agentID string, sourceID string) (*api.Agent, error) + Create(ctx context.Context, agentUpdate apiAgent.AgentStatusUpdate) (*api.Agent, error) + Delete(ctx context.Context, id string, softDeletion bool) error + InitialMigration() error +} + +type AgentStore struct { + db *gorm.DB + log logrus.FieldLogger +} + +func NewAgentSource(db *gorm.DB, log logrus.FieldLogger) Agent { + return &AgentStore{db: db, log: log} +} + +func (a *AgentStore) InitialMigration() error { + return a.db.AutoMigrate(&model.Agent{}) +} + +// List lists all the agents. +// +// If includeSoftDeleted is true, it lists the agents soft-deleted. +func (a *AgentStore) List(ctx context.Context, filter *AgentQueryFilter, opts *AgentQueryOptions) (*api.AgentList, error) { + var agents model.AgentList + tx := a.db + + if filter != nil { + for _, fn := range filter.QueryFn { + tx = fn(tx) + } + } + + if opts != nil { + for _, fn := range opts.QueryFn { + tx = fn(tx) + } + } + + if err := tx.Model(&agents).Find(&agents).Error; err != nil { + return nil, err + } + + apiAgentList := agents.ToApiResource() + return &apiAgentList, nil +} + +// Create creates an agent from api model. +func (a *AgentStore) Create(ctx context.Context, agentUpdate apiAgent.AgentStatusUpdate) (*api.Agent, error) { + agent := model.NewAgentFromApiResource(&agentUpdate) + + if err := a.db.WithContext(ctx).Create(agent).Error; err != nil { + return nil, err + } + + createdResource := agent.ToApiResource() + return &createdResource, nil +} + +// Update updates an agent from api model. +func (a *AgentStore) Update(ctx context.Context, agentUpdate apiAgent.AgentStatusUpdate) (*api.Agent, error) { + agent := model.NewAgentFromApiResource(&agentUpdate) + + if err := a.db.WithContext(ctx).First(&model.Agent{ID: agentUpdate.Id}).Error; err != nil { + return nil, err + } + + if tx := a.db.WithContext(ctx).Clauses(clause.Returning{}).Updates(&agent); tx.Error != nil { + return nil, tx.Error + } + + updatedAgent := agent.ToApiResource() + return &updatedAgent, nil +} + +// UpdateSourceID updates the sources id field of an agent. +// The source must exists. +func (a *AgentStore) UpdateSourceID(ctx context.Context, agentID string, sourceID string) (*api.Agent, error) { + agent := model.NewAgentFromID(agentID) + + if err := a.db.WithContext(ctx).First(agent).Error; err != nil { + return nil, err + } + + agent.SourceID = &sourceID + + if tx := a.db.WithContext(ctx).Clauses(clause.Returning{}).Updates(&agent); tx.Error != nil { + return nil, tx.Error + } + + updatedAgent := agent.ToApiResource() + return &updatedAgent, nil +} + +// Get returns an agent based on its id. +func (a *AgentStore) Get(ctx context.Context, id string) (*api.Agent, error) { + agent := model.NewAgentFromID(id) + + if err := a.db.WithContext(ctx).First(&agent).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + return nil, err + } + + agentSource := agent.ToApiResource() + return &agentSource, nil +} + +// Delete removes an agent. +// If softDeletion is true, the agent is soft-deleted. +func (a *AgentStore) Delete(ctx context.Context, id string, softDeletion bool) error { + agent := model.NewAgentFromID(id) + tx := a.db + if !softDeletion { + tx = tx.Unscoped() + } + result := tx.Delete(&agent) + if result.Error != nil && !errors.Is(result.Error, gorm.ErrRecordNotFound) { + a.log.Infof("ERROR: %v", result.Error) + return result.Error + } + return nil +} + +type BaseAgentQuerier struct { + QueryFn []func(tx *gorm.DB) *gorm.DB +} + +type AgentQueryFilter = BaseAgentQuerier + +func NewAgentQueryFilter() *AgentQueryFilter { + return &AgentQueryFilter{QueryFn: make([]func(tx *gorm.DB) *gorm.DB, 0)} +} + +func (qf *AgentQueryFilter) BySourceID(sourceID string) *AgentQueryFilter { + qf.QueryFn = append(qf.QueryFn, func(tx *gorm.DB) *gorm.DB { + return tx.Where("source_id = ?", sourceID) + }) + return qf +} + +func (qf *AgentQueryFilter) BySoftDeleted(isSoftDeleted bool) *AgentQueryFilter { + qf.QueryFn = append(qf.QueryFn, func(tx *gorm.DB) *gorm.DB { + if isSoftDeleted { + return tx.Unscoped().Where("deleted_at IS NOT NULL") + } + return tx + }) + return qf +} + +type AgentQueryOptions = BaseAgentQuerier + +func NewAgentQueryOptions() *AgentQueryOptions { + return &AgentQueryOptions{QueryFn: make([]func(tx *gorm.DB) *gorm.DB, 0)} +} + +func (o *AgentQueryOptions) WithIncludeSoftDeleted(includeSoftDeleted bool) *AgentQueryOptions { + o.QueryFn = append(o.QueryFn, func(tx *gorm.DB) *gorm.DB { + if includeSoftDeleted { + return tx.Unscoped() + } + return tx + }) + return o +} + +func (o *AgentQueryOptions) WithSortOrder(sort SortOrder) *AgentQueryOptions { + o.QueryFn = append(o.QueryFn, func(tx *gorm.DB) *gorm.DB { + switch sort { + case SortByID: + return tx.Order("id") + case SortByUpdatedTime: + return tx.Order("updated_at") + default: + return tx + } + }) + return o +} diff --git a/internal/store/agent_test.go b/internal/store/agent_test.go new file mode 100644 index 0000000..1084250 --- /dev/null +++ b/internal/store/agent_test.go @@ -0,0 +1,384 @@ +package store_test + +import ( + "context" + "fmt" + "time" + + apiAgent "github.com/kubev2v/migration-planner/api/v1alpha1/agent" + "github.com/kubev2v/migration-planner/internal/config" + "github.com/kubev2v/migration-planner/internal/store" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/sirupsen/logrus" + "gorm.io/gorm" +) + +const ( + insertAgentStm = "INSERT INTO agents (id, status, status_info, cred_url) VALUES ('%s', '%s', '%s', '%s');" + insertAgentWithSourceStm = "INSERT INTO agents (id, status, status_info, cred_url, source_id) VALUES ('%s', '%s', '%s', '%s', '%s');" + insertAgentWithUpdateAtStm = "INSERT INTO agents (id, status, status_info, cred_url, updated_at) VALUES ('%s', '%s', '%s', '%s', '%s');" + insertAgentWithDeletedAtStm = "INSERT INTO agents (id, status, status_info, cred_url, deleted_at) VALUES ('%s', '%s', '%s', '%s', '%s');" + insertSourceStm = "INSERT INTO sources (id, name) VALUES ('%s', '%s');" +) + +var _ = Describe("agent store", Ordered, func() { + var ( + s store.Store + gormdb *gorm.DB + ) + + BeforeAll(func() { + log := logrus.New() + db, err := store.InitDB(config.NewDefault(), log) + Expect(err).To(BeNil()) + + s = store.NewStore(db, log) + err = s.InitialMigration() + Expect(err).To(BeNil()) + gormdb = db + }) + + AfterAll(func() { + gormdb.Exec("DROP TABLE agents;") + gormdb.Exec("DROP TABLE sources;") + s.Close() + }) + + Context("list", func() { + It("successfuly list all the agents -- without filter and options", func() { + tx := gormdb.Exec(fmt.Sprintf(insertAgentStm, "agent-1", "not-connected", "status-info-1", "cred_url-1")) + Expect(tx.Error).To(BeNil()) + tx = gormdb.Exec(fmt.Sprintf(insertAgentStm, "agent-2", "not-connected", "status-info-2", "cred_url-2")) + Expect(tx.Error).To(BeNil()) + tx = gormdb.Exec(fmt.Sprintf(insertAgentStm, "agent-3", "not-connected", "status-info-3", "cred_url-3")) + Expect(tx.Error).To(BeNil()) + + agents, err := s.Agent().List(context.TODO(), store.NewAgentQueryFilter(), store.NewAgentQueryOptions()) + Expect(err).To(BeNil()) + Expect(*agents).To(HaveLen(3)) + + Expect((*agents)[0].Id).Should(BeElementOf("agent-1", "agent-2", "agent-3")) + Expect(string((*agents)[0].Status)).To(Equal("not-connected")) + Expect((*agents)[0].StatusInfo).Should(BeElementOf("status-info-1", "status-info-2", "status-info-3")) + Expect((*agents)[0].CredentialUrl).Should(BeElementOf("cred_url-1", "cred_url-2", "cred_url-3")) + Expect((*agents)[1].Id).Should(BeElementOf("agent-1", "agent-2", "agent-3")) + Expect((*agents)[2].Id).Should(BeElementOf("agent-1", "agent-2", "agent-3")) + }) + + It("list all the agents -- no agents to be found in the db", func() { + agents, err := s.Agent().List(context.TODO(), store.NewAgentQueryFilter(), store.NewAgentQueryOptions()) + Expect(err).To(BeNil()) + Expect(*agents).To(HaveLen(0)) + }) + + It("successfuly list all the agents -- with options order by id", func() { + tx := gormdb.Exec(fmt.Sprintf(insertAgentStm, "agent-1", "not-connected", "status-info-1", "cred_url-1")) + Expect(tx.Error).To(BeNil()) + tx = gormdb.Exec(fmt.Sprintf(insertAgentStm, "agent-2", "not-connected", "status-info-2", "cred_url-2")) + Expect(tx.Error).To(BeNil()) + tx = gormdb.Exec(fmt.Sprintf(insertAgentStm, "agent-3", "not-connected", "status-info-3", "cred_url-3")) + Expect(tx.Error).To(BeNil()) + + agents, err := s.Agent().List(context.TODO(), store.NewAgentQueryFilter(), store.NewAgentQueryOptions().WithSortOrder(store.SortByID)) + Expect(err).To(BeNil()) + Expect(*agents).To(HaveLen(3)) + + Expect((*agents)[0].Id).To(Equal("agent-1")) + }) + + It("successfuly list all the agents -- with options order by update_id", func() { + // 24h from now + tx := gormdb.Exec(fmt.Sprintf(insertAgentWithUpdateAtStm, "agent-1", "not-connected", "status-info-1", "cred_url-1", time.Now().Add(24*time.Hour).Format(time.RFC3339))) + Expect(tx.Error).To(BeNil()) + + // this one should be the first + tx = gormdb.Exec(fmt.Sprintf(insertAgentWithUpdateAtStm, "agent-2", "not-connected", "status-info-2", "cred_url-2", time.Now().Format(time.RFC3339))) + Expect(tx.Error).To(BeNil()) + + // 36h from now + tx = gormdb.Exec(fmt.Sprintf(insertAgentWithUpdateAtStm, "agent-3", "not-connected", "status-info-3", "cred_url-3", time.Now().Add(36*time.Hour).Format(time.RFC3339))) + Expect(tx.Error).To(BeNil()) + + agents, err := s.Agent().List(context.TODO(), store.NewAgentQueryFilter(), store.NewAgentQueryOptions().WithSortOrder(store.SortByUpdatedTime)) + Expect(err).To(BeNil()) + Expect(*agents).To(HaveLen(3)) + + Expect((*agents)[0].Id).To(Equal("agent-2")) + }) + + It("successfuly list the agents -- with filter by source-id", func() { + tx := gormdb.Exec(fmt.Sprintf(insertSourceStm, "source-1", "source-name-1")) + Expect(tx.Error).To(BeNil()) + tx = gormdb.Exec(fmt.Sprintf(insertSourceStm, "source-2", "source-name-1")) + Expect(tx.Error).To(BeNil()) + tx = gormdb.Exec(fmt.Sprintf(insertAgentWithSourceStm, "agent-1", "not-connected", "status-info-1", "cred_url-1", "source-1")) + Expect(tx.Error).To(BeNil()) + tx = gormdb.Exec(fmt.Sprintf(insertAgentWithSourceStm, "agent-2", "not-connected", "status-info-2", "cred_url-2", "source-2")) + Expect(tx.Error).To(BeNil()) + tx = gormdb.Exec(fmt.Sprintf(insertAgentStm, "agent-3", "not-connected", "status-info-3", "cred_url-3")) + Expect(tx.Error).To(BeNil()) + + agents, err := s.Agent().List(context.TODO(), store.NewAgentQueryFilter().BySourceID("source-1"), store.NewAgentQueryOptions()) + Expect(err).To(BeNil()) + Expect(*agents).To(HaveLen(1)) + + Expect((*agents)[0].Id).To(Equal("agent-1")) + }) + + It("successfuly list the agents -- with filter by soft deletion", func() { + tx := gormdb.Exec(fmt.Sprintf(insertAgentWithDeletedAtStm, "agent-1", "not-connected", "status-info-1", "cred_url-1", time.Now().Format(time.RFC3339))) + Expect(tx.Error).To(BeNil()) + tx = gormdb.Exec(fmt.Sprintf(insertAgentStm, "agent-2", "not-connected", "status-info-2", "cred_url-2")) + Expect(tx.Error).To(BeNil()) + tx = gormdb.Exec(fmt.Sprintf(insertAgentStm, "agent-3", "not-connected", "status-info-3", "cred_url-3")) + Expect(tx.Error).To(BeNil()) + + agents, err := s.Agent().List(context.TODO(), store.NewAgentQueryFilter().BySoftDeleted(true), store.NewAgentQueryOptions()) + Expect(err).To(BeNil()) + Expect(*agents).To(HaveLen(1)) + + Expect((*agents)[0].Id).To(Equal("agent-1")) + }) + + It("successfuly list the agents -- without filter by soft deletion", func() { + tx := gormdb.Exec(fmt.Sprintf(insertAgentWithDeletedAtStm, "agent-1", "not-connected", "status-info-1", "cred_url-1", time.Now().Format(time.RFC3339))) + Expect(tx.Error).To(BeNil()) + tx = gormdb.Exec(fmt.Sprintf(insertAgentStm, "agent-2", "not-connected", "status-info-2", "cred_url-2")) + Expect(tx.Error).To(BeNil()) + tx = gormdb.Exec(fmt.Sprintf(insertAgentStm, "agent-3", "not-connected", "status-info-3", "cred_url-3")) + Expect(tx.Error).To(BeNil()) + + agents, err := s.Agent().List(context.TODO(), store.NewAgentQueryFilter(), store.NewAgentQueryOptions()) + Expect(err).To(BeNil()) + Expect(*agents).To(HaveLen(2)) + }) + + AfterEach(func() { + gormdb.Exec("DELETE FROM agents;") + gormdb.Exec("DELETE FROM sources;") + }) + }) + + Context("create", func() { + It("successfuly creates an agent", func() { + agentData := apiAgent.AgentStatusUpdate{ + Id: "agent-1", + CredentialUrl: "creds-url-1", + Status: "waiting-for-credentials", + StatusInfo: "status-info-1", + } + + agent, err := s.Agent().Create(context.TODO(), agentData) + Expect(err).To(BeNil()) + + count := 0 + tx := gormdb.Raw("SELECT COUNT(*) from agents;").Scan(&count) + Expect(tx.Error).To(BeNil()) + Expect(count).To(Equal(1)) + + Expect(agent).ToNot(BeNil()) + Expect(agent.Id).To(Equal("agent-1")) + Expect(string(agent.Status)).To(Equal("waiting-for-credentials")) + }) + + AfterEach(func() { + gormdb.Exec("DELETE FROM agents;") + }) + }) + + Context("get", func() { + It("successfuly return nil when agent is not found", func() { + agent, err := s.Agent().Get(context.TODO(), "id") + Expect(err).To(BeNil()) + Expect(agent).To(BeNil()) + }) + + It("successfuly return the agent", func() { + tx := gormdb.Exec(fmt.Sprintf(insertAgentStm, "agent-1", "not-connected", "status-info-1", "cred_url-1")) + Expect(tx.Error).To(BeNil()) + + agent, err := s.Agent().Get(context.TODO(), "agent-1") + Expect(err).To(BeNil()) + + Expect(agent.Id).To(Equal("agent-1")) + Expect(string(agent.Status)).To(Equal("not-connected")) + Expect(agent.StatusInfo).To(Equal("status-info-1")) + Expect(agent.CredentialUrl).To(Equal("cred_url-1")) + }) + + It("successfuly return the agent connected to a source", func() { + tx := gormdb.Exec(fmt.Sprintf(insertSourceStm, "source-1", "source-name-1")) + Expect(tx.Error).To(BeNil()) + tx = gormdb.Exec(fmt.Sprintf(insertAgentWithSourceStm, "agent-1", "not-connected", "status-info-1", "cred_url-1", "source-1")) + Expect(tx.Error).To(BeNil()) + + agent, err := s.Agent().Get(context.TODO(), "agent-1") + Expect(err).To(BeNil()) + + Expect(agent.Id).To(Equal("agent-1")) + Expect(string(agent.Status)).To(Equal("not-connected")) + Expect(agent.StatusInfo).To(Equal("status-info-1")) + Expect(agent.CredentialUrl).To(Equal("cred_url-1")) + Expect(agent.SourceId).ToNot(BeNil()) + Expect(*agent.SourceId).To(Equal("source-1")) + }) + + AfterEach(func() { + gormdb.Exec("DELETE FROM agents;") + gormdb.Exec("DELETE FROM sources;") + }) + }) + + Context("update", func() { + It("successfuly updates an agent -- status field", func() { + tx := gormdb.Exec(fmt.Sprintf(insertAgentStm, "agent-1", "not-connected", "status-info-1", "cred_url-1")) + Expect(tx.Error).To(BeNil()) + + agentData := apiAgent.AgentStatusUpdate{ + Id: "agent-1", + CredentialUrl: "creds-url-1", + Status: "waiting-for-credentials", + StatusInfo: "status-info-1", + } + + agent, err := s.Agent().Update(context.TODO(), agentData) + Expect(err).To(BeNil()) + Expect(agent).NotTo(BeNil()) + Expect(string(agent.Status)).To(Equal("waiting-for-credentials")) + + rawStatus := "" + gormdb.Raw("select status from agents where id = 'agent-1';").Scan(&rawStatus) + Expect(rawStatus).To(Equal("waiting-for-credentials")) + }) + + It("successfuly updates an agent -- status-info field", func() { + tx := gormdb.Exec(fmt.Sprintf(insertAgentStm, "agent-1", "not-connected", "status-info-1", "cred_url-1")) + Expect(tx.Error).To(BeNil()) + + agentData := apiAgent.AgentStatusUpdate{ + Id: "agent-1", + CredentialUrl: "creds-url-1", + Status: "not-connected", + StatusInfo: "another-status-info-1", + } + + agent, err := s.Agent().Update(context.TODO(), agentData) + Expect(err).To(BeNil()) + Expect(agent).NotTo(BeNil()) + Expect(agent.StatusInfo).To(Equal("another-status-info-1")) + + rawStatusInfo := "" + gormdb.Raw("select status_info from agents where id = 'agent-1';").Scan(&rawStatusInfo) + Expect(rawStatusInfo).To(Equal("another-status-info-1")) + }) + + It("successfuly updates an agent -- source_id field", func() { + tx := gormdb.Exec(fmt.Sprintf(insertSourceStm, "source-1", "source-name-1")) + Expect(tx.Error).To(BeNil()) + tx = gormdb.Exec(fmt.Sprintf(insertAgentStm, "agent-1", "not-connected", "status-info-1", "cred_url-1")) + Expect(tx.Error).To(BeNil()) + + agent, err := s.Agent().UpdateSourceID(context.TODO(), "agent-1", "source-1") + Expect(err).To(BeNil()) + Expect(agent).NotTo(BeNil()) + Expect(*agent.SourceId).To(Equal("source-1")) + + rawSourceID := "" + gormdb.Raw("select source_id from agents where id = 'agent-1';").Scan(&rawSourceID) + Expect(rawSourceID).To(Equal("source-1")) + }) + + It("fails updates an agent -- agent is missing", func() { + tx := gormdb.Exec(fmt.Sprintf(insertAgentStm, "agent-1", "not-connected", "status-info-1", "cred_url-1")) + Expect(tx.Error).To(BeNil()) + + agent, err := s.Agent().UpdateSourceID(context.TODO(), "agent-1", "source-1") + Expect(err).ToNot(BeNil()) + Expect(agent).To(BeNil()) + }) + + It("fails updates an agent -- source is missing", func() { + tx := gormdb.Exec(fmt.Sprintf(insertAgentStm, "agent-1", "not-connected", "status-info-1", "cred_url-1")) + Expect(tx.Error).To(BeNil()) + + agentData := apiAgent.AgentStatusUpdate{ + Id: "unknown-id", + } + + agent, err := s.Agent().Update(context.TODO(), agentData) + Expect(err).ToNot(BeNil()) + Expect(agent).To(BeNil()) + }) + + AfterEach(func() { + gormdb.Exec("DELETE FROM agents;") + gormdb.Exec("DELETE FROM sources;") + }) + }) + + Context("delete", func() { + It("successfuly delete an agents -- soft deletion", func() { + tx := gormdb.Exec(fmt.Sprintf(insertAgentStm, "agent-1", "not-connected", "status-info-1", "cred_url-1")) + Expect(tx.Error).To(BeNil()) + tx = gormdb.Exec(fmt.Sprintf(insertAgentStm, "agent-2", "not-connected", "status-info-2", "cred_url-2")) + Expect(tx.Error).To(BeNil()) + tx = gormdb.Exec(fmt.Sprintf(insertAgentStm, "agent-3", "not-connected", "status-info-3", "cred_url-3")) + Expect(tx.Error).To(BeNil()) + + softDeletion := true + err := s.Agent().Delete(context.TODO(), "agent-1", softDeletion) + Expect(err).To(BeNil()) + + agents, err := s.Agent().List(context.TODO(), store.NewAgentQueryFilter(), store.NewAgentQueryOptions().WithIncludeSoftDeleted(true)) + Expect(err).To(BeNil()) + Expect(*agents).To(HaveLen(3)) + }) + + It("successfuly delete an agent -- hard deletion", func() { + tx := gormdb.Exec(fmt.Sprintf(insertAgentStm, "agent-1", "not-connected", "status-info-1", "cred_url-1")) + Expect(tx.Error).To(BeNil()) + tx = gormdb.Exec(fmt.Sprintf(insertAgentStm, "agent-2", "not-connected", "status-info-2", "cred_url-2")) + Expect(tx.Error).To(BeNil()) + tx = gormdb.Exec(fmt.Sprintf(insertAgentStm, "agent-3", "not-connected", "status-info-3", "cred_url-3")) + Expect(tx.Error).To(BeNil()) + + softDeletion := false + err := s.Agent().Delete(context.TODO(), "agent-1", softDeletion) + Expect(err).To(BeNil()) + + agents, err := s.Agent().List(context.TODO(), store.NewAgentQueryFilter(), store.NewAgentQueryOptions()) + Expect(err).To(BeNil()) + Expect(*agents).To(HaveLen(2)) + }) + + It("successfuly delete an agent -- soft and hard deletion", func() { + tx := gormdb.Exec(fmt.Sprintf(insertAgentStm, "agent-1", "not-connected", "status-info-1", "cred_url-1")) + Expect(tx.Error).To(BeNil()) + tx = gormdb.Exec(fmt.Sprintf(insertAgentStm, "agent-2", "not-connected", "status-info-2", "cred_url-2")) + Expect(tx.Error).To(BeNil()) + tx = gormdb.Exec(fmt.Sprintf(insertAgentStm, "agent-3", "not-connected", "status-info-3", "cred_url-3")) + Expect(tx.Error).To(BeNil()) + + softDeletion := true + err := s.Agent().Delete(context.TODO(), "agent-1", softDeletion) + Expect(err).To(BeNil()) + + agents, err := s.Agent().List(context.TODO(), store.NewAgentQueryFilter(), store.NewAgentQueryOptions().WithIncludeSoftDeleted(true)) + Expect(err).To(BeNil()) + Expect(*agents).To(HaveLen(3)) + + softDeletion = false + err = s.Agent().Delete(context.TODO(), "agent-1", softDeletion) + Expect(err).To(BeNil()) + + agents, err = s.Agent().List(context.TODO(), store.NewAgentQueryFilter(), store.NewAgentQueryOptions()) + Expect(err).To(BeNil()) + Expect(*agents).To(HaveLen(2)) + }) + + AfterEach(func() { + gormdb.Exec("DELETE FROM agents;") + }) + }) +}) diff --git a/internal/store/model/agent.go b/internal/store/model/agent.go new file mode 100644 index 0000000..7d41ee6 --- /dev/null +++ b/internal/store/model/agent.go @@ -0,0 +1,64 @@ +package model + +import ( + "encoding/json" + + api "github.com/kubev2v/migration-planner/api/v1alpha1" + apiAgent "github.com/kubev2v/migration-planner/api/v1alpha1/agent" + "gorm.io/gorm" +) + +type Agent struct { + gorm.Model + ID string `json:"id" gorm:"primaryKey"` + Status string + StatusInfo string + CredUrl string + SourceID *string + Source *Source +} + +type AgentList []Agent + +func (a Agent) String() string { + v, _ := json.Marshal(a) + return string(v) +} + +func NewAgentFromID(id string) *Agent { + return &Agent{ID: id} +} + +func NewAgentFromApiResource(resource *apiAgent.AgentStatusUpdate) *Agent { + return &Agent{ + ID: resource.Id, + Status: resource.Status, + StatusInfo: resource.StatusInfo, + CredUrl: resource.CredentialUrl, + } +} + +func (a *Agent) ToApiResource() api.Agent { + agent := api.Agent{ + Id: a.ID, + Status: api.StringToAgentStatus(a.Status), + StatusInfo: a.StatusInfo, + CreatedAt: a.CreatedAt, + UpdatedAt: a.UpdatedAt, + CredentialUrl: a.CredUrl, + } + + if a.SourceID != nil { + agent.SourceId = a.SourceID + } + + return agent +} + +func (al AgentList) ToApiResource() api.AgentList { + agentList := make([]api.Agent, len(al)) + for i, agent := range al { + agentList[i] = agent.ToApiResource() + } + return agentList +} diff --git a/internal/store/model/source.go b/internal/store/model/source.go index 2c8d27e..7821553 100644 --- a/internal/store/model/source.go +++ b/internal/store/model/source.go @@ -20,6 +20,7 @@ type Source struct { StatusInfo string Inventory *JSONField[api.Inventory] `gorm:"type:jsonb"` CredUrl *string + Agents []Agent `gorm:"constraint:OnDelete:SET NULL;"` } type SourceList []Source diff --git a/internal/store/store.go b/internal/store/store.go index 3310f4c..5e5a482 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -6,18 +6,21 @@ import ( ) type Store interface { + Agent() Agent Source() Source InitialMigration() error Close() error } type DataStore struct { + agent Agent source Source db *gorm.DB } func NewStore(db *gorm.DB, log logrus.FieldLogger) Store { return &DataStore{ + agent: NewAgentSource(db, log), source: NewSource(db, log), db: db, } @@ -27,10 +30,17 @@ func (s *DataStore) Source() Source { return s.source } +func (s *DataStore) Agent() Agent { + return s.agent +} + func (s *DataStore) InitialMigration() error { if err := s.Source().InitialMigration(); err != nil { return err } + if err := s.Agent().InitialMigration(); err != nil { + return err + } return nil } diff --git a/internal/store/store_suite_test.go b/internal/store/store_suite_test.go new file mode 100644 index 0000000..e79bb60 --- /dev/null +++ b/internal/store/store_suite_test.go @@ -0,0 +1,13 @@ +package store_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestStore(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Store Suite") +}