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
|
@ -375,7 +375,13 @@ func (handler *Handler) createEdgeAgentEndpoint(tx dataservices.DataStoreTx, pay
|
||||||
ID: portainer.EndpointID(endpointID),
|
ID: portainer.EndpointID(endpointID),
|
||||||
Name: payload.Name,
|
Name: payload.Name,
|
||||||
URL: portainerHost,
|
URL: portainerHost,
|
||||||
Type: portainer.EdgeAgentOnDockerEnvironment,
|
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,
|
ContainerEngine: payload.ContainerEngine,
|
||||||
GroupID: portainer.EndpointGroupID(payload.GroupID),
|
GroupID: portainer.EndpointGroupID(payload.GroupID),
|
||||||
Gpus: payload.Gpus,
|
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">
|
<information-panel ng-if="state.kubernetesEndpoint && (!state.edgeEndpoint || state.edgeAssociated)" title-text="Kubernetes features configuration">
|
||||||
<span class="small text-muted vertical-center">
|
<span class="small text-muted vertical-center">
|
||||||
<pr-icon icon="'wrench'" mode="'primary'"></pr-icon>
|
<pr-icon icon="'wrench'" mode="'primary'"></pr-icon>
|
||||||
|
<div>
|
||||||
You should configure the features available in this Kubernetes environment in the
|
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.
|
<a ui-sref="kubernetes.cluster.setup({endpointId: endpoint.Id})">Kubernetes configuration</a> view.
|
||||||
|
</div>
|
||||||
</span>
|
</span>
|
||||||
</information-panel>
|
</information-panel>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -205,6 +205,8 @@ export enum EnvironmentCreationTypes {
|
||||||
export enum ContainerEngine {
|
export enum ContainerEngine {
|
||||||
Docker = 'docker',
|
Docker = 'docker',
|
||||||
Podman = 'podman',
|
Podman = 'podman',
|
||||||
|
// an empty container engine means that the endpoint is a Kubernetes endpoint
|
||||||
|
Kubernetes = '',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum PlatformType {
|
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 { Zap, UploadCloud } from 'lucide-react';
|
||||||
import _ from 'lodash';
|
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 { commandsTabs } from '@/react/edge/components/EdgeScriptForm/scripts';
|
||||||
import { FeatureId } from '@/react/portainer/feature-flags/enums';
|
import { FeatureId } from '@/react/portainer/feature-flags/enums';
|
||||||
import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
|
import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
|
||||||
|
@ -98,6 +101,7 @@ export function WizardKubernetes({ onCreate }: Props) {
|
||||||
onCreate(environment, 'kubernetesEdgeAgentStandard')
|
onCreate(environment, 'kubernetesEdgeAgentStandard')
|
||||||
}
|
}
|
||||||
commands={[{ ...commandsTabs.k8sLinux, label: 'Linux' }]}
|
commands={[{ ...commandsTabs.k8sLinux, label: 'Linux' }]}
|
||||||
|
containerEngine={ContainerEngine.Kubernetes}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 'edgeAgentAsync':
|
case 'edgeAgentAsync':
|
||||||
|
@ -108,6 +112,7 @@ export function WizardKubernetes({ onCreate }: Props) {
|
||||||
onCreate(environment, 'kubernetesEdgeAgentAsync')
|
onCreate(environment, 'kubernetesEdgeAgentAsync')
|
||||||
}
|
}
|
||||||
commands={[{ ...commandsTabs.k8sLinux, label: 'Linux' }]}
|
commands={[{ ...commandsTabs.k8sLinux, label: 'Linux' }]}
|
||||||
|
containerEngine={ContainerEngine.Kubernetes}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 'kubeconfig':
|
case 'kubeconfig':
|
||||||
|
|
Loading…
Reference in New Issue