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/12191/merge
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