diff --git a/Makefile b/Makefile index e1fd32d84..5dab6cdad 100644 --- a/Makefile +++ b/Makefile @@ -85,6 +85,8 @@ dev-client: ## Run the client in development mode dev-server: build-server ## Run the server in development mode @./dev/run_container.sh +dev-server-podman: build-server ## Run the server in development mode + @./dev/run_container_podman.sh ##@ Format .PHONY: format format-client format-server diff --git a/api/datastore/test_data/output_24_to_latest.json b/api/datastore/test_data/output_24_to_latest.json index 3f9e0d46c..54107d616 100644 --- a/api/datastore/test_data/output_24_to_latest.json +++ b/api/datastore/test_data/output_24_to_latest.json @@ -38,6 +38,7 @@ "TenantID": "" }, "ComposeSyntaxMaxVersion": "", + "ContainerEngine": "", "Edge": { "AsyncMode": false, "CommandInterval": 0, diff --git a/api/http/handler/endpoints/endpoint_create.go b/api/http/handler/endpoints/endpoint_create.go index c1ef2f393..1c6415023 100644 --- a/api/http/handler/endpoints/endpoint_create.go +++ b/api/http/handler/endpoints/endpoint_create.go @@ -40,6 +40,7 @@ type endpointCreatePayload struct { AzureAuthenticationKey string TagIDs []portainer.TagID EdgeCheckinInterval int + ContainerEngine string } type endpointCreationEnum int @@ -66,6 +67,11 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error { } payload.EndpointCreationType = endpointCreationEnum(endpointCreationType) + payload.ContainerEngine, err = request.RetrieveMultiPartFormValue(r, "ContainerEngine", true) + if err != nil || (payload.ContainerEngine != "" && payload.ContainerEngine != portainer.ContainerEngineDocker && payload.ContainerEngine != portainer.ContainerEnginePodman) { + return errors.New("invalid container engine value. Value must be one of: 'docker' or 'podman'") + } + groupID, _ := request.RetrieveNumericMultiPartFormValue(r, "GroupID", true) if groupID == 0 { groupID = 1 @@ -186,6 +192,7 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error { // @produce json // @param Name formData string true "Name that will be used to identify this environment(endpoint) (example: my-environment)" // @param EndpointCreationType formData integer true "Environment(Endpoint) type. Value must be one of: 1 (Local Docker environment), 2 (Agent environment), 3 (Azure environment), 4 (Edge agent environment) or 5 (Local Kubernetes Environment)" Enum(1,2,3,4,5) +// @param ContainerEngine formData string false "Container engine used by the environment(endpoint). Value must be one of: 'docker' or 'podman'" // @param URL formData string false "URL or IP address of a Docker host (example: docker.mydomain.tld:2375). Defaults to local if not specified (Linux: /var/run/docker.sock, Windows: //./pipe/docker_engine). Cannot be empty if EndpointCreationType is set to 4 (Edge agent environment)" // @param PublicURL formData string false "URL or IP address where exposed containers will be reachable. Defaults to URL if not specified (example: docker.mydomain.tld:2375)" // @param GroupID formData int false "Environment(Endpoint) group identifier. If not specified will default to 1 (unassigned)." @@ -371,12 +378,13 @@ 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, - GroupID: portainer.EndpointGroupID(payload.GroupID), - Gpus: payload.Gpus, + ID: portainer.EndpointID(endpointID), + Name: payload.Name, + URL: portainerHost, + Type: portainer.EdgeAgentOnDockerEnvironment, + ContainerEngine: payload.ContainerEngine, + GroupID: portainer.EndpointGroupID(payload.GroupID), + Gpus: payload.Gpus, TLSConfig: portainer.TLSConfiguration{ TLS: false, }, @@ -424,13 +432,14 @@ func (handler *Handler) createUnsecuredEndpoint(tx dataservices.DataStoreTx, pay endpointID := tx.Endpoint().GetNextIdentifier() endpoint := &portainer.Endpoint{ - ID: portainer.EndpointID(endpointID), - Name: payload.Name, - URL: payload.URL, - Type: endpointType, - GroupID: portainer.EndpointGroupID(payload.GroupID), - PublicURL: payload.PublicURL, - Gpus: payload.Gpus, + ID: portainer.EndpointID(endpointID), + Name: payload.Name, + URL: payload.URL, + Type: endpointType, + ContainerEngine: payload.ContainerEngine, + GroupID: portainer.EndpointGroupID(payload.GroupID), + PublicURL: payload.PublicURL, + Gpus: payload.Gpus, TLSConfig: portainer.TLSConfiguration{ TLS: false, }, @@ -486,13 +495,14 @@ func (handler *Handler) createKubernetesEndpoint(tx dataservices.DataStoreTx, pa func (handler *Handler) createTLSSecuredEndpoint(tx dataservices.DataStoreTx, payload *endpointCreatePayload, endpointType portainer.EndpointType, agentVersion string) (*portainer.Endpoint, *httperror.HandlerError) { endpointID := tx.Endpoint().GetNextIdentifier() endpoint := &portainer.Endpoint{ - ID: portainer.EndpointID(endpointID), - Name: payload.Name, - URL: payload.URL, - Type: endpointType, - GroupID: portainer.EndpointGroupID(payload.GroupID), - PublicURL: payload.PublicURL, - Gpus: payload.Gpus, + ID: portainer.EndpointID(endpointID), + Name: payload.Name, + URL: payload.URL, + Type: endpointType, + ContainerEngine: payload.ContainerEngine, + GroupID: portainer.EndpointGroupID(payload.GroupID), + PublicURL: payload.PublicURL, + Gpus: payload.Gpus, TLSConfig: portainer.TLSConfiguration{ TLS: payload.TLS, TLSSkipVerify: payload.TLSSkipVerify, diff --git a/api/portainer.go b/api/portainer.go index 91a77a44b..b3677f3de 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -373,6 +373,8 @@ type ( Name string `json:"Name" example:"my-environment"` // Environment(Endpoint) environment(endpoint) type. 1 for a Docker environment(endpoint), 2 for an agent on Docker environment(endpoint) or 3 for an Azure environment(endpoint). Type EndpointType `json:"Type" example:"1"` + // ContainerEngine represents the container engine type. This can be 'docker' or 'podman' when interacting directly with these environmentes, otherwise '' for kubernetes environments. + ContainerEngine string `json:"ContainerEngine" example:"docker"` // URL or IP address of the Docker host associated to this environment(endpoint) URL string `json:"URL" example:"docker.mydomain.tld:2375"` // Environment(Endpoint) group identifier @@ -1727,7 +1729,7 @@ const ( const ( _ EndpointType = iota - // DockerEnvironment represents an environment(endpoint) connected to a Docker environment(endpoint) + // DockerEnvironment represents an environment(endpoint) connected to a Docker environment(endpoint) via the Docker API or Socket DockerEnvironment // AgentOnDockerEnvironment represents an environment(endpoint) connected to a Portainer agent deployed on a Docker environment(endpoint) AgentOnDockerEnvironment @@ -2113,3 +2115,8 @@ const ( PerDevConfigsTypeFile PerDevConfigsFilterType = "file" PerDevConfigsTypeDir PerDevConfigsFilterType = "dir" ) + +const ( + ContainerEngineDocker = "docker" + ContainerEnginePodman = "podman" +) diff --git a/api/stacks/deployments/deployer_remote.go b/api/stacks/deployments/deployer_remote.go index fb56c25f7..9d7d5a59a 100644 --- a/api/stacks/deployments/deployer_remote.go +++ b/api/stacks/deployments/deployer_remote.go @@ -184,7 +184,9 @@ func (d *stackDeployer) remoteStack(stack *portainer.Stack, endpoint *portainer. if err != nil { return errors.Wrap(err, "unable to get agent info") } - targetSocketBind := getTargetSocketBind(info.OSType) + // ensure the targetSocketBindHost is changed to podman for podman environments + targetSocketBindHost := getTargetSocketBindHost(info.OSType, endpoint.ContainerEngine) + targetSocketBindContainer := getTargetSocketBindContainer(info.OSType) composeDestination := filesystem.JoinPaths(stack.ProjectPath, composePathPrefix) @@ -206,7 +208,7 @@ func (d *stackDeployer) remoteStack(stack *portainer.Stack, endpoint *portainer. }, &container.HostConfig{ Binds: []string{ fmt.Sprintf("%s:%s", composeDestination, composeDestination), - fmt.Sprintf("%s:%s", targetSocketBind, targetSocketBind), + fmt.Sprintf("%s:%s", targetSocketBindHost, targetSocketBindContainer), }, }, nil, nil, fmt.Sprintf("portainer-unpacker-%d-%s-%d", stack.ID, stack.Name, rand.Intn(100))) @@ -327,7 +329,19 @@ func getUnpackerImage() string { return image } -func getTargetSocketBind(osType string) string { +func getTargetSocketBindHost(osType string, containerEngine string) string { + targetSocketBind := "//./pipe/docker_engine" + if strings.EqualFold(osType, "linux") { + if containerEngine == portainer.ContainerEnginePodman { + targetSocketBind = "/run/podman/podman.sock" + } else { + targetSocketBind = "/var/run/docker.sock" + } + } + return targetSocketBind +} + +func getTargetSocketBindContainer(osType string) string { targetSocketBind := "//./pipe/docker_engine" if strings.EqualFold(osType, "linux") { targetSocketBind = "/var/run/docker.sock" diff --git a/app/assets/css/bootstrap-override.css b/app/assets/css/bootstrap-override.css index 8064cee07..19e63397d 100644 --- a/app/assets/css/bootstrap-override.css +++ b/app/assets/css/bootstrap-override.css @@ -203,7 +203,7 @@ input:checked + .slider:before { /* Widget */ .widget .widget-icon { - @apply mr-1 !p-2 text-lg; + @apply mr-1 !p-2 text-lg flex-none; @apply bg-blue-3 text-blue-8; @apply th-dark:bg-gray-9 th-dark:text-blue-3; diff --git a/app/assets/ico/docker-edge-environment.svg b/app/assets/ico/docker-edge-environment.svg new file mode 100644 index 000000000..b53383825 --- /dev/null +++ b/app/assets/ico/docker-edge-environment.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/assets/ico/kubernetes-edge-environment.svg b/app/assets/ico/kubernetes-edge-environment.svg new file mode 100644 index 000000000..880d85d03 --- /dev/null +++ b/app/assets/ico/kubernetes-edge-environment.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/app/assets/ico/podman-edge-environment.svg b/app/assets/ico/podman-edge-environment.svg new file mode 100644 index 000000000..1157684dc --- /dev/null +++ b/app/assets/ico/podman-edge-environment.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/assets/ico/vendor/podman-icon.svg b/app/assets/ico/vendor/podman-icon.svg new file mode 100644 index 000000000..05d314417 --- /dev/null +++ b/app/assets/ico/vendor/podman-icon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/app/assets/ico/vendor/podman.svg b/app/assets/ico/vendor/podman.svg new file mode 100644 index 000000000..4b0af515f --- /dev/null +++ b/app/assets/ico/vendor/podman.svg @@ -0,0 +1 @@ + diff --git a/app/assets/images/edge_endpoint.png b/app/assets/images/edge_endpoint.png deleted file mode 100644 index 1bd096746..000000000 Binary files a/app/assets/images/edge_endpoint.png and /dev/null differ diff --git a/app/assets/images/kubernetes_edge_endpoint.png b/app/assets/images/kubernetes_edge_endpoint.png deleted file mode 100644 index d4d21f4f8..000000000 Binary files a/app/assets/images/kubernetes_edge_endpoint.png and /dev/null differ diff --git a/app/docker/models/containerStats.ts b/app/docker/models/containerStats.ts index 547a3f311..5f0d9cf6d 100644 --- a/app/docker/models/containerStats.ts +++ b/app/docker/models/containerStats.ts @@ -47,6 +47,14 @@ export class ContainerStatsViewModel { this.NumProcs = data.num_procs || 0; this.isWindows = true; } + // Podman has memory limit and usage but not stats + else if ( + data?.memory_stats?.usage !== undefined && + data?.memory_stats?.stats === undefined + ) { + this.MemoryUsage = data.memory_stats.usage || 0; + this.MemoryCache = 0; + } // Linux else if ( data?.memory_stats?.stats === undefined || diff --git a/app/docker/react/components/index.ts b/app/docker/react/components/index.ts index d1116bda3..df725f32f 100644 --- a/app/docker/react/components/index.ts +++ b/app/docker/react/components/index.ts @@ -118,7 +118,7 @@ const ngModule = angular ) .component( 'dockerContainerProcessesDatatable', - r2a(ProcessesDatatable, ['dataset', 'headers']) + r2a(withUIRouter(withReactQuery(withCurrentUser(ProcessesDatatable))), []) ) .component('dockerEventsDatatable', r2a(EventsDatatable, ['dataset'])) .component( diff --git a/app/docker/services/containerService.js b/app/docker/services/containerService.js index 88d30f89b..2e284be3d 100644 --- a/app/docker/services/containerService.js +++ b/app/docker/services/containerService.js @@ -17,7 +17,7 @@ import { resizeTTY } from '@/react/docker/containers/queries/useContainerResizeT import { updateContainer } from '@/react/docker/containers/queries/useUpdateContainer'; import { createExec } from '@/react/docker/containers/queries/useCreateExecMutation'; import { containerStats } from '@/react/docker/containers/queries/useContainerStats'; -import { containerTop } from '@/react/docker/containers/queries/useContainerTop'; +import { getContainerTop } from '@/react/docker/containers/queries/useContainerTop'; import { ContainerDetailsViewModel } from '../models/containerDetails'; import { ContainerStatsViewModel } from '../models/containerStats'; @@ -45,7 +45,7 @@ function ContainerServiceFactory(AngularToReact) { updateRestartPolicy: useAxios(updateRestartPolicyAngularJS), // container edit createExec: useAxios(createExec), // container console containerStats: useAxios(containerStatsAngularJS), // container stats - containerTop: useAxios(containerTop), // container stats + containerTop: useAxios(getContainerTop), // container stats inspect: useAxios(getContainer), // container inspect logs: useAxios(containerLogsAngularJS), // container logs }; diff --git a/app/docker/services/pluginService.js b/app/docker/services/pluginService.js index b0d0d5e4f..e1df43a14 100644 --- a/app/docker/services/pluginService.js +++ b/app/docker/services/pluginService.js @@ -1,6 +1,6 @@ import { isFulfilled } from '@/portainer/helpers/promise-utils'; import { getInfo } from '@/react/docker/proxy/queries/useInfo'; -import { aggregateData, getPlugins } from '@/react/docker/proxy/queries/useServicePlugins'; +import { aggregateData, getPlugins } from '@/react/docker/proxy/queries/usePlugins'; angular.module('portainer.docker').factory('PluginService', PluginServiceFactory); diff --git a/app/docker/views/containers/edit/container.html b/app/docker/views/containers/edit/container.html index 8d42fa32b..3d86871cc 100644 --- a/app/docker/views/containers/edit/container.html +++ b/app/docker/views/containers/edit/container.html @@ -43,11 +43,12 @@ Remove -
+
- + Duplicate/Edit @@ -218,7 +225,7 @@
- + diff --git a/app/docker/views/images/build/buildimage.html b/app/docker/views/images/build/buildimage.html index 28d60ce02..6d5446944 100644 --- a/app/docker/views/images/build/buildimage.html +++ b/app/docker/views/images/build/buildimage.html @@ -30,7 +30,7 @@
A name must be specified in one of the following formats: name:tag, repository/name:tag or - registryfqdn:port/repository/name:tag format. If you omit the tag the default latest value is assumed. + registry:port/repository/name:tag format. If you omit the tag the default latest value is assumed.
diff --git a/app/docker/views/images/edit/image.html b/app/docker/views/images/edit/image.html index 8e5eac87b..7bee83a2b 100644 --- a/app/docker/views/images/edit/image.html +++ b/app/docker/views/images/edit/image.html @@ -86,7 +86,7 @@
- +
@@ -103,7 +103,7 @@ - +
IDID {{ image.Id }} - +
{{ k }}{{ k }} {{ v }}
diff --git a/app/docker/views/images/edit/imageController.js b/app/docker/views/images/edit/imageController.js index a461d7bb1..4b6bb51da 100644 --- a/app/docker/views/images/edit/imageController.js +++ b/app/docker/views/images/edit/imageController.js @@ -2,6 +2,7 @@ import _ from 'lodash-es'; import { PorImageRegistryModel } from 'Docker/models/porImageRegistry'; import { confirmImageExport } from '@/react/docker/images/common/ConfirmExportModal'; import { confirmDelete } from '@@/modals/confirm'; +import { fullURIIntoRepoAndTag } from '@/react/docker/images/utils'; angular.module('portainer.docker').controller('ImageController', [ '$async', @@ -71,8 +72,9 @@ angular.module('portainer.docker').controller('ImageController', [ const registryModel = $scope.formValues.RegistryModel; const image = ImageHelper.createImageConfigForContainer(registryModel); + const { repo, tag } = fullURIIntoRepoAndTag(image.fromImage); - ImageService.tagImage($transition$.params().id, image.fromImage) + ImageService.tagImage($transition$.params().id, repo, tag) .then(function success() { Notifications.success('Success', 'Image successfully tagged'); $state.go('docker.images.image', { id: $transition$.params().id }, { reload: true }); diff --git a/app/docker/views/images/import/importImageController.js b/app/docker/views/images/import/importImageController.js index a6449976e..dfdb8ab1a 100644 --- a/app/docker/views/images/import/importImageController.js +++ b/app/docker/views/images/import/importImageController.js @@ -1,15 +1,17 @@ import { PorImageRegistryModel } from 'Docker/models/porImageRegistry'; +import { fullURIIntoRepoAndTag } from '@/react/docker/images/utils'; angular.module('portainer.docker').controller('ImportImageController', [ '$scope', '$state', + '$async', 'ImageService', 'Notifications', 'HttpRequestHelper', 'Authentication', 'ImageHelper', 'endpoint', - function ($scope, $state, ImageService, Notifications, HttpRequestHelper, Authentication, ImageHelper, endpoint) { + function ($scope, $state, $async, ImageService, Notifications, HttpRequestHelper, Authentication, ImageHelper, endpoint) { $scope.state = { actionInProgress: false, }; @@ -33,15 +35,20 @@ angular.module('portainer.docker').controller('ImportImageController', [ const registryModel = $scope.formValues.RegistryModel; if (registryModel.Image) { const image = ImageHelper.createImageConfigForContainer(registryModel); + const { repo, tag } = fullURIIntoRepoAndTag(image.fromImage); try { - await ImageService.tagImage(id, image.fromImage); + await ImageService.tagImage(id, repo, tag); } catch (err) { Notifications.error('Failure', err, 'Unable to tag image'); } } } - $scope.uploadImage = async function () { + $scope.uploadImage = function () { + return $async(uploadImageAsync); + }; + + async function uploadImageAsync() { $scope.state.actionInProgress = true; var nodeName = $scope.formValues.NodeName; @@ -52,7 +59,8 @@ angular.module('portainer.docker').controller('ImportImageController', [ if (data.error) { Notifications.error('Failure', data.error, 'Unable to upload image'); } else if (data.stream) { - var regex = /Loaded.*?: (.*?)\n$/g; + // docker has /n at the end of the stream, podman doesn't + var regex = /Loaded.*?: (.*?)(?:\n|$)/g; var imageIds = regex.exec(data.stream); if (imageIds && imageIds.length == 2) { await tagImage(imageIds[1]); @@ -67,6 +75,6 @@ angular.module('portainer.docker').controller('ImportImageController', [ } finally { $scope.state.actionInProgress = false; } - }; + } }, ]); diff --git a/app/docker/views/images/import/importimage.html b/app/docker/views/images/import/importimage.html index cda5dce97..9780dc900 100644 --- a/app/docker/views/images/import/importimage.html +++ b/app/docker/views/images/import/importimage.html @@ -12,7 +12,7 @@
- + {{ formValues.UploadFile.name }} @@ -27,7 +27,7 @@
-
+
@@ -51,7 +51,7 @@
{{ volume.Id }} + diff --git a/app/docker/views/volumes/edit/volumeController.js b/app/docker/views/volumes/edit/volumeController.js index 67eae5108..b5e38213c 100644 --- a/app/docker/views/volumes/edit/volumeController.js +++ b/app/docker/views/volumes/edit/volumeController.js @@ -9,10 +9,12 @@ angular.module('portainer.docker').controller('VolumeController', [ 'ContainerService', 'Notifications', 'HttpRequestHelper', + 'Authentication', 'endpoint', - function ($scope, $state, $transition$, VolumeService, ContainerService, Notifications, HttpRequestHelper, endpoint) { + function ($scope, $state, $transition$, VolumeService, ContainerService, Notifications, HttpRequestHelper, Authentication, endpoint) { $scope.resourceType = ResourceControlType.Volume; $scope.endpoint = endpoint; + $scope.showBrowseAction = false; $scope.onUpdateResourceControlSuccess = function () { $state.reload(); @@ -41,6 +43,7 @@ angular.module('portainer.docker').controller('VolumeController', [ function initView() { HttpRequestHelper.setPortainerAgentTargetHeader($transition$.params().nodeName); + $scope.showBrowseAction = $scope.applicationState.endpoint.mode.agentProxy && (Authentication.isAdmin() || endpoint.SecuritySettings.allowVolumeBrowserForRegularUsers); VolumeService.volume($transition$.params().id) .then(function success(data) { diff --git a/app/portainer/filters/filters.js b/app/portainer/filters/filters.js index 5e75f9d7b..bdaadd247 100644 --- a/app/portainer/filters/filters.js +++ b/app/portainer/filters/filters.js @@ -1,12 +1,6 @@ import moment from 'moment'; import _ from 'lodash-es'; import filesize from 'filesize'; -import { Cloud } from 'lucide-react'; - -import Kube from '@/assets/ico/kube.svg?c'; -import DockerIcon from '@/assets/ico/vendor/docker-icon.svg?c'; -import MicrosoftIcon from '@/assets/ico/vendor/microsoft-icon.svg?c'; -import { EnvironmentType } from '@/react/portainer/environments/types'; export function truncateLeftRight(text, max, left, right) { max = isNaN(max) ? 50 : max; @@ -105,24 +99,6 @@ export function endpointTypeName(type) { return ''; } -export function environmentTypeIcon(type) { - switch (type) { - case EnvironmentType.Azure: - return MicrosoftIcon; - case EnvironmentType.EdgeAgentOnDocker: - return Cloud; - case EnvironmentType.AgentOnKubernetes: - case EnvironmentType.EdgeAgentOnKubernetes: - case EnvironmentType.KubernetesLocal: - return Kube; - case EnvironmentType.AgentOnDocker: - case EnvironmentType.Docker: - return DockerIcon; - default: - throw new Error(`type ${type}-${EnvironmentType[type]} is not supported`); - } -} - export function truncate(text, length, end) { if (isNaN(length)) { length = 10; diff --git a/app/portainer/filters/index.js b/app/portainer/filters/index.js index 0e817d29f..d261caed6 100644 --- a/app/portainer/filters/index.js +++ b/app/portainer/filters/index.js @@ -4,7 +4,6 @@ import _ from 'lodash-es'; import { ownershipIcon } from '@/react/docker/components/datatable/createOwnershipColumn'; import { arrayToStr, - environmentTypeIcon, endpointTypeName, getPairKey, getPairValue, @@ -34,5 +33,4 @@ angular .filter('arraytostr', () => arrayToStr) .filter('labelsToStr', () => labelsToStr) .filter('endpointtypename', () => endpointTypeName) - .filter('endpointtypeicon', () => environmentTypeIcon) .filter('ownershipicon', () => ownershipIcon); diff --git a/app/portainer/views/endpoints/edit/endpointController.js b/app/portainer/views/endpoints/edit/endpointController.js index 4b8d3d585..3658a6c13 100644 --- a/app/portainer/views/endpoints/edit/endpointController.js +++ b/app/portainer/views/endpoints/edit/endpointController.js @@ -53,7 +53,7 @@ function EndpointController( showAMTInfo: false, showTLSConfig: false, edgeScriptCommands: { - linux: _.compact([commandsTabs.k8sLinux, commandsTabs.swarmLinux, commandsTabs.standaloneLinux]), + linux: _.compact([commandsTabs.k8sLinux, commandsTabs.swarmLinux, commandsTabs.standaloneLinux, commandsTabs.podmanLinux]), win: [commandsTabs.swarmWindows, commandsTabs.standaloneWindow], }, }; @@ -297,7 +297,6 @@ function EndpointController( return $async(async () => { try { const [endpoint, groups, settings] = await Promise.all([EndpointService.endpoint($transition$.params().id), GroupService.groups(), SettingsService.settings()]); - if (isDockerAPIEnvironment(endpoint)) { $scope.state.showTLSConfig = true; } diff --git a/app/portainer/views/stacks/create/createstack.html b/app/portainer/views/stacks/create/createstack.html index c7e9ffd77..4ddd88261 100644 --- a/app/portainer/views/stacks/create/createstack.html +++ b/app/portainer/views/stacks/create/createstack.html @@ -162,7 +162,7 @@
)} diff --git a/app/react/edge/components/EdgeScriptForm/EdgeScriptSettingsFieldset.tsx b/app/react/edge/components/EdgeScriptForm/EdgeScriptSettingsFieldset.tsx index 3aed3f6bd..226886efa 100644 --- a/app/react/edge/components/EdgeScriptForm/EdgeScriptSettingsFieldset.tsx +++ b/app/react/edge/components/EdgeScriptForm/EdgeScriptSettingsFieldset.tsx @@ -57,6 +57,7 @@ export function EdgeScriptSettingsFieldset({ type="text" value={values.edgeIdGenerator} name="edgeIdGenerator" + placeholder="e.g. uuidgen" id="edge-id-generator-input" onChange={(e) => setFieldValue(e.target.name, e.target.value)} data-cy="edge-id-generator-input" @@ -81,7 +82,7 @@ export function EdgeScriptSettingsFieldset({ diff --git a/app/react/edge/components/EdgeScriptForm/scripts.ts b/app/react/edge/components/EdgeScriptForm/scripts.ts index 1f547502f..2e005b2fb 100644 --- a/app/react/edge/components/EdgeScriptForm/scripts.ts +++ b/app/react/edge/components/EdgeScriptForm/scripts.ts @@ -35,6 +35,11 @@ export const commandsTabs: Record = { label: 'Docker Standalone', command: buildLinuxStandaloneCommand, }, + podmanLinux: { + id: 'podman', + label: 'Podman', + command: buildLinuxPodmanCommand, + }, swarmWindows: { id: 'swarm', label: 'Docker Swarm', @@ -83,6 +88,45 @@ docker run -d \\ `; } +function buildLinuxPodmanCommand( + agentVersion: string, + edgeKey: string, + properties: ScriptFormValues, + useAsyncMode: boolean, + edgeId?: string, + agentSecret?: string +) { + const { allowSelfSignedCertificates, edgeIdGenerator, envVars } = properties; + + const env = buildDockerEnvVars(envVars, [ + ...buildDefaultDockerEnvVars( + edgeKey, + allowSelfSignedCertificates, + !edgeIdGenerator ? edgeId : undefined, + agentSecret, + useAsyncMode + ), + ...metaEnvVars(properties), + ]); + + return `${ + edgeIdGenerator ? `PORTAINER_EDGE_ID=$(${edgeIdGenerator}) \n\n` : '' + }\ +sudo systemctl enable --now podman.socket +sudo podman volume create portainer +sudo podman run -d \\ + -v /run/podman/podman.sock:/var/run/docker.sock \\ + -v /var/lib/containers/storage/volumes:/var/lib/docker/volumes \\ + -v /:/host \\ + -v portainer_agent_data:/data \\ + --restart always \\ + --privileged \\ + ${env} \\ + --name portainer_edge_agent \\ + portainer/agent:${agentVersion} + `; +} + export function buildWindowsStandaloneCommand( agentVersion: string, edgeKey: string, diff --git a/app/react/edge/components/EdgeScriptForm/types.ts b/app/react/edge/components/EdgeScriptForm/types.ts index 0e702655c..4113563e7 100644 --- a/app/react/edge/components/EdgeScriptForm/types.ts +++ b/app/react/edge/components/EdgeScriptForm/types.ts @@ -3,7 +3,7 @@ import { EnvironmentGroupId } from '@/react/portainer/environments/environment-g import { EdgeGroup } from '../../edge-groups/types'; -export type Platform = 'standalone' | 'swarm' | 'k8s'; +export type Platform = 'standalone' | 'swarm' | 'podman' | 'k8s'; export type OS = 'win' | 'linux'; export interface ScriptFormValues { diff --git a/app/react/edge/edge-devices/WaitingRoomView/WaitingRoomView.tsx b/app/react/edge/edge-devices/WaitingRoomView/WaitingRoomView.tsx index 0a6428611..062e1dc42 100644 --- a/app/react/edge/edge-devices/WaitingRoomView/WaitingRoomView.tsx +++ b/app/react/edge/edge-devices/WaitingRoomView/WaitingRoomView.tsx @@ -26,9 +26,15 @@ function WaitingRoomView() {
- Only environments generated from the AEEC script will appear here, - manually added environments and edge devices will bypass the - waiting room. + Only environments generated from the{' '} + + auto onboarding + {' '} + script will appear here, manually added environments and edge + devices will bypass the waiting room.
diff --git a/app/react/hooks/useEnvironmentId.ts b/app/react/hooks/useEnvironmentId.ts index 7ba1667b8..be75e2c54 100644 --- a/app/react/hooks/useEnvironmentId.ts +++ b/app/react/hooks/useEnvironmentId.ts @@ -2,6 +2,12 @@ import { useCurrentStateAndParams } from '@uirouter/react'; import { EnvironmentId } from '@/react/portainer/environments/types'; +/** + * useEnvironmentId is a hook that returns the environmentId from the url params. + * use only when endpointId is set in the path. + * for example: /kubernetes/clusters/:endpointId + * for `:id` paths, use a different hook + */ export function useEnvironmentId(force = true): EnvironmentId { const { params: { endpointId: environmentId }, diff --git a/app/react/portainer/HomeView/EnvironmentList/EnvironmentItem/EngineVersion.tsx b/app/react/portainer/HomeView/EnvironmentList/EnvironmentItem/EngineVersion.tsx index 727010574..83e337686 100644 --- a/app/react/portainer/HomeView/EnvironmentList/EnvironmentItem/EngineVersion.tsx +++ b/app/react/portainer/HomeView/EnvironmentList/EnvironmentItem/EngineVersion.tsx @@ -1,17 +1,25 @@ import { DockerSnapshot } from '@/react/docker/snapshots/types'; +import { useIsPodman } from '@/react/portainer/environments/queries/useIsPodman'; import { Environment, PlatformType, KubernetesSnapshot, } from '@/react/portainer/environments/types'; import { getPlatformType } from '@/react/portainer/environments/utils'; +import { getDockerEnvironmentType } from '@/react/portainer/environments/utils/getDockerEnvironmentType'; export function EngineVersion({ environment }: { environment: Environment }) { const platform = getPlatformType(environment.Type); + const isPodman = useIsPodman(environment.Id); switch (platform) { case PlatformType.Docker: - return ; + return ( + + ); case PlatformType.Kubernetes: return ( - {snapshot.Swarm ? 'Swarm' : 'Standalone'} {snapshot.DockerVersion} + {type} {snapshot.DockerVersion} ); } diff --git a/app/react/portainer/HomeView/EnvironmentList/EnvironmentItem/EnvironmentIcon.tsx b/app/react/portainer/HomeView/EnvironmentList/EnvironmentItem/EnvironmentIcon.tsx index f87458e72..8580baa04 100644 --- a/app/react/portainer/HomeView/EnvironmentList/EnvironmentItem/EnvironmentIcon.tsx +++ b/app/react/portainer/HomeView/EnvironmentList/EnvironmentItem/EnvironmentIcon.tsx @@ -1,43 +1,86 @@ -import { environmentTypeIcon } from '@/portainer/filters/filters'; -import dockerEdge from '@/assets/images/edge_endpoint.png'; +import { getEnvironmentTypeIcon } from '@/react/portainer/environments/utils'; +import dockerEdge from '@/assets/ico/docker-edge-environment.svg'; +import podmanEdge from '@/assets/ico/podman-edge-environment.svg'; import kube from '@/assets/images/kubernetes_endpoint.png'; -import kubeEdge from '@/assets/images/kubernetes_edge_endpoint.png'; -import { EnvironmentType } from '@/react/portainer/environments/types'; +import kubeEdge from '@/assets/ico/kubernetes-edge-environment.svg'; +import { + ContainerEngine, + EnvironmentType, +} from '@/react/portainer/environments/types'; import azure from '@/assets/ico/vendor/azure.svg'; import docker from '@/assets/ico/vendor/docker.svg'; +import podman from '@/assets/ico/vendor/podman.svg'; import { Icon } from '@@/Icon'; interface Props { type: EnvironmentType; + containerEngine?: ContainerEngine; } -export function EnvironmentIcon({ type }: Props) { +export function EnvironmentIcon({ type, containerEngine }: Props) { switch (type) { case EnvironmentType.AgentOnDocker: case EnvironmentType.Docker: + if (containerEngine === ContainerEngine.Podman) { + return ( + + ); + } return ( - + ); case EnvironmentType.Azure: return ( - + ); case EnvironmentType.EdgeAgentOnDocker: + if (containerEngine === ContainerEngine.Podman) { + return ( + + ); + } return ( - + ); case EnvironmentType.KubernetesLocal: case EnvironmentType.AgentOnKubernetes: - return ; + return ; case EnvironmentType.EdgeAgentOnKubernetes: return ( - + ); default: return ( ); diff --git a/app/react/portainer/HomeView/EnvironmentList/EnvironmentItem/EnvironmentItem.tsx b/app/react/portainer/HomeView/EnvironmentList/EnvironmentItem/EnvironmentItem.tsx index a09713c8f..48c939e6c 100644 --- a/app/react/portainer/HomeView/EnvironmentList/EnvironmentItem/EnvironmentItem.tsx +++ b/app/react/portainer/HomeView/EnvironmentList/EnvironmentItem/EnvironmentItem.tsx @@ -66,7 +66,10 @@ export function EnvironmentItem({ params={dashboardRoute.params} >
- +
diff --git a/app/react/portainer/HomeView/EnvironmentList/EnvironmentList.tsx b/app/react/portainer/HomeView/EnvironmentList/EnvironmentList.tsx index 57cd47312..e2c20111a 100644 --- a/app/react/portainer/HomeView/EnvironmentList/EnvironmentList.tsx +++ b/app/react/portainer/HomeView/EnvironmentList/EnvironmentList.tsx @@ -265,6 +265,12 @@ export function EnvironmentList({ onClickBrowse, onRefresh }: Props) { EnvironmentType.AgentOnDocker, EnvironmentType.EdgeAgentOnDocker, ], + // for podman keep the env type as docker (the containerEngine distinguishes podman from docker) + [PlatformType.Podman]: [ + EnvironmentType.Docker, + EnvironmentType.AgentOnDocker, + EnvironmentType.EdgeAgentOnDocker, + ], [PlatformType.Azure]: [EnvironmentType.Azure], [PlatformType.Kubernetes]: [ EnvironmentType.KubernetesLocal, diff --git a/app/react/portainer/HomeView/EnvironmentList/EnvironmentListFilters.tsx b/app/react/portainer/HomeView/EnvironmentList/EnvironmentListFilters.tsx index 0a64c7396..3c4d993e4 100644 --- a/app/react/portainer/HomeView/EnvironmentList/EnvironmentListFilters.tsx +++ b/app/react/portainer/HomeView/EnvironmentList/EnvironmentListFilters.tsx @@ -171,6 +171,13 @@ function getConnectionTypeOptions(platformTypes: PlatformType[]) { ConnectionType.EdgeAgentStandard, ConnectionType.EdgeAgentAsync, ], + [PlatformType.Podman]: [ + // api includes a socket connection, so keep this for podman + ConnectionType.API, + ConnectionType.Agent, + ConnectionType.EdgeAgentStandard, + ConnectionType.EdgeAgentAsync, + ], [PlatformType.Azure]: [ConnectionType.API], [PlatformType.Kubernetes]: [ ConnectionType.Agent, diff --git a/app/react/portainer/custom-templates/components/TemplateTypeSelector.tsx b/app/react/portainer/custom-templates/components/TemplateTypeSelector.tsx index 482fc1947..7016eec41 100644 --- a/app/react/portainer/custom-templates/components/TemplateTypeSelector.tsx +++ b/app/react/portainer/custom-templates/components/TemplateTypeSelector.tsx @@ -5,7 +5,7 @@ import { Select } from '@@/form-components/Input'; const typeOptions = [ { label: 'Swarm', value: StackType.DockerSwarm }, - { label: 'Standalone', value: StackType.DockerCompose }, + { label: 'Standalone / Podman', value: StackType.DockerCompose }, ]; export function TemplateTypeSelector({ diff --git a/app/react/portainer/environments/ListView/columns/type.tsx b/app/react/portainer/environments/ListView/columns/type.tsx index 5f82deace..58d984317 100644 --- a/app/react/portainer/environments/ListView/columns/type.tsx +++ b/app/react/portainer/environments/ListView/columns/type.tsx @@ -1,28 +1,41 @@ import { CellContext } from '@tanstack/react-table'; -import { environmentTypeIcon } from '@/portainer/filters/filters'; import { - Environment, - EnvironmentType, -} from '@/react/portainer/environments/types'; -import { getPlatformTypeName } from '@/react/portainer/environments/utils'; + getEnvironmentTypeIcon, + getPlatformTypeName, +} from '@/react/portainer/environments/utils'; import { Icon } from '@@/Icon'; +import { EnvironmentListItem } from '../types'; +import { EnvironmentType, ContainerEngine } from '../../types'; + import { columnHelper } from './helper'; -export const type = columnHelper.accessor('Type', { - header: 'Type', - cell: Cell, -}); +type TypeCellContext = { + type: EnvironmentType; + containerEngine?: ContainerEngine; +}; + +export const type = columnHelper.accessor( + (rowItem): TypeCellContext => ({ + type: rowItem.Type, + containerEngine: rowItem.ContainerEngine, + }), + { + header: 'Type', + cell: Cell, + id: 'Type', + } +); -function Cell({ getValue }: CellContext) { - const type = getValue(); +function Cell({ getValue }: CellContext) { + const { type, containerEngine } = getValue(); return ( - - {getPlatformTypeName(type)} + + {getPlatformTypeName(type, containerEngine)} ); } diff --git a/app/react/portainer/environments/environment.service/create.ts b/app/react/portainer/environments/environment.service/create.ts index a2cd00c62..f07a62397 100644 --- a/app/react/portainer/environments/environment.service/create.ts +++ b/app/react/portainer/environments/environment.service/create.ts @@ -7,7 +7,11 @@ import { type EnvironmentGroupId } from '@/react/portainer/environments/environm import { type TagId } from '@/portainer/tags/types'; import { EdgeAsyncIntervalsValues } from '@/react/edge/components/EdgeAsyncIntervalsForm'; -import { type Environment, EnvironmentCreationTypes } from '../types'; +import { + type Environment, + ContainerEngine, + EnvironmentCreationTypes, +} from '../types'; import { buildUrl } from './utils'; @@ -21,6 +25,7 @@ interface CreateLocalDockerEnvironment { socketPath?: string; publicUrl?: string; meta?: EnvironmentMetadata; + containerEngine?: ContainerEngine; } export async function createLocalDockerEnvironment({ @@ -28,6 +33,7 @@ export async function createLocalDockerEnvironment({ socketPath = '', publicUrl = '', meta = { tagIds: [] }, + containerEngine, }: CreateLocalDockerEnvironment) { const url = prefixPath(socketPath); @@ -38,6 +44,7 @@ export async function createLocalDockerEnvironment({ url, publicUrl, meta, + containerEngine, } ); @@ -115,6 +122,7 @@ export interface EnvironmentOptions { pollFrequency?: number; edge?: EdgeSettings; tunnelServerAddr?: string; + containerEngine?: ContainerEngine; } interface CreateRemoteEnvironment { @@ -125,6 +133,7 @@ interface CreateRemoteEnvironment { >; url: string; options?: Omit; + containerEngine?: ContainerEngine; } export async function createRemoteEnvironment({ @@ -143,11 +152,13 @@ export interface CreateAgentEnvironmentValues { name: string; environmentUrl: string; meta: EnvironmentMetadata; + containerEngine?: ContainerEngine; } export function createAgentEnvironment({ name, environmentUrl, + containerEngine = ContainerEngine.Docker, meta = { tagIds: [] }, }: CreateAgentEnvironmentValues) { return createRemoteEnvironment({ @@ -160,6 +171,7 @@ export function createAgentEnvironment({ skipVerify: true, skipClientVerify: true, }, + containerEngine, }, }); } @@ -171,6 +183,7 @@ interface CreateEdgeAgentEnvironment { meta?: EnvironmentMetadata; pollFrequency: number; edge: EdgeSettings; + containerEngine: ContainerEngine; } export function createEdgeAgentEnvironment({ @@ -179,6 +192,7 @@ export function createEdgeAgentEnvironment({ meta = { tagIds: [] }, pollFrequency, edge, + containerEngine, }: CreateEdgeAgentEnvironment) { return createEnvironment( name, @@ -192,6 +206,7 @@ export function createEdgeAgentEnvironment({ pollFrequency, edge, meta, + containerEngine, } ); } @@ -207,7 +222,8 @@ async function createEnvironment( }; if (options) { - const { groupId, tagIds = [] } = options.meta || {}; + const { tls, azure, meta, containerEngine } = options; + const { groupId, tagIds = [] } = meta || {}; payload = { ...payload, @@ -216,10 +232,9 @@ async function createEnvironment( GroupID: groupId, TagIds: arrayToJson(tagIds), EdgeCheckinInterval: options.pollFrequency, + ContainerEngine: containerEngine, }; - const { tls, azure } = options; - if (tls) { payload = { ...payload, diff --git a/app/react/portainer/environments/queries/useIsPodman.ts b/app/react/portainer/environments/queries/useIsPodman.ts new file mode 100644 index 000000000..12f272012 --- /dev/null +++ b/app/react/portainer/environments/queries/useIsPodman.ts @@ -0,0 +1,15 @@ +import { ContainerEngine, EnvironmentId } from '../types'; + +import { useEnvironment } from './useEnvironment'; + +/** + * useIsPodman returns true if the current environment is using podman as container engine. + * @returns isPodman boolean, can also be undefined if the environment hasn't loaded yet. + */ +export function useIsPodman(envId: EnvironmentId) { + const { data: isPodman } = useEnvironment( + envId, + (env) => env.ContainerEngine === ContainerEngine.Podman + ); + return isPodman; +} diff --git a/app/react/portainer/environments/types.ts b/app/react/portainer/environments/types.ts index 47b5a4284..ed87643cd 100644 --- a/app/react/portainer/environments/types.ts +++ b/app/react/portainer/environments/types.ts @@ -4,6 +4,9 @@ import { DockerSnapshot } from '@/react/docker/snapshots/types'; export type EnvironmentId = number; +/** + * matches portainer.EndpointType in app/portainer.go + */ export enum EnvironmentType { // Docker represents an environment(endpoint) connected to a Docker environment(endpoint) Docker = 1, @@ -124,6 +127,7 @@ export type Environment = { Agent: { Version: string }; Id: EnvironmentId; Type: EnvironmentType; + ContainerEngine?: ContainerEngine; TagIds: TagId[]; GroupId: EnvironmentGroupId; DeploymentOptions: DeploymentOptions | null; @@ -168,8 +172,14 @@ export enum EnvironmentCreationTypes { KubeConfigEnvironment, } +export enum ContainerEngine { + Docker = 'docker', + Podman = 'podman', +} + export enum PlatformType { Docker, Kubernetes, Azure, + Podman, } diff --git a/app/react/portainer/environments/utils/get-platform-icon.ts b/app/react/portainer/environments/utils/get-platform-icon.ts index c26231a05..a84e4ab63 100644 --- a/app/react/portainer/environments/utils/get-platform-icon.ts +++ b/app/react/portainer/environments/utils/get-platform-icon.ts @@ -1,8 +1,10 @@ import { getPlatformType } from '@/react/portainer/environments/utils'; import { + ContainerEngine, EnvironmentType, PlatformType, } from '@/react/portainer/environments/types'; +import Podman from '@/assets/ico/vendor/podman.svg?c'; import Docker from './docker.svg?c'; import Azure from './azure.svg?c'; @@ -12,12 +14,16 @@ const icons: { [key in PlatformType]: SvgrComponent; } = { [PlatformType.Docker]: Docker, + [PlatformType.Podman]: Podman, [PlatformType.Kubernetes]: Kubernetes, [PlatformType.Azure]: Azure, }; -export function getPlatformIcon(type: EnvironmentType) { - const platform = getPlatformType(type); +export function getPlatformIcon( + type: EnvironmentType, + containerEngine?: ContainerEngine +) { + const platform = getPlatformType(type, containerEngine); return icons[platform]; } diff --git a/app/react/portainer/environments/utils/getDockerEnvironmentType.ts b/app/react/portainer/environments/utils/getDockerEnvironmentType.ts new file mode 100644 index 000000000..7f1e8297a --- /dev/null +++ b/app/react/portainer/environments/utils/getDockerEnvironmentType.ts @@ -0,0 +1,6 @@ +export function getDockerEnvironmentType(isSwarm: boolean, isPodman?: boolean) { + if (isPodman) { + return 'Podman'; + } + return isSwarm ? 'Swarm' : 'Standalone'; +} diff --git a/app/react/portainer/environments/utils/index.ts b/app/react/portainer/environments/utils/index.ts index 2ae3185dc..35fc4982c 100644 --- a/app/react/portainer/environments/utils/index.ts +++ b/app/react/portainer/environments/utils/index.ts @@ -1,6 +1,21 @@ -import { Environment, EnvironmentType, PlatformType } from '../types'; +import { Cloud } from 'lucide-react'; -export function getPlatformType(envType: EnvironmentType) { +import Kube from '@/assets/ico/kube.svg?c'; +import PodmanIcon from '@/assets/ico/vendor/podman-icon.svg?c'; +import DockerIcon from '@/assets/ico/vendor/docker-icon.svg?c'; +import MicrosoftIcon from '@/assets/ico/vendor/microsoft-icon.svg?c'; + +import { + Environment, + EnvironmentType, + ContainerEngine, + PlatformType, +} from '../types'; + +export function getPlatformType( + envType: EnvironmentType, + containerEngine?: ContainerEngine +) { switch (envType) { case EnvironmentType.KubernetesLocal: case EnvironmentType.AgentOnKubernetes: @@ -9,6 +24,9 @@ export function getPlatformType(envType: EnvironmentType) { case EnvironmentType.Docker: case EnvironmentType.AgentOnDocker: case EnvironmentType.EdgeAgentOnDocker: + if (containerEngine === ContainerEngine.Podman) { + return PlatformType.Podman; + } return PlatformType.Docker; case EnvironmentType.Azure: return PlatformType.Azure; @@ -25,8 +43,11 @@ export function isKubernetesEnvironment(envType: EnvironmentType) { return getPlatformType(envType) === PlatformType.Kubernetes; } -export function getPlatformTypeName(envType: EnvironmentType): string { - return PlatformType[getPlatformType(envType)]; +export function getPlatformTypeName( + envType: EnvironmentType, + containerEngine?: ContainerEngine +): string { + return PlatformType[getPlatformType(envType, containerEngine)]; } export function isAgentEnvironment(envType: EnvironmentType) { @@ -104,3 +125,27 @@ export function getDashboardRoute(environment: Environment) { } } } + +export function getEnvironmentTypeIcon( + type: EnvironmentType, + containerEngine?: ContainerEngine +) { + switch (type) { + case EnvironmentType.Azure: + return MicrosoftIcon; + case EnvironmentType.EdgeAgentOnDocker: + return Cloud; + case EnvironmentType.AgentOnKubernetes: + case EnvironmentType.EdgeAgentOnKubernetes: + case EnvironmentType.KubernetesLocal: + return Kube; + case EnvironmentType.AgentOnDocker: + case EnvironmentType.Docker: + if (containerEngine === ContainerEngine.Podman) { + return PodmanIcon; + } + return DockerIcon; + default: + throw new Error(`type ${type}-${EnvironmentType[type]} is not supported`); + } +} diff --git a/app/react/portainer/environments/wizard/EnvironmentTypeSelectView/EndpointTypeView.tsx b/app/react/portainer/environments/wizard/EnvironmentTypeSelectView/EnvironmentTypeSelectView.tsx similarity index 96% rename from app/react/portainer/environments/wizard/EnvironmentTypeSelectView/EndpointTypeView.tsx rename to app/react/portainer/environments/wizard/EnvironmentTypeSelectView/EnvironmentTypeSelectView.tsx index ee4ad0ce6..6786ed8ad 100644 --- a/app/react/portainer/environments/wizard/EnvironmentTypeSelectView/EndpointTypeView.tsx +++ b/app/react/portainer/environments/wizard/EnvironmentTypeSelectView/EnvironmentTypeSelectView.tsx @@ -15,6 +15,7 @@ import { EnvironmentOptionValue, existingEnvironmentTypes, newEnvironmentTypes, + environmentTypes, } from './environment-types'; export function EnvironmentTypeSelectView() { @@ -65,6 +66,7 @@ export function EnvironmentTypeSelectView() { disabled={types.length === 0} data-cy="start-wizard-button" onClick={() => startWizard()} + className="!ml-0" > Start Wizard @@ -80,11 +82,6 @@ export function EnvironmentTypeSelectView() { return; } - const environmentTypes = [ - ...existingEnvironmentTypes, - ...newEnvironmentTypes, - ]; - const steps = _.compact( types.map((id) => environmentTypes.find((eType) => eType.id === id)) ); diff --git a/app/react/portainer/environments/wizard/EnvironmentTypeSelectView/environment-types.ts b/app/react/portainer/environments/wizard/EnvironmentTypeSelectView/environment-types.ts index 132554ba3..695feb53e 100644 --- a/app/react/portainer/environments/wizard/EnvironmentTypeSelectView/environment-types.ts +++ b/app/react/portainer/environments/wizard/EnvironmentTypeSelectView/environment-types.ts @@ -1,5 +1,6 @@ import { FeatureId } from '@/react/portainer/feature-flags/enums'; import Docker from '@/assets/ico/vendor/docker.svg?c'; +import Podman from '@/assets/ico/vendor/podman.svg?c'; import Kubernetes from '@/assets/ico/vendor/kubernetes.svg?c'; import Azure from '@/assets/ico/vendor/azure.svg?c'; import KaaS from '@/assets/ico/vendor/kaas-icon.svg?c'; @@ -10,6 +11,7 @@ import { BoxSelectorOption } from '@@/BoxSelector'; export type EnvironmentOptionValue = | 'dockerStandalone' | 'dockerSwarm' + | 'podman' | 'kubernetes' | 'aci' | 'kaas' @@ -20,7 +22,6 @@ export interface EnvironmentOption id: EnvironmentOptionValue; value: EnvironmentOptionValue; } - export const existingEnvironmentTypes: EnvironmentOption[] = [ { id: 'dockerStandalone', @@ -38,6 +39,14 @@ export const existingEnvironmentTypes: EnvironmentOption[] = [ iconType: 'logo', description: 'Connect to Docker Swarm via URL/IP, API or Socket', }, + { + id: 'podman', + value: 'podman', + label: 'Podman', + icon: Podman, + iconType: 'logo', + description: 'Connect to Podman via URL/IP or Socket', + }, { id: 'kubernetes', value: 'kubernetes', @@ -80,7 +89,7 @@ export const newEnvironmentTypes: EnvironmentOption[] = [ }, ]; -export const environmentTypes = [ +export const environmentTypes: EnvironmentOption[] = [ ...existingEnvironmentTypes, ...newEnvironmentTypes, ]; @@ -88,6 +97,7 @@ export const environmentTypes = [ export const formTitles: Record = { dockerStandalone: 'Connect to your Docker Standalone environment', dockerSwarm: 'Connect to your Docker Swarm environment', + podman: 'Connect to your Podman environment', kubernetes: 'Connect to your Kubernetes environment', aci: 'Connect to your ACI environment', kaas: 'Provision a KaaS environment', diff --git a/app/react/portainer/environments/wizard/EnvironmentTypeSelectView/index.ts b/app/react/portainer/environments/wizard/EnvironmentTypeSelectView/index.ts index 7bb47f3d9..5db681c12 100644 --- a/app/react/portainer/environments/wizard/EnvironmentTypeSelectView/index.ts +++ b/app/react/portainer/environments/wizard/EnvironmentTypeSelectView/index.ts @@ -1 +1 @@ -export { EnvironmentTypeSelectView } from './EndpointTypeView'; +export { EnvironmentTypeSelectView } from './EnvironmentTypeSelectView'; diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/EnvironmentsCreationView.module.css b/app/react/portainer/environments/wizard/EnvironmentsCreationView/EnvironmentsCreationView.module.css index 179949606..5ae1223e2 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/EnvironmentsCreationView.module.css +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/EnvironmentsCreationView.module.css @@ -7,7 +7,7 @@ .wizard-wrapper { display: grid; - grid-template-columns: 1fr 400px; + grid-template-columns: 2fr minmax(300px, 1fr); grid-template-areas: 'main sidebar' 'footer sidebar'; diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/EnvironmentsCreationView.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/EnvironmentsCreationView.tsx index ff9e24104..a56095bfc 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/EnvironmentsCreationView.tsx +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/EnvironmentsCreationView.tsx @@ -22,6 +22,7 @@ import { EnvironmentOptionValue, environmentTypes, formTitles, + EnvironmentOption, } from '../EnvironmentTypeSelectView/environment-types'; import { WizardDocker } from './WizardDocker'; @@ -30,6 +31,7 @@ import { WizardKubernetes } from './WizardKubernetes'; import { AnalyticsState, AnalyticsStateKey } from './types'; import styles from './EnvironmentsCreationView.module.css'; import { WizardEndpointsList } from './WizardEndpointsList'; +import { WizardPodman } from './WizardPodman'; export function EnvironmentCreationView() { const { @@ -161,7 +163,7 @@ function useParamEnvironmentTypes(): EnvironmentOptionValue[] { } function useStepper( - steps: (typeof environmentTypes)[number][], + steps: EnvironmentOption[][number][], onFinish: () => void ) { const [currentStepIndex, setCurrentStepIndex] = useState(0); @@ -197,6 +199,8 @@ function useStepper( case 'dockerStandalone': case 'dockerSwarm': return WizardDocker; + case 'podman': + return WizardPodman; case 'aci': return WizardAzure; case 'kubernetes': @@ -211,14 +215,18 @@ function useAnalyticsState() { const [analytics, setAnalyticsState] = useState({ dockerAgent: 0, dockerApi: 0, + dockerEdgeAgentAsync: 0, + dockerEdgeAgentStandard: 0, + podmanAgent: 0, + podmanEdgeAgentAsync: 0, + podmanEdgeAgentStandard: 0, + podmanLocalEnvironment: 0, kubernetesAgent: 0, kubernetesEdgeAgentAsync: 0, kubernetesEdgeAgentStandard: 0, kaasAgent: 0, aciApi: 0, localEndpoint: 0, - dockerEdgeAgentAsync: 0, - dockerEdgeAgentStandard: 0, }); return { analytics, setAnalytics }; diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/DeploymentScripts.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/DeploymentScripts.tsx index c747c8d2e..fcb5c2149 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/DeploymentScripts.tsx +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/DeploymentScripts.tsx @@ -4,6 +4,7 @@ import { CopyButton } from '@@/buttons/CopyButton'; import { Code } from '@@/Code'; import { NavTabs } from '@@/NavTabs'; import { NavContainer } from '@@/NavTabs/NavContainer'; +import { TextTip } from '@@/Tip/TextTip'; const deployments = [ { @@ -45,10 +46,10 @@ interface DeployCodeProps { function DeployCode({ code }: DeployCodeProps) { return ( <> - + When using the socket, ensure that you have started the Portainer container with the following Docker flag: - + {code}
diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/AgentTab/AgentTab.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/AgentTab/AgentTab.tsx index 7e9eac9d0..6cfe40fcc 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/AgentTab/AgentTab.tsx +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/AgentTab/AgentTab.tsx @@ -1,4 +1,7 @@ -import { Environment } from '@/react/portainer/environments/types'; +import { + ContainerEngine, + Environment, +} from '@/react/portainer/environments/types'; import { AgentForm } from '../../shared/AgentForm/AgentForm'; @@ -15,7 +18,10 @@ export function AgentTab({ onCreate, isDockerStandalone }: Props) {
- +
); diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/SocketTab/SocketForm.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/SocketTab/SocketForm.tsx index b8eeecb80..f8344ea96 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/SocketTab/SocketForm.tsx +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/SocketTab/SocketForm.tsx @@ -4,7 +4,10 @@ import { Plug2 } from 'lucide-react'; import { useCreateLocalDockerEnvironmentMutation } from '@/react/portainer/environments/queries/useCreateEnvironmentMutation'; import { notifySuccess } from '@/portainer/services/notifications'; -import { Environment } from '@/react/portainer/environments/types'; +import { + ContainerEngine, + Environment, +} from '@/react/portainer/environments/types'; import { LoadingButton } from '@@/buttons/LoadingButton'; import { FormControl } from '@@/form-components/FormControl'; @@ -19,9 +22,10 @@ import { FormValues } from './types'; interface Props { onCreate(environment: Environment): void; + containerEngine: ContainerEngine; } -export function SocketForm({ onCreate }: Props) { +export function SocketForm({ onCreate, containerEngine }: Props) { const [formKey, clearForm] = useReducer((state) => state + 1, 0); const initialValues: FormValues = { name: '', @@ -74,6 +78,7 @@ export function SocketForm({ onCreate }: Props) { name: values.name, socketPath: values.overridePath ? values.socketPath : '', meta: values.meta, + containerEngine, }, { onSuccess(environment) { diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/SocketTab/SocketTab.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/SocketTab/SocketTab.tsx index a93b0e253..32b0bdacb 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/SocketTab/SocketTab.tsx +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/SocketTab/SocketTab.tsx @@ -1,4 +1,7 @@ -import { Environment } from '@/react/portainer/environments/types'; +import { + ContainerEngine, + Environment, +} from '@/react/portainer/environments/types'; import { DeploymentScripts } from '../APITab/DeploymentScripts'; @@ -14,7 +17,10 @@ export function SocketTab({ onCreate }: Props) {
- +
); diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/WizardDocker.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/WizardDocker.tsx index 88867811f..5aab71935 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/WizardDocker.tsx +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/WizardDocker.tsx @@ -2,7 +2,10 @@ import { useState } from 'react'; import { Zap, Network, Plug2 } 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 { isBE } from '@/react/portainer/feature-flags/feature-flags.service'; import EdgeAgentStandardIcon from '@/react/edge/components/edge-agent-standard.svg?c'; @@ -64,6 +67,8 @@ const options: BoxSelectorOption< }, ]); +const containerEngine = ContainerEngine.Docker; + export function WizardDocker({ onCreate, isDockerStandalone }: Props) { const [creationType, setCreationType] = useState(options[0].value); @@ -135,6 +140,7 @@ export function WizardDocker({ onCreate, isDockerStandalone }: Props) { ? [commandsTabs.standaloneWindow] : [commandsTabs.swarmWindows], }} + containerEngine={containerEngine} /> ); case 'edgeAgentAsync': @@ -152,6 +158,7 @@ export function WizardDocker({ onCreate, isDockerStandalone }: Props) { ? [commandsTabs.standaloneWindow] : [commandsTabs.swarmWindows], }} + containerEngine={containerEngine} /> ); default: diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardEndpointsList/WizardEndpointsList.module.css b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardEndpointsList/WizardEndpointsList.module.css index 65feb01cc..c0d4b42ae 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardEndpointsList/WizardEndpointsList.module.css +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardEndpointsList/WizardEndpointsList.module.css @@ -22,8 +22,6 @@ .wizard-list-image { grid-area: image; - font-size: 35px; - color: #337ab7; } .wizard-list-title { diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardEndpointsList/WizardEndpointsList.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardEndpointsList/WizardEndpointsList.tsx index 50a884f28..99778edd1 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardEndpointsList/WizardEndpointsList.tsx +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardEndpointsList/WizardEndpointsList.tsx @@ -1,15 +1,13 @@ import { Plug2 } from 'lucide-react'; +import clsx from 'clsx'; +import { endpointTypeName, stripProtocol } from '@/portainer/filters/filters'; import { - environmentTypeIcon, - endpointTypeName, - stripProtocol, -} from '@/portainer/filters/filters'; -import { EnvironmentId } from '@/react/portainer/environments/types'; -import { + getEnvironmentTypeIcon, isEdgeEnvironment, isUnassociatedEdgeEnvironment, } from '@/react/portainer/environments/utils'; +import { EnvironmentId } from '@/react/portainer/environments/types'; import { ENVIRONMENTS_POLLING_INTERVAL, useEnvironmentList, @@ -51,9 +49,17 @@ export function WizardEndpointsList({ environmentIds }: Props) { {environments.map((environment) => (
-
+
diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardPodman/AgentTab/AgentTab.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardPodman/AgentTab/AgentTab.tsx new file mode 100644 index 000000000..6ccbfd16b --- /dev/null +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardPodman/AgentTab/AgentTab.tsx @@ -0,0 +1,27 @@ +import { + ContainerEngine, + Environment, +} from '@/react/portainer/environments/types'; + +import { AgentForm } from '../../shared/AgentForm/AgentForm'; + +import { DeploymentScripts } from './DeploymentScripts'; + +interface Props { + onCreate(environment: Environment): void; +} + +export function AgentTab({ onCreate }: Props) { + return ( + <> + + +
+ +
+ + ); +} diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardPodman/AgentTab/DeploymentScripts.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardPodman/AgentTab/DeploymentScripts.tsx new file mode 100644 index 000000000..31ca3fe7d --- /dev/null +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardPodman/AgentTab/DeploymentScripts.tsx @@ -0,0 +1,86 @@ +import { useState } from 'react'; + +import { useAgentDetails } from '@/react/portainer/environments/queries/useAgentDetails'; + +import { CopyButton } from '@@/buttons/CopyButton'; +import { Code } from '@@/Code'; +import { NavTabs } from '@@/NavTabs'; +import { NavContainer } from '@@/NavTabs/NavContainer'; + +const deploymentPodman = [ + { + id: 'all', + label: 'Linux (CentOS)', + command: linuxPodmanCommandRootful, + }, +]; + +export function DeploymentScripts() { + const deployments = deploymentPodman; + const [deployType, setDeployType] = useState(deployments[0].id); + + const agentDetailsQuery = useAgentDetails(); + + if (!agentDetailsQuery) { + return null; + } + + const { agentVersion, agentSecret } = agentDetailsQuery; + + const options = deployments.map((c) => { + const code = c.command(agentVersion, agentSecret); + + return { + id: c.id, + label: c.label, + children: , + }; + }); + + return ( + + setDeployType(id)} + selectedId={deployType} + /> + + ); +} + +interface DeployCodeProps { + code: string; +} + +function DeployCode({ code }: DeployCodeProps) { + return ( + <> +
+ {code} +
+
+ + Copy command + +
+ + ); +} + +function linuxPodmanCommandRootful(agentVersion: string, agentSecret: string) { + const secret = + agentSecret === '' ? '' : `\\\n -e AGENT_SECRET=${agentSecret} `; + + return `sudo systemctl enable --now podman.socket\n +sudo podman volume create portainer\n +sudo podman run -d \\ +-p 9001:9001 ${secret}\\ +--name portainer_agent \\ +--restart=always \\ +--privileged \\ +-v /run/podman/podman.sock:/var/run/docker.sock \\ +-v /var/lib/containers/storage/volumes:/var/lib/docker/volumes \\ +-v /:/host \\ +portainer/agent:${agentVersion} +`; +} diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardPodman/AgentTab/index.ts b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardPodman/AgentTab/index.ts new file mode 100644 index 000000000..fb72ea615 --- /dev/null +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardPodman/AgentTab/index.ts @@ -0,0 +1 @@ +export { AgentTab } from './AgentTab'; diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardPodman/SocketTab/DeploymentScripts.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardPodman/SocketTab/DeploymentScripts.tsx new file mode 100644 index 000000000..c24a50643 --- /dev/null +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardPodman/SocketTab/DeploymentScripts.tsx @@ -0,0 +1,68 @@ +import { useState } from 'react'; + +import { CopyButton } from '@@/buttons/CopyButton'; +import { Code } from '@@/Code'; +import { NavTabs } from '@@/NavTabs'; +import { NavContainer } from '@@/NavTabs/NavContainer'; +import { TextTip } from '@@/Tip/TextTip'; + +const deployments = [ + { + id: 'linux', + label: 'Linux (CentOS)', + command: `sudo systemctl enable --now podman.socket`, + }, +]; + +export function DeploymentScripts() { + const [deployType, setDeployType] = useState(deployments[0].id); + + const options = deployments.map((c) => ({ + id: c.id, + label: c.label, + children: , + })); + + return ( + + setDeployType(id)} + selectedId={deployType} + /> + + ); +} + +interface DeployCodeProps { + code: string; +} + +function DeployCode({ code }: DeployCodeProps) { + const bindMountCode = `-v "/run/podman/podman.sock:/var/run/docker.sock"`; + return ( + <> + + When using the socket, ensure that you have started the Portainer + container with the following Podman flag: + + {bindMountCode} +
+ + Copy command + +
+ + + To use the socket, ensure that you have started the Podman rootful + socket: + + {code} +
+ + Copy command + +
+ + ); +} diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardPodman/SocketTab/SocketForm.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardPodman/SocketTab/SocketForm.tsx new file mode 100644 index 000000000..00824f318 --- /dev/null +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardPodman/SocketTab/SocketForm.tsx @@ -0,0 +1,125 @@ +import { Field, Form, Formik, useFormikContext } from 'formik'; +import { useReducer } from 'react'; +import { Plug2 } from 'lucide-react'; + +import { notifySuccess } from '@/portainer/services/notifications'; +import { useCreateLocalDockerEnvironmentMutation } from '@/react/portainer/environments/queries/useCreateEnvironmentMutation'; +import { + ContainerEngine, + Environment, +} from '@/react/portainer/environments/types'; + +import { LoadingButton } from '@@/buttons/LoadingButton'; +import { FormControl } from '@@/form-components/FormControl'; +import { Input } from '@@/form-components/Input'; +import { SwitchField } from '@@/form-components/SwitchField'; + +import { NameField } from '../../shared/NameField'; +import { MoreSettingsSection } from '../../shared/MoreSettingsSection'; + +import { useValidation } from './SocketForm.validation'; +import { FormValues } from './types'; + +interface Props { + onCreate(environment: Environment): void; + containerEngine: ContainerEngine; +} + +export function SocketForm({ onCreate, containerEngine }: Props) { + const [formKey, clearForm] = useReducer((state) => state + 1, 0); + const initialValues: FormValues = { + name: '', + socketPath: '', + overridePath: false, + meta: { groupId: 1, tagIds: [] }, + }; + + const mutation = useCreateLocalDockerEnvironmentMutation(); + const validation = useValidation(); + + return ( + + {({ isValid, dirty }) => ( +
+ + + + + + +
+
+ + Connect + +
+
+ + )} +
+ ); + + function handleSubmit(values: FormValues) { + mutation.mutate( + { + name: values.name, + socketPath: values.overridePath ? values.socketPath : '', + meta: values.meta, + containerEngine, + }, + { + onSuccess(environment) { + notifySuccess('Environment created', environment.Name); + clearForm(); + onCreate(environment); + }, + } + ); + } +} + +function OverrideSocketFieldset() { + const { values, setFieldValue, errors } = useFormikContext(); + + return ( + <> +
+
+ setFieldValue('overridePath', checked)} + label="Override default socket path" + labelClass="col-sm-3 col-lg-2" + /> +
+
+ {values.overridePath && ( + + + + )} + + ); +} diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardPodman/SocketTab/SocketForm.validation.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardPodman/SocketTab/SocketForm.validation.tsx new file mode 100644 index 000000000..f8b48cf0d --- /dev/null +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardPodman/SocketTab/SocketForm.validation.tsx @@ -0,0 +1,23 @@ +import { boolean, object, SchemaOf, string } from 'yup'; + +import { metadataValidation } from '../../shared/MetadataFieldset/validation'; +import { useNameValidation } from '../../shared/NameField'; + +import { FormValues } from './types'; + +export function useValidation(): SchemaOf { + return object({ + name: useNameValidation(), + meta: metadataValidation(), + overridePath: boolean().default(false), + socketPath: string() + .default('') + .when('overridePath', (overridePath, schema) => + overridePath + ? schema.required( + 'Socket Path is required when override path is enabled' + ) + : schema + ), + }); +} diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardPodman/SocketTab/SocketTab.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardPodman/SocketTab/SocketTab.tsx new file mode 100644 index 000000000..fb6ef3dce --- /dev/null +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardPodman/SocketTab/SocketTab.tsx @@ -0,0 +1,33 @@ +import { + ContainerEngine, + Environment, +} from '@/react/portainer/environments/types'; + +import { TextTip } from '@@/Tip/TextTip'; + +import { DeploymentScripts } from './DeploymentScripts'; +import { SocketForm } from './SocketForm'; + +interface Props { + onCreate(environment: Environment): void; +} + +export function SocketTab({ onCreate }: Props) { + return ( + <> + + To connect via socket, Portainer server must be running in a Podman + container. + + + + +
+ +
+ + ); +} diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardPodman/SocketTab/index.ts b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardPodman/SocketTab/index.ts new file mode 100644 index 000000000..96425ea31 --- /dev/null +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardPodman/SocketTab/index.ts @@ -0,0 +1 @@ +export { SocketTab } from './SocketTab'; diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardPodman/SocketTab/types.ts b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardPodman/SocketTab/types.ts new file mode 100644 index 000000000..9bc7fdad1 --- /dev/null +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardPodman/SocketTab/types.ts @@ -0,0 +1,8 @@ +import { EnvironmentMetadata } from '@/react/portainer/environments/environment.service/create'; + +export interface FormValues { + name: string; + socketPath: string; + overridePath: boolean; + meta: EnvironmentMetadata; +} diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardPodman/WizardPodman.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardPodman/WizardPodman.tsx new file mode 100644 index 000000000..61025d342 --- /dev/null +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardPodman/WizardPodman.tsx @@ -0,0 +1,133 @@ +import { useState } from 'react'; +import { Zap, Plug2 } from 'lucide-react'; +import _ from 'lodash'; + +import { + ContainerEngine, + Environment, +} from '@/react/portainer/environments/types'; +import { commandsTabs } from '@/react/edge/components/EdgeScriptForm/scripts'; +import { isBE } from '@/react/portainer/feature-flags/feature-flags.service'; +import EdgeAgentStandardIcon from '@/react/edge/components/edge-agent-standard.svg?c'; +import EdgeAgentAsyncIcon from '@/react/edge/components/edge-agent-async.svg?c'; + +import { BoxSelector, type BoxSelectorOption } from '@@/BoxSelector'; +import { BadgeIcon } from '@@/BadgeIcon'; +import { TextTip } from '@@/Tip/TextTip'; + +import { AnalyticsStateKey } from '../types'; +import { EdgeAgentTab } from '../shared/EdgeAgentTab'; + +import { AgentTab } from './AgentTab'; +import { SocketTab } from './SocketTab'; + +interface Props { + onCreate(environment: Environment, analytics: AnalyticsStateKey): void; +} + +const options: BoxSelectorOption< + 'agent' | 'api' | 'socket' | 'edgeAgentStandard' | 'edgeAgentAsync' +>[] = _.compact([ + { + id: 'agent', + icon: , + label: 'Agent', + description: '', + value: 'agent', + }, + { + id: 'socket', + icon: , + label: 'Socket', + description: '', + value: 'socket', + }, + { + id: 'edgeAgentStandard', + icon: , + label: 'Edge Agent Standard', + description: '', + value: 'edgeAgentStandard', + }, + isBE && { + id: 'edgeAgentAsync', + icon: , + label: 'Edge Agent Async', + description: '', + value: 'edgeAgentAsync', + }, +]); + +const containerEngine = ContainerEngine.Podman; + +export function WizardPodman({ onCreate }: Props) { + const [creationType, setCreationType] = useState(options[0].value); + + const tab = getTab(creationType); + + return ( +
+ setCreationType(v)} + options={options} + value={creationType} + radioName="creation-type" + /> + + Currently, Portainer only supports Podman 5 running in rootful + (privileged) mode on CentOS 9 Linux environments. Rootless mode + and other Linux distros may work, but aren't officially supported. + + {tab} +
+ ); + + function getTab( + creationType: + | 'agent' + | 'api' + | 'socket' + | 'edgeAgentStandard' + | 'edgeAgentAsync' + ) { + switch (creationType) { + case 'agent': + return ( + onCreate(environment, 'podmanAgent')} + /> + ); + case 'socket': + return ( + + onCreate(environment, 'podmanLocalEnvironment') + } + /> + ); + case 'edgeAgentStandard': + return ( + + onCreate(environment, 'podmanEdgeAgentStandard') + } + commands={[commandsTabs.podmanLinux]} + containerEngine={containerEngine} + /> + ); + case 'edgeAgentAsync': + return ( + + onCreate(environment, 'podmanEdgeAgentAsync') + } + commands={[commandsTabs.podmanLinux]} + containerEngine={containerEngine} + /> + ); + default: + return null; + } + } +} diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardPodman/index.ts b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardPodman/index.ts new file mode 100644 index 000000000..5042118cf --- /dev/null +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardPodman/index.ts @@ -0,0 +1 @@ +export { WizardPodman } from './WizardPodman'; diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/AgentForm/AgentForm.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/AgentForm/AgentForm.tsx index 53e2885d1..86505ec8c 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/AgentForm/AgentForm.tsx +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/AgentForm/AgentForm.tsx @@ -4,7 +4,10 @@ import { Plug2 } from 'lucide-react'; import { useCreateAgentEnvironmentMutation } from '@/react/portainer/environments/queries/useCreateEnvironmentMutation'; import { notifySuccess } from '@/portainer/services/notifications'; -import { Environment } from '@/react/portainer/environments/types'; +import { + ContainerEngine, + Environment, +} from '@/react/portainer/environments/types'; import { CreateAgentEnvironmentValues } from '@/react/portainer/environments/environment.service/create'; import { LoadingButton } from '@@/buttons/LoadingButton'; @@ -18,6 +21,7 @@ import { useValidation } from './AgentForm.validation'; interface Props { onCreate(environment: Environment): void; envDefaultPort?: string; + containerEngine?: ContainerEngine; } const initialValues: CreateAgentEnvironmentValues = { @@ -29,7 +33,11 @@ const initialValues: CreateAgentEnvironmentValues = { }, }; -export function AgentForm({ onCreate, envDefaultPort }: Props) { +export function AgentForm({ + onCreate, + envDefaultPort, + containerEngine = ContainerEngine.Docker, +}: Props) { const [formKey, clearForm] = useReducer((state) => state + 1, 0); const mutation = useCreateAgentEnvironmentMutation(); @@ -70,12 +78,15 @@ export function AgentForm({ onCreate, envDefaultPort }: Props) { ); function handleSubmit(values: CreateAgentEnvironmentValues) { - mutation.mutate(values, { - onSuccess(environment) { - notifySuccess('Environment created', environment.Name); - clearForm(); - onCreate(environment); - }, - }); + mutation.mutate( + { ...values, containerEngine }, + { + onSuccess(environment) { + notifySuccess('Environment created', environment.Name); + clearForm(); + onCreate(environment); + }, + } + ); } } diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/AgentForm/AgentForm.validation.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/AgentForm/AgentForm.validation.tsx index 4829d9bbf..090b03165 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/AgentForm/AgentForm.validation.tsx +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/AgentForm/AgentForm.validation.tsx @@ -1,4 +1,4 @@ -import { object, SchemaOf, string } from 'yup'; +import { mixed, object, SchemaOf, string } from 'yup'; import { CreateAgentEnvironmentValues } from '@/react/portainer/environments/environment.service/create'; @@ -10,6 +10,7 @@ export function useValidation(): SchemaOf { name: useNameValidation(), environmentUrl: environmentValidation(), meta: metadataValidation(), + containerEngine: mixed().oneOf(['docker', 'podman']), }); } diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/EdgeAgentForm.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/EdgeAgentForm.tsx index 9b4e89330..94531ef6b 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/EdgeAgentForm.tsx +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/EdgeAgentForm.tsx @@ -1,7 +1,10 @@ import { Formik, Form } from 'formik'; import { Plug2 } from 'lucide-react'; -import { Environment } from '@/react/portainer/environments/types'; +import { + ContainerEngine, + Environment, +} from '@/react/portainer/environments/types'; import { useCreateEdgeAgentEnvironmentMutation } from '@/react/portainer/environments/queries/useCreateEnvironmentMutation'; import { Settings } from '@/react/portainer/settings/types'; import { EdgeCheckinIntervalField } from '@/react/edge/components/EdgeCheckInIntervalField'; @@ -26,9 +29,15 @@ interface Props { onCreate(environment: Environment): void; readonly: boolean; asyncMode: boolean; + containerEngine: ContainerEngine; } -export function EdgeAgentForm({ onCreate, readonly, asyncMode }: Props) { +export function EdgeAgentForm({ + onCreate, + readonly, + asyncMode, + containerEngine, +}: Props) { const settingsQuery = useSettings(); const createMutation = useCreateEdgeAgentEnvironmentMutation(); @@ -100,6 +109,7 @@ export function EdgeAgentForm({ onCreate, readonly, asyncMode }: Props) { ...values.edge, asyncMode, }, + containerEngine, }, { onSuccess(environment) { diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentTab.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentTab.tsx index 8f5e619da..c1cbbfa15 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentTab.tsx +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentTab.tsx @@ -1,7 +1,10 @@ import { v4 as uuid } from 'uuid'; import { useReducer, useState } from 'react'; -import { Environment } from '@/react/portainer/environments/types'; +import { + ContainerEngine, + Environment, +} from '@/react/portainer/environments/types'; import { EdgeScriptForm } from '@/react/edge/components/EdgeScriptForm'; import { CommandTab } from '@/react/edge/components/EdgeScriptForm/scripts'; import { OS, EdgeInfo } from '@/react/edge/components/EdgeScriptForm/types'; @@ -15,9 +18,15 @@ interface Props { onCreate: (environment: Environment) => void; commands: CommandTab[] | Partial>; asyncMode?: boolean; + containerEngine?: ContainerEngine; } -export function EdgeAgentTab({ onCreate, commands, asyncMode = false }: Props) { +export function EdgeAgentTab({ + onCreate, + commands, + asyncMode = false, + containerEngine = ContainerEngine.Docker, +}: Props) { const [edgeInfo, setEdgeInfo] = useState(); const [formKey, clearForm] = useReducer((state) => state + 1, 0); @@ -28,6 +37,7 @@ export function EdgeAgentTab({ onCreate, commands, asyncMode = false }: Props) { readonly={!!edgeInfo} key={formKey} asyncMode={asyncMode} + containerEngine={containerEngine} /> {edgeInfo && ( diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/types.ts b/app/react/portainer/environments/wizard/EnvironmentsCreationView/types.ts index 13317d129..185491ea6 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/types.ts +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/types.ts @@ -3,12 +3,16 @@ export interface AnalyticsState { dockerApi: number; dockerEdgeAgentStandard: number; dockerEdgeAgentAsync: number; + podmanAgent: number; + podmanEdgeAgentStandard: number; + podmanEdgeAgentAsync: number; + podmanLocalEnvironment: number; // podman socket kubernetesAgent: number; kubernetesEdgeAgentStandard: number; kubernetesEdgeAgentAsync: number; kaasAgent: number; aciApi: number; - localEndpoint: number; + localEndpoint: number; // docker socket } export type AnalyticsStateKey = keyof AnalyticsState; diff --git a/app/react/portainer/environments/wizard/HomeView/useFetchOrCreateLocalEnvironment.ts b/app/react/portainer/environments/wizard/HomeView/useFetchOrCreateLocalEnvironment.ts index dec99359c..48e8e8d58 100644 --- a/app/react/portainer/environments/wizard/HomeView/useFetchOrCreateLocalEnvironment.ts +++ b/app/react/portainer/environments/wizard/HomeView/useFetchOrCreateLocalEnvironment.ts @@ -3,6 +3,7 @@ import { useMutation } from '@tanstack/react-query'; import { useEnvironmentList } from '@/react/portainer/environments/queries/useEnvironmentList'; import { + ContainerEngine, Environment, EnvironmentType, } from '@/react/portainer/environments/types'; @@ -62,11 +63,30 @@ function getStatus( } async function createLocalEnvironment() { - try { - return await createLocalKubernetesEnvironment({ name: 'local' }); - } catch (err) { - return await createLocalDockerEnvironment({ name: 'local' }); + const name = 'local'; + const attempts = [ + () => createLocalKubernetesEnvironment({ name }), + () => + createLocalDockerEnvironment({ + name, + containerEngine: ContainerEngine.Podman, + }), + () => + createLocalDockerEnvironment({ + name, + containerEngine: ContainerEngine.Docker, + }), + ]; + + for (let i = 0; i < attempts.length; i++) { + try { + return await attempts[i](); + } catch (err) { + // Continue to next attempt + } } + + throw new Error('Failed to create local environment with any method'); } function useFetchLocalEnvironment() { diff --git a/app/react/portainer/templates/custom-templates/CreateView/CreateView.tsx b/app/react/portainer/templates/custom-templates/CreateView/CreateView.tsx index 8320c6ec1..c19d8f10f 100644 --- a/app/react/portainer/templates/custom-templates/CreateView/CreateView.tsx +++ b/app/react/portainer/templates/custom-templates/CreateView/CreateView.tsx @@ -1,6 +1,7 @@ import { useEnvironmentId } from '@/react/hooks/useEnvironmentId'; import { useIsSwarm } from '@/react/docker/proxy/queries/useInfo'; import { StackType } from '@/react/common/stacks/types'; +import { ContainerEngine } from '@/react/portainer/environments/types'; import { PageHeader } from '@@/PageHeader'; import { Widget } from '@@/Widget'; @@ -12,16 +13,18 @@ import { CreateForm } from './CreateForm'; export function CreateView() { const viewType = useViewType(); const environmentId = useEnvironmentId(false); - const isSwarm = useIsSwarm(environmentId, { enabled: viewType === 'docker' }); + const isSwarm = useIsSwarm(environmentId, { + enabled: viewType === ContainerEngine.Docker, + }); const defaultType = getDefaultType(viewType, isSwarm); return (
diff --git a/app/react/portainer/templates/custom-templates/ListView/ListView.tsx b/app/react/portainer/templates/custom-templates/ListView/ListView.tsx index bce1d18e7..9530a7cbf 100644 --- a/app/react/portainer/templates/custom-templates/ListView/ListView.tsx +++ b/app/react/portainer/templates/custom-templates/ListView/ListView.tsx @@ -1,5 +1,6 @@ import { notifySuccess } from '@/portainer/services/notifications'; import { useParamState } from '@/react/hooks/useParamState'; +import { ContainerEngine } from '@/react/portainer/environments/types'; import { PageHeader } from '@@/PageHeader'; import { confirmDelete } from '@@/modals/confirm'; @@ -28,7 +29,7 @@ export function ListView() { <> - {viewType === 'docker' && !!selectedTemplateId && ( + {viewType === ContainerEngine.Docker && !!selectedTemplateId && ( )} diff --git a/app/react/sidebar/EnvironmentSidebar.tsx b/app/react/sidebar/EnvironmentSidebar.tsx index bcae59f38..2e51b449c 100644 --- a/app/react/sidebar/EnvironmentSidebar.tsx +++ b/app/react/sidebar/EnvironmentSidebar.tsx @@ -85,6 +85,7 @@ function Content({ environment, onClear }: ContentProps) { } = { [PlatformType.Azure]: AzureSidebar, [PlatformType.Docker]: DockerSidebar, + [PlatformType.Podman]: DockerSidebar, // same as docker for now, until pod management is added [PlatformType.Kubernetes]: KubernetesSidebar, }; @@ -124,7 +125,10 @@ interface TitleProps { function Title({ environment, onClear }: TitleProps) { const { isOpen } = useSidebarState(); - const EnvironmentIcon = getPlatformIcon(environment.Type); + const EnvironmentIcon = getPlatformIcon( + environment.Type, + environment.ContainerEngine + ); if (!isOpen) { return ( diff --git a/dev/run_container_podman.sh b/dev/run_container_podman.sh new file mode 100755 index 000000000..1893efd0b --- /dev/null +++ b/dev/run_container_podman.sh @@ -0,0 +1,22 @@ +#!/bin/bash +set -euo pipefail + +PORTAINER_DATA=${PORTAINER_DATA:-/tmp/portainer} +PORTAINER_PROJECT=${PORTAINER_PROJECT:-$(pwd)} +PORTAINER_FLAGS=${PORTAINER_FLAGS:-} + +sudo podman rm -f portainer + +# rootful podman (sudo required) +sudo podman run -d \ + -p 8000:8000 \ + -p 9000:9000 \ + -p 9443:9443 \ + -v "$PORTAINER_PROJECT/dist:/app" \ + -v "$PORTAINER_DATA:/data" \ + -v /run/podman/podman.sock:/var/run/docker.sock \ + -v /tmp:/tmp \ + --privileged \ + --name portainer \ + portainer/base \ + /app/portainer $PORTAINER_FLAGS