mirror of https://github.com/portainer/portainer
				
				
				
			fix(pending-actions): Small improvements to pending actions (R8S-350) (#949)
							parent
							
								
									2c08becf6c
								
							
						
					
					
						commit
						129b9d5db9
					
				| 
						 | 
				
			
			@ -2,6 +2,7 @@ package postinit
 | 
			
		|||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	"github.com/docker/docker/api/types/container"
 | 
			
		||||
	"github.com/docker/docker/client"
 | 
			
		||||
| 
						 | 
				
			
			@ -83,17 +84,27 @@ func (postInitMigrator *PostInitMigrator) PostInitMigrate() error {
 | 
			
		|||
 | 
			
		||||
// try to create a post init migration pending action. If it already exists, do nothing
 | 
			
		||||
// this function exists for readability, not reusability
 | 
			
		||||
// TODO: This should be moved into pending actions as part of the pending action migration
 | 
			
		||||
func (postInitMigrator *PostInitMigrator) createPostInitMigrationPendingAction(environmentID portainer.EndpointID) error {
 | 
			
		||||
	// If there are no pending actions for the given endpoint, create one
 | 
			
		||||
	err := postInitMigrator.dataStore.PendingActions().Create(&portainer.PendingAction{
 | 
			
		||||
	action := portainer.PendingAction{
 | 
			
		||||
		EndpointID: environmentID,
 | 
			
		||||
		Action:     actions.PostInitMigrateEnvironment,
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Error().Err(err).Msgf("Error creating pending action for environment %d", environmentID)
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
	pendingActions, err := postInitMigrator.dataStore.PendingActions().ReadAll()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("failed to retrieve pending actions: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, dba := range pendingActions {
 | 
			
		||||
		if dba.EndpointID == action.EndpointID && dba.Action == action.Action {
 | 
			
		||||
			log.Debug().
 | 
			
		||||
				Str("action", action.Action).
 | 
			
		||||
				Int("endpoint_id", int(action.EndpointID)).
 | 
			
		||||
				Msg("pending action already exists for environment, skipping...")
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return postInitMigrator.dataStore.PendingActions().Create(&action)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// MigrateEnvironment runs migrations on a single environment
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,10 +8,12 @@ import (
 | 
			
		|||
 | 
			
		||||
	portainer "github.com/portainer/portainer/api"
 | 
			
		||||
	"github.com/portainer/portainer/api/datastore"
 | 
			
		||||
	"github.com/portainer/portainer/api/pendingactions/actions"
 | 
			
		||||
 | 
			
		||||
	"github.com/docker/docker/api/types/container"
 | 
			
		||||
	"github.com/docker/docker/client"
 | 
			
		||||
	"github.com/segmentio/encoding/json"
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
	"github.com/stretchr/testify/require"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -73,3 +75,96 @@ func TestMigrateGPUs(t *testing.T) {
 | 
			
		|||
	require.False(t, migratedEndpoint.PostInitMigrations.MigrateGPUs)
 | 
			
		||||
	require.True(t, migratedEndpoint.EnableGPUManagement)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestPostInitMigrate_PendingActionsCreated(t *testing.T) {
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name                   string
 | 
			
		||||
		existingPendingActions []*portainer.PendingAction
 | 
			
		||||
		expectedPendingActions int
 | 
			
		||||
		expectedAction         string
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name: "when existing non-matching action exists, should add migration action",
 | 
			
		||||
			existingPendingActions: []*portainer.PendingAction{
 | 
			
		||||
				{
 | 
			
		||||
					EndpointID: 7,
 | 
			
		||||
					Action:     "some-other-action",
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			expectedPendingActions: 2,
 | 
			
		||||
			expectedAction:         actions.PostInitMigrateEnvironment,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "when matching action exists, should not add duplicate",
 | 
			
		||||
			existingPendingActions: []*portainer.PendingAction{
 | 
			
		||||
				{
 | 
			
		||||
					EndpointID: 7,
 | 
			
		||||
					Action:     actions.PostInitMigrateEnvironment,
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			expectedPendingActions: 1,
 | 
			
		||||
			expectedAction:         actions.PostInitMigrateEnvironment,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:                   "when no actions exist, should add migration action",
 | 
			
		||||
			existingPendingActions: []*portainer.PendingAction{},
 | 
			
		||||
			expectedPendingActions: 1,
 | 
			
		||||
			expectedAction:         actions.PostInitMigrateEnvironment,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		t.Run(tt.name, func(t *testing.T) {
 | 
			
		||||
			is := assert.New(t)
 | 
			
		||||
			_, store := datastore.MustNewTestStore(t, true, true)
 | 
			
		||||
 | 
			
		||||
			// Create test endpoint
 | 
			
		||||
			endpoint := &portainer.Endpoint{
 | 
			
		||||
				ID:          7,
 | 
			
		||||
				UserTrusted: true,
 | 
			
		||||
				Type:        portainer.EdgeAgentOnDockerEnvironment,
 | 
			
		||||
				Edge: portainer.EnvironmentEdgeSettings{
 | 
			
		||||
					AsyncMode: false,
 | 
			
		||||
				},
 | 
			
		||||
				EdgeID: "edgeID",
 | 
			
		||||
			}
 | 
			
		||||
			err := store.Endpoint().Create(endpoint)
 | 
			
		||||
			is.NoError(err, "error creating endpoint")
 | 
			
		||||
 | 
			
		||||
			// Create any existing pending actions
 | 
			
		||||
			for _, action := range tt.existingPendingActions {
 | 
			
		||||
				err = store.PendingActions().Create(action)
 | 
			
		||||
				is.NoError(err, "error creating pending action")
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			migrator := NewPostInitMigrator(
 | 
			
		||||
				nil, // kubeFactory not needed for this test
 | 
			
		||||
				nil, // dockerFactory not needed for this test
 | 
			
		||||
				store,
 | 
			
		||||
				"",  // assetsPath not needed for this test
 | 
			
		||||
				nil, // kubernetesDeployer not needed for this test
 | 
			
		||||
			)
 | 
			
		||||
 | 
			
		||||
			err = migrator.PostInitMigrate()
 | 
			
		||||
			is.NoError(err, "PostInitMigrate should not return error")
 | 
			
		||||
 | 
			
		||||
			// Verify the results
 | 
			
		||||
			pendingActions, err := store.PendingActions().ReadAll()
 | 
			
		||||
			is.NoError(err, "error reading pending actions")
 | 
			
		||||
			is.Len(pendingActions, tt.expectedPendingActions, "unexpected number of pending actions")
 | 
			
		||||
 | 
			
		||||
			// If we expect any actions, verify at least one has the expected action type
 | 
			
		||||
			if tt.expectedPendingActions > 0 {
 | 
			
		||||
				hasExpectedAction := false
 | 
			
		||||
				for _, action := range pendingActions {
 | 
			
		||||
					if action.Action == tt.expectedAction {
 | 
			
		||||
						hasExpectedAction = true
 | 
			
		||||
						is.Equal(endpoint.ID, action.EndpointID, "action should reference correct endpoint")
 | 
			
		||||
						break
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
				is.True(hasExpectedAction, "should have found action of expected type")
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -460,3 +460,39 @@ func WithStacks(stacks []portainer.Stack) datastoreOption {
 | 
			
		|||
		d.stack = &stubStacksService{stacks: stacks}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type stubPendingActionService struct {
 | 
			
		||||
	actions []portainer.PendingAction
 | 
			
		||||
	dataservices.PendingActionsService
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func WithPendingActions(pendingActions []portainer.PendingAction) datastoreOption {
 | 
			
		||||
	return func(d *testDatastore) {
 | 
			
		||||
		d.pendingActionsService = &stubPendingActionService{
 | 
			
		||||
			actions: pendingActions,
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *stubPendingActionService) ReadAll(predicates ...func(portainer.PendingAction) bool) ([]portainer.PendingAction, error) {
 | 
			
		||||
	filtered := s.actions
 | 
			
		||||
 | 
			
		||||
	for _, predicate := range predicates {
 | 
			
		||||
		filtered = slicesx.Filter(filtered, predicate)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return filtered, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *stubPendingActionService) Delete(ID portainer.PendingActionID) error {
 | 
			
		||||
	actions := []portainer.PendingAction{}
 | 
			
		||||
 | 
			
		||||
	for _, action := range s.actions {
 | 
			
		||||
		if action.ID != ID {
 | 
			
		||||
			actions = append(actions, action)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	s.actions = actions
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -71,10 +71,14 @@ func (service *PendingActionsService) execute(environmentID portainer.EndpointID
 | 
			
		|||
 | 
			
		||||
	isKubernetesEndpoint := endpointutils.IsKubernetesEndpoint(endpoint) && !endpointutils.IsEdgeEndpoint(endpoint)
 | 
			
		||||
 | 
			
		||||
	// EndpointStatusUp is only relevant for non-Kubernetes endpoints
 | 
			
		||||
	// Sometimes the endpoint is UP but the status is not updated in the database
 | 
			
		||||
	if !isKubernetesEndpoint {
 | 
			
		||||
		if endpoint.Status != portainer.EndpointStatusUp {
 | 
			
		||||
		// Edge environments check the heartbeat
 | 
			
		||||
		// Other environments check the endpoint status
 | 
			
		||||
		if endpointutils.IsEdgeEndpoint(endpoint) {
 | 
			
		||||
			if !endpoint.Heartbeat {
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
		} else if endpoint.Status != portainer.EndpointStatusUp {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,89 @@
 | 
			
		|||
package pendingactions
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	portainer "github.com/portainer/portainer/api"
 | 
			
		||||
	"github.com/portainer/portainer/api/internal/testhelpers"
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestExecute(t *testing.T) {
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name           string
 | 
			
		||||
		endpoint       *portainer.Endpoint
 | 
			
		||||
		pendingActions []portainer.PendingAction
 | 
			
		||||
		shouldExecute  bool
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name: "Edge endpoint with heartbeat should execute",
 | 
			
		||||
			// Create test endpoint
 | 
			
		||||
			endpoint: &portainer.Endpoint{
 | 
			
		||||
				ID:        1,
 | 
			
		||||
				Heartbeat: true,
 | 
			
		||||
				Type:      portainer.EdgeAgentOnDockerEnvironment,
 | 
			
		||||
				EdgeID:    "edge-1",
 | 
			
		||||
			},
 | 
			
		||||
			pendingActions: []portainer.PendingAction{
 | 
			
		||||
				{ID: 1, EndpointID: 1, Action: "test-action"},
 | 
			
		||||
			},
 | 
			
		||||
			shouldExecute: true,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "Edge endpoint without heartbeat should not execute",
 | 
			
		||||
			endpoint: &portainer.Endpoint{
 | 
			
		||||
				ID:        2,
 | 
			
		||||
				EdgeID:    "edge-2",
 | 
			
		||||
				Heartbeat: false,
 | 
			
		||||
				Type:      portainer.EdgeAgentOnDockerEnvironment,
 | 
			
		||||
			},
 | 
			
		||||
			pendingActions: []portainer.PendingAction{
 | 
			
		||||
				{ID: 2, EndpointID: 2, Action: "test-action"},
 | 
			
		||||
			},
 | 
			
		||||
			shouldExecute: false,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "Regular endpoint with status UP should execute",
 | 
			
		||||
			endpoint: &portainer.Endpoint{
 | 
			
		||||
				ID:     3,
 | 
			
		||||
				Status: portainer.EndpointStatusUp,
 | 
			
		||||
				Type:   portainer.AgentOnDockerEnvironment,
 | 
			
		||||
			},
 | 
			
		||||
			pendingActions: []portainer.PendingAction{
 | 
			
		||||
				{ID: 3, EndpointID: 3, Action: "test-action"},
 | 
			
		||||
			},
 | 
			
		||||
			shouldExecute: true,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "Regular endpoint with status DOWN should not execute",
 | 
			
		||||
			endpoint: &portainer.Endpoint{
 | 
			
		||||
				ID:     4,
 | 
			
		||||
				Status: portainer.EndpointStatusDown,
 | 
			
		||||
				Type:   portainer.AgentOnDockerEnvironment,
 | 
			
		||||
			},
 | 
			
		||||
			pendingActions: []portainer.PendingAction{
 | 
			
		||||
				{ID: 4, EndpointID: 4, Action: "test-action"},
 | 
			
		||||
			},
 | 
			
		||||
			shouldExecute: false,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		t.Run(tt.name, func(t *testing.T) {
 | 
			
		||||
			// Setup services
 | 
			
		||||
			store := testhelpers.NewDatastore(testhelpers.WithEndpoints([]portainer.Endpoint{*tt.endpoint}), testhelpers.WithPendingActions(tt.pendingActions))
 | 
			
		||||
			service := NewService(store, nil)
 | 
			
		||||
 | 
			
		||||
			// Execute
 | 
			
		||||
			service.execute(tt.endpoint.ID)
 | 
			
		||||
 | 
			
		||||
			// Verify expectations
 | 
			
		||||
			pendingActions, _ := store.PendingActions().ReadAll()
 | 
			
		||||
			if tt.shouldExecute {
 | 
			
		||||
				assert.Equal(t, len(tt.pendingActions)-1, len(pendingActions))
 | 
			
		||||
			} else {
 | 
			
		||||
				assert.Equal(t, len(tt.pendingActions), len(pendingActions))
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
		Reference in New Issue