mirror of https://github.com/portainer/portainer
				
				
				
			fix(environments): create k8s specific edge agent before connecting [r8s-438] (#1088)
Merging because this change is unrelated to the failing kubernetes/tests/helm-oci.spec.ts testspull/12833/head
							parent
							
								
									2ce8788487
								
							
						
					
					
						commit
						35aa525bd2
					
				| 
						 | 
				
			
			@ -372,10 +372,16 @@ func (handler *Handler) createEdgeAgentEndpoint(tx dataservices.DataStoreTx, pay
 | 
			
		|||
	edgeKey := handler.ReverseTunnelService.GenerateEdgeKey(payload.URL, portainerHost, endpointID)
 | 
			
		||||
 | 
			
		||||
	endpoint := &portainer.Endpoint{
 | 
			
		||||
		ID:              portainer.EndpointID(endpointID),
 | 
			
		||||
		Name:            payload.Name,
 | 
			
		||||
		URL:             portainerHost,
 | 
			
		||||
		Type:            portainer.EdgeAgentOnDockerEnvironment,
 | 
			
		||||
		ID:   portainer.EndpointID(endpointID),
 | 
			
		||||
		Name: payload.Name,
 | 
			
		||||
		URL:  portainerHost,
 | 
			
		||||
		Type: func() portainer.EndpointType {
 | 
			
		||||
			// an empty container engine means that the endpoint is a Kubernetes endpoint
 | 
			
		||||
			if payload.ContainerEngine == "" {
 | 
			
		||||
				return portainer.EdgeAgentOnKubernetesEnvironment
 | 
			
		||||
			}
 | 
			
		||||
			return portainer.EdgeAgentOnDockerEnvironment
 | 
			
		||||
		}(),
 | 
			
		||||
		ContainerEngine: payload.ContainerEngine,
 | 
			
		||||
		GroupID:         portainer.EndpointGroupID(payload.GroupID),
 | 
			
		||||
		Gpus:            payload.Gpus,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,172 @@
 | 
			
		|||
package endpoints
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	portainer "github.com/portainer/portainer/api"
 | 
			
		||||
	"github.com/portainer/portainer/api/chisel"
 | 
			
		||||
	"github.com/portainer/portainer/api/datastore"
 | 
			
		||||
	"github.com/portainer/portainer/api/internal/testhelpers"
 | 
			
		||||
	"github.com/portainer/portainer/pkg/fips"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
	"github.com/stretchr/testify/require"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// EE-only kubeconfig validation tests removed for CE
 | 
			
		||||
 | 
			
		||||
func TestSaveEndpointAndUpdateAuthorizations(t *testing.T) {
 | 
			
		||||
	_, store := datastore.MustNewTestStore(t, true, false)
 | 
			
		||||
 | 
			
		||||
	endpointGroup := &portainer.EndpointGroup{
 | 
			
		||||
		ID:   1,
 | 
			
		||||
		Name: "test-endpoint-group",
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err := store.EndpointGroup().Create(endpointGroup)
 | 
			
		||||
	require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
	h := &Handler{
 | 
			
		||||
		DataStore: store,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	testCases := []struct {
 | 
			
		||||
		name           string
 | 
			
		||||
		endpointType   portainer.EndpointType
 | 
			
		||||
		expectRelation bool
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name:           "create azure environment, expect no relation to be created",
 | 
			
		||||
			endpointType:   portainer.AzureEnvironment,
 | 
			
		||||
			expectRelation: false,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:           "create edge agent environment, expect relation to be created",
 | 
			
		||||
			endpointType:   portainer.EdgeAgentOnDockerEnvironment,
 | 
			
		||||
			expectRelation: true,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:           "create kubernetes environment, expect no relation to be created",
 | 
			
		||||
			endpointType:   portainer.KubernetesLocalEnvironment,
 | 
			
		||||
			expectRelation: false,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:           "create kubeconfig environment, expect no relation to be created",
 | 
			
		||||
			endpointType:   portainer.AgentOnKubernetesEnvironment,
 | 
			
		||||
			expectRelation: false,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:           "create agent docker environment, expect no relation to be created",
 | 
			
		||||
			endpointType:   portainer.AgentOnDockerEnvironment,
 | 
			
		||||
			expectRelation: false,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:           "create unsecured environment, expect no relation to be created",
 | 
			
		||||
			endpointType:   portainer.DockerEnvironment,
 | 
			
		||||
			expectRelation: false,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, testCase := range testCases {
 | 
			
		||||
		t.Run(testCase.name, func(t *testing.T) {
 | 
			
		||||
			endpoint := &portainer.Endpoint{
 | 
			
		||||
				ID:      portainer.EndpointID(store.Endpoint().GetNextIdentifier()),
 | 
			
		||||
				Type:    testCase.endpointType,
 | 
			
		||||
				GroupID: portainer.EndpointGroupID(endpointGroup.ID),
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			err := h.saveEndpointAndUpdateAuthorizations(store, endpoint)
 | 
			
		||||
			require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
			relation, relationErr := store.EndpointRelation().EndpointRelation(endpoint.ID)
 | 
			
		||||
			if testCase.expectRelation {
 | 
			
		||||
				require.NoError(t, relationErr)
 | 
			
		||||
				require.NotNil(t, relation)
 | 
			
		||||
			} else {
 | 
			
		||||
				require.Error(t, relationErr)
 | 
			
		||||
				require.True(t, store.IsErrObjectNotFound(relationErr))
 | 
			
		||||
				require.Nil(t, relation)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestCreateEndpointFailure(t *testing.T) {
 | 
			
		||||
	fips.InitFIPS(false)
 | 
			
		||||
 | 
			
		||||
	_, store := datastore.MustNewTestStore(t, true, false)
 | 
			
		||||
 | 
			
		||||
	h := NewHandler(testhelpers.NewTestRequestBouncer())
 | 
			
		||||
	h.DataStore = store
 | 
			
		||||
 | 
			
		||||
	payload := &endpointCreatePayload{
 | 
			
		||||
		Name:                 "Test Endpoint",
 | 
			
		||||
		EndpointCreationType: agentEnvironment,
 | 
			
		||||
		TLS:                  true,
 | 
			
		||||
		TLSCertFile:          []byte("invalid data"),
 | 
			
		||||
		TLSKeyFile:           []byte("invalid data"),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	endpoint, httpErr := h.createEndpoint(store, payload)
 | 
			
		||||
	require.NotNil(t, httpErr)
 | 
			
		||||
	require.Equal(t, http.StatusInternalServerError, httpErr.StatusCode)
 | 
			
		||||
	require.Nil(t, endpoint)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestCreateEdgeAgentEndpoint_ContainerEngineMapping(t *testing.T) {
 | 
			
		||||
	fips.InitFIPS(false)
 | 
			
		||||
 | 
			
		||||
	_, store := datastore.MustNewTestStore(t, true, false)
 | 
			
		||||
 | 
			
		||||
	// required group for save flow
 | 
			
		||||
	endpointGroup := &portainer.EndpointGroup{ID: 1, Name: "test-group"}
 | 
			
		||||
	err := store.EndpointGroup().Create(endpointGroup)
 | 
			
		||||
	require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
	h := &Handler{
 | 
			
		||||
		DataStore:            store,
 | 
			
		||||
		ReverseTunnelService: chisel.NewService(store, nil, nil),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name     string
 | 
			
		||||
		engine   string
 | 
			
		||||
		wantType portainer.EndpointType
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name:     "empty engine -> EdgeAgentOnKubernetesEnvironment",
 | 
			
		||||
			engine:   "",
 | 
			
		||||
			wantType: portainer.EdgeAgentOnKubernetesEnvironment,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:     "docker engine -> EdgeAgentOnDockerEnvironment",
 | 
			
		||||
			engine:   portainer.ContainerEngineDocker,
 | 
			
		||||
			wantType: portainer.EdgeAgentOnDockerEnvironment,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:     "podman engine -> EdgeAgentOnDockerEnvironment",
 | 
			
		||||
			engine:   portainer.ContainerEnginePodman,
 | 
			
		||||
			wantType: portainer.EdgeAgentOnDockerEnvironment,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tc := range tests {
 | 
			
		||||
		t.Run(tc.name, func(t *testing.T) {
 | 
			
		||||
			payload := &endpointCreatePayload{
 | 
			
		||||
				Name:                 "edge-endpoint",
 | 
			
		||||
				EndpointCreationType: edgeAgentEnvironment,
 | 
			
		||||
				ContainerEngine:      tc.engine,
 | 
			
		||||
				GroupID:              1,
 | 
			
		||||
				URL:                  "https://portainer.example:9443",
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			ep, httpErr := h.createEdgeAgentEndpoint(store, payload)
 | 
			
		||||
			require.Nil(t, httpErr)
 | 
			
		||||
			require.NotNil(t, ep)
 | 
			
		||||
 | 
			
		||||
			assert.Equal(t, tc.wantType, ep.Type)
 | 
			
		||||
			assert.Equal(t, tc.engine, ep.ContainerEngine)
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -57,8 +57,10 @@
 | 
			
		|||
  <information-panel ng-if="state.kubernetesEndpoint && (!state.edgeEndpoint || state.edgeAssociated)" title-text="Kubernetes features configuration">
 | 
			
		||||
    <span class="small text-muted vertical-center">
 | 
			
		||||
      <pr-icon icon="'wrench'" mode="'primary'"></pr-icon>
 | 
			
		||||
      You should configure the features available in this Kubernetes environment in the
 | 
			
		||||
      <a ui-sref="kubernetes.cluster.setup({endpointId: endpoint.Id})">Kubernetes configuration</a> view.
 | 
			
		||||
      <div>
 | 
			
		||||
        You should configure the features available in this Kubernetes environment in the
 | 
			
		||||
        <a ui-sref="kubernetes.cluster.setup({endpointId: endpoint.Id})">Kubernetes configuration</a> view.
 | 
			
		||||
      </div>
 | 
			
		||||
    </span>
 | 
			
		||||
  </information-panel>
 | 
			
		||||
</div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -205,6 +205,8 @@ export enum EnvironmentCreationTypes {
 | 
			
		|||
export enum ContainerEngine {
 | 
			
		||||
  Docker = 'docker',
 | 
			
		||||
  Podman = 'podman',
 | 
			
		||||
  // an empty container engine means that the endpoint is a Kubernetes endpoint
 | 
			
		||||
  Kubernetes = '',
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export enum PlatformType {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,94 @@
 | 
			
		|||
import { HttpResponse } from 'msw';
 | 
			
		||||
import { render, waitFor } from '@testing-library/react';
 | 
			
		||||
import userEvent from '@testing-library/user-event';
 | 
			
		||||
 | 
			
		||||
import { withTestQueryProvider } from '@/react/test-utils/withTestQuery';
 | 
			
		||||
import { server, http } from '@/setup-tests/server';
 | 
			
		||||
 | 
			
		||||
import { WizardKubernetes } from './WizardKubernetes';
 | 
			
		||||
 | 
			
		||||
function renderComponent() {
 | 
			
		||||
  // minimal settings so EdgeAgentForm can render
 | 
			
		||||
  server.use(
 | 
			
		||||
    http.get('/api/settings', () =>
 | 
			
		||||
      HttpResponse.json({
 | 
			
		||||
        AgentSecret: 'secret',
 | 
			
		||||
        EdgePortainerUrl: 'https://example.com',
 | 
			
		||||
        Edge: {
 | 
			
		||||
          PingInterval: 60,
 | 
			
		||||
          SnapshotInterval: 60,
 | 
			
		||||
          CommandInterval: 60,
 | 
			
		||||
          AsyncMode: false,
 | 
			
		||||
          TunnelServerAddress: 'portainer.test:8000',
 | 
			
		||||
        },
 | 
			
		||||
      })
 | 
			
		||||
    ),
 | 
			
		||||
    http.get('/api/custom_templates', () => HttpResponse.json([])),
 | 
			
		||||
    http.get('/api/system/status', () =>
 | 
			
		||||
      HttpResponse.json({ Version: '2.19.0', Edition: 'CE', InstanceID: '1' })
 | 
			
		||||
    ),
 | 
			
		||||
    http.get('/api/endpoints', () =>
 | 
			
		||||
      HttpResponse.json([], {
 | 
			
		||||
        headers: {
 | 
			
		||||
          'x-total-available': '0',
 | 
			
		||||
          'x-total-count': '0',
 | 
			
		||||
        },
 | 
			
		||||
      })
 | 
			
		||||
    )
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const Wrapped = withTestQueryProvider(() => (
 | 
			
		||||
    <WizardKubernetes onCreate={() => {}} />
 | 
			
		||||
  ));
 | 
			
		||||
  return render(<Wrapped />);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
describe('WizardKubernetes', () => {
 | 
			
		||||
  test('renders Edge Agent Standard form when selected', async () => {
 | 
			
		||||
    const { getByText, queryByTestId, findByTestId } = renderComponent();
 | 
			
		||||
 | 
			
		||||
    // select Edge Agent Standard
 | 
			
		||||
    await userEvent.click(getByText('Edge Agent Standard'));
 | 
			
		||||
 | 
			
		||||
    // verify submit button is visible (smallest sanity check for setup)
 | 
			
		||||
    await expect(
 | 
			
		||||
      findByTestId('edge-agent-form-submit-button')
 | 
			
		||||
    ).resolves.toBeVisible();
 | 
			
		||||
    expect(
 | 
			
		||||
      queryByTestId('endpointCreate-portainerServerUrlInput')
 | 
			
		||||
    ).toBeInTheDocument();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  test('submits ContainerEngine as empty string for Kubernetes', async () => {
 | 
			
		||||
    let observedEntries: Array<[string, string]> = [];
 | 
			
		||||
 | 
			
		||||
    server.use(
 | 
			
		||||
      http.post('/api/endpoints', async ({ request }) => {
 | 
			
		||||
        const form = await request.formData();
 | 
			
		||||
        observedEntries = Array.from(form.entries()).map(([key, value]) => [
 | 
			
		||||
          key,
 | 
			
		||||
          typeof value === 'string' ? value : 'binary',
 | 
			
		||||
        ]);
 | 
			
		||||
        return HttpResponse.json({});
 | 
			
		||||
      })
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const { getByText, getByTestId, findByTestId } = renderComponent();
 | 
			
		||||
 | 
			
		||||
    await userEvent.click(getByText('Edge Agent Standard'));
 | 
			
		||||
 | 
			
		||||
    await userEvent.type(getByTestId('environmentCreate-nameInput'), 'k8s-env');
 | 
			
		||||
 | 
			
		||||
    const submitBtn = await findByTestId('edge-agent-form-submit-button');
 | 
			
		||||
    await waitFor(() => expect(submitBtn).not.toBeDisabled());
 | 
			
		||||
    await userEvent.click(submitBtn);
 | 
			
		||||
 | 
			
		||||
    // assert POST happened and ContainerEngine key exists with empty string
 | 
			
		||||
    await waitFor(() => {
 | 
			
		||||
      expect(observedEntries.length).toBeGreaterThan(0);
 | 
			
		||||
      expect(
 | 
			
		||||
        observedEntries.some(([k, v]) => k === 'ContainerEngine' && v === '')
 | 
			
		||||
      ).toBe(true);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -2,7 +2,10 @@ import { useState } from 'react';
 | 
			
		|||
import { Zap, UploadCloud } from 'lucide-react';
 | 
			
		||||
import _ from 'lodash';
 | 
			
		||||
 | 
			
		||||
import { Environment } from '@/react/portainer/environments/types';
 | 
			
		||||
import {
 | 
			
		||||
  ContainerEngine,
 | 
			
		||||
  Environment,
 | 
			
		||||
} from '@/react/portainer/environments/types';
 | 
			
		||||
import { commandsTabs } from '@/react/edge/components/EdgeScriptForm/scripts';
 | 
			
		||||
import { FeatureId } from '@/react/portainer/feature-flags/enums';
 | 
			
		||||
import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
 | 
			
		||||
| 
						 | 
				
			
			@ -98,6 +101,7 @@ export function WizardKubernetes({ onCreate }: Props) {
 | 
			
		|||
              onCreate(environment, 'kubernetesEdgeAgentStandard')
 | 
			
		||||
            }
 | 
			
		||||
            commands={[{ ...commandsTabs.k8sLinux, label: 'Linux' }]}
 | 
			
		||||
            containerEngine={ContainerEngine.Kubernetes}
 | 
			
		||||
          />
 | 
			
		||||
        );
 | 
			
		||||
      case 'edgeAgentAsync':
 | 
			
		||||
| 
						 | 
				
			
			@ -108,6 +112,7 @@ export function WizardKubernetes({ onCreate }: Props) {
 | 
			
		|||
              onCreate(environment, 'kubernetesEdgeAgentAsync')
 | 
			
		||||
            }
 | 
			
		||||
            commands={[{ ...commandsTabs.k8sLinux, label: 'Linux' }]}
 | 
			
		||||
            containerEngine={ContainerEngine.Kubernetes}
 | 
			
		||||
          />
 | 
			
		||||
        );
 | 
			
		||||
      case 'kubeconfig':
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue