diff --git a/apis/cmk/cmk-ui.yaml b/apis/cmk/cmk-ui.yaml index f12646a8..663e3729 100644 --- a/apis/cmk/cmk-ui.yaml +++ b/apis/cmk/cmk-ui.yaml @@ -960,7 +960,7 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/DetailedWorkflow" + $ref: "#/components/schemas/Workflow" "400": $ref: "#/components/responses/400" "403": @@ -1453,6 +1453,7 @@ components: - DISCONNECTED - PROCESSING - FAILED + - UNDER_WORKFLOW keyConfigurationName: description: The name of the key configuration type: string @@ -1467,6 +1468,9 @@ components: type: object readOnly: true properties: + worfklow: + $ref: "#/components/schemas/Workflow" + description: Workflow associated with the System if it's state is UNDER_WORKFLOW errorCode: type: string description: The code of an error of a failed system @@ -2558,35 +2562,24 @@ components: example: example_key_configuration_name parametersResourceType: $ref: "#/components/schemas/WorkflowParametersResourceType" - DetailedWorkflow: - allOf: - - $ref: "#/components/schemas/Workflow" - - type: object - readOnly: true - required: - - approverGroups - - decisions - - availableTransitions - - approvalSummary - description: Detailed Workflow including system metadata - properties: - approverGroups: - description: The list of Groups responsible for approving the Workflow - type: array - items: - $ref: "#/components/schemas/Group" - decisions: - description: The list of decisions made by the approvers - type: array - items: - $ref: "#/components/schemas/WorkflowApprover" - availableTransitions: - description: The list of available transitions for the Workflow - type: array - items: - $ref: "#/components/schemas/WorkflowTransitionValue" - approvalSummary: - $ref: "#/components/schemas/WorkflowApprovalSummary" + approverGroups: + description: The list of Groups responsible for approving the Workflow (only populated in detailed responses) + type: array + items: + $ref: "#/components/schemas/Group" + decisions: + description: The list of decisions made by the approvers (only populated in detailed responses) + type: array + items: + $ref: "#/components/schemas/WorkflowApprover" + availableTransitions: + description: The list of available transitions for the Workflow (only populated in detailed responses) + type: array + items: + $ref: "#/components/schemas/WorkflowTransitionValue" + approvalSummary: + description: Summary of the approval decisions (only populated in detailed responses) + $ref: "#/components/schemas/WorkflowApprovalSummary" WorkflowMetadata: readOnly: true type: object diff --git a/internal/api/cmkapi/cmkapi.go b/internal/api/cmkapi/cmkapi.go index 685e643f..7960184c 100644 --- a/internal/api/cmkapi/cmkapi.go +++ b/internal/api/cmkapi/cmkapi.go @@ -59,10 +59,11 @@ const ( // Defines values for SystemStatus. const ( - SystemStatusCONNECTED SystemStatus = "CONNECTED" - SystemStatusDISCONNECTED SystemStatus = "DISCONNECTED" - SystemStatusFAILED SystemStatus = "FAILED" - SystemStatusPROCESSING SystemStatus = "PROCESSING" + SystemStatusCONNECTED SystemStatus = "CONNECTED" + SystemStatusDISCONNECTED SystemStatus = "DISCONNECTED" + SystemStatusFAILED SystemStatus = "FAILED" + SystemStatusPROCESSING SystemStatus = "PROCESSING" + SystemStatusUNDERWORKFLOW SystemStatus = "UNDER_WORKFLOW" ) // Defines values for SystemRecoveryActionBodyAction. @@ -211,54 +212,6 @@ type DetailedError struct { Target *string `json:"target,omitempty"` } -// DetailedWorkflow defines model for DetailedWorkflow. -type DetailedWorkflow struct { - ActionType WorkflowActionType `json:"actionType"` - - // ApprovalSummary Summary of the approval decisions - ApprovalSummary *WorkflowApprovalSummary `json:"approvalSummary,omitempty"` - - // ApproverGroups The list of Groups responsible for approving the Workflow - ApproverGroups []Group `json:"approverGroups"` - - // ArtifactID The ID of the artifact that the Workflow is associated with - ArtifactID openapi_types.UUID `json:"artifactID"` - - // ArtifactName The name of the artifact that the Workflow is associated with - ArtifactName *string `json:"artifactName,omitempty"` - ArtifactType WorkflowArtifactType `json:"artifactType"` - - // AvailableTransitions The list of available transitions for the Workflow - AvailableTransitions []WorkflowTransitionValue `json:"availableTransitions"` - - // Decisions The list of decisions made by the approvers - Decisions []WorkflowApprover `json:"decisions"` - - // ExpiresAt The datetime of when the workflow expires (RFC3339 format) - ExpiresAt *time.Time `json:"expiresAt,omitempty"` - - // FailureReason The reason for the failure of the Workflow - FailureReason *string `json:"failureReason,omitempty"` - - // Id The ID of the Workflow - Id *openapi_types.UUID `json:"id,omitempty"` - - // InitiatorID The ID of the User who initiated the Workflow - InitiatorID string `json:"initiatorID"` - - // InitiatorName The name of the User who initiated the Workflow - InitiatorName string `json:"initiatorName"` - Metadata *WorkflowMetadata `json:"metadata,omitempty"` - - // Parameters Parameters required to execute the Workflow - Parameters *string `json:"parameters,omitempty"` - - // ParametersResourceName The name of the resource derived from the Workflow parameters - ParametersResourceName *string `json:"parametersResourceName,omitempty"` - ParametersResourceType *WorkflowParametersResourceType `json:"parametersResourceType,omitempty"` - State WorkflowState `json:"state"` -} - // Error A structured api_errors response type Error struct { // Code Technical code of the api_errors, used to identify the api_errors condition. This value is stable and can be used to handle specific api_errors cases. @@ -732,7 +685,8 @@ type SystemMetadata struct { ErrorCode *string `json:"errorCode,omitempty"` // ErrorMessage The details of an error of a failed system - ErrorMessage *string `json:"errorMessage,omitempty"` + ErrorMessage *string `json:"errorMessage,omitempty"` + Worfklow *Workflow `json:"worfklow,omitempty"` } // SystemPatch Request to update a System object. @@ -862,6 +816,12 @@ type UserInfo struct { type Workflow struct { ActionType WorkflowActionType `json:"actionType"` + // ApprovalSummary Summary of the approval decisions + ApprovalSummary *WorkflowApprovalSummary `json:"approvalSummary,omitempty"` + + // ApproverGroups The list of Groups responsible for approving the Workflow (only populated in detailed responses) + ApproverGroups *[]Group `json:"approverGroups,omitempty"` + // ArtifactID The ID of the artifact that the Workflow is associated with ArtifactID openapi_types.UUID `json:"artifactID"` @@ -869,6 +829,12 @@ type Workflow struct { ArtifactName *string `json:"artifactName,omitempty"` ArtifactType WorkflowArtifactType `json:"artifactType"` + // AvailableTransitions The list of available transitions for the Workflow (only populated in detailed responses) + AvailableTransitions *[]WorkflowTransitionValue `json:"availableTransitions,omitempty"` + + // Decisions The list of decisions made by the approvers (only populated in detailed responses) + Decisions *[]WorkflowApprover `json:"decisions,omitempty"` + // ExpiresAt The datetime of when the workflow expires (RFC3339 format) ExpiresAt *time.Time `json:"expiresAt,omitempty"` @@ -5174,7 +5140,7 @@ type GetWorkflowByIDResponseObject interface { VisitGetWorkflowByIDResponse(w http.ResponseWriter) error } -type GetWorkflowByID200JSONResponse DetailedWorkflow +type GetWorkflowByID200JSONResponse Workflow func (response GetWorkflowByID200JSONResponse) VisitGetWorkflowByIDResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") @@ -6637,196 +6603,196 @@ func (sh *strictHandler) TransitionWorkflow(w http.ResponseWriter, r *http.Reque // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "JHWTmC6pl+Th54f/HwAA//9cyaYuagsBAA==", + "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/internal/api/transform/system/system.go b/internal/api/transform/system/system.go index c5157bd6..62ddbd0a 100644 --- a/internal/api/transform/system/system.go +++ b/internal/api/transform/system/system.go @@ -1,19 +1,45 @@ package system import ( + "context" "errors" "github.com/openkcm/cmk/internal/api/cmkapi" + "github.com/openkcm/cmk/internal/api/transform/workflow" "github.com/openkcm/cmk/internal/config" "github.com/openkcm/cmk/internal/constants" "github.com/openkcm/cmk/internal/model" + "github.com/openkcm/cmk/internal/pluginregistry/service/api/identitymanagement" "github.com/openkcm/cmk/utils/sanitise" ) var ErrFromAPI = errors.New("failed to transform system from API") +// ToAPIOpt is a functional option for customizing the ToAPI transformation. +type ToAPIOpt func(*cmkapi.System) error + +// WithWorkflow sets the workflow field on the API system metadata. +func WithWorkflow( + ctx context.Context, + wf *model.Workflow, + idm identitymanagement.IdentityManagement, + opts ...workflow.ToAPIOpt, +) ToAPIOpt { + return func(s *cmkapi.System) error { + apiWorkflow, err := workflow.ToAPI(ctx, *wf, idm, opts...) + if s.Metadata == nil { + s.Metadata = &cmkapi.SystemMetadata{ + Worfklow: apiWorkflow, + } + } else { + s.Metadata.Worfklow = apiWorkflow + } + return err + } +} + // ToAPI transforms a system model to an API system. -func ToAPI(system model.System, systemCfg *config.System) (*cmkapi.System, error) { +func ToAPI(system model.System, systemCfg *config.System, opts ...ToAPIOpt) (*cmkapi.System, error) { err := sanitise.Sanitize(&system) if err != nil { return nil, err @@ -58,6 +84,14 @@ func ToAPI(system model.System, systemCfg *config.System) (*cmkapi.System, error } } + // Apply optional transformations + for _, opt := range opts { + err := opt(apiSystem) + if err != nil { + return nil, err + } + } + return apiSystem, nil } diff --git a/internal/api/transform/workflow/workflow.go b/internal/api/transform/workflow/workflow.go index cb8758c0..9a9cac69 100644 --- a/internal/api/transform/workflow/workflow.go +++ b/internal/api/transform/workflow/workflow.go @@ -20,11 +20,71 @@ import ( var ErrExpiryGreaterThanMaximum = errors.New("expiry exceeds maximum") +// ToAPIOpt is a functional option for customizing the ToAPI transformation. +type ToAPIOpt func(*cmkapi.Workflow) error + +// WithDetailed enriches the workflow with detailed information (approvers, groups, transitions, summary). +func WithDetailed( + ctx context.Context, + approvers []*model.WorkflowApprover, + idm identitymanagement.IdentityManagement, + approverGroups []*model.Group, + transitions []wfMechanism.Transition, + approvalSummary *wfMechanism.ApprovalSummary, +) ToAPIOpt { + return func(w *cmkapi.Workflow) error { + if approvers != nil { + decisions := make([]cmkapi.WorkflowApprover, 0, len(approvers)) + for _, approver := range approvers { + apiApprover, err := ApproverToAPI(ctx, *approver, idm) + if err != nil { + return err + } + decisions = append(decisions, apiApprover) + } + w.Decisions = &decisions + } + + if approverGroups != nil { + apiApproverGroups := make([]cmkapi.Group, 0, len(approverGroups)) + for _, group := range approverGroups { + apiGroup, err := groupTransform.ToAPI(*group) + if err != nil { + return err + } + apiApproverGroups = append(apiApproverGroups, *apiGroup) + } + w.ApproverGroups = &apiApproverGroups + } + + if transitions != nil { + availableTransitions := make([]cmkapi.WorkflowTransitionValue, 0, len(transitions)) + for _, transition := range transitions { + apiTransition := cmkapi.WorkflowTransitionValue(transition) + availableTransitions = append(availableTransitions, apiTransition) + } + w.AvailableTransitions = &availableTransitions + } + + if approvalSummary != nil { + w.ApprovalSummary = &cmkapi.WorkflowApprovalSummary{ + Approved: ptr.PointTo(approvalSummary.Approvals), + Rejected: ptr.PointTo(approvalSummary.Rejections), + Pending: ptr.PointTo(approvalSummary.Pending), + TargetScore: ptr.PointTo(approvalSummary.TargetScore), + } + } + + return nil + } +} + // ToAPI converts a workflow model to an API workflow presentation. func ToAPI( ctx context.Context, w model.Workflow, identityManager identitymanagement.IdentityManagement, + opts ...ToAPIOpt, ) (*cmkapi.Workflow, error) { err := sanitise.Sanitize(&w) if err != nil { @@ -42,7 +102,7 @@ func ToAPI( return nil, err } - return &cmkapi.Workflow{ + base := &cmkapi.Workflow{ Id: ptr.PointTo(w.ID), InitiatorID: w.InitiatorID, InitiatorName: initiatorName, @@ -60,75 +120,17 @@ func ToAPI( UpdatedAt: ptr.PointTo(w.UpdatedAt), }), ExpiresAt: w.ExpiryDate, - }, nil -} - -//nolint:funlen -func ToAPIDetailed( - ctx context.Context, - w model.Workflow, - approvers []*model.WorkflowApprover, - approverGroups []*model.Group, - transitions []wfMechanism.Transition, - approvalSummary *wfMechanism.ApprovalSummary, - identityManager identitymanagement.IdentityManagement, -) (*cmkapi.DetailedWorkflow, error) { - base, err := ToAPI(ctx, w, identityManager) - if err != nil { - return nil, err - } - - detailed := &cmkapi.DetailedWorkflow{ - Id: base.Id, - InitiatorID: base.InitiatorID, - InitiatorName: base.InitiatorName, - State: base.State, - ActionType: base.ActionType, - ArtifactType: base.ArtifactType, - ArtifactName: base.ArtifactName, - ArtifactID: base.ArtifactID, - Parameters: base.Parameters, - ParametersResourceName: base.ParametersResourceName, - ParametersResourceType: base.ParametersResourceType, - FailureReason: base.FailureReason, - Metadata: base.Metadata, - ExpiresAt: base.ExpiresAt, - ApprovalSummary: &cmkapi.WorkflowApprovalSummary{ - Approved: ptr.PointTo(approvalSummary.Approvals), - Rejected: ptr.PointTo(approvalSummary.Rejections), - Pending: ptr.PointTo(approvalSummary.Pending), - TargetScore: ptr.PointTo(approvalSummary.TargetScore), - }, } - decisions := make([]cmkapi.WorkflowApprover, 0, len(approvers)) - for _, approver := range approvers { - apiApprover, err := ApproverToAPI(ctx, *approver, identityManager) + // Apply optional transformations + for _, opt := range opts { + err := opt(base) if err != nil { return nil, err } - decisions = append(decisions, apiApprover) - } - detailed.Decisions = decisions - - apiApproverGroups := make([]cmkapi.Group, 0, len(approverGroups)) - for _, group := range approverGroups { - apiGroup, err := groupTransform.ToAPI(*group) - if err != nil { - return nil, err - } - apiApproverGroups = append(apiApproverGroups, *apiGroup) - } - detailed.ApproverGroups = apiApproverGroups - - availableTransitions := make([]cmkapi.WorkflowTransitionValue, 0, len(transitions)) - for _, transition := range transitions { - apiTransition := cmkapi.WorkflowTransitionValue(transition) - availableTransitions = append(availableTransitions, apiTransition) } - detailed.AvailableTransitions = availableTransitions - return detailed, nil + return base, nil } // FromAPI converts an API workflow presentation to a workflow model. diff --git a/internal/controllers/cmk/system_controller.go b/internal/controllers/cmk/system_controller.go index a375c0eb..32aed6f9 100644 --- a/internal/controllers/cmk/system_controller.go +++ b/internal/controllers/cmk/system_controller.go @@ -2,13 +2,18 @@ package cmk import ( "context" + "slices" "github.com/openkcm/cmk/internal/api/cmkapi" "github.com/openkcm/cmk/internal/api/transform/system" + wfWorkflow "github.com/openkcm/cmk/internal/api/transform/workflow" "github.com/openkcm/cmk/internal/apierrors" "github.com/openkcm/cmk/internal/constants" "github.com/openkcm/cmk/internal/errs" + "github.com/openkcm/cmk/internal/manager" + "github.com/openkcm/cmk/internal/model" "github.com/openkcm/cmk/internal/repo" + "github.com/openkcm/cmk/internal/workflow" "github.com/openkcm/cmk/utils/odata" "github.com/openkcm/cmk/utils/ptr" ) @@ -92,7 +97,13 @@ func (c *APIController) GetSystemByID(ctx context.Context, return nil, err } - systemResponse, err := system.ToAPI(*sys, &c.config.ContextModels.System) + var systemResponse *cmkapi.System + if sys.Status == cmkapi.SystemStatusUNDERWORKFLOW { + systemResponse, err = c.handleSystemUnderWorkflow(ctx, sys) + } else { + systemResponse, err = system.ToAPI(*sys, &c.config.ContextModels.System) + } + if err != nil { return nil, errs.Wrap(apierrors.ErrTransformSystemToAPI, err) } @@ -170,3 +181,75 @@ func (c *APIController) UnlinkSystemAction( return cmkapi.UnlinkSystemAction204Response(struct{}{}), nil } + +//nolint:cyclop,funlen +func (c *APIController) handleSystemUnderWorkflow( + ctx context.Context, + sys *model.System, +) (*cmkapi.System, error) { + workflows, _, err := c.Manager.Workflow.GetWorkflows(ctx, manager.WorkflowFilter{ + ArtifactType: workflow.ArtifactTypeSystem.String(), + ArtifactID: sys.ID, + }) + if err != nil || len(workflows) < 1 { + return nil, errs.Wrapf(err, "error finding workflow") + } + wf := workflows[0] + + approvers, _, err := c.Manager.Workflow.ListWorkflowApprovers(ctx, wf.ID, true, repo.Pagination{}) + if err != nil { + return nil, err + } + approverGroups, err := c.Manager.Workflow.GetWorkflowApproverGroups(ctx, wf) + if err != nil { + return nil, err + } + + user, err := c.Manager.User.GetUserInfo(ctx) + if err != nil { + return nil, err + } + + isApprover := slices.ContainsFunc(approvers, func(e *model.WorkflowApprover) bool { + return e.UserID == user.Identifier + }) + + idm, err := c.pluginCatalog.IdentityManagement() + if err != nil { + return nil, err + } + + if user.Identifier != wf.InitiatorID && !isApprover { + return system.ToAPI( + *sys, + &c.config.ContextModels.System, + system.WithWorkflow( + ctx, + wf, + idm, + wfWorkflow.WithDetailed(ctx, nil, idm, approverGroups, nil, nil), + ), + ) + } + + transitions, err := c.Manager.Workflow.GetWorkflowAvailableTransitions(ctx, wf) + if err != nil { + return nil, err + } + + approvalSummary, err := c.Manager.Workflow.GetWorkflowApprovalSummary(ctx, wf) + if err != nil { + return nil, err + } + + return system.ToAPI( + *sys, + &c.config.ContextModels.System, + system.WithWorkflow( + ctx, + wf, + idm, + wfWorkflow.WithDetailed(ctx, approvers, idm, approverGroups, transitions, approvalSummary), + ), + ) +} diff --git a/internal/controllers/cmk/system_controller_test.go b/internal/controllers/cmk/system_controller_test.go index 3c4b55a4..4897696f 100644 --- a/internal/controllers/cmk/system_controller_test.go +++ b/internal/controllers/cmk/system_controller_test.go @@ -24,6 +24,7 @@ import ( "github.com/openkcm/cmk/internal/repo/sql" "github.com/openkcm/cmk/internal/testutils" "github.com/openkcm/cmk/internal/testutils/testplugins" + "github.com/openkcm/cmk/internal/workflow" cmkcontext "github.com/openkcm/cmk/utils/context" "github.com/openkcm/cmk/utils/ptr" ) @@ -527,43 +528,111 @@ func TestAPIController_GetAllSystemsPagination(t *testing.T) { // TestAPIController_GetSystemByID tests the GetSystemByID function of SystemController func TestAPIController_GetSystemByID(t *testing.T) { - db, sv, tenant := startAPISystems(t, testutils.TestAPIServerConfig{}) + db, sv, tenant := startAPISystems(t, testutils.TestAPIServerConfig{ + Plugins: []catalog.BuiltInPlugin{testplugins.NewIdentityManagement()}, + }) ctx := cmkcontext.CreateTenantContext(t.Context(), tenant) r := sql.NewRepository(db) authClient := testutils.NewAuthClient(ctx, t, r, testutils.WithKeyAdminRole()) + authClientAuditor := testutils.NewAuthClient(ctx, t, r, testutils.WithAuditorRole()) keyConfig := testutils.NewKeyConfig(func(_ *model.KeyConfiguration) {}, testutils.WithAuthClientDataKC(authClient)) - system := testutils.NewSystem(func(s *model.System) { s.KeyConfigurationID = ptr.PointTo(keyConfig.ID) }) - systemInvalidKeyConfig := testutils.NewSystem(func(s *model.System) { - s.KeyConfigurationID = ptr.PointTo(uuid.New()) + system := testutils.NewSystem(func(s *model.System) { + s.KeyConfigurationID = ptr.PointTo(keyConfig.ID) + }) + systemUnderWorkflowFullDetails := testutils.NewSystem(func(s *model.System) { + s.KeyConfigurationID = ptr.PointTo(keyConfig.ID) + s.Status = cmkapi.SystemStatusUNDERWORKFLOW + }) + wfFullDetails := testutils.NewWorkflow(func(w *model.Workflow) { + w.InitiatorID = authClient.Identifier + w.ArtifactType = workflow.ArtifactTypeSystem.String() + w.ArtifactID = systemUnderWorkflowFullDetails.ID + w.State = workflow.StateWaitApproval.String() }) - testutils.CreateTestEntities(ctx, t, r, system, keyConfig, systemInvalidKeyConfig) + systemUnderWorkflowApproversDetails := testutils.NewSystem(func(s *model.System) { + s.KeyConfigurationID = ptr.PointTo(keyConfig.ID) + s.Status = cmkapi.SystemStatusUNDERWORKFLOW + }) + wfApproverDetails := testutils.NewWorkflow(func(w *model.Workflow) { + w.ArtifactType = workflow.ArtifactTypeSystem.String() + w.ArtifactID = systemUnderWorkflowApproversDetails.ID + w.State = workflow.StateWaitApproval.String() + }) + testutils.CreateTestEntities( + ctx, + t, + r, + system, + keyConfig, + systemUnderWorkflowFullDetails, + wfFullDetails, + systemUnderWorkflowApproversDetails, + wfApproverDetails, + ) tests := []struct { name string id string expectedStatus int expectedErrorCode string + expectedSystem *model.System + assertFn func(t *testing.T, res cmkapi.System) + authClient testutils.AuthClientData }{ { name: "SystemGETByIdSuccess", expectedStatus: http.StatusOK, id: system.ID.String(), + expectedSystem: system, + authClient: authClient, }, { name: "SystemGETByIdInvalidId", expectedStatus: http.StatusBadRequest, id: "invalid-id", expectedErrorCode: apierrors.ParamsErr, + authClient: authClient, }, { name: "SystemGETByIdNotFound", expectedStatus: http.StatusNotFound, id: uuid.NewString(), expectedErrorCode: "GET_SYSTEM_BY_ID", + authClient: authClient, + }, + { + name: "Should get full detailed workflow on approver", + expectedStatus: http.StatusOK, + id: systemUnderWorkflowFullDetails.ID.String(), + expectedSystem: systemUnderWorkflowFullDetails, + assertFn: func(t *testing.T, res cmkapi.System) { + t.Helper() + + assert.NotNil(t, res.Metadata.Worfklow.ApproverGroups) + assert.NotNil(t, res.Metadata.Worfklow.ApprovalSummary) + assert.NotNil(t, res.Metadata.Worfklow.Decisions) + assert.NotNil(t, res.Metadata.Worfklow.AvailableTransitions) + }, + authClient: authClient, + }, + { + name: "Should only have workflow user groups on non initiator/approver", + expectedStatus: http.StatusOK, + id: systemUnderWorkflowApproversDetails.ID.String(), + expectedSystem: systemUnderWorkflowApproversDetails, + assertFn: func(t *testing.T, res cmkapi.System) { + t.Helper() + + assert.NotNil(t, res.Metadata.Worfklow.ApproverGroups) + assert.Nil(t, res.Metadata.Worfklow.ApprovalSummary) + assert.Nil(t, res.Metadata.Worfklow.Decisions) + assert.Nil(t, res.Metadata.Worfklow.AvailableTransitions) + }, + authClient: authClientAuditor, }, } @@ -573,7 +642,7 @@ func TestAPIController_GetSystemByID(t *testing.T) { Method: http.MethodGet, Endpoint: "/systems/" + tt.id, Tenant: tenant, - AdditionalContext: authClient.GetClientMap(), + AdditionalContext: tt.authClient.GetClientMap(), }) assert.Equal(t, tt.expectedStatus, w.Code) @@ -581,8 +650,12 @@ func TestAPIController_GetSystemByID(t *testing.T) { if tt.expectedStatus == http.StatusOK { response := testutils.GetJSONBody[cmkapi.System](t, w) - assert.Equal(t, &system.ID, response.ID) - assert.Equal(t, system.Identifier, *response.Identifier) + assert.Equal(t, &tt.expectedSystem.ID, response.ID) + assert.Equal(t, tt.expectedSystem.Identifier, *response.Identifier) + + if tt.assertFn != nil { + tt.assertFn(t, response) + } } else { var response *cmkapi.ErrorMessage diff --git a/internal/controllers/cmk/workflow_controller.go b/internal/controllers/cmk/workflow_controller.go index 5e4c9af5..68f73c0f 100644 --- a/internal/controllers/cmk/workflow_controller.go +++ b/internal/controllers/cmk/workflow_controller.go @@ -2,20 +2,14 @@ package cmk import ( "context" - "encoding/json" - "log/slog" "slices" - "github.com/google/uuid" - "github.com/openkcm/cmk/internal/api/cmkapi" wfTransform "github.com/openkcm/cmk/internal/api/transform/workflow" "github.com/openkcm/cmk/internal/apierrors" "github.com/openkcm/cmk/internal/constants" "github.com/openkcm/cmk/internal/errs" - "github.com/openkcm/cmk/internal/log" "github.com/openkcm/cmk/internal/manager" - "github.com/openkcm/cmk/internal/model" "github.com/openkcm/cmk/internal/repo" wfMechanism "github.com/openkcm/cmk/internal/workflow" "github.com/openkcm/cmk/utils/odata" @@ -56,7 +50,7 @@ func (c *APIController) CheckWorkflow( return response, nil } -var getWorkflowsSchema = odata.FilterSchema{ +var GetWorkflowsSchema = odata.FilterSchema{ Entries: []odata.FilterSchemaEntry{ { FilterName: "artifactId", @@ -113,7 +107,7 @@ func (c *APIController) GetWorkflows( ctx context.Context, request cmkapi.GetWorkflowsRequestObject, ) (cmkapi.GetWorkflowsResponseObject, error) { - odataQueryMapper := odata.NewQueryOdataMapper(getWorkflowsSchema) + odataQueryMapper := odata.NewQueryOdataMapper(GetWorkflowsSchema) err := odataQueryMapper.ParseFilter(request.Params.Filter) if err != nil { @@ -206,7 +200,7 @@ func (c *APIController) GetWorkflowByID(ctx context.Context, } // Expand approver groups - approverGroups, err := c.getApproverGroups(ctx, workflow) + approverGroups, err := c.Manager.Workflow.GetWorkflowApproverGroups(ctx, workflow) if err != nil { return nil, err } @@ -227,14 +221,12 @@ func (c *APIController) GetWorkflowByID(ctx context.Context, if err != nil { return nil, err } - apiWorkflow, err := wfTransform.ToAPIDetailed( + + apiWorkflow, err := wfTransform.ToAPI( ctx, *workflow, - approvers, - approverGroups, - transitions, - approvalSummary, idm, + wfTransform.WithDetailed(ctx, approvers, idm, approverGroups, transitions, approvalSummary), ) if err != nil { return nil, err @@ -269,39 +261,3 @@ func (c *APIController) TransitionWorkflow( return cmkapi.TransitionWorkflow200JSONResponse(*apiWorkflow), nil } - -func (c *APIController) getApproverGroups( - ctx context.Context, - workflow *model.Workflow, -) ([]*model.Group, error) { - var IDs []uuid.UUID - - if workflow.ApproverGroupIDs == nil { - return []*model.Group{}, nil - } - - err := json.Unmarshal(workflow.ApproverGroupIDs, &IDs) - if err != nil { - return nil, err - } - - groups := make([]*model.Group, 0, len(IDs)) - for _, id := range IDs { - group, err := c.Manager.Group.GetGroupByID(ctx, id) - if err != nil { - log.Warn(ctx, "failed to expand workflow approver group", slog.Any("error", err)) - - // Return a placeholder group if the group cannot be found. We can still make use of the ID. - groups = append(groups, &model.Group{ - ID: id, - Name: "NOT_AVAILABLE", - Role: constants.KeyAdminRole, - }) - continue - } - - groups = append(groups, group) - } - - return groups, nil -} diff --git a/internal/controllers/cmk/workflow_controller_test.go b/internal/controllers/cmk/workflow_controller_test.go index 8bcd23b1..fcbd7fda 100644 --- a/internal/controllers/cmk/workflow_controller_test.go +++ b/internal/controllers/cmk/workflow_controller_test.go @@ -671,14 +671,15 @@ func TestWorkflowControllerGetByID(t *testing.T) { assert.Equal(t, tt.expectedStatus, w.Code) if tt.expectedStatus == http.StatusOK { - response := testutils.GetJSONBody[cmkapi.DetailedWorkflow](t, w) + response := testutils.GetJSONBody[cmkapi.Workflow](t, w) assert.Equal(t, tt.workflowID, response.Id.String()) assert.Equal(t, tt.userID, response.InitiatorID) assert.NotNil(t, response.ArtifactName) assert.NotEmpty(t, response.AvailableTransitions) assert.NotNil(t, response.ApprovalSummary) + approverGroups := *response.ApproverGroups if tt.approverGroupName != "" { - assert.Equal(t, tt.approverGroupName, response.ApproverGroups[0].Name) + assert.Equal(t, tt.approverGroupName, approverGroups[0].Name) } } }) diff --git a/internal/manager/workflow.go b/internal/manager/workflow.go index 2a6e7501..2eb91a5d 100644 --- a/internal/manager/workflow.go +++ b/internal/manager/workflow.go @@ -65,6 +65,7 @@ type Workflow interface { ) ([]*model.WorkflowApprover, int, error) GetWorkflowAvailableTransitions(ctx context.Context, workflow *model.Workflow) ([]wf.Transition, error) GetWorkflowApprovalSummary(ctx context.Context, workflow *model.Workflow) (*wf.ApprovalSummary, error) + GetWorkflowApproverGroups(ctx context.Context, workflow *model.Workflow) ([]*model.Group, error) WorkflowCanExpire(ctx context.Context, workflow *model.Workflow) (bool, error) TransitionWorkflow( ctx context.Context, @@ -337,6 +338,11 @@ func (w *WorkflowManager) CreateWorkflow( return errs.Wrap(ErrCreateWorkflowDB, err) } + err = w.handleNewWorkflow(ctx, workflow) + if err != nil { + return errs.Wrap(ErrCreateWorkflowDB, err) + } + err = w.createAutoAssignApproversAsyncTask(ctx, *workflow) if err != nil { return err @@ -497,6 +503,42 @@ func (w *WorkflowManager) GetWorkflowApprovalSummary( return summary, nil } +func (w *WorkflowManager) GetWorkflowApproverGroups( + ctx context.Context, + workflow *model.Workflow, +) ([]*model.Group, error) { + var IDs []uuid.UUID + + if workflow.ApproverGroupIDs == nil { + return []*model.Group{}, nil + } + + err := json.Unmarshal(workflow.ApproverGroupIDs, &IDs) + if err != nil { + return nil, err + } + + groups := make([]*model.Group, 0, len(IDs)) + for _, id := range IDs { + group, err := w.groupManager.GetGroupByID(ctx, id) + if err != nil { + log.Warn(ctx, "failed to expand workflow approver group", slog.Any("error", err)) + + // Return a placeholder group if the group cannot be found. We can still make use of the ID. + groups = append(groups, &model.Group{ + ID: id, + Name: "NOT_AVAILABLE", + Role: constants.KeyAdminRole, + }) + continue + } + + groups = append(groups, group) + } + + return groups, nil +} + func (w *WorkflowManager) TransitionWorkflow( ctx context.Context, workflowID uuid.UUID, @@ -602,6 +644,23 @@ func (w *WorkflowManager) CleanupTerminalWorkflows(ctx context.Context) error { return nil } +func (w *WorkflowManager) handleNewWorkflow(ctx context.Context, workflow *model.Workflow) error { + switch workflow.ArtifactType { + case wf.ArtifactTypeSystem.String(): + system := &model.System{ + ID: workflow.ArtifactID, + Status: cmkapi.SystemStatusUNDERWORKFLOW, + } + _, err := w.repo.Patch(ctx, system, *repo.NewQuery()) + if err != nil { + return err + } + default: + // empty + } + return nil +} + func isInvalidAction(err error) bool { return errs.IsAnyError(err, ErrConnectSystemNoPrimaryKey, ErrNotAllSystemsConnected, ErrAlreadyPrimaryKey) } diff --git a/internal/manager/workflow_test.go b/internal/manager/workflow_test.go index 4bf4de38..dc693127 100644 --- a/internal/manager/workflow_test.go +++ b/internal/manager/workflow_test.go @@ -386,7 +386,7 @@ func TestWorkflowManager_CheckWorkflow(t *testing.T) { } func TestWorkflowManager_CreateWorkflow(t *testing.T) { - m, repo, tenant := SetupWorkflowManager(t, &config.Config{ + m, r, tenant := SetupWorkflowManager(t, &config.Config{ ContextModels: config.ContextModels{ System: config.System{ OptionalProperties: map[string]config.SystemProperty{ @@ -408,7 +408,7 @@ func TestWorkflowManager_CreateWorkflow(t *testing.T) { Identifier: constants.SystemUser.String(), }, ) - keyConfig, key := createTestObjects(t, repo, ctxSys) + keyConfig, key := createTestObjects(t, r, ctxSys) t.Run("Should error on existing workflow", func(t *testing.T) { wf := testutils.NewWorkflow(func(w *model.Workflow) { @@ -417,7 +417,7 @@ func TestWorkflowManager_CreateWorkflow(t *testing.T) { w.ArtifactType = workflow.ArtifactTypeKey.String() w.ArtifactID = key.ID }) - err := repo.Create(ctx, wf) + err := r.Create(ctx, wf) assert.NoError(t, err) _, err = m.CreateWorkflow(ctxSys, wf) @@ -426,7 +426,7 @@ func TestWorkflowManager_CreateWorkflow(t *testing.T) { ) t.Run("Should create workflow", func(t *testing.T) { - createAuditorGroup(ctx, t, repo) + createAuditorGroup(ctx, t, r) ctxSys := context.WithValue( ctx, @@ -435,7 +435,7 @@ func TestWorkflowManager_CreateWorkflow(t *testing.T) { }, ) - _, key := createTestObjects(t, repo, ctxSys) + _, key := createTestObjects(t, r, ctxSys) wf := testutils.NewWorkflow(func(w *model.Workflow) { w.State = workflow.StateInitial.String() w.ActionType = workflow.ActionTypeDelete.String() @@ -454,7 +454,7 @@ func TestWorkflowManager_CreateWorkflow(t *testing.T) { "NameOfTheSystem": "MySystem", } }) - testutils.CreateTestEntities(ctxSys, t, repo, system) + testutils.CreateTestEntities(ctxSys, t, r, system) expected := &model.Workflow{ ID: uuid.New(), @@ -470,12 +470,29 @@ func TestWorkflowManager_CreateWorkflow(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "MySystem", *res.ArtifactName) assert.Equal(t, keyConfig.Name, *res.ParametersResourceName) - }, - ) + }) + + t.Run("Should put system state under_workflow on workflow creation", func(t *testing.T) { + system := testutils.NewSystem(func(s *model.System) {}) + testutils.CreateTestEntities(ctxSys, t, r, system) + + wf := testutils.NewWorkflow(func(w *model.Workflow) { + w.ArtifactType = workflow.ArtifactTypeSystem.String() + w.ArtifactID = system.ID + }) + + _, err := m.CreateWorkflow(ctxSys, wf) + assert.NoError(t, err) + + _, err = r.First(ctxSys, system, *repo.NewQuery()) + assert.NoError(t, err) + + assert.Equal(t, cmkapi.SystemStatusUNDERWORKFLOW, system.Status) + }) t.Run("Should create system workflow with artifact name from identifier", func(t *testing.T) { system := testutils.NewSystem(func(s *model.System) {}) - testutils.CreateTestEntities(ctxSys, t, repo, system) + testutils.CreateTestEntities(ctxSys, t, r, system) expected := &model.Workflow{ ID: uuid.New(),