feat(app): limit the docker API version supported by the frontend (#11855)

pull/11920/head
LP B 2024-06-10 20:54:31 +02:00 committed by GitHub
parent 4ba16f1b04
commit 6a8e6734f3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
212 changed files with 4439 additions and 3281 deletions

View File

@ -122,6 +122,7 @@ func (handler *Handler) executeServiceWebhook(
_ = rc.Close()
}(rc)
}
_, err = dockerClient.ServiceUpdate(context.Background(), resourceID, service.Version, service.Spec, serviceUpdateOptions)
if err != nil {

View File

@ -20,11 +20,16 @@ type postDockerfileRequest struct {
}
// buildOperation inspects the "Content-Type" header to determine if it needs to alter the request.
//
// If the value of the header is empty, it means that a Dockerfile is posted via upload, the function
// will extract the file content from the request body, tar it, and rewrite the body.
// !! THIS IS ONLY TRUE WHEN THE UPLOADED DOCKERFILE FILE HAS NO EXTENSION (the generated file.type in the frontend will be empty)
// If the Dockerfile is named like Dockerfile.yaml or has an internal type, a non-empty Content-Type header will be generated
//
// If the value of the header contains "application/json", it means that the content of a Dockerfile is posted
// in the request payload as JSON, the function will create a new file called Dockerfile inside a tar archive and
// rewrite the body of the request.
//
// In any other case, it will leave the request unaltered.
func buildOperation(request *http.Request) error {
contentTypeHeader := request.Header.Get("Content-Type")

View File

@ -84,11 +84,25 @@ func (transport *Transport) RoundTrip(request *http.Request) (*http.Response, er
return transport.ProxyDockerRequest(request)
}
var prefixProxyFuncMap = map[string]func(*Transport, *http.Request, string) (*http.Response, error){
"configs": (*Transport).proxyConfigRequest,
"containers": (*Transport).proxyContainerRequest,
"services": (*Transport).proxyServiceRequest,
"volumes": (*Transport).proxyVolumeRequest,
"networks": (*Transport).proxyNetworkRequest,
"secrets": (*Transport).proxySecretRequest,
"swarm": (*Transport).proxySwarmRequest,
"nodes": (*Transport).proxyNodeRequest,
"tasks": (*Transport).proxyTaskRequest,
"build": (*Transport).proxyBuildRequest,
"images": (*Transport).proxyImageRequest,
"v2": (*Transport).proxyAgentRequest,
}
// ProxyDockerRequest intercepts a Docker API request and apply logic based
// on the requested operation.
func (transport *Transport) ProxyDockerRequest(request *http.Request) (*http.Response, error) {
requestPath := apiVersionRe.ReplaceAllString(request.URL.Path, "")
request.URL.Path = requestPath
unversionedPath := apiVersionRe.ReplaceAllString(request.URL.Path, "")
if transport.endpoint.Type == portainer.AgentOnDockerEnvironment || transport.endpoint.Type == portainer.EdgeAgentOnDockerEnvironment {
signature, err := transport.signatureService.CreateSignature(portainer.PortainerAgentSignatureMessage)
@ -100,34 +114,12 @@ func (transport *Transport) ProxyDockerRequest(request *http.Request) (*http.Res
request.Header.Set(portainer.PortainerAgentSignatureHeader, signature)
}
switch {
case strings.HasPrefix(requestPath, "/configs"):
return transport.proxyConfigRequest(request)
case strings.HasPrefix(requestPath, "/containers"):
return transport.proxyContainerRequest(request)
case strings.HasPrefix(requestPath, "/services"):
return transport.proxyServiceRequest(request)
case strings.HasPrefix(requestPath, "/volumes"):
return transport.proxyVolumeRequest(request)
case strings.HasPrefix(requestPath, "/networks"):
return transport.proxyNetworkRequest(request)
case strings.HasPrefix(requestPath, "/secrets"):
return transport.proxySecretRequest(request)
case strings.HasPrefix(requestPath, "/swarm"):
return transport.proxySwarmRequest(request)
case strings.HasPrefix(requestPath, "/nodes"):
return transport.proxyNodeRequest(request)
case strings.HasPrefix(requestPath, "/tasks"):
return transport.proxyTaskRequest(request)
case strings.HasPrefix(requestPath, "/build"):
return transport.proxyBuildRequest(request)
case strings.HasPrefix(requestPath, "/images"):
return transport.proxyImageRequest(request)
case strings.HasPrefix(requestPath, "/v2"):
return transport.proxyAgentRequest(request)
default:
return transport.executeDockerRequest(request)
prefix := strings.Split(strings.TrimPrefix(unversionedPath, "/"), "/")[0]
if proxyFunc := prefixProxyFuncMap[prefix]; proxyFunc != nil {
return proxyFunc(transport, request, unversionedPath)
}
return transport.executeDockerRequest(request)
}
func (transport *Transport) executeDockerRequest(request *http.Request) (*http.Response, error) {
@ -144,8 +136,8 @@ func (transport *Transport) executeDockerRequest(request *http.Request) (*http.R
return response, err
}
func (transport *Transport) proxyAgentRequest(r *http.Request) (*http.Response, error) {
requestPath := strings.TrimPrefix(r.URL.Path, "/v2")
func (transport *Transport) proxyAgentRequest(r *http.Request, unversionedPath string) (*http.Response, error) {
requestPath := strings.TrimPrefix(unversionedPath, "/v2")
switch {
case strings.HasPrefix(requestPath, "/browse"):
@ -203,8 +195,10 @@ func (transport *Transport) proxyAgentRequest(r *http.Request) (*http.Response,
return transport.executeDockerRequest(r)
}
func (transport *Transport) proxyConfigRequest(request *http.Request) (*http.Response, error) {
switch requestPath := request.URL.Path; requestPath {
func (transport *Transport) proxyConfigRequest(request *http.Request, unversionedPath string) (*http.Response, error) {
requestPath := unversionedPath
switch requestPath {
case "/configs/create":
return transport.decorateGenericResourceCreationOperation(request, configObjectIdentifier, portainer.ConfigResourceControl)
@ -225,8 +219,10 @@ func (transport *Transport) proxyConfigRequest(request *http.Request) (*http.Res
}
}
func (transport *Transport) proxyContainerRequest(request *http.Request) (*http.Response, error) {
switch requestPath := request.URL.Path; requestPath {
func (transport *Transport) proxyContainerRequest(request *http.Request, unversionedPath string) (*http.Response, error) {
requestPath := unversionedPath
switch requestPath {
case "/containers/create":
return transport.decorateContainerCreationOperation(request, containerObjectIdentifier, portainer.ContainerResourceControl)
@ -261,8 +257,10 @@ func (transport *Transport) proxyContainerRequest(request *http.Request) (*http.
}
}
func (transport *Transport) proxyServiceRequest(request *http.Request) (*http.Response, error) {
switch requestPath := request.URL.Path; requestPath {
func (transport *Transport) proxyServiceRequest(request *http.Request, unversionedPath string) (*http.Response, error) {
requestPath := unversionedPath
switch requestPath {
case "/services/create":
return transport.decorateServiceCreationOperation(request)
@ -292,8 +290,10 @@ func (transport *Transport) proxyServiceRequest(request *http.Request) (*http.Re
}
}
func (transport *Transport) proxyVolumeRequest(request *http.Request) (*http.Response, error) {
switch requestPath := request.URL.Path; requestPath {
func (transport *Transport) proxyVolumeRequest(request *http.Request, unversionedPath string) (*http.Response, error) {
requestPath := unversionedPath
switch requestPath {
case "/volumes/create":
return transport.decorateVolumeResourceCreationOperation(request, portainer.VolumeResourceControl)
@ -309,8 +309,10 @@ func (transport *Transport) proxyVolumeRequest(request *http.Request) (*http.Res
}
}
func (transport *Transport) proxyNetworkRequest(request *http.Request) (*http.Response, error) {
switch requestPath := request.URL.Path; requestPath {
func (transport *Transport) proxyNetworkRequest(request *http.Request, unversionedPath string) (*http.Response, error) {
requestPath := unversionedPath
switch requestPath {
case "/networks/create":
return transport.decorateGenericResourceCreationOperation(request, networkObjectIdentifier, portainer.NetworkResourceControl)
@ -330,8 +332,10 @@ func (transport *Transport) proxyNetworkRequest(request *http.Request) (*http.Re
}
}
func (transport *Transport) proxySecretRequest(request *http.Request) (*http.Response, error) {
switch requestPath := request.URL.Path; requestPath {
func (transport *Transport) proxySecretRequest(request *http.Request, unversionedPath string) (*http.Response, error) {
requestPath := unversionedPath
switch requestPath {
case "/secrets/create":
return transport.decorateGenericResourceCreationOperation(request, secretObjectIdentifier, portainer.SecretResourceControl)
@ -351,8 +355,8 @@ func (transport *Transport) proxySecretRequest(request *http.Request) (*http.Res
}
}
func (transport *Transport) proxyNodeRequest(request *http.Request) (*http.Response, error) {
requestPath := request.URL.Path
func (transport *Transport) proxyNodeRequest(request *http.Request, unversionedPath string) (*http.Response, error) {
requestPath := unversionedPath
// assume /nodes/{id}
if path.Base(requestPath) != "nodes" {
@ -362,8 +366,10 @@ func (transport *Transport) proxyNodeRequest(request *http.Request) (*http.Respo
return transport.executeDockerRequest(request)
}
func (transport *Transport) proxySwarmRequest(request *http.Request) (*http.Response, error) {
switch requestPath := request.URL.Path; requestPath {
func (transport *Transport) proxySwarmRequest(request *http.Request, unversionedPath string) (*http.Response, error) {
requestPath := unversionedPath
switch requestPath {
case "/swarm":
return transport.rewriteOperation(request, swarmInspectOperation)
default:
@ -372,8 +378,10 @@ func (transport *Transport) proxySwarmRequest(request *http.Request) (*http.Resp
}
}
func (transport *Transport) proxyTaskRequest(request *http.Request) (*http.Response, error) {
switch requestPath := request.URL.Path; requestPath {
func (transport *Transport) proxyTaskRequest(request *http.Request, unversionedPath string) (*http.Response, error) {
requestPath := unversionedPath
switch requestPath {
case "/tasks":
return transport.rewriteOperation(request, transport.taskListOperation)
default:
@ -382,7 +390,7 @@ func (transport *Transport) proxyTaskRequest(request *http.Request) (*http.Respo
}
}
func (transport *Transport) proxyBuildRequest(request *http.Request) (*http.Response, error) {
func (transport *Transport) proxyBuildRequest(request *http.Request, _ string) (*http.Response, error) {
err := transport.updateDefaultGitBranch(request)
if err != nil {
return nil, err
@ -408,8 +416,10 @@ func (transport *Transport) updateDefaultGitBranch(request *http.Request) error
return nil
}
func (transport *Transport) proxyImageRequest(request *http.Request) (*http.Response, error) {
switch requestPath := request.URL.Path; requestPath {
func (transport *Transport) proxyImageRequest(request *http.Request, unversionedPath string) (*http.Response, error) {
requestPath := unversionedPath
switch requestPath {
case "/images/create":
return transport.replaceRegistryAuthenticationHeader(request)
default:

View File

@ -5,7 +5,6 @@ function ImageHelperFactory() {
return {
isValidTag,
createImageConfigForContainer,
getImagesNamesForDownload,
removeDigestFromRepository,
imageContainsURL,
};
@ -14,20 +13,6 @@ function ImageHelperFactory() {
return tag.match(/^(?![\.\-])([a-zA-Z0-9\_\.\-])+$/g);
}
/**
*
* @param {Array<{tags: Array<string>; id: string;}>} images
* @returns {{names: string[]}}}
*/
function getImagesNamesForDownload(images) {
var names = images.map(function (image) {
return image.tags[0] !== '<none>:<none>' ? image.tags[0] : image.id;
});
return {
names,
};
}
/**
*
* @param {PorImageRegistryModel} registry

View File

@ -3,14 +3,6 @@ angular.module('portainer.docker').factory('VolumeHelper', [
'use strict';
var helper = {};
helper.createDriverOptions = function (optionArray) {
var options = {};
optionArray.forEach(function (option) {
options[option.name] = option.value;
});
return options;
};
helper.isVolumeUsedByAService = function (volume, services) {
for (var i = 0; i < services.length; i++) {
var service = services[i];

View File

@ -0,0 +1,33 @@
type Data = {
stream: string;
errorDetail: { message: string };
};
export class ImageBuildModel {
hasError: boolean = false;
buildLogs: string[];
constructor(data: Data[]) {
const buildLogs: string[] = [];
data.forEach((line) => {
if (line.stream) {
// convert unicode chars to readable chars
const logLine = line.stream.replace(
// eslint-disable-next-line no-control-regex
/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g,
''
);
buildLogs.push(logLine);
}
if (line.errorDetail) {
buildLogs.push(line.errorDetail.message);
this.hasError = true;
}
});
this.buildLogs = buildLogs;
}
}

View File

@ -1,30 +0,0 @@
import { ResourceControlViewModel } from '@/react/portainer/access-control/models/ResourceControlViewModel';
function b64DecodeUnicode(str) {
try {
return decodeURIComponent(
atob(str)
.split('')
.map(function (c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
})
.join('')
);
} catch (err) {
return atob(str);
}
}
export function ConfigViewModel(data) {
this.Id = data.ID;
this.CreatedAt = data.CreatedAt;
this.UpdatedAt = data.UpdatedAt;
this.Version = data.Version.Index;
this.Name = data.Spec.Name;
this.Labels = data.Spec.Labels;
this.Data = b64DecodeUnicode(data.Spec.Data);
if (data.Portainer && data.Portainer.ResourceControl) {
this.ResourceControl = new ResourceControlViewModel(data.Portainer.ResourceControl);
}
}

View File

@ -0,0 +1,54 @@
import { Config } from 'docker-types/generated/1.41';
import { IResource } from '@/react/docker/components/datatable/createOwnershipColumn';
import { PortainerResponse } from '@/react/docker/types';
import { ResourceControlViewModel } from '@/react/portainer/access-control/models/ResourceControlViewModel';
export class ConfigViewModel implements IResource {
Id: string;
CreatedAt: string;
UpdatedAt: string;
Version: number;
Name: string;
Labels: Record<string, string>;
Data: string;
ResourceControl?: ResourceControlViewModel;
constructor(data: PortainerResponse<Config>) {
this.Id = data.ID || '';
this.CreatedAt = data.CreatedAt || '';
this.UpdatedAt = data.UpdatedAt || '';
this.Version = data.Version?.Index || 0;
this.Name = data.Spec?.Name || '';
this.Labels = data.Spec?.Labels || {};
this.Data = b64DecodeUnicode(data.Spec?.Data || '');
if (data.Portainer && data.Portainer.ResourceControl) {
this.ResourceControl = new ResourceControlViewModel(
data.Portainer.ResourceControl
);
}
}
}
function b64DecodeUnicode(str: string) {
try {
return decodeURIComponent(
window
.atob(str)
.toString()
.split('')
.map((c) => `%${`00${c.charCodeAt(0).toString(16)}`.slice(-2)}`)
.join('')
);
} catch (err) {
return window.atob(str);
}
}

View File

@ -1,145 +0,0 @@
import _ from 'lodash-es';
import { ResourceControlViewModel } from '@/react/portainer/access-control/models/ResourceControlViewModel';
export function createStatus(statusText) {
var status = _.toLower(statusText);
if (status.indexOf('paused') > -1) {
return 'paused';
} else if (status.indexOf('dead') > -1) {
return 'dead';
} else if (status.indexOf('created') > -1) {
return 'created';
} else if (status.indexOf('exited') > -1) {
return 'stopped';
} else if (status.indexOf('(healthy)') > -1) {
return 'healthy';
} else if (status.indexOf('(unhealthy)') > -1) {
return 'unhealthy';
} else if (status.indexOf('(health: starting)') > -1) {
return 'starting';
}
return 'running';
}
export function ContainerViewModel(data) {
this.Id = data.Id;
this.Status = createStatus(data.Status);
this.State = data.State;
this.Created = data.Created;
this.Names = data.Names;
// Unavailable in Docker < 1.10
if (data.NetworkSettings && !_.isEmpty(data.NetworkSettings.Networks)) {
this.IP = data.NetworkSettings.Networks[Object.keys(data.NetworkSettings.Networks)[0]].IPAddress;
}
this.NetworkSettings = data.NetworkSettings;
this.Image = data.Image;
this.ImageID = data.ImageID;
this.Command = data.Command;
this.Checked = false;
this.Labels = data.Labels;
if (this.Labels && this.Labels['com.docker.compose.project']) {
this.StackName = this.Labels['com.docker.compose.project'];
} else if (this.Labels && this.Labels['com.docker.stack.namespace']) {
this.StackName = this.Labels['com.docker.stack.namespace'];
}
this.Mounts = data.Mounts;
this.IsPortainer = data.IsPortainer;
this.Ports = [];
if (data.Ports) {
for (var i = 0; i < data.Ports.length; ++i) {
var p = data.Ports[i];
if (p.PublicPort) {
this.Ports.push({ host: p.IP, private: p.PrivatePort, public: p.PublicPort });
}
}
}
if (data.Portainer) {
if (data.Portainer.ResourceControl) {
this.ResourceControl = new ResourceControlViewModel(data.Portainer.ResourceControl);
}
if (data.Portainer.Agent && data.Portainer.Agent.NodeName) {
this.NodeName = data.Portainer.Agent.NodeName;
}
}
}
export function ContainerStatsViewModel(data) {
this.read = data.read;
this.preread = data.preread;
if (data.memory_stats.privateworkingset !== undefined) {
// Windows
this.MemoryUsage = data.memory_stats.privateworkingset;
this.MemoryCache = 0;
this.NumProcs = data.num_procs;
this.isWindows = true;
} else {
// Linux
if (data.memory_stats.stats === undefined || data.memory_stats.usage === undefined) {
this.MemoryUsage = this.MemoryCache = 0;
} else {
this.MemoryCache = 0;
if (data.memory_stats.stats.cache !== undefined) {
// cgroups v1
this.MemoryCache = data.memory_stats.stats.cache;
}
this.MemoryUsage = data.memory_stats.usage - this.MemoryCache;
}
}
this.PreviousCPUTotalUsage = data.precpu_stats.cpu_usage.total_usage;
this.PreviousCPUSystemUsage = data.precpu_stats.system_cpu_usage;
this.CurrentCPUTotalUsage = data.cpu_stats.cpu_usage.total_usage;
this.CurrentCPUSystemUsage = data.cpu_stats.system_cpu_usage;
this.CPUCores = 1;
if (data.cpu_stats.cpu_usage.percpu_usage) {
this.CPUCores = data.cpu_stats.cpu_usage.percpu_usage.length;
} else {
if (data.cpu_stats.online_cpus !== undefined) {
this.CPUCores = data.cpu_stats.online_cpus;
}
}
this.Networks = _.values(data.networks);
if (data.blkio_stats !== undefined && data.blkio_stats.io_service_bytes_recursive !== null) {
//TODO: take care of multiple block devices
var readData = data.blkio_stats.io_service_bytes_recursive.find((d) => d.op === 'Read');
if (readData === undefined) {
// try the cgroups v2 version
readData = data.blkio_stats.io_service_bytes_recursive.find((d) => d.op === 'read');
}
if (readData !== undefined) {
this.BytesRead = readData.value;
}
var writeData = data.blkio_stats.io_service_bytes_recursive.find((d) => d.op === 'Write');
if (writeData === undefined) {
// try the cgroups v2 version
writeData = data.blkio_stats.io_service_bytes_recursive.find((d) => d.op === 'write');
}
if (writeData !== undefined) {
this.BytesWrite = writeData.value;
}
} else {
//no IO related data is available
this.noIOdata = true;
}
}
export function ContainerDetailsViewModel(data) {
this.Model = data;
this.Id = data.Id;
this.State = data.State;
this.Created = data.Created;
this.Name = data.Name;
this.NetworkSettings = data.NetworkSettings;
this.Args = data.Args;
this.Image = data.Image;
this.Config = data.Config;
this.HostConfig = data.HostConfig;
this.Mounts = data.Mounts;
if (data.Portainer && data.Portainer.ResourceControl) {
this.ResourceControl = new ResourceControlViewModel(data.Portainer.ResourceControl);
}
this.IsPortainer = data.IsPortainer;
}

View File

@ -0,0 +1,56 @@
import { IResource } from '@/react/docker/components/datatable/createOwnershipColumn';
import { ContainerDetailsResponse } from '@/react/docker/containers/queries/useContainer';
import { PortainerResponse } from '@/react/docker/types';
import { ResourceControlViewModel } from '@/react/portainer/access-control/models/ResourceControlViewModel';
export class ContainerDetailsViewModel
implements IResource, Pick<PortainerResponse<unknown>, 'IsPortainer'>
{
Model: ContainerDetailsResponse;
Id: ContainerDetailsResponse['Id'];
State: ContainerDetailsResponse['State'];
Created: ContainerDetailsResponse['Created'];
Name: ContainerDetailsResponse['Name'];
NetworkSettings: ContainerDetailsResponse['NetworkSettings'];
Args: ContainerDetailsResponse['Args'];
Image: ContainerDetailsResponse['Image'];
Config: ContainerDetailsResponse['Config'];
HostConfig: ContainerDetailsResponse['HostConfig'];
Mounts: ContainerDetailsResponse['Mounts'];
// IResource
ResourceControl?: ResourceControlViewModel;
// PortainerResponse
IsPortainer?: ContainerDetailsResponse['IsPortainer'];
constructor(data: ContainerDetailsResponse) {
this.Model = data;
this.Id = data.Id;
this.State = data.State;
this.Created = data.Created;
this.Name = data.Name;
this.NetworkSettings = data.NetworkSettings;
this.Args = data.Args;
this.Image = data.Image;
this.Config = data.Config;
this.HostConfig = data.HostConfig;
this.Mounts = data.Mounts;
if (data.Portainer && data.Portainer.ResourceControl) {
this.ResourceControl = new ResourceControlViewModel(
data.Portainer.ResourceControl
);
}
this.IsPortainer = data.IsPortainer;
}
}

View File

@ -0,0 +1,113 @@
import { values } from 'lodash';
import { ContainerStats } from '@/react/docker/containers/queries/useContainerStats';
import { ValueOf } from '@/types';
/**
* This type is arbitrary and only defined based on what we use / observed from the API responses.
*/
export class ContainerStatsViewModel {
read: string;
preread: string;
MemoryUsage: number;
MemoryCache: number = 0;
NumProcs: number = 0;
isWindows: boolean = false;
PreviousCPUTotalUsage: number;
PreviousCPUSystemUsage: number;
CurrentCPUTotalUsage: number;
CurrentCPUSystemUsage: number;
CPUCores: number;
Networks: ValueOf<NonNullable<ContainerStats['networks']>>[];
BytesRead: number = 0;
BytesWrite: number = 0;
noIOdata: boolean = false;
constructor(data: ContainerStats) {
this.read = data.read || '';
this.preread = data.preread || '';
if (data?.memory_stats?.privateworkingset !== undefined) {
// Windows
this.MemoryUsage = data?.memory_stats?.privateworkingset;
this.MemoryCache = 0;
this.NumProcs = data.num_procs || 0;
this.isWindows = true;
}
// Linux
else if (
data?.memory_stats?.stats === undefined ||
data?.memory_stats?.usage === undefined
) {
this.MemoryUsage = 0;
this.MemoryCache = 0;
} else {
this.MemoryCache = 0;
if (data?.memory_stats?.stats?.cache !== undefined) {
// cgroups v1
this.MemoryCache = data.memory_stats.stats.cache;
}
this.MemoryUsage = data.memory_stats.usage - this.MemoryCache;
}
this.PreviousCPUTotalUsage =
data?.precpu_stats?.cpu_usage?.total_usage || 0;
this.PreviousCPUSystemUsage = data?.precpu_stats?.system_cpu_usage || 0;
this.CurrentCPUTotalUsage = data?.cpu_stats?.cpu_usage?.total_usage || 0;
this.CurrentCPUSystemUsage = data?.cpu_stats?.system_cpu_usage || 0;
this.CPUCores = 1;
this.CPUCores =
data?.cpu_stats?.cpu_usage?.percpu_usage?.length ??
data?.cpu_stats?.online_cpus ??
1;
this.Networks = values(data.networks);
if (
data.blkio_stats !== undefined &&
data.blkio_stats.io_service_bytes_recursive !== null
) {
// TODO: take care of multiple block devices
let readData = data?.blkio_stats?.io_service_bytes_recursive?.find(
(d) => d.op === 'Read'
);
if (readData === undefined) {
// try the cgroups v2 version
readData = data?.blkio_stats?.io_service_bytes_recursive?.find(
(d) => d.op === 'read'
);
}
if (readData !== undefined) {
this.BytesRead = readData.value;
}
let writeData = data?.blkio_stats?.io_service_bytes_recursive?.find(
(d) => d.op === 'Write'
);
if (writeData === undefined) {
// try the cgroups v2 version
writeData = data?.blkio_stats?.io_service_bytes_recursive?.find(
(d) => d.op === 'write'
);
}
if (writeData !== undefined) {
this.BytesWrite = writeData.value;
}
} else {
// no IO related data is available
this.noIOdata = true;
}
}
}

View File

@ -1,174 +0,0 @@
function createEventDetails(event) {
var eventAttr = event.Actor.Attributes;
var details = '';
var action = event.Action;
var extra = '';
var hasColon = action.indexOf(':');
if (hasColon != -1) {
extra = action.substring(hasColon);
action = action.substring(0, hasColon);
}
switch (event.Type) {
case 'container':
switch (action) {
case 'stop':
details = 'Container ' + eventAttr.name + ' stopped';
break;
case 'destroy':
details = 'Container ' + eventAttr.name + ' deleted';
break;
case 'create':
details = 'Container ' + eventAttr.name + ' created';
break;
case 'start':
details = 'Container ' + eventAttr.name + ' started';
break;
case 'kill':
details = 'Container ' + eventAttr.name + ' killed';
break;
case 'die':
details = 'Container ' + eventAttr.name + ' exited with status code ' + eventAttr.exitCode;
break;
case 'commit':
details = 'Container ' + eventAttr.name + ' committed';
break;
case 'restart':
details = 'Container ' + eventAttr.name + ' restarted';
break;
case 'pause':
details = 'Container ' + eventAttr.name + ' paused';
break;
case 'unpause':
details = 'Container ' + eventAttr.name + ' unpaused';
break;
case 'attach':
details = 'Container ' + eventAttr.name + ' attached';
break;
case 'detach':
details = 'Container ' + eventAttr.name + ' detached';
break;
case 'copy':
details = 'Container ' + eventAttr.name + ' copied';
break;
case 'export':
details = 'Container ' + eventAttr.name + ' exported';
break;
case 'health_status':
details = 'Container ' + eventAttr.name + ' executed health status';
break;
case 'oom':
details = 'Container ' + eventAttr.name + ' goes in out of memory';
break;
case 'rename':
details = 'Container ' + eventAttr.name + ' renamed';
break;
case 'resize':
details = 'Container ' + eventAttr.name + ' resized';
break;
case 'top':
details = 'Showed running processes for container ' + eventAttr.name;
break;
case 'update':
details = 'Container ' + eventAttr.name + ' updated';
break;
case 'exec_create':
details = 'Exec instance created';
break;
case 'exec_start':
details = 'Exec instance started';
break;
case 'exec_die':
details = 'Exec instance exited';
break;
default:
details = 'Unsupported event';
}
break;
case 'image':
switch (action) {
case 'delete':
details = 'Image deleted';
break;
case 'import':
details = 'Image ' + event.Actor.ID + ' imported';
break;
case 'load':
details = 'Image ' + event.Actor.ID + ' loaded';
break;
case 'tag':
details = 'New tag created for ' + eventAttr.name;
break;
case 'untag':
details = 'Image untagged';
break;
case 'save':
details = 'Image ' + event.Actor.ID + ' saved';
break;
case 'pull':
details = 'Image ' + event.Actor.ID + ' pulled';
break;
case 'push':
details = 'Image ' + event.Actor.ID + ' pushed';
break;
default:
details = 'Unsupported event';
}
break;
case 'network':
switch (action) {
case 'create':
details = 'Network ' + eventAttr.name + ' created';
break;
case 'destroy':
details = 'Network ' + eventAttr.name + ' deleted';
break;
case 'remove':
details = 'Network ' + eventAttr.name + ' removed';
break;
case 'connect':
details = 'Container connected to ' + eventAttr.name + ' network';
break;
case 'disconnect':
details = 'Container disconnected from ' + eventAttr.name + ' network';
break;
default:
details = 'Unsupported event';
}
break;
case 'volume':
switch (action) {
case 'create':
details = 'Volume ' + event.Actor.ID + ' created';
break;
case 'destroy':
details = 'Volume ' + event.Actor.ID + ' deleted';
break;
case 'mount':
details = 'Volume ' + event.Actor.ID + ' mounted';
break;
case 'unmount':
details = 'Volume ' + event.Actor.ID + ' unmounted';
break;
default:
details = 'Unsupported event';
}
break;
default:
details = 'Unsupported event';
}
return details + extra;
}
export function EventViewModel(data) {
// Type, Action, Actor unavailable in Docker < 1.10
this.Time = data.time;
if (data.Type) {
this.Type = data.Type;
this.Details = createEventDetails(data);
} else {
this.Type = data.status;
this.Details = data.from;
}
}

134
app/docker/models/event.ts Normal file
View File

@ -0,0 +1,134 @@
import { EventMessage } from 'docker-types/generated/1.41';
type EventType = NonNullable<EventMessage['Type']>;
type Action = string;
type Attributes = {
id: string;
name: string;
exitCode: string;
};
type EventToTemplateMap = Record<EventType, ActionToTemplateMap>;
type ActionToTemplateMap = Record<Action, TemplateBuilder>;
type TemplateBuilder = (attr: Attributes) => string;
/**
* {
* [EventType]: {
* [Action]: TemplateBuilder,
* [Action]: TemplateBuilder
* },
* [EventType]: {
* [Action]: TemplateBuilder,
* }
* }
*
* EventType are known and defined by Docker specs
* Action are unknown and specific for each EventType
*/
const templates: EventToTemplateMap = {
builder: {},
config: {},
container: {
stop: ({ name }) => `Container ${name} stopped`,
destroy: ({ name }) => `Container ${name} deleted`,
create: ({ name }) => `Container ${name} created`,
start: ({ name }) => `Container ${name} started`,
kill: ({ name }) => `Container ${name} killed`,
die: ({ name, exitCode }) =>
`Container ${name} exited with status code ${exitCode}`,
commit: ({ name }) => `Container ${name} committed`,
restart: ({ name }) => `Container ${name} restarted`,
pause: ({ name }) => `Container ${name} paused`,
unpause: ({ name }) => `Container ${name} unpaused`,
attach: ({ name }) => `Container ${name} attached`,
detach: ({ name }) => `Container ${name} detached`,
copy: ({ name }) => `Container ${name} copied`,
export: ({ name }) => `Container ${name} exported`,
health_status: ({ name }) => `Container ${name} executed health status`,
oom: ({ name }) => `Container ${name} goes in out of memory`,
rename: ({ name }) => `Container ${name} renamed`,
resize: ({ name }) => `Container ${name} resized`,
top: ({ name }) => `Showed running processes for container ${name}`,
update: ({ name }) => `Container ${name} updated`,
exec_create: () => `Exec instance created`,
exec_start: () => `Exec instance started`,
exec_die: () => `Exec instance exited`,
},
daemon: {},
image: {
delete: () => `Image deleted`,
import: ({ id }) => `Image ${id} imported`,
load: ({ id }) => `Image ${id} loaded`,
tag: ({ name }) => `New tag created for ${name}`,
untag: () => `Image untagged`,
save: ({ id }) => `Image ${id} saved`,
pull: ({ id }) => `Image ${id} pulled`,
push: ({ id }) => `Image ${id} pushed`,
},
network: {
create: ({ name }) => `Network ${name} created`,
destroy: ({ name }) => `Network ${name} deleted`,
remove: ({ name }) => `Network ${name} removed`,
connect: ({ name }) => `Container connected to ${name} network`,
disconnect: ({ name }) => `Container disconnected from ${name} network`,
prune: () => `Networks pruned`,
},
node: {},
plugin: {},
secret: {},
service: {},
volume: {
create: ({ id }) => `Volume ${id} created`,
destroy: ({ id }) => `Volume ${id} deleted`,
mount: ({ id }) => `Volume ${id} mounted`,
unmount: ({ id }) => `Volume ${id} unmounted`,
},
};
function createEventDetails(event: EventMessage) {
const eventType = event.Type ?? '';
// An action can be `action:extra`
// For example `docker exec -it CONTAINER sh`
// Generates the action `exec_create: sh`
let extra = '';
let action = event.Action ?? '';
const hasColon = action?.indexOf(':') ?? -1;
if (hasColon !== -1) {
extra = action?.substring(hasColon) ?? '';
action = action?.substring(0, hasColon);
}
const attr: Attributes = {
id: event.Actor?.ID || '',
name: event.Actor?.Attributes?.name || '',
exitCode: event.Actor?.Attributes?.exitCode || '',
};
// Event types are defined by the docker API specs
// Each event has it own set of actions, which a unknown/not defined by specs
// If the received event or action has no builder associated to it
// We consider the event unsupported and we provide the raw data
const detailsBuilder = templates[eventType as EventType]?.[action];
const details = detailsBuilder
? detailsBuilder(attr)
: `Unsupported event: ${eventType} / ${action}`;
return details + extra;
}
export class EventViewModel {
Time: EventMessage['time'];
Type: EventMessage['Type'];
Details: string;
constructor(data: EventMessage) {
this.Time = data.time;
this.Type = data.Type;
this.Details = createEventDetails(data);
}
}

View File

@ -1,45 +0,0 @@
export function ImageViewModel(data) {
this.Id = data.Id;
this.Tag = data.Tag;
this.Repository = data.Repository;
this.Created = data.Created;
this.Checked = false;
this.RepoTags = data.RepoTags;
if ((!this.RepoTags || this.RepoTags.length === 0) && data.RepoDigests) {
this.RepoTags = [];
for (var i = 0; i < data.RepoDigests.length; i++) {
var digest = data.RepoDigests[i];
var repository = digest.substring(0, digest.indexOf('@'));
this.RepoTags.push(repository + ':<none>');
}
}
this.Size = data.Size;
this.Used = data.Used;
if (data.Portainer && data.Portainer.Agent && data.Portainer.Agent.NodeName) {
this.NodeName = data.Portainer.Agent.NodeName;
}
this.Labels = data.Labels;
}
export function ImageBuildModel(data) {
this.hasError = false;
var buildLogs = [];
for (var i = 0; i < data.length; i++) {
var line = data[i];
if (line.stream) {
line = line.stream.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '');
buildLogs.push(line);
}
if (line.errorDetail) {
buildLogs.push(line.errorDetail.message);
this.hasError = true;
}
}
this.buildLogs = buildLogs;
}

View File

@ -0,0 +1,47 @@
import { ImageSummary } from 'docker-types/generated/1.41';
import { PortainerResponse } from '@/react/docker/types';
export type ImageId = ImageSummary['Id'];
export type ImageName = string;
/**
* Partial copy of ImageSummary
*/
export class ImageViewModel {
Id: ImageId;
Created: ImageSummary['Created'];
RepoTags: ImageSummary['RepoTags'];
Size: ImageSummary['Size'];
Labels: ImageSummary['Labels'];
// internal
NodeName: string;
Used: boolean = false;
constructor(data: PortainerResponse<ImageSummary>, used: boolean = false) {
this.Id = data.Id;
// this.Tag = data.Tag; // doesn't seem to be used?
// this.Repository = data.Repository; // doesn't seem to be used?
this.Created = data.Created;
this.RepoTags = data.RepoTags;
if ((!this.RepoTags || this.RepoTags.length === 0) && data.RepoDigests) {
this.RepoTags = [];
data.RepoDigests.forEach((digest) => {
const repository = digest.substring(0, digest.indexOf('@'));
this.RepoTags.push(`${repository}:<none>`);
});
}
this.Size = data.Size;
this.NodeName = data.Portainer?.Agent?.NodeName || '';
this.Labels = data.Labels;
this.Used = used;
}
}

View File

@ -1,27 +0,0 @@
export function ImageDetailsViewModel(data) {
this.Id = data.Id;
this.Tag = data.Tag;
this.Parent = data.Parent;
this.Repository = data.Repository;
this.Created = data.Created;
this.Checked = false;
this.RepoTags = data.RepoTags;
this.Size = data.Size;
this.DockerVersion = data.DockerVersion;
this.Os = data.Os;
this.Architecture = data.Architecture;
this.Author = data.Author;
this.Command = data.Config.Cmd;
let config = {};
if (data.Config) {
config = data.Config; // this is part of OCI images-spec
} else if (data.ContainerConfig != null) {
config = data.ContainerConfig; // not OCI ; has been removed in Docker 26 (API v1.45) along with .Container
}
this.Entrypoint = config.Entrypoint ? config.Entrypoint : '';
this.ExposedPorts = config.ExposedPorts ? Object.keys(config.ExposedPorts) : [];
this.Volumes = config.Volumes ? Object.keys(config.Volumes) : [];
this.Env = config.Env ? config.Env : [];
this.Labels = config.Labels;
}

View File

@ -0,0 +1,70 @@
import { ImageInspect } from 'docker-types/generated/1.41';
type ImageInspectConfig = NonNullable<ImageInspect['Config']>;
export class ImageDetailsViewModel {
Id: ImageInspect['Id'];
Parent: ImageInspect['Parent'];
Created: ImageInspect['Created'];
RepoTags: ImageInspect['RepoTags'];
Size: ImageInspect['Size'];
DockerVersion: ImageInspect['DockerVersion'];
Os: ImageInspect['Os'];
Architecture: ImageInspect['Architecture'];
Author: ImageInspect['Author'];
// Config sub fields
Command: ImageInspectConfig['Cmd'];
Entrypoint: Required<ImageInspectConfig['Entrypoint']>;
ExposedPorts: Required<ImageInspectConfig['ExposedPorts']>;
Volumes: Required<ImageInspectConfig>['Volumes'];
Env: Required<ImageInspectConfig>['Env'];
Labels: ImageInspectConfig['Labels'];
// computed fields
Used: boolean = false;
constructor(data: ImageInspect) {
this.Id = data.Id;
// this.Tag = data.Tag; // doesn't seem to be used?
this.Parent = data.Parent;
this.Created = data.Created;
// this.Repository = data.Repository; // doesn't seem to be used?
this.RepoTags = data.RepoTags;
this.Size = data.Size;
this.DockerVersion = data.DockerVersion;
this.Os = data.Os;
this.Architecture = data.Architecture;
this.Author = data.Author;
this.Command = data.Config?.Cmd;
let config: ImageInspect['Config'] = {};
if (data.Config) {
config = data.Config; // this is part of OCI images-spec
} else if (data.ContainerConfig) {
config = data.ContainerConfig; // not OCI ; has been removed in Docker 26 (API v1.45) along with .Container
}
this.Entrypoint = config.Entrypoint ?? [''];
this.ExposedPorts = config.ExposedPorts
? Object.keys(config.ExposedPorts)
: [];
this.Volumes = config.Volumes ? Object.keys(config.Volumes) : [];
this.Env = config.Env ?? [];
this.Labels = config.Labels;
}
}

View File

@ -1,9 +0,0 @@
export function ImageLayerViewModel(order, data) {
this.Order = order;
this.Id = data.Id;
this.Created = data.Created;
this.CreatedBy = data.CreatedBy;
this.Size = data.Size;
this.Comment = data.Comment;
this.Tags = data.Tags;
}

View File

@ -0,0 +1,27 @@
import { ImageLayer } from '@/react/docker/proxy/queries/images/useImageHistory';
export class ImageLayerViewModel implements ImageLayer {
Id: ImageLayer['Id'];
Created: ImageLayer['Created'];
CreatedBy: ImageLayer['CreatedBy'];
Size: ImageLayer['Size'];
Comment: ImageLayer['Comment'];
Tags: ImageLayer['Tags'];
constructor(
public Order: number,
data: ImageLayer
) {
this.Id = data.Id;
this.Created = data.Created;
this.CreatedBy = data.CreatedBy;
this.Size = data.Size;
this.Comment = data.Comment;
this.Tags = data.Tags;
}
}

View File

@ -2,7 +2,20 @@ import { IPAM, Network, NetworkContainer } from 'docker-types/generated/1.41';
import { ResourceControlViewModel } from '@/react/portainer/access-control/models/ResourceControlViewModel';
import { IResource } from '@/react/docker/components/datatable/createOwnershipColumn';
import { PortainerMetadata } from '@/react/docker/types';
import { PortainerResponse } from '@/react/docker/types';
// TODO later: aggregate NetworkViewModel and DockerNetwork types
//
// type MacvlanNetwork = {
// ConfigFrom?: { Network: string };
// ConfigOnly?: boolean;
// };
//
// type NetworkViewModel = Network & {
// StackName?: string;
// NodeName?: string;
// ResourceControl?: ResourceControlViewModel;
// } & MacvlanNetwork;
export class NetworkViewModel implements IResource {
Id: string;
@ -38,8 +51,7 @@ export class NetworkViewModel implements IResource {
ResourceControl?: ResourceControlViewModel;
constructor(
data: Network & {
Portainer?: PortainerMetadata;
data: PortainerResponse<Network> & {
ConfigFrom?: { Network: string };
ConfigOnly?: boolean;
}

View File

@ -10,8 +10,6 @@ import {
ResourceObject,
} from 'docker-types/generated/1.41';
import { WithRequiredProperty } from '@/types';
export class NodeViewModel {
Model: Node;
@ -55,7 +53,7 @@ export class NodeViewModel {
Status: NodeStatus['State'];
Addr: WithRequiredProperty<NodeStatus, 'Addr'>['Addr'] = '';
Addr: Required<NodeStatus>['Addr'] = '';
Leader: ManagerStatus['Leader'];

View File

@ -1,9 +0,0 @@
// This model is based on https://github.com/moby/moby/blob/0ac25dfc751fa4304ab45afd5cd8705c2235d101/api/types/plugin.go#L8-L31
// instead of the official documentation.
// See: https://github.com/moby/moby/issues/34241
export function PluginViewModel(data) {
this.Id = data.Id;
this.Name = data.Name;
this.Enabled = data.Enabled;
this.Config = data.Config;
}

View File

@ -1,7 +1,7 @@
import { Secret } from 'docker-types/generated/1.41';
import { ResourceControlViewModel } from '@/react/portainer/access-control/models/ResourceControlViewModel';
import { PortainerMetadata } from '@/react/docker/types';
import { PortainerResponse } from '@/react/docker/types';
import { IResource } from '@/react/docker/components/datatable/createOwnershipColumn';
export class SecretViewModel implements IResource {
@ -19,7 +19,7 @@ export class SecretViewModel implements IResource {
ResourceControl?: ResourceControlViewModel;
constructor(data: Secret & { Portainer?: PortainerMetadata }) {
constructor(data: PortainerResponse<Secret>) {
this.Id = data.ID || '';
this.CreatedAt = data.CreatedAt || '';
this.UpdatedAt = data.UpdatedAt || '';

View File

@ -9,15 +9,13 @@ import {
} from 'docker-types/generated/1.41';
import { ResourceControlViewModel } from '@/react/portainer/access-control/models/ResourceControlViewModel';
import { PortainerMetadata } from '@/react/docker/types';
import { WithRequiredProperty } from '@/types';
import { PortainerResponse } from '@/react/docker/types';
import { TaskViewModel } from './task';
type ContainerSpec = WithRequiredProperty<
TaskSpec,
'ContainerSpec'
>['ContainerSpec'];
type ContainerSpec = Required<TaskSpec>['ContainerSpec'];
export type ServiceId = string;
export class ServiceViewModel {
Model: Service;
@ -140,7 +138,7 @@ export class ServiceViewModel {
ResourceControl?: ResourceControlViewModel;
constructor(data: Service & { Portainer?: PortainerMetadata }) {
constructor(data: PortainerResponse<Service>) {
this.Model = data;
this.Id = data.ID || '';
this.Tasks = [];

View File

@ -1,3 +0,0 @@
export function SwarmViewModel(data) {
this.Id = data.ID;
}

View File

@ -1,25 +1,27 @@
import { Task, TaskSpec, TaskState } from 'docker-types/generated/1.41';
import { Task } from 'docker-types/generated/1.41';
import { DeepPick } from '@/types/deepPick';
export class TaskViewModel {
Id: string;
Id: NonNullable<Task['ID']>;
Created: string;
Created: NonNullable<Task['CreatedAt']>;
Updated: string;
Updated: NonNullable<Task['UpdatedAt']>;
Slot: number;
Slot: NonNullable<Task['Slot']>;
Spec?: TaskSpec;
Spec?: Task['Spec'];
Status: Task['Status'];
Status?: Task['Status'];
DesiredState: TaskState;
DesiredState: NonNullable<Task['DesiredState']>;
ServiceId: string;
ServiceId: NonNullable<Task['ServiceID']>;
NodeId: string;
NodeId: NonNullable<Task['NodeID']>;
ContainerId: string = '';
ContainerId: DeepPick<Task, 'Status.ContainerStatus.ContainerID'>;
constructor(data: Task) {
this.Id = data.ID || '';

View File

@ -2,32 +2,32 @@ import { Volume } from 'docker-types/generated/1.41';
import { ResourceControlViewModel } from '@/react/portainer/access-control/models/ResourceControlViewModel';
import { IResource } from '@/react/docker/components/datatable/createOwnershipColumn';
import { PortainerMetadata } from '@/react/docker/types';
import { PortainerResponse } from '@/react/docker/types';
export class VolumeViewModel implements IResource {
Id: string;
Id: Volume['Name'];
CreatedAt: string | undefined;
CreatedAt?: Volume['CreatedAt'];
Driver: string;
Driver: Volume['Driver'];
Options: Record<string, string>;
Options: Volume['Options'];
Labels: Record<string, string>;
Labels: Volume['Labels'];
StackName?: string;
Mountpoint: Volume['Mountpoint'];
Mountpoint: string;
// Portainer properties
ResourceId?: string;
NodeName?: string;
StackName?: string;
ResourceControl?: ResourceControlViewModel;
constructor(
data: Volume & { Portainer?: PortainerMetadata; ResourceID?: string }
) {
constructor(data: PortainerResponse<Volume> & { ResourceID?: string }) {
this.Id = data.Name;
this.CreatedAt = data.CreatedAt;
this.Driver = data.Driver;

View File

@ -1,28 +0,0 @@
import { API_ENDPOINT_ENDPOINTS } from '@/constants';
import { jsonObjectsToArrayHandler } from './response/handlers';
angular.module('portainer.docker').factory('Build', [
'$resource',
function BuildFactory($resource) {
'use strict';
return $resource(
API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/build',
{},
{
buildImage: {
method: 'POST',
ignoreLoadingBar: true,
transformResponse: jsonObjectsToArrayHandler,
isArray: true,
headers: { 'Content-Type': 'application/x-tar' },
},
buildImageOverride: {
method: 'POST',
ignoreLoadingBar: true,
transformResponse: jsonObjectsToArrayHandler,
isArray: true,
},
}
);
},
]);

View File

@ -1,14 +0,0 @@
angular.module('portainer.docker').factory('Commit', [
'$resource',
'API_ENDPOINT_ENDPOINTS',
function CommitFactory($resource, API_ENDPOINT_ENDPOINTS) {
'use strict';
return $resource(
API_ENDPOINT_ENDPOINTS + '/:environmentId/docker/commit',
{},
{
commitContainer: { method: 'POST', params: { container: '@id', repo: '@repo' }, ignoreLoadingBar: true },
}
);
},
]);

View File

@ -1,19 +0,0 @@
angular.module('portainer.docker').factory('Config', [
'$resource',
'API_ENDPOINT_ENDPOINTS',
function ConfigFactory($resource, API_ENDPOINT_ENDPOINTS) {
'use strict';
return $resource(
API_ENDPOINT_ENDPOINTS + '/:environmentId/docker/configs/:id/:action',
{
environmentId: '@environmentId',
},
{
get: { method: 'GET', params: { id: '@id' } },
query: { method: 'GET', isArray: true },
create: { method: 'POST', params: { action: 'create' }, ignoreLoadingBar: true },
remove: { method: 'DELETE', params: { id: '@id' } },
}
);
},
]);

View File

@ -1,73 +0,0 @@
import { genericHandler, logsHandler } from './response/handlers';
angular.module('portainer.docker').factory('Container', [
'$resource',
'API_ENDPOINT_ENDPOINTS',
function ContainerFactory($resource, API_ENDPOINT_ENDPOINTS) {
'use strict';
return $resource(
API_ENDPOINT_ENDPOINTS + '/:environmentId/docker/containers/:id/:action',
{
name: '@name',
environmentId: '@environmentId',
},
{
query: {
method: 'GET',
params: { all: 0, action: 'json', filters: '@filters' },
isArray: true,
},
get: {
method: 'GET',
params: { action: 'json' },
},
logs: {
method: 'GET',
params: { id: '@id', action: 'logs' },
ignoreLoadingBar: true,
transformResponse: logsHandler,
},
stats: {
method: 'GET',
params: { id: '@id', stream: false, action: 'stats' },
ignoreLoadingBar: true,
},
top: {
method: 'GET',
params: { id: '@id', action: 'top' },
ignoreLoadingBar: true,
},
create: {
method: 'POST',
params: { action: 'create' },
transformResponse: genericHandler,
ignoreLoadingBar: true,
},
exec: {
method: 'POST',
params: { id: '@id', action: 'exec' },
transformResponse: genericHandler,
ignoreLoadingBar: true,
},
inspect: {
method: 'GET',
params: { id: '@id', action: 'json' },
},
update: {
method: 'POST',
params: { id: '@id', action: 'update' },
},
prune: {
method: 'POST',
params: { action: 'prune', filters: '@filters' },
},
resize: {
method: 'POST',
params: { id: '@id', action: 'resize', h: '@height', w: '@width' },
transformResponse: genericHandler,
ignoreLoadingBar: true,
},
}
);
},
]);

View File

@ -1,23 +0,0 @@
import { genericHandler } from './response/handlers';
angular.module('portainer.docker').factory('Exec', [
'$resource',
'API_ENDPOINT_ENDPOINTS',
function ExecFactory($resource, API_ENDPOINT_ENDPOINTS) {
'use strict';
return $resource(
API_ENDPOINT_ENDPOINTS + '/:environmentId/docker/exec/:id/:action',
{
environmentId: '@environmentId',
},
{
resize: {
method: 'POST',
params: { id: '@id', action: 'resize', h: '@height', w: '@width' },
transformResponse: genericHandler,
ignoreLoadingBar: true,
},
}
);
},
]);

View File

@ -1,57 +0,0 @@
import { deleteImageHandler, jsonObjectsToArrayHandler } from './response/handlers';
import { imageGetResponse } from './response/image';
angular.module('portainer.docker').factory('Image', [
'$resource',
'API_ENDPOINT_ENDPOINTS',
'EndpointProvider',
'HttpRequestHelper',
function ImageFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, HttpRequestHelper) {
'use strict';
return $resource(
API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/images/:id/:action',
{
endpointId: EndpointProvider.endpointID,
},
{
query: { method: 'GET', params: { all: 0, action: 'json' }, isArray: true },
get: { method: 'GET', params: { action: 'json' } },
search: { method: 'GET', params: { action: 'search' } },
history: { method: 'GET', params: { action: 'history' }, isArray: true },
insert: { method: 'POST', params: { id: '@id', action: 'insert' } },
tag: { method: 'POST', params: { id: '@id', action: 'tag', force: 0, repo: '@repo' }, ignoreLoadingBar: true },
inspect: { method: 'GET', params: { id: '@id', action: 'json' } },
push: {
method: 'POST',
params: { action: 'push', id: '@imageName' },
isArray: true,
transformResponse: jsonObjectsToArrayHandler,
headers: { 'X-Registry-Auth': HttpRequestHelper.registryAuthenticationHeader },
ignoreLoadingBar: true,
},
create: {
method: 'POST',
params: { action: 'create', fromImage: '@fromImage' },
isArray: true,
transformResponse: jsonObjectsToArrayHandler,
headers: { 'X-Registry-Auth': HttpRequestHelper.registryAuthenticationHeader },
ignoreLoadingBar: true,
},
download: {
method: 'GET',
params: { action: 'get', names: '@names' },
transformResponse: imageGetResponse,
responseType: 'blob',
ignoreLoadingBar: true,
},
remove: {
method: 'DELETE',
params: { id: '@id', force: '@force' },
isArray: true,
transformResponse: deleteImageHandler,
},
}
);
},
]);

View File

@ -1,44 +0,0 @@
import { genericHandler } from './response/handlers';
angular.module('portainer.docker').factory('Network', [
'$resource',
'API_ENDPOINT_ENDPOINTS',
'EndpointProvider',
function NetworkFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
'use strict';
return $resource(
API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/networks/:id/:action',
{
id: '@id',
endpointId: EndpointProvider.endpointID,
},
{
query: {
method: 'GET',
isArray: true,
},
get: {
method: 'GET',
},
create: {
method: 'POST',
params: { action: 'create' },
transformResponse: genericHandler,
ignoreLoadingBar: true,
},
remove: {
method: 'DELETE',
transformResponse: genericHandler,
},
connect: {
method: 'POST',
params: { action: 'connect' },
},
disconnect: {
method: 'POST',
params: { action: 'disconnect' },
},
}
);
},
]);

View File

@ -1,20 +0,0 @@
angular.module('portainer.docker').factory('Node', [
'$resource',
'API_ENDPOINT_ENDPOINTS',
'EndpointProvider',
function NodeFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
'use strict';
return $resource(
API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/nodes/:id/:action',
{
endpointId: EndpointProvider.endpointID,
},
{
query: { method: 'GET', isArray: true },
get: { method: 'GET', params: { id: '@id' } },
update: { method: 'POST', params: { id: '@id', action: 'update', version: '@version' } },
remove: { method: 'DELETE', params: { id: '@id' } },
}
);
},
]);

View File

@ -1,17 +0,0 @@
angular.module('portainer.docker').factory('Plugin', [
'$resource',
'API_ENDPOINT_ENDPOINTS',
'EndpointProvider',
function PluginFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
'use strict';
return $resource(
API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/plugins/:id/:action',
{
endpointId: EndpointProvider.endpointID,
},
{
query: { method: 'GET', isArray: true },
}
);
},
]);

View File

@ -1,81 +0,0 @@
function isJSONArray(jsonString) {
return Object.prototype.toString.call(jsonString) === '[object Array]';
}
function isJSON(jsonString) {
try {
var o = JSON.parse(jsonString);
if (o && typeof o === 'object') {
return o;
}
} catch (e) {
//empty
}
return false;
}
// The Docker API often returns a list of JSON object.
// This handler wrap the JSON objects in an array.
// Used by the API in: Image push, Image create, Events query.
export function jsonObjectsToArrayHandler(data) {
// catching empty data helps the function not to fail and prevents unwanted error message to user.
if (!data) {
return [];
}
var str = '[' + data.replace(/\n/g, ' ').replace(/\}\s*\{/g, '}, {') + ']';
return angular.fromJson(str);
}
// The Docker API often returns an empty string or a valid JSON object on success (Docker 1.9 -> Docker 1.12).
// On error, it returns either an error message as a string (Docker < 1.12) or a JSON object with the field message
// container the error (Docker = 1.12)
// This handler ensure a valid JSON object is returned in any case.
// Used by the API in: container deletion, network deletion, network creation, volume creation,
// container exec, exec resize.
export function genericHandler(data) {
var response = {};
// No data is returned when deletion is successful (Docker 1.9 -> 1.12)
if (!data) {
return response;
}
// A string is returned on failure (Docker < 1.12)
else if (!isJSON(data)) {
response.message = data;
}
// Docker 1.12 returns a valid JSON object when an error occurs
else {
response = angular.fromJson(data);
}
return response;
}
// The Docker API returns the logs as a single string.
// This handler wraps the data in a JSON object under the "logs" property.
export function logsHandler(data) {
return {
logs: data,
};
}
// Image delete API returns an array on success (Docker 1.9 -> Docker 1.12).
// On error, it returns either an error message as a string (Docker < 1.12) or a JSON object with the field message
// container the error (Docker = 1.12).
// This handler returns the original array on success or a newly created array containing
// only one JSON object with the field message filled with the error message on failure.
export function deleteImageHandler(data) {
// A string is returned on failure (Docker < 1.12)
var response = [];
if (!isJSON(data)) {
response.push({ message: data });
}
// A JSON object is returned on failure (Docker = 1.12)
else if (!isJSONArray(data)) {
var json = angular.fromJson(data);
response.push(json);
}
// An array is returned on success (Docker 1.9 -> 1.12)
else {
response = angular.fromJson(data);
}
return response;
}

View File

@ -1,9 +0,0 @@
// The get action of the Image service returns a file.
// ngResource will transform it as an array of chars.
// This functions simply creates a response object and assign
// the data to a field.
export function imageGetResponse(data) {
var response = {};
response.file = data;
return response;
}

View File

@ -1,20 +0,0 @@
angular.module('portainer.docker').factory('Secret', [
'$resource',
'API_ENDPOINT_ENDPOINTS',
'EndpointProvider',
function SecretFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
'use strict';
return $resource(
API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/secrets/:id/:action',
{
endpointId: EndpointProvider.endpointID,
},
{
get: { method: 'GET', params: { id: '@id' } },
query: { method: 'GET', isArray: true },
create: { method: 'POST', params: { action: 'create' }, ignoreLoadingBar: true },
remove: { method: 'DELETE', params: { id: '@id' } },
}
);
},
]);

View File

@ -1,45 +0,0 @@
import { logsHandler } from './response/handlers';
angular.module('portainer.docker').factory('Service', [
'$resource',
'API_ENDPOINT_ENDPOINTS',
'EndpointProvider',
'HttpRequestHelper',
function ServiceFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, HttpRequestHelper) {
'use strict';
return $resource(
API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/services/:id/:action',
{
endpointId: EndpointProvider.endpointID,
},
{
get: { method: 'GET', params: { id: '@id' } },
query: { method: 'GET', isArray: true, params: { filters: '@filters' } },
create: {
method: 'POST',
params: { action: 'create' },
headers: {
'X-Registry-Auth': HttpRequestHelper.registryAuthenticationHeader,
version: '1.29',
},
ignoreLoadingBar: true,
},
update: {
method: 'POST',
params: { id: '@id', action: 'update', version: '@version', rollback: '@rollback' },
headers: {
'X-Registry-Auth': (config) => btoa(JSON.stringify({ registryId: config.data.registryId })),
version: '1.29',
},
},
remove: { method: 'DELETE', params: { id: '@id' } },
logs: {
method: 'GET',
params: { id: '@id', action: 'logs' },
ignoreLoadingBar: true,
transformResponse: logsHandler,
},
}
);
},
]);

View File

@ -1,17 +0,0 @@
angular.module('portainer.docker').factory('Swarm', [
'$resource',
'API_ENDPOINT_ENDPOINTS',
'EndpointProvider',
function SwarmFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
'use strict';
return $resource(
API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/swarm',
{
endpointId: EndpointProvider.endpointID,
},
{
get: { method: 'GET' },
}
);
},
]);

View File

@ -1,32 +0,0 @@
import { jsonObjectsToArrayHandler } from './response/handlers';
angular.module('portainer.docker').factory('System', [
'$resource',
'API_ENDPOINT_ENDPOINTS',
'EndpointProvider',
function SystemFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
'use strict';
return $resource(
API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/:action/:subAction',
{
name: '@name',
endpointId: EndpointProvider.endpointID,
},
{
info: {
method: 'GET',
params: { action: 'info' },
},
version: { method: 'GET', params: { action: 'version' } },
events: {
method: 'GET',
params: { action: 'events', since: '@since', until: '@until' },
isArray: true,
transformResponse: jsonObjectsToArrayHandler,
},
auth: { method: 'POST', params: { action: 'auth' } },
dataUsage: { method: 'GET', params: { action: 'system', subAction: 'df' } },
}
);
},
]);

View File

@ -1,26 +0,0 @@
import { logsHandler } from './response/handlers';
angular.module('portainer.docker').factory('Task', [
'$resource',
'API_ENDPOINT_ENDPOINTS',
'EndpointProvider',
function TaskFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
'use strict';
return $resource(
API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/tasks/:id/:action',
{
endpointId: EndpointProvider.endpointID,
},
{
get: { method: 'GET', params: { id: '@id' } },
query: { method: 'GET', isArray: true, params: { filters: '@filters' } },
logs: {
method: 'GET',
params: { id: '@id', action: 'logs' },
ignoreLoadingBar: true,
transformResponse: logsHandler,
},
}
);
},
]);

View File

@ -1,37 +0,0 @@
import { genericHandler } from './response/handlers';
angular.module('portainer.docker').factory('Volume', [
'$resource',
'API_ENDPOINT_ENDPOINTS',
'EndpointProvider',
function VolumeFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
'use strict';
function addVolumeNameToHeader(config) {
return config.data.Name || '';
}
return $resource(
API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/volumes/:id/:action',
{
endpointId: EndpointProvider.endpointID,
},
{
query: { method: 'GET' },
get: { method: 'GET', params: { id: '@id' } },
create: {
method: 'POST',
params: { action: 'create' },
transformResponse: genericHandler,
ignoreLoadingBar: true,
headers: { 'X-Portainer-VolumeName': addVolumeNameToHeader },
},
remove: {
method: 'DELETE',
transformResponse: genericHandler,
params: { id: '@id' },
},
}
);
},
]);

View File

@ -1,91 +1,65 @@
import { ImageBuildModel } from '../models/image';
import {
buildImageFromDockerfileContent,
buildImageFromDockerfileContentAndFiles,
buildImageFromURL,
buildImageFromUpload,
} from '@/react/docker/images/queries/useBuildImageMutation';
angular.module('portainer.docker').factory('BuildService', [
'$q',
'Build',
'FileUploadService',
function BuildServiceFactory($q, Build, FileUploadService) {
'use strict';
var service = {};
import { ImageBuildModel } from '../models/build';
service.buildImageFromUpload = function (endpointID, names, file, path) {
var deferred = $q.defer();
angular.module('portainer.docker').factory('BuildService', BuildServiceFactory);
FileUploadService.buildImage(endpointID, names, file, path)
.then(function success(response) {
var model = new ImageBuildModel(response.data);
deferred.resolve(model);
})
.catch(function error(err) {
deferred.reject(err);
});
/* @ngInject */
function BuildServiceFactory(AngularToReact) {
const { useAxios } = AngularToReact;
return deferred.promise;
};
return {
buildImageFromUpload: useAxios(buildImageFromUploadAngularJS), // build image
buildImageFromURL: useAxios(buildImageFromURLAngularJS), // build image
buildImageFromDockerfileContent: useAxios(buildImageFromDockerfileContentAngularJS), // build image
buildImageFromDockerfileContentAndFiles: useAxios(buildImageFromDockerfileContentAndFilesAngularJS), // build image
};
service.buildImageFromURL = function (endpointId, names, url, path) {
var params = {
endpointId,
t: names,
remote: url,
dockerfile: path,
};
/**
* @param {EnvironmentId} environmentId
* @param {string[]} names
* @param {File} file
* @param {string} path
*/
async function buildImageFromUploadAngularJS(environmentId, names, file, path) {
const data = await buildImageFromUpload(environmentId, names, file, path);
return new ImageBuildModel(data);
}
var deferred = $q.defer();
/**
* @param {EnvironmentId} environmentId
* @param {string[]} names
* @param {string} url
* @param {string} path
*/
async function buildImageFromURLAngularJS(environmentId, names, url, path) {
const data = await buildImageFromURL(environmentId, names, url, path);
return new ImageBuildModel(data);
}
Build.buildImage(params, {})
.$promise.then(function success(data) {
var model = new ImageBuildModel(data);
deferred.resolve(model);
})
.catch(function error(err) {
deferred.reject(err);
});
/**
* @param {EnvironmentId} environmentId
* @param {string[]} names
* @param {string} content
*/
async function buildImageFromDockerfileContentAngularJS(environmentId, names, content) {
const data = await buildImageFromDockerfileContent(environmentId, names, content);
return new ImageBuildModel(data);
}
return deferred.promise;
};
service.buildImageFromDockerfileContent = function (endpointId, names, content) {
var params = {
endpointId,
t: names,
};
var payload = {
content: content,
};
var deferred = $q.defer();
Build.buildImageOverride(params, payload)
.$promise.then(function success(data) {
var model = new ImageBuildModel(data);
deferred.resolve(model);
})
.catch(function error(err) {
deferred.reject(err);
});
return deferred.promise;
};
service.buildImageFromDockerfileContentAndFiles = function (endpointID, names, content, files) {
var dockerfile = new Blob([content], { type: 'text/plain' });
var uploadFiles = [dockerfile].concat(files);
var deferred = $q.defer();
FileUploadService.buildImageFromFiles(endpointID, names, uploadFiles)
.then(function success(response) {
var model = new ImageBuildModel(response.data);
deferred.resolve(model);
})
.catch(function error(err) {
deferred.reject(err);
});
return deferred.promise;
};
return service;
},
]);
/**
* @param {EnvironmentId} environmentId
* @param {string[]} names
* @param {string} content
* @param {File[]} files
*/
async function buildImageFromDockerfileContentAndFilesAngularJS(environmentId, names, content, files) {
const data = await buildImageFromDockerfileContentAndFiles(environmentId, names, content, files);
return new ImageBuildModel(data);
}
}

View File

@ -1,66 +1,37 @@
import { getConfig } from '@/react/docker/configs/queries/useConfig';
import { getConfigs } from '@/react/docker/configs/queries/useConfigs';
import { deleteConfig } from '@/react/docker/configs/queries/useDeleteConfigMutation';
import { createConfig } from '@/react/docker/configs/queries/useCreateConfigMutation';
import { ConfigViewModel } from '../models/config';
angular.module('portainer.docker').factory('ConfigService', [
'$q',
'Config',
function ConfigServiceFactory($q, Config) {
'use strict';
var service = {};
angular.module('portainer.docker').factory('ConfigService', ConfigServiceFactory);
service.config = function (environmentId, configId) {
var deferred = $q.defer();
/* @ngInspect */
function ConfigServiceFactory(AngularToReact) {
const { useAxios } = AngularToReact;
Config.get({ id: configId, environmentId })
.$promise.then(function success(data) {
var config = new ConfigViewModel(data);
deferred.resolve(config);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve config details', err: err });
});
return {
configs: useAxios(listConfigsAngularJS), // config list + service create + service edit
config: useAxios(getConfigAngularJS), // config create + config edit
remove: useAxios(deleteConfig), // config list + config edit
create: useAxios(createConfig), // config create
};
return deferred.promise;
};
/**
* @param {EnvironmentId} environmentId
*/
async function listConfigsAngularJS(environmentId) {
const data = await getConfigs(environmentId);
return data.map((c) => new ConfigViewModel(c));
}
service.configs = function (environmentId) {
var deferred = $q.defer();
Config.query({ environmentId })
.$promise.then(function success(data) {
var configs = data.map(function (item) {
return new ConfigViewModel(item);
});
deferred.resolve(configs);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve configs', err: err });
});
return deferred.promise;
};
service.remove = function (environmentId, configId) {
var deferred = $q.defer();
Config.remove({ environmentId, id: configId })
.$promise.then(function success(data) {
if (data.message) {
deferred.reject({ msg: data.message });
} else {
deferred.resolve();
}
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to remove config', err: err });
});
return deferred.promise;
};
service.create = function (environmentId, config) {
return Config.create({ environmentId }, config).$promise;
};
return service;
},
]);
/**
* @param {EnvironmentId} environmentId
* @param {ConfigId} configId
*/
async function getConfigAngularJS(environmentId, configId) {
const data = await getConfig(environmentId, configId);
return new ConfigViewModel(data);
}
}

View File

@ -9,178 +9,110 @@ import {
startContainer,
stopContainer,
recreateContainer,
getContainerLogs,
} from '@/react/docker/containers/containers.service';
import { ContainerDetailsViewModel, ContainerStatsViewModel, ContainerViewModel } from '../models/container';
import { getContainers } from '@/react/docker/containers/queries/useContainers';
import { getContainer } from '@/react/docker/containers/queries/useContainer';
import { resizeTTY } from '@/react/docker/containers/queries/useContainerResizeTTYMutation';
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 { ContainerDetailsViewModel } from '../models/containerDetails';
import { ContainerStatsViewModel } from '../models/containerStats';
import { formatLogs } from '../helpers/logHelper';
angular.module('portainer.docker').factory('ContainerService', ContainerServiceFactory);
/* @ngInject */
function ContainerServiceFactory($q, Container, $timeout) {
const service = {
killContainer,
pauseContainer,
renameContainer,
restartContainer,
resumeContainer,
startContainer,
stopContainer,
recreateContainer,
remove: removeContainer,
updateRestartPolicy,
updateLimits,
function ContainerServiceFactory(AngularToReact) {
const { useAxios } = AngularToReact;
return {
killContainer: useAxios(killContainer), // container edit
pauseContainer: useAxios(pauseContainer), // container edit
renameContainer: useAxios(renameContainer), // container edit
restartContainer: useAxios(restartContainer), // container edit
resumeContainer: useAxios(resumeContainer), // container edit
startContainer: useAxios(startContainer), // container edit
stopContainer: useAxios(stopContainer), // container edit
recreateContainer: useAxios(recreateContainer), // container edit
remove: useAxios(removeContainer), // container edit
container: useAxios(getContainerAngularJS), // container console + container edit + container stats
containers: useAxios(getContainers), // dashboard + services list + service edit + voluem edit + stackservice + stack create + stack edit
resizeTTY: useAxios(resizeTTYAngularJS), // container console
updateRestartPolicy: useAxios(updateRestartPolicyAngularJS), // container edit
createExec: useAxios(createExec), // container console
containerStats: useAxios(containerStatsAngularJS), // container stats
containerTop: useAxios(containerTop), // container stats
inspect: useAxios(getContainer), // container inspect
logs: useAxios(containerLogsAngularJS), // container logs
};
service.container = function (environmentId, id) {
var deferred = $q.defer();
Container.get({ environmentId, id })
.$promise.then(function success(data) {
var container = new ContainerDetailsViewModel(data);
deferred.resolve(container);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve container information', err: err });
});
return deferred.promise;
};
service.containers = function (environmentId, all, filters) {
var deferred = $q.defer();
Container.query({ environmentId, all, filters })
.$promise.then(function success(data) {
var containers = data.map(function (item) {
return new ContainerViewModel(item);
});
deferred.resolve(containers);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve containers', err: err });
});
return deferred.promise;
};
service.resizeTTY = function (environmentId, id, width, height, timeout) {
var deferred = $q.defer();
$timeout(function () {
Container.resize({}, { environmentId, id, width, height })
.$promise.then(function success(data) {
if (data.message) {
deferred.reject({ msg: 'Unable to resize tty of container ' + id, err: data.message });
} else {
deferred.resolve(data);
}
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to resize tty of container ' + id, err: err });
});
}, timeout);
return deferred.promise;
};
function updateRestartPolicy(environmentId, id, restartPolicy, maximumRetryCounts) {
return Container.update({ environmentId, id }, { RestartPolicy: { Name: restartPolicy, MaximumRetryCount: maximumRetryCounts } }).$promise;
/**
* @param {EnvironmentId} environmentId
* @param {ContainerId} id
* @param {*} param2
*/
async function getContainerAngularJS(environmentId, id, { nodeName } = {}) {
const data = await getContainer(environmentId, id, { nodeName });
return new ContainerDetailsViewModel(data);
}
function updateLimits(environmentId, id, config) {
return Container.update(
{ environmentId, id },
{
// MemorySwap: must be set
// -1: non limits, 0: treated as unset(cause update error).
MemoryReservation: config.HostConfig.MemoryReservation,
Memory: config.HostConfig.Memory,
MemorySwap: -1,
NanoCpus: config.HostConfig.NanoCpus,
}
).$promise;
/**
* @param {EnvironmentId} environmentId
* @param {string} containerId
* @param {number} width
* @param {number} height
* @param timeout DEPRECATED: Previously used in pure AJS implementation
*/
async function resizeTTYAngularJS(environmentId, containerId, width, height) {
return resizeTTY(environmentId, containerId, { width, height });
}
service.createContainer = function (environmentId, configuration) {
var deferred = $q.defer();
Container.create({ environmentId }, configuration)
.$promise.then(function success(data) {
deferred.resolve(data);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to create container', err: err });
});
return deferred.promise;
};
/**
* @param {EnvironmentId} environmentId
* @param {ContainerId} id
* @param {RestartPolicy['Name']} restartPolicy
* @param {RestartPolicy['MaximumRetryCount']} maximumRetryCounts
*/
async function updateRestartPolicyAngularJS(environmentId, id, restartPolicy, maximumRetryCounts) {
return updateContainer(environmentId, id, {
RestartPolicy: {
Name: restartPolicy,
MaximumRetryCount: maximumRetryCounts,
},
});
}
service.createExec = function (environmentId, execConfig) {
var deferred = $q.defer();
/**
* @param {EnvironmentId} environmentId
* @param {ContainerId} id
*/
async function containerStatsAngularJS(environmentId, id) {
const data = await containerStats(environmentId, id);
return new ContainerStatsViewModel(data);
}
Container.exec({ environmentId }, execConfig)
.$promise.then(function success(data) {
if (data.message) {
deferred.reject({ msg: data.message, err: data.message });
} else {
deferred.resolve(data);
}
})
.catch(function error(err) {
deferred.reject(err);
});
return deferred.promise;
};
service.logs = function (environmentId, id, stdout, stderr, timestamps, since, tail, stripHeaders) {
var deferred = $q.defer();
var parameters = {
id: id,
stdout: stdout || 0,
stderr: stderr || 0,
timestamps: timestamps || 0,
since: since || 0,
tail: tail || 'all',
environmentId,
};
Container.logs(parameters)
.$promise.then(function success(data) {
var logs = formatLogs(data.logs, { stripHeaders, withTimestamps: !!timestamps });
deferred.resolve(logs);
})
.catch(function error(err) {
deferred.reject(err);
});
return deferred.promise;
};
service.containerStats = function (environmentId, id) {
var deferred = $q.defer();
Container.stats({ environmentId, id })
.$promise.then(function success(data) {
var containerStats = new ContainerStatsViewModel(data);
deferred.resolve(containerStats);
})
.catch(function error(err) {
deferred.reject(err);
});
return deferred.promise;
};
service.containerTop = function (environmentId, id) {
return Container.top({ environmentId, id }).$promise;
};
service.inspect = function (environmentId, id) {
return Container.inspect({ environmentId, id }).$promise;
};
service.prune = function (environmentId, filters) {
return Container.prune({ environmentId, filters }).$promise;
};
return service;
/**
* @param {EnvironmentId} environmentId
* @param {Containerid} id
* @param {boolean?} stdout
* @param {boolean?} stderr
* @param {boolean?} timestamps
* @param {number?} since
* @param {number?} tail
* @param {boolean?} stripHeaders
*/
async function containerLogsAngularJS(environmentId, id, stdout = false, stderr = false, timestamps = false, since = 0, tail = 'all', stripHeaders) {
const data = await getContainerLogs(environmentId, id, {
since,
stderr,
stdout,
tail,
timestamps,
});
return formatLogs(data, { stripHeaders, withTimestamps: !!timestamps });
}
}

View File

@ -1,31 +1,23 @@
angular.module('portainer.docker').factory('ExecService', [
'$q',
'$timeout',
'Exec',
function ExecServiceFactory($q, $timeout, Exec) {
'use strict';
var service = {};
import { resizeTTY } from '@/react/docker/proxy/queries/useExecResizeTTYMutation';
service.resizeTTY = function (environmentId, execId, width, height, timeout) {
var deferred = $q.defer();
angular.module('portainer.docker').factory('ExecService', ExecServiceFactory);
$timeout(function () {
Exec.resize({ environmentId }, { id: execId, height: height, width: width })
.$promise.then(function success(data) {
if (data.message) {
deferred.reject({ msg: 'Unable to resize tty of exec', err: data.message });
} else {
deferred.resolve(data);
}
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to resize tty of exec', err: err });
});
}, timeout);
/* @ngInject */
function ExecServiceFactory(AngularToReact) {
const { useAxios, injectEnvironmentId } = AngularToReact;
return deferred.promise;
};
return {
resizeTTY: useAxios(injectEnvironmentId(resizeTTYAngularJS)),
};
return service;
},
]);
/**
* @param {EnvironmentId} environmentId Injected
* @param {string} execId
* @param {number} width
* @param {number} height
* @param timeout DEPRECATED: Previously used in pure AJS implementation
*/
async function resizeTTYAngularJS(environmentId, execId, width, height) {
return resizeTTY(environmentId, execId, { width, height });
}
}

View File

@ -1,208 +1,91 @@
import _ from 'lodash';
import { groupBy } from 'lodash';
import { getUniqueTagListFromImages } from '@/react/docker/images/utils';
import { getImage } from '@/react/docker/proxy/queries/images/useImage';
import { parseAxiosError } from '@/portainer/services/axios';
import { getImages } from '@/react/docker/proxy/queries/images/useImages';
import { getContainers } from '@/react/docker/containers/queries/useContainers';
import { getImageHistory } from '@/react/docker/proxy/queries/images/useImageHistory';
import { pullImage } from '@/react/docker/images/queries/usePullImageMutation';
import { pushImage } from '@/react/docker/images/queries/usePushImageMutation';
import { removeImage } from '@/react/docker/proxy/queries/images/useRemoveImageMutation';
import { tagImage } from '@/react/docker/proxy/queries/images/useTagImageMutation';
import { downloadImages } from '@/react/docker/proxy/queries/images/useDownloadImages';
import { uploadImages } from '@/react/docker/proxy/queries/images/useUploadImageMutation';
import { ImageViewModel } from '../models/image';
import { ImageDetailsViewModel } from '../models/imageDetails';
import { ImageLayerViewModel } from '../models/imageLayer';
angular.module('portainer.docker').factory('ImageService', [
'$q',
'Image',
'ImageHelper',
'RegistryService',
'HttpRequestHelper',
'ContainerService',
'FileUploadService',
function ImageServiceFactory($q, Image, ImageHelper, RegistryService, HttpRequestHelper, ContainerService, FileUploadService) {
'use strict';
var service = {};
angular.module('portainer.docker').factory('ImageService', ImageServiceFactory);
service.image = function (imageId) {
var deferred = $q.defer();
Image.get({ id: imageId })
.$promise.then(function success(data) {
if (data.message) {
deferred.reject({ msg: data.message });
} else {
var image = new ImageDetailsViewModel(data);
deferred.resolve(image);
}
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve image details', err: err });
});
return deferred.promise;
};
/* @ngInject */
function ImageServiceFactory(AngularToReact) {
const { useAxios, injectEnvironmentId } = AngularToReact;
service.images = function ({ environmentId, withUsage } = {}) {
var deferred = $q.defer();
return {
image: useAxios(injectEnvironmentId(imageAngularJS)), // container console + image edit
images: useAxios(injectEnvironmentId(imagesAngularJS)), // por image registry controller + dashboard + service edit
history: useAxios(injectEnvironmentId(historyAngularJS)), // image edit
pushImage: useAxios(injectEnvironmentId(pushImageAngularJS)), // image edit
pullImage: useAxios(injectEnvironmentId(pullImageAngularJS)), // images list + image edit + templates list
tagImage: useAxios(injectEnvironmentId(tagImage)), // image edit + image import
downloadImages: useAxios(injectEnvironmentId(downloadImages)), // image list + image edit
uploadImage: useAxios(injectEnvironmentId(uploadImages)), // image import
deleteImage: useAxios(injectEnvironmentId(removeImage)), // image list + image edit
getUniqueTagListFromImages, // por image registry controller + service edit
};
$q.all({
containers: withUsage ? ContainerService.containers(environmentId, 1) : [],
images: Image.query({}).$promise,
})
.then(function success(data) {
var containers = data.containers;
const containerByImageId = _.groupBy(containers, 'ImageID');
async function imageAngularJS(environmentId, imageId) {
const image = await getImage(environmentId, imageId);
return new ImageDetailsViewModel(image);
}
var images = data.images.map(function (item) {
item.Used = !!containerByImageId[item.Id] && containerByImageId[item.Id].length > 0;
return new ImageViewModel(item);
});
deferred.resolve(images);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve images', err: err });
});
return deferred.promise;
};
service.history = function (imageId) {
var deferred = $q.defer();
Image.history({ id: imageId })
.$promise.then(function success(data) {
if (data.message) {
deferred.reject({ msg: data.message });
} else {
var layers = [];
var order = data.length;
angular.forEach(data, function (imageLayer) {
layers.push(new ImageLayerViewModel(order, imageLayer));
order--;
});
deferred.resolve(layers);
}
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve image details', err: err });
});
return deferred.promise;
};
service.pushImage = pushImage;
/**
*
* @param {PorImageRegistryModel} registryModel
*/
function pushImage(registryModel) {
var deferred = $q.defer();
var authenticationDetails = registryModel.Registry.Authentication ? RegistryService.encodedCredentials(registryModel.Registry) : '';
HttpRequestHelper.setRegistryAuthenticationHeader(authenticationDetails);
const imageConfiguration = ImageHelper.createImageConfigForContainer(registryModel);
Image.push({ imageName: imageConfiguration.fromImage })
.$promise.then(function success(data) {
if (data[data.length - 1].error) {
deferred.reject({ msg: data[data.length - 1].error });
} else {
deferred.resolve();
}
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to push image tag', err: err });
});
return deferred.promise;
async function imagesAngularJS(environmentId, withUsage) {
try {
const [containers, images] = await Promise.all([withUsage ? getContainers(environmentId) : [], getImages(environmentId)]);
const containerByImageId = groupBy(containers, 'ImageID');
return images.map((item) => new ImageViewModel(item, !!containerByImageId[item.Id] && containerByImageId[item.Id].length > 0));
} catch (e) {
throw parseAxiosError(e, 'Unable to retrieve images');
}
}
/**
* PULL IMAGE
*/
function pullImageAndIgnoreErrors(imageConfiguration) {
var deferred = $q.defer();
Image.create({}, imageConfiguration)
.$promise.catch(() => {
// left empty to ignore errors
})
.finally(function final() {
deferred.resolve();
});
return deferred.promise;
async function historyAngularJS(environmentId, imageId) {
try {
const layers = await getImageHistory(environmentId, imageId);
return layers.reverse().map((layer, idx) => new ImageLayerViewModel(idx, layer));
} catch (e) {
throw parseAxiosError(e, 'Unable to retrieve image history');
}
}
function pullImageAndAcknowledgeErrors(imageConfiguration) {
var deferred = $q.defer();
/**
* type PorImageRegistryModel = {
* UseRegistry: bool;
* Registry?: Registry;
* Image: string;
* }
*/
Image.create({}, imageConfiguration)
.$promise.then(function success(data) {
var err = data.length > 0 && data[data.length - 1].message;
if (err) {
var detail = data[data.length - 1];
deferred.reject({ msg: detail.message });
} else {
deferred.resolve(data);
}
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to pull image', err: err });
});
/**
* @param {EnvironmentId} environmentId Autofilled by AngularToReact
* @param {PorImageRegistryModel} registryModel
*/
async function pushImageAngularJS(environmentId, registryModel) {
const { UseRegistry, Registry, Image } = registryModel;
const registry = UseRegistry ? Registry : undefined;
return pushImage({ environmentId, image: Image, registry });
}
return deferred.promise;
}
service.pullImage = pullImage;
/**
*
* @param {PorImageRegistryModel} registry
* @param {bool} ignoreErrors
*/
function pullImage(registry, ignoreErrors) {
var authenticationDetails = registry.Registry.Authentication ? RegistryService.encodedCredentials(registry.Registry) : '';
HttpRequestHelper.setRegistryAuthenticationHeader(authenticationDetails);
var imageConfiguration = ImageHelper.createImageConfigForContainer(registry);
if (ignoreErrors) {
return pullImageAndIgnoreErrors(imageConfiguration);
}
return pullImageAndAcknowledgeErrors(imageConfiguration);
}
/**
* ! PULL IMAGE
*/
service.tagImage = function (id, image) {
return Image.tag({ id: id, repo: image }).$promise;
};
/**
*
* @param {Array<{tags: Array<string>; id: string;}>} images
* @returns {Promise<unknown>}
*/
service.downloadImages = function (images) {
var names = ImageHelper.getImagesNamesForDownload(images);
return Image.download(names).$promise;
};
service.uploadImage = function (file) {
return FileUploadService.loadImages(file);
};
service.deleteImage = function (id, forceRemoval) {
var deferred = $q.defer();
Image.remove({ id: id, force: forceRemoval })
.$promise.then(function success(data) {
if (data[0].message) {
deferred.reject({ msg: data[0].message });
} else {
deferred.resolve();
}
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to remove image', err: err });
});
return deferred.promise;
};
service.getUniqueTagListFromImages = getUniqueTagListFromImages;
return service;
},
]);
/**
* @param {EnvironmentId} environmentId Autofilled by AngularToReact
* @param {PorImageRegistryModel} registryModel
* @param {string?} nodeName
*/
async function pullImageAngularJS(environmentId, registryModel, nodeName) {
const { UseRegistry, Registry, Image } = registryModel;
const registry = UseRegistry ? Registry : undefined;
return pullImage({ environmentId, image: Image, nodeName, registry });
}
}

View File

@ -1,91 +1,57 @@
import { createNetwork } from '@/react/docker/networks/queries/useCreateNetworkMutation';
import { getNetwork } from '@/react/docker/networks/queries/useNetwork';
import { getNetworks } from '@/react/docker/networks/queries/useNetworks';
import { deleteNetwork } from '@/react/docker/networks/queries/useDeleteNetworkMutation';
import { disconnectContainer } from '@/react/docker/networks/queries/useDisconnectContainerMutation';
import { connectContainer } from '@/react/docker/networks/queries/useConnectContainerMutation';
import { NetworkViewModel } from '../models/network';
angular.module('portainer.docker').factory('NetworkService', [
'$q',
'Network',
function NetworkServiceFactory($q, Network) {
'use strict';
var service = {};
angular.module('portainer.docker').factory('NetworkService', NetworkServiceFactory);
service.create = function (networkConfiguration) {
var deferred = $q.defer();
/* @ngInject */
function NetworkServiceFactory(AngularToReact) {
const { useAxios, injectEnvironmentId } = AngularToReact;
Network.create(networkConfiguration)
.$promise.then(function success(data) {
deferred.resolve(data);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to create network', err: err });
});
return deferred.promise;
};
return {
create: useAxios(injectEnvironmentId(createNetwork)), // create network
network: useAxios(injectEnvironmentId(networkAngularJS)), // service edit
networks: useAxios(injectEnvironmentId(networksAngularJS)), // macvlan form + container edit + dashboard + service create + service edit + custom templates list + templates list
remove: useAxios(injectEnvironmentId(deleteNetwork)), // networks list
disconnectContainer: useAxios(injectEnvironmentId(disconnectContainer)), // container edit
connectContainer: useAxios(injectEnvironmentId(connectContainerAngularJS)), // container edit
};
service.network = function (id) {
var deferred = $q.defer();
/**
* @param {EnvironmentId} environmentId filled by AngularToReact
* @param {NetworkId} networkId
* @param {string?} nodeName
* @returns NetworkViewModel
*/
async function networkAngularJS(environmentId, networkId, nodeName) {
const data = await getNetwork(environmentId, networkId, { nodeName });
return new NetworkViewModel(data);
}
Network.get({ id: id })
.$promise.then(function success(data) {
var network = new NetworkViewModel(data);
deferred.resolve(network);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve network details', err: err });
});
/**
* @param {EnvironmentId} environmentId filled by AngularToReact
* @param {boolean?} localNetworks
* @param {boolean?} swarmNetworks
* @param {boolean?} swarmAttachableNetworks
* @param {*} filters
* @returns NetworkViewModel[]
*/
async function networksAngularJS(environmentId, local, swarm, swarmAttachable, filters) {
const data = await getNetworks(environmentId, { local, swarm, swarmAttachable, filters });
return data.map((n) => new NetworkViewModel(n));
}
return deferred.promise;
};
service.networks = function (localNetworks, swarmNetworks, swarmAttachableNetworks, filters) {
var deferred = $q.defer();
Network.query({ filters: filters })
.$promise.then(function success(data) {
var networks = data;
var filteredNetworks = networks
.filter(function (network) {
if (localNetworks && network.Scope === 'local') {
return network;
}
if (swarmNetworks && network.Scope === 'swarm') {
return network;
}
if (swarmAttachableNetworks && network.Scope === 'swarm' && network.Attachable === true) {
return network;
}
})
.map(function (item) {
return new NetworkViewModel(item);
});
deferred.resolve(filteredNetworks);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve networks', err: err });
});
return deferred.promise;
};
service.remove = function (id) {
return Network.remove({ id: id }).$promise;
};
service.disconnectContainer = function (networkId, containerId, force) {
return Network.disconnect({ id: networkId }, { Container: containerId, Force: force }).$promise;
};
service.connectContainer = function (networkId, containerId, aliases) {
var payload = {
Container: containerId,
};
if (aliases) {
payload.EndpointConfig = {
Aliases: aliases,
};
}
return Network.connect({ id: networkId }, payload).$promise;
};
return service;
},
]);
/**
* @param {EnvironmentId} environmentId filled by AngularToReact
* @param {NetworkId} networkId
* @param {ContainerId} containerId
*/
async function connectContainerAngularJS(environmentId, networkId, containerId) {
return connectContainer({ environmentId, containerId, networkId });
}
}

View File

@ -1,73 +1,43 @@
import { getNode } from '@/react/docker/proxy/queries/nodes/useNode';
import { getNodes } from '@/react/docker/proxy/queries/nodes/useNodes';
import { updateNode } from '@/react/docker/proxy/queries/nodes/useUpdateNodeMutation';
import { NodeViewModel } from '../models/node';
angular.module('portainer.docker').factory('NodeService', [
'$q',
'Node',
function NodeServiceFactory($q, Node) {
'use strict';
var service = {};
angular.module('portainer.docker').factory('NodeService', NodeServiceFactory);
service.nodes = nodes;
service.node = node;
service.updateNode = updateNode;
service.getActiveManager = getActiveManager;
/* @ngInject */
function NodeServiceFactory(AngularToReact) {
const { useAxios, injectEnvironmentId } = AngularToReact;
function node(id) {
var deferred = $q.defer();
Node.get({ id: id })
.$promise.then(function onNodeLoaded(rawNode) {
var node = new NodeViewModel(rawNode);
return deferred.resolve(node);
})
.catch(function onFailed(err) {
deferred.reject({ msg: 'Unable to retrieve node', err: err });
});
return {
nodes: useAxios(injectEnvironmentId(nodesAngularJS)), // macvlan form + services list + service create + service edit + swarm visualizer + stack edit
node: useAxios(injectEnvironmentId(nodeAngularJS)), // node browser + node details
updateNode: useAxios(injectEnvironmentId(updateNodeAngularJS)), // swarm node details panel
};
return deferred.promise;
}
/**
* @param {EnvironmentId} environmentId
* @param {NodeId} id
*/
async function nodeAngularJS(environmentId, id) {
const data = await getNode(environmentId, id);
return new NodeViewModel(data);
}
function nodes() {
var deferred = $q.defer();
/**
* @param {EnvironmentId} environmentId
*/
async function nodesAngularJS(environmentId) {
const data = await getNodes(environmentId);
return data.map((n) => new NodeViewModel(n));
}
Node.query({})
.$promise.then(function success(data) {
var nodes = data.map(function (item) {
return new NodeViewModel(item);
});
deferred.resolve(nodes);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve nodes', err: err });
});
return deferred.promise;
}
function updateNode(node) {
return Node.update({ id: node.Id, version: node.Version }, node).$promise;
}
function getActiveManager() {
var deferred = $q.defer();
service
.nodes()
.then(function success(data) {
for (var i = 0; i < data.length; ++i) {
var node = data[i];
if (node.Role === 'manager' && node.Availability === 'active' && node.Status === 'ready' && node.Addr !== '0.0.0.0') {
deferred.resolve(node);
break;
}
}
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve nodes', err: err });
});
return deferred.promise;
}
return service;
},
]);
/**
* @param {EnvironmentId} environmentId
* @param {NodeSpec & { Id: string; Version: number }} nodeConfig
*/
async function updateNodeAngularJS(environmentId, nodeConfig) {
return updateNode(environmentId, nodeConfig.Id, nodeConfig, nodeConfig.Version);
}
}

View File

@ -1,76 +1,51 @@
import _ from 'lodash-es';
import { PluginViewModel } from '../models/plugin';
import { isFulfilled } from '@/portainer/helpers/promise-utils';
import { getInfo } from '@/react/docker/proxy/queries/useInfo';
import { aggregateData, getPlugins } from '@/react/docker/proxy/queries/useServicePlugins';
angular.module('portainer.docker').factory('PluginService', [
'$q',
'Plugin',
'SystemService',
function PluginServiceFactory($q, Plugin, SystemService) {
'use strict';
var service = {};
angular.module('portainer.docker').factory('PluginService', PluginServiceFactory);
service.plugins = function () {
var deferred = $q.defer();
var plugins = [];
/* @ngInject */
function PluginServiceFactory(AngularToReact) {
const { useAxios, injectEnvironmentId } = AngularToReact;
Plugin.query({})
.$promise.then(function success(data) {
for (var i = 0; i < data.length; i++) {
var plugin = new PluginViewModel(data[i]);
plugins.push(plugin);
}
})
.finally(function final() {
deferred.resolve(plugins);
});
return {
volumePlugins: useAxios(injectEnvironmentId(volumePlugins)), // volume create
networkPlugins: useAxios(injectEnvironmentId(networksPlugins)), // network create
loggingPlugins: useAxios(injectEnvironmentId(loggingPlugins)), // service create + service edit
};
}
return deferred.promise;
};
/**
* @param {EnvironmentId} environmentId Injected
* @param {boolean} systemOnly
*/
async function volumePlugins(environmentId, systemOnly) {
const { systemPluginsData, pluginsData } = await getAllPlugins(environmentId);
return aggregateData(systemPluginsData, pluginsData, systemOnly, 'Volume');
}
function servicePlugins(systemOnly, pluginType, pluginVersion) {
var deferred = $q.defer();
/**
* @param {EnvironmentId} environmentId Injected
* @param {boolean} systemOnly
*/
async function networksPlugins(environmentId, systemOnly) {
const { systemPluginsData, pluginsData } = await getAllPlugins(environmentId);
return aggregateData(systemPluginsData, pluginsData, systemOnly, 'Network');
}
$q.all({
system: SystemService.plugins(),
plugins: systemOnly ? [] : service.plugins(),
})
.then(function success(data) {
var aggregatedPlugins = [];
var systemPlugins = data.system;
var plugins = data.plugins;
/**
* @param {EnvironmentId} environmentId Injected
* @param {boolean} systemOnly
*/
async function loggingPlugins(environmentId, systemOnly) {
const { systemPluginsData, pluginsData } = await getAllPlugins(environmentId);
return aggregateData(systemPluginsData, pluginsData, systemOnly, 'Log');
}
if (systemPlugins[pluginType]) {
aggregatedPlugins = aggregatedPlugins.concat(systemPlugins[pluginType]);
}
async function getAllPlugins(environmentId) {
const [system, plugins] = await Promise.allSettled([getInfo(environmentId), getPlugins(environmentId)]);
const systemPluginsData = isFulfilled(system) ? system.value.Plugins : undefined;
const pluginsData = isFulfilled(plugins) ? plugins.value : undefined;
for (var i = 0; i < plugins.length; i++) {
var plugin = plugins[i];
if (plugin.Enabled && _.includes(plugin.Config.Interface.Types, pluginVersion)) {
aggregatedPlugins.push(plugin.Name);
}
}
deferred.resolve(aggregatedPlugins);
})
.catch(function error(err) {
deferred.reject({ msg: err.msg, err: err });
});
return deferred.promise;
}
service.volumePlugins = function (systemOnly) {
return servicePlugins(systemOnly, 'Volume', 'docker.volumedriver/1.0');
};
service.networkPlugins = function (systemOnly) {
return servicePlugins(systemOnly, 'Network', 'docker.networkdriver/1.0');
};
service.loggingPlugins = function (systemOnly) {
return servicePlugins(systemOnly, 'Log', 'docker.logdriver/1.0');
};
return service;
},
]);
return { systemPluginsData, pluginsData };
}

View File

@ -1,66 +1,37 @@
import { getSecret } from '@/react/docker/proxy/queries/secrets/useSecret';
import { getSecrets } from '@/react/docker/proxy/queries/secrets/useSecrets';
import { removeSecret } from '@/react/docker/proxy/queries/secrets/useRemoveSecretMutation';
import { createSecret } from '@/react/docker/proxy/queries/secrets/useCreateSecretMutation';
import { SecretViewModel } from '../models/secret';
angular.module('portainer.docker').factory('SecretService', [
'$q',
'Secret',
function SecretServiceFactory($q, Secret) {
'use strict';
var service = {};
angular.module('portainer.docker').factory('SecretService', SecretServiceFactory);
service.secret = function (secretId) {
var deferred = $q.defer();
/* @ngInject */
function SecretServiceFactory(AngularToReact) {
const { useAxios, injectEnvironmentId } = AngularToReact;
Secret.get({ id: secretId })
.$promise.then(function success(data) {
var secret = new SecretViewModel(data);
deferred.resolve(secret);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve secret details', err: err });
});
return {
secret: useAxios(injectEnvironmentId(secretAngularJS)), // secret edit
secrets: useAxios(injectEnvironmentId(secretsAngularJS)), // secret list + service create + service edit
remove: useAxios(injectEnvironmentId(removeSecret)), // secret list + secret edit
create: useAxios(injectEnvironmentId(createSecret)), // secret create
};
return deferred.promise;
};
/**
* @param {EnvironmentId} environmentId Injected
* @param {SecretId} id
*/
async function secretAngularJS(environmentId, id) {
const data = await getSecret(environmentId, id);
return new SecretViewModel(data);
}
service.secrets = function () {
var deferred = $q.defer();
Secret.query({})
.$promise.then(function success(data) {
var secrets = data.map(function (item) {
return new SecretViewModel(item);
});
deferred.resolve(secrets);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve secrets', err: err });
});
return deferred.promise;
};
service.remove = function (secretId) {
var deferred = $q.defer();
Secret.remove({ id: secretId })
.$promise.then(function success(data) {
if (data.message) {
deferred.reject({ msg: data.message });
} else {
deferred.resolve();
}
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to remove secret', err: err });
});
return deferred.promise;
};
service.create = function (secretConfig) {
return Secret.create(secretConfig).$promise;
};
return service;
},
]);
/**
* @param {EnvironmentId} environmentId Injected
*/
async function secretsAngularJS(environmentId) {
const data = await getSecrets(environmentId);
return data.map((s) => new SecretViewModel(s));
}
}

View File

@ -1,100 +1,97 @@
import { formatLogs } from '../helpers/logHelper';
import { removeService } from '@/react/docker/services/ListView/ServicesDatatable/useRemoveServicesMutation';
import { createService } from '@/react/docker/services/queries/useCreateServiceMutation';
import { getService } from '@/react/docker/services/queries/useService';
import { getServices } from '@/react/docker/services/queries/useServices';
import { updateService } from '@/react/docker/services/queries/useUpdateServiceMutation';
import { getServiceLogs } from '@/react/docker/services/queries/useServiceLogs';
import { ServiceViewModel } from '../models/service';
import { formatLogs } from '../helpers/logHelper';
angular.module('portainer.docker').factory('ServiceService', [
'$q',
'Service',
function ServiceServiceFactory($q, Service) {
'use strict';
var service = {};
angular.module('portainer.docker').factory('ServiceService', ServiceServiceFactory);
service.services = function (filters) {
var deferred = $q.defer();
/* @ngInject */
function ServiceServiceFactory(AngularToReact) {
const { useAxios, injectEnvironmentId } = AngularToReact;
Service.query({ filters: filters ? filters : {} })
.$promise.then(function success(data) {
var services = data.map(function (item) {
return new ServiceViewModel(item);
});
deferred.resolve(services);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve services', err: err });
});
return {
services: useAxios(injectEnvironmentId(getServicesAngularJS)), // dashboard + service list + swarm visualizer + volume list + stackservice + stack edit
service: useAxios(injectEnvironmentId(getServiceAngularJS)), // service edit + task edit
remove: useAxios(injectEnvironmentId(removeServiceAngularJS)), // service edit
update: useAxios(injectEnvironmentId(updateServiceAngularJS)), // service edit
create: useAxios(injectEnvironmentId(createServiceAngularJS)), // service create
logs: useAxios(injectEnvironmentId(serviceLogsAngularJS)), // service logs
};
return deferred.promise;
};
/**
* @param {EnvironmentId} environmentId Injected
* @param {*} filters
*/
async function getServicesAngularJS(environmentId, filters) {
const data = await getServices(environmentId, filters);
return data.map((s) => new ServiceViewModel(s));
}
service.service = function (id) {
var deferred = $q.defer();
/**
* @param {EnvironmentId} environmentId Injected
* @param {ServiceId} serviceId
*/
async function getServiceAngularJS(environmentId, serviceId) {
const data = await getService(environmentId, serviceId);
return new ServiceViewModel(data);
}
Service.get({ id: id })
.$promise.then(function success(data) {
var service = new ServiceViewModel(data);
deferred.resolve(service);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve service details', err: err });
});
/**
* @param {EnvironmentId} environmentId Injected
* @param {ServiceViewModel} service
*/
async function removeServiceAngularJS(environmentId, service) {
return removeService(environmentId, service.Id);
}
return deferred.promise;
};
/**
* @param {EnvironmentId} environmentId Injected
* @param {ServiceViewModel} service
* @param {ServiceUpdateConfig} config
* @param {string?} rollback
*/
async function updateServiceAngularJS(environmentId, service, config, rollback) {
return updateService({
environmentId,
config,
serviceId: service.Id,
version: service.Version,
registryId: config.registryId,
rollback,
});
}
service.remove = function (service) {
var deferred = $q.defer();
/**
* @param {EnvironmentId} environmentId Injected
* @param {Service} config
* @param {RegistryId} registryId
*/
async function createServiceAngularJS(environmentId, config, registryId) {
return createService({ environmentId, config, registryId });
}
Service.remove({ id: service.Id })
.$promise.then(function success(data) {
if (data.message) {
deferred.reject({ msg: data.message, err: data.message });
} else {
deferred.resolve();
}
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to remove service', err: err });
});
return deferred.promise;
};
service.update = function (serv, config, rollback) {
return service.service(serv.Id).then((data) => {
const params = {
id: serv.Id,
version: data.Version,
};
if (rollback) {
params.rollback = rollback;
}
return Service.update(params, config).$promise;
});
};
service.logs = function (id, stdout, stderr, timestamps, since, tail) {
var deferred = $q.defer();
var parameters = {
id: id,
stdout: stdout || 0,
stderr: stderr || 0,
timestamps: timestamps || 0,
since: since || 0,
tail: tail || 'all',
};
Service.logs(parameters)
.$promise.then(function success(data) {
var logs = formatLogs(data.logs, { stripHeaders: true, withTimestamps: !!timestamps });
deferred.resolve(logs);
})
.catch(function error(err) {
deferred.reject(err);
});
return deferred.promise;
};
return service;
},
]);
/**
* @param {EnvironmentId} environmentId Injected
* @param {ServiceId} id
* @param {boolean?} stdout
* @param {boolean?} stderr
* @param {boolean?} timestamps
* @param {number?} since
* @param {number?} tail
*/
async function serviceLogsAngularJS(environmentId, id, stdout = false, stderr = false, timestamps = false, since = 0, tail = 'all') {
const data = await getServiceLogs(environmentId, id, {
since,
stderr,
stdout,
tail,
timestamps,
});
return formatLogs(data, { stripHeaders: true, withTimestamps: !!timestamps });
}
}

View File

@ -1,27 +1,12 @@
import { SwarmViewModel } from '../models/swarm';
import { getSwarm } from '@/react/docker/proxy/queries/useSwarm';
angular.module('portainer.docker').factory('SwarmService', [
'$q',
'Swarm',
function SwarmServiceFactory($q, Swarm) {
'use strict';
var service = {};
angular.module('portainer.docker').factory('SwarmService', SwarmServiceFactory);
service.swarm = function (endpointId) {
var deferred = $q.defer();
/* @ngInject */
function SwarmServiceFactory(AngularToReact) {
const { useAxios } = AngularToReact;
Swarm.get(endpointId ? { endpointId } : undefined)
.$promise.then(function success(data) {
var swarm = new SwarmViewModel(data);
deferred.resolve(swarm);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve Swarm details', err: err });
});
return deferred.promise;
};
return service;
},
]);
return {
swarm: useAxios(getSwarm), // stack service
};
}

View File

@ -1,59 +1,28 @@
import { ping } from '@/react/docker/proxy/queries/usePing';
import { getInfo } from '@/react/docker/proxy/queries/useInfo';
import { getVersion } from '@/react/docker/proxy/queries/useVersion';
import { getEvents } from '@/react/docker/proxy/queries/useEvents';
import { EventViewModel } from '../models/event';
import { ping } from './ping';
angular.module('portainer.docker').factory('SystemService', [
'$q',
'System',
function SystemServiceFactory($q, System) {
'use strict';
var service = {};
angular.module('portainer.docker').factory('SystemService', SystemServiceFactory);
service.plugins = function () {
var deferred = $q.defer();
System.info({})
.$promise.then(function success(data) {
var plugins = data.Plugins;
deferred.resolve(plugins);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve plugins information from system', err: err });
});
return deferred.promise;
};
/* @ngInject */
function SystemServiceFactory(AngularToReact) {
const { useAxios, injectEnvironmentId } = AngularToReact;
service.info = function () {
return System.info({}).$promise;
};
return {
info: useAxios(injectEnvironmentId(getInfo)), // dashboard + docker host view + docker host browser + swarm inspect views + stateManager (update endpoint state)
ping: useAxios(ping), // docker/__module onEnter abstract /docker subpath
version: useAxios(injectEnvironmentId(getVersion)), // docker host view + swarm inspect view + stateManager (update endpoint state)
events: useAxios(injectEnvironmentId(eventsAngularJS)), // events list
};
service.ping = function (endpointId) {
return ping(endpointId);
};
service.version = function () {
return System.version({}).$promise;
};
service.events = function (from, to) {
var deferred = $q.defer();
System.events({ since: from, until: to })
.$promise.then(function success(data) {
var events = data.map(function (item) {
return new EventViewModel(item);
});
deferred.resolve(events);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve engine events', err: err });
});
return deferred.promise;
};
service.dataUsage = function () {
return System.dataUsage().$promise;
};
return service;
},
]);
/**
* @param {EnvironmentId} environmentId Injected
* @param {{since: string; until: string;}} param1
*/
async function eventsAngularJS(environmentId, { since, until }) {
const data = await getEvents(environmentId, { since, until });
return data.map((e) => new EventViewModel(e));
}
}

View File

@ -1,69 +1,57 @@
import { formatLogs } from '../helpers/logHelper';
import { getTask } from '@/react/docker/tasks/queries/useTask';
import { getTasks } from '@/react/docker/proxy/queries/tasks/useTasks';
import { getTaskLogs } from '@/react/docker/tasks/queries/useTaskLogs';
import { TaskViewModel } from '../models/task';
import { formatLogs } from '../helpers/logHelper';
angular.module('portainer.docker').factory('TaskService', [
'$q',
'Task',
function TaskServiceFactory($q, Task) {
'use strict';
var service = {};
angular.module('portainer.docker').factory('TaskService', TaskServiceFactory);
service.task = function (id) {
var deferred = $q.defer();
/* @ngInject */
function TaskServiceFactory(AngularToReact) {
const { useAxios, injectEnvironmentId } = AngularToReact;
Task.get({ id: id })
.$promise.then(function success(data) {
var task = new TaskViewModel(data);
deferred.resolve(task);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve task details', err: err });
});
return {
task: useAxios(injectEnvironmentId(taskAngularJS)), // task edit
tasks: useAxios(injectEnvironmentId(tasksAngularJS)), // services list + service edit + swarm visualizer + stack edit
logs: useAxios(injectEnvironmentId(taskLogsAngularJS)), // task logs
};
return deferred.promise;
};
/**
* @param {EnvironmentId} environmentId Injected
* @param {TaskId} id
*/
async function taskAngularJS(environmentId, id) {
const data = await getTask(environmentId, id);
return new TaskViewModel(data);
}
service.tasks = function (filters) {
var deferred = $q.defer();
/**
* @param {EnvironmentId} environmentId Injected
* @param {*} filters
*/
async function tasksAngularJS(environmentId, filters) {
const data = await getTasks(environmentId, filters);
return data.map((t) => new TaskViewModel(t));
}
Task.query({ filters: filters ? filters : {} })
.$promise.then(function success(data) {
var tasks = data.map(function (item) {
return new TaskViewModel(item);
});
deferred.resolve(tasks);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve tasks', err: err });
});
return deferred.promise;
};
service.logs = function (id, stdout, stderr, timestamps, since, tail) {
var deferred = $q.defer();
var parameters = {
id: id,
stdout: stdout || 0,
stderr: stderr || 0,
timestamps: timestamps || 0,
since: since || 0,
tail: tail || 'all',
};
Task.logs(parameters)
.$promise.then(function success(data) {
var logs = formatLogs(data.logs, { stripHeaders: true, withTimestamps: !!timestamps });
deferred.resolve(logs);
})
.catch(function error(err) {
deferred.reject(err);
});
return deferred.promise;
};
return service;
},
]);
/**
* @param {EnvironmentId} environmentId
* @param {TaskId} id
* @param {boolean?} stdout
* @param {boolean?} stderr
* @param {boolean?} timestamps
* @param {number?} since
* @param {number?} tail
*/
async function taskLogsAngularJS(environmentId, id, stdout = false, stderr = false, timestamps = false, since = 0, tail = 'all') {
const data = await getTaskLogs(environmentId, id, {
since,
stderr,
stdout,
tail,
timestamps,
});
return formatLogs(data, { stripHeaders: true, withTimestamps: !!timestamps });
}
}

View File

@ -1,97 +1,79 @@
import { getVolumes } from '@/react/docker/volumes/queries/useVolumes';
import { getVolume } from '@/react/docker/volumes/queries/useVolume';
import { removeVolume } from '@/react/docker/volumes/queries/useRemoveVolumeMutation';
import { createVolume } from '@/react/docker/volumes/queries/useCreateVolumeMutation';
import { VolumeViewModel } from '../models/volume';
angular.module('portainer.docker').factory('VolumeService', [
'$q',
'Volume',
'VolumeHelper',
function VolumeServiceFactory($q, Volume, VolumeHelper) {
'use strict';
var service = {};
angular.module('portainer.docker').factory('VolumeService', VolumeServiceFactory);
service.volumes = function (params) {
var deferred = $q.defer();
Volume.query(params)
.$promise.then(function success(data) {
var volumes = data.Volumes || [];
volumes = volumes.map(function (item) {
return new VolumeViewModel(item);
});
deferred.resolve(volumes);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve volumes', err: err });
});
return deferred.promise;
/* @ngInject */
function VolumeServiceFactory(AngularToReact) {
const { useAxios, injectEnvironmentId } = AngularToReact;
return {
volumes: useAxios(injectEnvironmentId(volumesAngularJS)), // dashboard + service create + service edit + volume list
volume: useAxios(injectEnvironmentId(volumeAngularJS)), // volume edit
getVolumes: useAxios(injectEnvironmentId(getVolumesAngularJS)), // template list
remove: useAxios(injectEnvironmentId(removeAngularJS)), // volume list + volume edit
createVolume: useAxios(injectEnvironmentId(createAngularJS)), // volume create
createVolumeConfiguration, // volume create
};
/**
* @param {EnvironmentId} environmentId Injected
* @param {Filters} filters
*/
async function volumesAngularJS(environmentId, filters) {
const data = await getVolumes(environmentId, filters);
return data.map((v) => new VolumeViewModel(v));
}
/**
* @param {EnvironmentId} environmentId Injected
* @param {string} id
*/
async function volumeAngularJS(environmentId, id) {
const data = await getVolume(environmentId, id);
return new VolumeViewModel(data);
}
/**
* @param {EnvironmentId} environmentId Injected
*/
async function getVolumesAngularJS(environmentId) {
return getVolumes(environmentId);
}
/**
* @param {EnvironmentId} environmentId Injected
* @param {string} name
* @param {string?} nodeName
*/
async function removeAngularJS(environmentId, name, nodeName) {
return removeVolume(environmentId, name, { nodeName });
}
/**
* @param {string} name
* @param {string} driver
* @param {{name: string; value: string;}[]} driverOptions
*/
function createVolumeConfiguration(name, driver, driverOptions) {
return {
Name: name,
Driver: driver,
DriverOpts: driverOptions.reduce((res, { name, value }) => ({ ...res, [name]: value }), {}),
};
}
service.volume = function (id) {
var deferred = $q.defer();
Volume.get({ id: id })
.$promise.then(function success(data) {
var volume = new VolumeViewModel(data);
deferred.resolve(volume);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve volume details', err: err });
});
return deferred.promise;
};
service.getVolumes = function () {
return Volume.query({}).$promise;
};
service.remove = function (volume) {
var deferred = $q.defer();
Volume.remove({ id: volume.Id })
.$promise.then(function success(data) {
if (data.message) {
deferred.reject({ msg: data.message, err: data.message });
} else {
deferred.resolve();
}
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to remove volume', err: err });
});
return deferred.promise;
};
service.createVolumeConfiguration = function (name, driver, driverOptions) {
var volumeConfiguration = {
Name: name,
Driver: driver,
DriverOpts: VolumeHelper.createDriverOptions(driverOptions),
};
return volumeConfiguration;
};
service.createVolume = function (volumeConfiguration) {
var deferred = $q.defer();
Volume.create(volumeConfiguration)
.$promise.then(function success(data) {
if (data.message) {
deferred.reject({ msg: data.message });
} else {
var volume = new VolumeViewModel(data);
deferred.resolve(volume);
}
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to create volume', err: err });
});
return deferred.promise;
};
service.createVolumes = function (volumeConfigurations) {
var createVolumeQueries = volumeConfigurations.map(function (volumeConfiguration) {
return service.createVolume(volumeConfiguration);
});
return $q.all(createVolumeQueries);
};
return service;
},
]);
/**
* @param {EnvironmentId} environmentId Injected
* @param {VolumeConfiguration} volumeConfiguration
* @param {string?} nodeName
*/
async function createAngularJS(environmentId, volumeConfiguration, nodeName) {
const data = await createVolume(environmentId, volumeConfiguration, { nodeName });
return new VolumeViewModel(data);
}
}

View File

@ -9,28 +9,12 @@ angular.module('portainer.docker').controller('ContainerConsoleController', [
'ContainerService',
'ImageService',
'Notifications',
'ContainerHelper',
'ExecService',
'HttpRequestHelper',
'LocalStorage',
'CONSOLE_COMMANDS_LABEL_PREFIX',
'SidebarService',
'endpoint',
function (
$scope,
$state,
$transition$,
ContainerService,
ImageService,
Notifications,
ContainerHelper,
ExecService,
HttpRequestHelper,
LocalStorage,
CONSOLE_COMMANDS_LABEL_PREFIX,
SidebarService,
endpoint
) {
function ($scope, $state, $transition$, ContainerService, ImageService, Notifications, ExecService, HttpRequestHelper, CONSOLE_COMMANDS_LABEL_PREFIX, SidebarService, endpoint) {
var socket, term;
let states = Object.freeze({
@ -97,7 +81,6 @@ angular.module('portainer.docker').controller('ContainerConsoleController', [
$scope.state = states.connecting;
var command = $scope.formValues.isCustomCommand ? $scope.formValues.customCommand : $scope.formValues.command;
var execConfig = {
id: $transition$.params().id,
AttachStdin: true,
AttachStdout: true,
AttachStderr: true,
@ -106,7 +89,7 @@ angular.module('portainer.docker').controller('ContainerConsoleController', [
Cmd: commandStringToArray(command),
};
ContainerService.createExec(endpoint.Id, execConfig)
ContainerService.createExec(endpoint.Id, $transition$.params().id, execConfig)
.then(function success(data) {
const params = {
endpointId: $state.params.endpointId,

View File

@ -5,6 +5,7 @@ import { confirmContainerDeletion } from '@/react/docker/containers/common/confi
import { FeatureId } from '@/react/portainer/feature-flags/enums';
import { ResourceControlType } from '@/react/portainer/access-control/types';
import { confirmContainerRecreation } from '@/react/docker/containers/ItemView/ConfirmRecreationModal';
import { commitContainer } from '@/react/docker/proxy/queries/useCommitContainerMutation';
angular.module('portainer.docker').controller('ContainerController', [
'$q',
@ -13,38 +14,14 @@ angular.module('portainer.docker').controller('ContainerController', [
'$transition$',
'$filter',
'$async',
'Commit',
'ContainerHelper',
'ContainerService',
'ImageHelper',
'NetworkService',
'Notifications',
'ResourceControlService',
'RegistryService',
'ImageService',
'HttpRequestHelper',
'Authentication',
'endpoint',
function (
$q,
$scope,
$state,
$transition$,
$filter,
$async,
Commit,
ContainerHelper,
ContainerService,
ImageHelper,
NetworkService,
Notifications,
ResourceControlService,
RegistryService,
ImageService,
HttpRequestHelper,
Authentication,
endpoint
) {
function ($q, $scope, $state, $transition$, $filter, $async, ContainerService, ImageHelper, NetworkService, Notifications, HttpRequestHelper, Authentication, endpoint) {
$scope.resourceType = ResourceControlType.Container;
$scope.endpoint = endpoint;
$scope.isAdmin = Authentication.isAdmin();
@ -227,7 +204,7 @@ angular.module('portainer.docker').controller('ContainerController', [
$scope.containerLeaveNetwork = function containerLeaveNetwork(container, networkId) {
$scope.state.leaveNetworkInProgress = true;
NetworkService.disconnectContainer(networkId, container.Id, false)
NetworkService.disconnectContainer(networkId, container.Id)
.then(function success() {
Notifications.success('Container left network', container.Id);
$state.reload();
@ -260,7 +237,7 @@ angular.module('portainer.docker').controller('ContainerController', [
const registryModel = $scope.config.RegistryModel;
const imageConfig = ImageHelper.createImageConfigForContainer(registryModel);
try {
await Commit.commitContainer({ environmentId: endpoint.Id }, { id: $transition$.params().id, repo: imageConfig.fromImage }).$promise;
await commitContainer(endpoint.Id, { container: $transition$.params().id, repo: imageConfig.fromImage });
Notifications.success('Image created', $transition$.params().id);
$state.reload();
} catch (err) {

View File

@ -6,10 +6,10 @@ angular.module('portainer.docker').controller('EventsController', [
'SystemService',
function ($scope, Notifications, SystemService) {
function initView() {
var from = moment().subtract(24, 'hour').unix();
var to = moment().unix();
const since = moment().subtract(24, 'hour').unix();
const until = moment().unix();
SystemService.events(from, to)
SystemService.events({ since, until })
.then(function success(data) {
$scope.events = data;
})

View File

@ -35,15 +35,10 @@ angular.module('portainer.docker').controller('ImagesController', [
const registryModel = $scope.formValues.RegistryModel;
var nodeName = $scope.formValues.NodeName;
HttpRequestHelper.setPortainerAgentTargetHeader(nodeName);
$scope.state.actionInProgress = true;
ImageService.pullImage(registryModel, false)
.then(function success(data) {
var err = data[data.length - 1].errorDetail;
if (err) {
return Notifications.error('Failure', err, 'Unable to pull image');
}
ImageService.pullImage(registryModel, nodeName)
.then(function success() {
Notifications.success('Image successfully pulled', registryModel.Image);
$state.reload();
})

View File

@ -230,11 +230,8 @@ angular.module('portainer.docker').controller('CreateNetworkController', [
}
function createNetwork(context) {
HttpRequestHelper.setPortainerAgentTargetHeader(context.nodeName);
HttpRequestHelper.setPortainerAgentManagerOperation(context.managerOperation);
$scope.state.actionInProgress = true;
NetworkService.create(context.networkConfiguration)
NetworkService.create(context.networkConfiguration, { nodeName: context.nodeName, agentManagerOperation: context.managerOperation })
.then(function success(data) {
const userId = context.userDetails.ID;
const accessControlData = context.accessControlData;

View File

@ -14,7 +14,7 @@ angular.module('portainer.docker').controller('CreateServiceController', [
'$scope',
'$state',
'$timeout',
'Service',
'ServiceService',
'ServiceHelper',
'ConfigService',
'ConfigHelper',
@ -29,8 +29,6 @@ angular.module('portainer.docker').controller('CreateServiceController', [
'Notifications',
'FormValidator',
'PluginService',
'RegistryService',
'HttpRequestHelper',
'NodeService',
'WebhookService',
'endpoint',
@ -39,7 +37,7 @@ angular.module('portainer.docker').controller('CreateServiceController', [
$scope,
$state,
$timeout,
Service,
ServiceService,
ServiceHelper,
ConfigService,
ConfigHelper,
@ -54,8 +52,6 @@ angular.module('portainer.docker').controller('CreateServiceController', [
Notifications,
FormValidator,
PluginService,
RegistryService,
HttpRequestHelper,
NodeService,
WebhookService,
endpoint
@ -523,11 +519,9 @@ angular.module('portainer.docker').controller('CreateServiceController', [
function createNewService(config, accessControlData) {
const registryModel = $scope.formValues.RegistryModel;
var authenticationDetails = registryModel.Registry.Authentication ? RegistryService.encodedCredentials(registryModel.Registry) : '';
HttpRequestHelper.setRegistryAuthenticationHeader(authenticationDetails);
Service.create(config)
.$promise.then(function success(data) {
ServiceService.create(config, registryModel.Registry.Authentication ? registryModel.Registry.Id : 0)
.then(function success(data) {
const serviceId = data.ID;
const resourceControl = data.Portainer.ResourceControl;
const userId = Authentication.getUserDetails().ID;

View File

@ -3,7 +3,6 @@ import { VolumesNFSFormData } from '../../../components/volumesNFSForm/volumesNF
import { VolumesCIFSFormData } from '../../../components/volumesCIFSForm/volumesCifsFormModel';
angular.module('portainer.docker').controller('CreateVolumeController', [
'$q',
'$scope',
'$state',
'VolumeService',
@ -12,9 +11,8 @@ angular.module('portainer.docker').controller('CreateVolumeController', [
'Authentication',
'Notifications',
'FormValidator',
'HttpRequestHelper',
'endpoint',
function ($q, $scope, $state, VolumeService, PluginService, ResourceControlService, Authentication, Notifications, FormValidator, HttpRequestHelper, endpoint) {
function ($scope, $state, VolumeService, PluginService, ResourceControlService, Authentication, Notifications, FormValidator, endpoint) {
$scope.endpoint = endpoint;
$scope.formValues = {
@ -126,10 +124,9 @@ angular.module('portainer.docker').controller('CreateVolumeController', [
}
var nodeName = $scope.formValues.NodeName;
HttpRequestHelper.setPortainerAgentTargetHeader(nodeName);
$scope.state.actionInProgress = true;
VolumeService.createVolume(volumeConfiguration)
VolumeService.createVolume(volumeConfiguration, nodeName)
.then(function success(data) {
const userId = userDetails.ID;
const resourceControl = data.ResourceControl;

View File

@ -21,7 +21,7 @@ angular.module('portainer.docker').controller('VolumeController', [
$scope.removeVolume = function removeVolume() {
confirmDelete('Do you want to remove this volume?').then((confirmed) => {
if (confirmed) {
VolumeService.remove($scope.volume)
VolumeService.remove($scope.volume.Id)
.then(function success() {
Notifications.success('Volume successfully removed', $transition$.params().id);
$state.go('docker.volumes', {});

View File

@ -8,14 +8,12 @@ angular.module('portainer.docker').controller('VolumesController', [
'ServiceService',
'VolumeHelper',
'Notifications',
'HttpRequestHelper',
'Authentication',
'endpoint',
function ($q, $scope, $state, VolumeService, ServiceService, VolumeHelper, Notifications, HttpRequestHelper, Authentication, endpoint) {
function ($q, $scope, $state, VolumeService, ServiceService, VolumeHelper, Notifications, Authentication, endpoint) {
$scope.removeAction = async function (selectedItems) {
async function doRemove(volume) {
HttpRequestHelper.setPortainerAgentTargetHeader(volume.NodeName);
return VolumeService.remove(volume)
return VolumeService.remove(volume.Id, volume.NodeName)
.then(function success() {
Notifications.success('Volume successfully removed', volume.Id);
var index = $scope.volumes.indexOf(volume);
@ -36,8 +34,8 @@ angular.module('portainer.docker').controller('VolumesController', [
var endpointRole = $scope.applicationState.endpoint.mode.role;
$q.all({
attached: VolumeService.volumes({ filters: { dangling: ['false'] } }),
dangling: VolumeService.volumes({ filters: { dangling: ['true'] } }),
attached: VolumeService.volumes({ dangling: ['false'] }),
dangling: VolumeService.volumes({ dangling: ['true'] }),
services: endpointProvider === 'DOCKER_SWARM_MODE' && endpointRole === 'MANAGER' ? ServiceService.services() : [],
})
.then(function success(data) {

24
app/global-axios.d.ts vendored Normal file
View File

@ -0,0 +1,24 @@
import {
MaxDockerAPIVersionKey,
MaxDockerAPIVersionType,
} from './portainer/services/dockerMaxApiVersion';
export * from 'axios';
declare module 'axios' {
interface CreateAxiosDefaults {
/**
* require to define a default max Docker API Version when creating an axios instance
*/
[MaxDockerAPIVersionKey]: MaxDockerAPIVersionType;
}
interface AxiosRequestConfig {
/**
* represents the maximum Docker API version supported for the request
*
* the default will be used when not specified in the request config
*/
[MaxDockerAPIVersionKey]?: MaxDockerAPIVersionType;
}
}

View File

@ -1,5 +1,4 @@
import { rawResponse } from 'Kubernetes/rest/response/transform';
import { logsHandler } from 'Docker/rest/response/handlers';
angular.module('portainer.kubernetes').factory('KubernetesPods', [
'$resource',
@ -48,3 +47,11 @@ angular.module('portainer.kubernetes').factory('KubernetesPods', [
};
},
]);
// The Docker API returns the logs as a single string.
// This handler wraps the data in a JSON object under the "logs" property.
function logsHandler(data) {
return {
logs: data,
};
}

View File

@ -0,0 +1,116 @@
import { EnvironmentId } from '@/react/portainer/environments/types';
import { EndpointProviderInterface } from './endpointProvider';
// see async.js
type AsyncInterface = (
asyncFunc: AsyncFunction,
...args: unknown[]
) => Promise<unknown>;
type AsyncFunction = (...params: unknown[]) => Promise<unknown>;
type AxiosFunction = (
environmentId: EnvironmentId | undefined,
...params: unknown[]
) => Promise<unknown>;
/* @ngInject */
export function AngularToReact(
EndpointProvider: EndpointProviderInterface,
$async: AsyncInterface
) {
return { useAxios, injectEnvironmentId };
/**
* Wraps the async axios function with `$async` to ensures the request runs inside the AngularJS digest cycle
*
* See `$async` (async.js) implementation and notes
*
* See `AngularToReact.injectEnvironmentId` to solve `environmentId` injection for services functions relying
* on `EndpointProvider.endpointID()` in their `$resource()` definition
*
* @example
* **Old AngularJS service**
* ```
* // file:: AngularJS service.js
*
* // ngInject
* function ServiceServiceFactory($q, Service) {
* return { getService };
*
* // the original signature doesn't have environmentId passed to it
* // it relies on EndpointProvider in $resource() definition
* // we will inject it on refactor
* // the function uses $q, which internally triggers a redraw of the UI when it resolves/rejects
* function getService(serviceId) {
* var deferred = $q.defer();
* [...]
* return deferred.promise;
* };
*
* // the original signature has environmentId passed to it
* // it doesn't rely on EndpointProvider in $resource() definition
* // we won't inject environmentId on refactor
* // the function uses $q, which internally triggers a redraw of the UI when it resolves/rejects
* function listServices(environmentId) {
* var deferred = $q.defer();
* [...]
* return deferred.promise;
* };
* }
* ```
*
* **New format**
* ```
* // file:: '@/react/.../useService.ts'
* // this function has `environmentId` as first parameter, which doesn't match the old AngularJS service signature
* export async function getService(environmentId: EnvironmentId, serviceId: ServiceId) {
* // axios.get()
* }
* // file:: '@/react/.../useServices.ts'
* // this function has `environmentId` as first parameter, which matches the old AngularJS service signature
* export async function listServices(environmentId: EnvironmentId, serviceId: ServiceId) {
* // axios.get()
* }
* // file:: AngularJS service.js
* import { getService } from '@/react/.../useService.ts';
* import { listServices } from '@/react/.../useServices.ts';
*
* // ngInject
* function ServiceServiceFactory(AngularToReact) {
* const { useAxios, injectEnvironmentId } = AngularToReact;
* return {
* // ask to inject environmentId to maintain the old signature
* getService: useAxios(injectEnvironmentId(getService)),
* // do not ask to inject environmentId as it was already in the old signature
* // and is already passed by the caller
* listServices: useAxios(listServices),
* };
* }
* ```
*/
function useAxios(axiosFunc: AxiosFunction) {
return (...params: unknown[]) =>
$async(axiosFunc as AsyncFunction, ...params);
}
/**
* Wraps the Axios function taking `endpointId` as first param to expose the old service format.
*
* Leverage injected `EndpointProvider` that was used in the rest file - `$resource()` definition.
*
* The axios function params **MUST** match the old AngularJS-service ones to use this helper without changing the service calls
*
* Should be used in conjunction with `AngularToReact.useAxios`
*
* @example
* See `AngularToReact.useAxios`
*
* @param {(environmentId: EnvironmentId, ...params: unknown[]) => Promise<unknown>} axiosFunc Axios function taking `environmentId` as first param
* @returns a function with the old AngularJS signature
*/
function injectEnvironmentId(axiosFunc: AxiosFunction) {
return async (...params: unknown[]) =>
axiosFunc(EndpointProvider.endpointID(), ...params);
}
}

View File

@ -54,11 +54,11 @@ angular.module('portainer.app').factory('StackService', [
SwarmService.swarm(targetEndpointId)
.then(function success(data) {
var swarm = data;
if (swarm.Id === stack.SwarmId) {
if (swarm.ID === stack.SwarmId) {
deferred.reject({ msg: 'Target environment is located in the same Swarm cluster as the current environment', err: null });
return;
}
return Stack.migrate({ id: stack.Id, endpointId: stack.EndpointId }, { EndpointID: targetEndpointId, SwarmID: swarm.Id, Name: newName }).$promise;
return Stack.migrate({ id: stack.Id, endpointId: stack.EndpointId }, { EndpointID: targetEndpointId, SwarmID: swarm.ID, Name: newName }).$promise;
})
.then(function success() {
deferred.resolve();
@ -182,10 +182,10 @@ angular.module('portainer.app').factory('StackService', [
service.swarmStacks = function (endpointId, includeExternalStacks, filters = {}) {
var deferred = $q.defer();
SwarmService.swarm()
SwarmService.swarm(endpointId)
.then(function success(data) {
var swarm = data;
filters = { SwarmID: swarm.Id, ...filters };
filters = { SwarmID: swarm.ID, ...filters };
return $q.all({
stacks: Stack.query({ filters: filters }).$promise,
@ -239,10 +239,10 @@ angular.module('portainer.app').factory('StackService', [
var deferred = $q.defer();
if (stack.Type == 1) {
SwarmService.swarm()
SwarmService.swarm(endpointId)
.then(function success(data) {
const swarm = data;
return Stack.associate({ id: stack.Id, endpointId: endpointId, swarmId: swarm.Id, orphanedRunning }).$promise;
return Stack.associate({ id: stack.Id, endpointId: endpointId, swarmId: swarm.ID, orphanedRunning }).$promise;
})
.then(function success(data) {
deferred.resolve(data);
@ -305,10 +305,10 @@ angular.module('portainer.app').factory('StackService', [
service.createSwarmStackFromFileUpload = function (name, stackFile, env, endpointId) {
var deferred = $q.defer();
SwarmService.swarm()
SwarmService.swarm(endpointId)
.then(function success(data) {
var swarm = data;
return FileUploadService.createSwarmStack(name, swarm.Id, stackFile, env, endpointId);
return FileUploadService.createSwarmStack(name, swarm.ID, stackFile, env, endpointId);
})
.then(function success(data) {
deferred.resolve(data.data);
@ -335,7 +335,7 @@ angular.module('portainer.app').factory('StackService', [
.then(function success(swarm) {
var payload = {
Name: name,
SwarmID: swarm.Id,
SwarmID: swarm.ID,
StackFileContent: stackFileContent,
Env: env,
};
@ -376,12 +376,12 @@ angular.module('portainer.app').factory('StackService', [
service.createSwarmStackFromGitRepository = function (name, repositoryOptions, env, endpointId) {
var deferred = $q.defer();
SwarmService.swarm()
SwarmService.swarm(endpointId)
.then(function success(data) {
var swarm = data;
var payload = {
Name: name,
SwarmID: swarm.Id,
SwarmID: swarm.ID,
RepositoryURL: repositoryOptions.RepositoryURL,
RepositoryReferenceName: repositoryOptions.RepositoryReferenceName,
ComposeFile: repositoryOptions.ComposeFilePathInRepository,

View File

@ -21,6 +21,8 @@ import {
portainerAgentManagerOperation,
portainerAgentTargetHeader,
} from './http-request.helper';
import { dockerMaxAPIVersionInterceptor } from './dockerMaxApiVersionInterceptor';
import { MAX_DOCKER_API_VERSION } from './dockerMaxApiVersion';
const portainerCacheHeader = 'X-Portainer-Cache';
@ -48,7 +50,10 @@ function headerInterpreter(
return 'not enough headers';
}
const axios = Axios.create({ baseURL: 'api' });
const axios = Axios.create({
baseURL: 'api',
maxDockerAPIVersion: MAX_DOCKER_API_VERSION,
});
axios.interceptors.request.use((req) => {
dispatchCacheRefreshEventIfNeeded(req);
return req;
@ -118,13 +123,14 @@ export function agentInterceptor(config: InternalAxiosRequestConfig) {
return newConfig;
}
axios.interceptors.request.use(dockerMaxAPIVersionInterceptor);
axios.interceptors.request.use(agentInterceptor);
axios.interceptors.response.use(undefined, (error) => {
if (
error.response?.status === 401 &&
!error.config.url.includes('/v2/') &&
!error.config.url.includes('/api/v4/') &&
!error.config.url.includes('/v2/') && // docker proxy through agent
!error.config.url.includes('/api/v4/') && // gitlab proxy
isTransitionRequiresAuthentication()
) {
// eslint-disable-next-line no-console
@ -188,6 +194,12 @@ export function defaultErrorParser(axiosError: AxiosError<unknown>) {
const error = new Error(message);
return { error, details };
}
if (isArrayResponse(axiosError.response?.data)) {
const message = axiosError.response?.data[0].message || '';
const details = axiosError.response?.data[0].details || message;
const error = new Error(message);
return { error, details };
}
const details = axiosError.response?.data
? axiosError.response?.data.toString()
@ -196,6 +208,16 @@ export function defaultErrorParser(axiosError: AxiosError<unknown>) {
return { error, details };
}
// handle jsonObjectsToArrayHandler transformation
function isArrayResponse(data: unknown): data is DefaultAxiosErrorType[] {
return (
!!data &&
Array.isArray(data) &&
'message' in data[0] &&
typeof data[0].message === 'string'
);
}
export function isDefaultResponse(
data: unknown
): data is DefaultAxiosErrorType {
@ -249,3 +271,18 @@ export function json2formData(json: Record<string, unknown>) {
return formData;
}
/**
* The Docker API often returns a list of JSON object.
* This handler wrap the JSON objects in an array.
* @param data Raw docker API response (stream of objects in a single string)
* @returns An array of parsed objects
*/
export function jsonObjectsToArrayHandler(data: string): unknown[] {
// catching empty data helps the function not to fail and prevents unwanted error message to user.
if (!data) {
return [];
}
const str = `[${data.replace(/\n/g, ' ').replace(/\}\s*\{/g, '}, {')}]`;
return JSON.parse(str);
}

View File

@ -0,0 +1,11 @@
// Key used in axios types definitions
export const MaxDockerAPIVersionKey = 'maxDockerAPIVersion' as const;
export type DockerAPIVersionType = number;
// this is the version we are using with the generated API types
export const MAX_DOCKER_API_VERSION: DockerAPIVersionType = 1.41;
// https://docs.docker.com/engine/api/#api-version-matrix
// Docker 26 = API 1.45
export const LATEST_DOCKER_API_VERSION: DockerAPIVersionType = 1.45;

View File

@ -0,0 +1,64 @@
import { SystemVersion } from 'docker-types/generated/1.41';
import Axios, { InternalAxiosRequestConfig } from 'axios';
import { setupCache, buildMemoryStorage } from 'axios-cache-interceptor';
import { buildDockerProxyUrl } from '@/react/docker/proxy/queries/buildDockerProxyUrl';
import PortainerError from '../error';
import { MAX_DOCKER_API_VERSION } from './dockerMaxApiVersion';
const envVersionAxios = Axios.create({
baseURL: 'api',
maxDockerAPIVersion: MAX_DOCKER_API_VERSION,
});
// setup a cache for the intermediary request sent by the interceptor
const envVersionCache = buildMemoryStorage();
setupCache(envVersionAxios, {
storage: envVersionCache,
ttl: 5 * 60 * 1000,
methods: ['get'],
});
export async function dockerMaxAPIVersionInterceptor(
rawConfig: InternalAxiosRequestConfig
) {
try {
const config = rawConfig;
const found = config.url?.match(
/endpoints\/(?<environmentId>\d+)\/docker\//
);
if (found && found.groups) {
const { environmentId } = found.groups;
const envId = parseInt(environmentId, 10);
// if we cannot parse the env ID, don't send a request that will fail,
// exit the interceptor and let the original request config pass through
if (Number.isNaN(envId)) {
return config;
}
const { data } = await envVersionAxios.get<SystemVersion>(
buildDockerProxyUrl(envId, 'version')
);
const apiVersion = parseFloat(data.ApiVersion ?? '0');
const { maxDockerAPIVersion } = config;
if (apiVersion > maxDockerAPIVersion) {
config.url = config.url?.replace(
/docker/,
`docker/v${maxDockerAPIVersion}`
);
}
}
return config;
} catch (err) {
throw new PortainerError(
'An error occurred while trying to limit request to the maximum supported Docker API version',
err
);
}
}

View File

@ -1,4 +1,4 @@
import { ping } from '@/docker/services/ping';
import { ping } from '@/react/docker/proxy/queries/usePing';
import { environmentStore } from '@/react/hooks/current-environment-store';
import {
Environment,

View File

@ -1,238 +1,209 @@
import { PortainerEndpointCreationTypes } from 'Portainer/models/endpoint/models';
import { genericHandler, jsonObjectsToArrayHandler } from '../../docker/rest/response/handlers';
angular.module('portainer.app').factory('FileUploadService', [
'$q',
'Upload',
'EndpointProvider',
function FileUploadFactory($q, Upload, EndpointProvider) {
'use strict';
angular.module('portainer.app').factory('FileUploadService', FileUploadFactory);
var service = {};
/* @ngInject */
function FileUploadFactory($q, Upload) {
var service = {
// createSchedule, // edge jobs service
// uploadBackup, // backup service
// createSwarmStack, // stack service
// createComposeStack, // stack service
// createEdgeStack, // edge stack service
// createCustomTemplate, // custom template service
// configureRegistry, // registry service
// createEndpoint, // endpoint service
// createAzureEndpoint, // endpoint service
// uploadLDAPTLSFiles, // auth settings controller
// uploadTLSFilesForEndpoint, // endpoint service
// uploadOwnershipVoucher, // import device controller
};
function uploadFile(url, file) {
return Upload.upload({ url: url, data: { file: file } });
function uploadFile(url, file) {
return Upload.upload({ url: url, data: { file: file } });
}
service.createSchedule = function (payload) {
return Upload.upload({
url: 'api/edge_jobs/create/file',
data: {
file: payload.File,
Name: payload.Name,
CronExpression: payload.CronExpression,
Image: payload.Image,
Endpoints: Upload.json(payload.Endpoints),
RetryCount: payload.RetryCount,
RetryInterval: payload.RetryInterval,
},
});
};
service.uploadBackup = function (file, password) {
return Upload.upload({
url: 'api/restore',
data: {
file,
password,
},
});
};
service.createSwarmStack = function (stackName, swarmId, file, env, endpointId, webhook) {
return Upload.upload({
url: `api/stacks/create/swarm/file?endpointId=${endpointId}`,
data: {
file: file,
Name: stackName,
SwarmID: swarmId,
Env: Upload.json(env),
Webhook: webhook,
},
ignoreLoadingBar: true,
});
};
service.createComposeStack = function (stackName, file, env, endpointId, webhook) {
return Upload.upload({
url: `api/stacks/create/standalone/file?endpointId=${endpointId}`,
data: {
file: file,
Name: stackName,
Env: Upload.json(env),
Webhook: webhook,
},
ignoreLoadingBar: true,
});
};
service.createEdgeStack = function createEdgeStack({ EdgeGroups, Registries, envVars, staggerConfig, ...payload }, file, dryrun) {
return Upload.upload({
url: `api/edge_stacks/create/file?dryrun=${dryrun}`,
data: {
file,
EdgeGroups: Upload.json(EdgeGroups),
Registries: Upload.json(Registries),
EnvVars: Upload.json(envVars),
StaggerConfig: Upload.json(staggerConfig),
...payload,
},
ignoreLoadingBar: true,
});
};
service.createCustomTemplate = function createCustomTemplate(data) {
return Upload.upload({
url: 'api/custom_templates/create/file',
data,
ignoreLoadingBar: true,
});
};
service.configureRegistry = function (registryId, registryManagementConfigurationModel) {
return Upload.upload({
url: 'api/registries/' + registryId + '/configure',
data: registryManagementConfigurationModel,
});
};
service.createEndpoint = function (
name,
creationType,
URL,
PublicURL,
groupID,
tagIds,
TLS,
TLSSkipVerify,
TLSSkipClientVerify,
TLSCAFile,
TLSCertFile,
TLSKeyFile,
checkinInterval,
EdgePingInterval,
EdgeSnapshotInterval,
EdgeCommandInterval
) {
return Upload.upload({
url: 'api/endpoints',
data: {
Name: name,
EndpointCreationType: creationType,
URL: URL,
PublicURL: PublicURL,
GroupID: groupID,
TagIds: Upload.json(tagIds),
TLS: TLS,
TLSSkipVerify: TLSSkipVerify,
TLSSkipClientVerify: TLSSkipClientVerify,
TLSCACertFile: TLSCAFile,
TLSCertFile: TLSCertFile,
TLSKeyFile: TLSKeyFile,
CheckinInterval: checkinInterval,
EdgePingInterval: EdgePingInterval,
EdgeSnapshotInterval: EdgeSnapshotInterval,
EdgeCommandInterval: EdgeCommandInterval,
},
ignoreLoadingBar: true,
});
};
service.createAzureEndpoint = function (name, applicationId, tenantId, authenticationKey, groupId, tagIds) {
return Upload.upload({
url: 'api/endpoints',
data: {
Name: name,
EndpointCreationType: PortainerEndpointCreationTypes.AzureEnvironment,
GroupID: groupId,
TagIds: Upload.json(tagIds),
AzureApplicationID: applicationId,
AzureTenantID: tenantId,
AzureAuthenticationKey: authenticationKey,
},
ignoreLoadingBar: true,
});
};
service.uploadLDAPTLSFiles = function (TLSCAFile, TLSCertFile, TLSKeyFile) {
var queue = [];
if (TLSCAFile) {
queue.push(uploadFile('api/upload/tls/ca?folder=ldap', TLSCAFile));
}
if (TLSCertFile) {
queue.push(uploadFile('api/upload/tls/cert?folder=ldap', TLSCertFile));
}
if (TLSKeyFile) {
queue.push(uploadFile('api/upload/tls/key?folder=ldap', TLSKeyFile));
}
service.buildImage = function (endpointID, names, file, path) {
return Upload.http({
url: `api/endpoints/${endpointID}/docker/build`,
headers: {
'Content-Type': file.type,
},
data: file,
params: {
t: names,
dockerfile: path,
},
ignoreLoadingBar: true,
transformResponse: function (data) {
return jsonObjectsToArrayHandler(data);
},
});
};
return $q.all(queue);
};
service.buildImageFromFiles = function (endpointID, names, files) {
return Upload.upload({
url: `api/endpoints/${endpointID}/docker/build`,
headers: {
'Content-Type': 'multipart/form-data',
},
data: { file: files },
params: {
t: names,
},
transformResponse: function (data) {
return jsonObjectsToArrayHandler(data);
},
});
};
service.uploadTLSFilesForEndpoint = function (endpointID, TLSCAFile, TLSCertFile, TLSKeyFile) {
var queue = [];
service.loadImages = function (file) {
var endpointID = EndpointProvider.endpointID();
return Upload.http({
url: 'api/endpoints/' + endpointID + '/docker/images/load',
headers: {
'Content-Type': file.type,
},
data: file,
ignoreLoadingBar: true,
transformResponse: genericHandler,
});
};
if (TLSCAFile) {
queue.push(uploadFile('api/upload/tls/ca?folder=' + endpointID, TLSCAFile));
}
if (TLSCertFile) {
queue.push(uploadFile('api/upload/tls/cert?folder=' + endpointID, TLSCertFile));
}
if (TLSKeyFile) {
queue.push(uploadFile('api/upload/tls/key?folder=' + endpointID, TLSKeyFile));
}
service.createSchedule = function (payload) {
return Upload.upload({
url: 'api/edge_jobs/create/file',
data: {
file: payload.File,
Name: payload.Name,
CronExpression: payload.CronExpression,
Image: payload.Image,
Endpoints: Upload.json(payload.Endpoints),
RetryCount: payload.RetryCount,
RetryInterval: payload.RetryInterval,
},
});
};
return $q.all(queue);
};
service.uploadBackup = function (file, password) {
return Upload.upload({
url: 'api/restore',
data: {
file,
password,
},
});
};
service.uploadOwnershipVoucher = function (voucherFile) {
return Upload.upload({
url: 'api/fdo/register',
data: {
voucher: voucherFile,
},
ignoreLoadingBar: true,
});
};
service.createSwarmStack = function (stackName, swarmId, file, env, endpointId) {
return Upload.upload({
url: `api/stacks/create/swarm/file?endpointId=${endpointId}`,
data: {
file: file,
Name: stackName,
SwarmID: swarmId,
Env: Upload.json(env),
},
ignoreLoadingBar: true,
});
};
service.createComposeStack = function (stackName, file, env, endpointId) {
return Upload.upload({
url: `api/stacks/create/standalone/file?endpointId=${endpointId}`,
data: {
file: file,
Name: stackName,
Env: Upload.json(env),
},
ignoreLoadingBar: true,
});
};
service.createEdgeStack = function createEdgeStack({ EdgeGroups, envVars, ...payload }, file) {
return Upload.upload({
url: `api/edge_stacks/create/file`,
data: {
file,
EdgeGroups: Upload.json(EdgeGroups),
EnvVars: Upload.json(envVars),
...payload,
},
ignoreLoadingBar: true,
});
};
service.createCustomTemplate = function createCustomTemplate(data) {
return Upload.upload({
url: 'api/custom_templates/create/file',
data,
ignoreLoadingBar: true,
});
};
service.configureRegistry = function (registryId, registryManagementConfigurationModel) {
return Upload.upload({
url: 'api/registries/' + registryId + '/configure',
data: registryManagementConfigurationModel,
});
};
service.createEndpoint = function (
name,
creationType,
URL,
PublicURL,
groupID,
tagIds,
TLS,
TLSSkipVerify,
TLSSkipClientVerify,
TLSCAFile,
TLSCertFile,
TLSKeyFile,
checkinInterval
) {
return Upload.upload({
url: 'api/endpoints',
data: {
Name: name,
EndpointCreationType: creationType,
URL: URL,
PublicURL: PublicURL,
GroupID: groupID,
TagIds: Upload.json(tagIds),
TLS: TLS,
TLSSkipVerify: TLSSkipVerify,
TLSSkipClientVerify: TLSSkipClientVerify,
TLSCACertFile: TLSCAFile,
TLSCertFile: TLSCertFile,
TLSKeyFile: TLSKeyFile,
CheckinInterval: checkinInterval,
},
ignoreLoadingBar: true,
});
};
service.createAzureEndpoint = function (name, applicationId, tenantId, authenticationKey, groupId, tagIds) {
return Upload.upload({
url: 'api/endpoints',
data: {
Name: name,
EndpointCreationType: PortainerEndpointCreationTypes.AzureEnvironment,
GroupID: groupId,
TagIds: Upload.json(tagIds),
AzureApplicationID: applicationId,
AzureTenantID: tenantId,
AzureAuthenticationKey: authenticationKey,
},
ignoreLoadingBar: true,
});
};
service.uploadLDAPTLSFiles = function (TLSCAFile, TLSCertFile, TLSKeyFile) {
var queue = [];
if (TLSCAFile) {
queue.push(uploadFile('api/upload/tls/ca?folder=ldap', TLSCAFile));
}
if (TLSCertFile) {
queue.push(uploadFile('api/upload/tls/cert?folder=ldap', TLSCertFile));
}
if (TLSKeyFile) {
queue.push(uploadFile('api/upload/tls/key?folder=ldap', TLSKeyFile));
}
return $q.all(queue);
};
service.uploadTLSFilesForEndpoint = function (endpointID, TLSCAFile, TLSCertFile, TLSKeyFile) {
var queue = [];
if (TLSCAFile) {
queue.push(uploadFile('api/upload/tls/ca?folder=' + endpointID, TLSCAFile));
}
if (TLSCertFile) {
queue.push(uploadFile('api/upload/tls/cert?folder=' + endpointID, TLSCertFile));
}
if (TLSKeyFile) {
queue.push(uploadFile('api/upload/tls/key?folder=' + endpointID, TLSKeyFile));
}
return $q.all(queue);
};
service.uploadOwnershipVoucher = function (voucherFile) {
return Upload.upload({
url: 'api/fdo/register',
data: {
voucher: voucherFile,
},
ignoreLoadingBar: true,
});
};
return service;
},
]);
return service;
}

View File

@ -4,9 +4,11 @@ import { apiServicesModule } from './api';
import { Notifications } from './notifications';
import { HttpRequestHelperAngular } from './http-request.helper';
import { EndpointProvider } from './endpointProvider';
import { AngularToReact } from './angularToReact';
export default angular
.module('portainer.app.services', [apiServicesModule])
.factory('Notifications', Notifications)
.factory('EndpointProvider', EndpointProvider)
.factory('HttpRequestHelper', HttpRequestHelperAngular).name;
.factory('HttpRequestHelper', HttpRequestHelperAngular)
.factory('AngularToReact', AngularToReact).name;

View File

@ -1,5 +1,5 @@
import { AccessControlFormData } from '@/react/portainer/access-control/types';
import { PortainerMetadata } from '@/react/docker/types';
import { PortainerResponse } from '@/react/docker/types';
import { PortMapping } from './container-instances/CreateView/PortsMappingField';
@ -47,14 +47,13 @@ interface ContainerGroupProperties {
osType: OS;
}
export type ContainerGroup = {
export type ContainerGroup = PortainerResponse<{
id: string;
name: string;
location: string;
type: string;
properties: ContainerGroupProperties;
Portainer?: PortainerMetadata;
};
}>;
export interface Subscription {
subscriptionId: string;

View File

@ -1,6 +1,6 @@
import { Box } from 'lucide-react';
import { DockerContainer } from '@/react/docker/containers/types';
import { ContainerListViewModel } from '@/react/docker/containers/types';
import { Environment } from '@/react/portainer/environments/types';
import { createStore } from '@/react/docker/containers/ListView/ContainersDatatable/datatable-store';
import { useColumns } from '@/react/docker/containers/ListView/ContainersDatatable/columns';
@ -20,7 +20,7 @@ import {
import { TableSettingsProvider } from '@@/datatables/useTableSettings';
import { useTableState } from '@@/datatables/useTableState';
import { useContainers } from '../../../docker/containers/queries/containers';
import { useContainers } from '../../../docker/containers/queries/useContainers';
import { RowProvider } from '../../../docker/containers/ListView/ContainersDatatable/RowContext';
const storageKey = 'stack-containers';
@ -71,7 +71,7 @@ export function StackContainersDatatable({ environment, stackName }: Props) {
data-cy="stack-containers-datatable"
renderTableSettings={(tableInstance) => (
<>
<ColumnVisibilityMenu<DockerContainer>
<ColumnVisibilityMenu<ContainerListViewModel>
table={tableInstance}
onChange={(hiddenColumns) => {
tableState.setHiddenColumns(hiddenColumns);

View File

@ -4,7 +4,7 @@ import axios, { parseAxiosError } from '@/portainer/services/axios';
import { EnvironmentId } from '@/react/portainer/environments/types';
import { queryKeys } from '../queries/utils';
import { buildDockerUrl } from '../queries/utils/root';
import { buildDockerUrl } from '../queries/utils/buildDockerUrl';
interface DashboardResponse {
containers: {
@ -29,7 +29,7 @@ export function useDashboard(envId: EnvironmentId) {
queryFn: async () => {
try {
const res = await axios.get<DashboardResponse>(
`${buildDockerUrl(envId)}/dashboard`
buildDockerUrl(envId, 'dashboard')
);
return res.data;
} catch (error) {

View File

@ -6,7 +6,7 @@ import axios, {
} from '@/portainer/services/axios';
import { EnvironmentId } from '@/react/portainer/environments/types';
import { buildUrl } from '../../proxy/queries/build-url';
import { buildDockerProxyUrl } from '../../proxy/queries/buildDockerProxyUrl';
export function useApiVersion(environmentId: EnvironmentId) {
return useQuery(['environment', environmentId, 'agent', 'ping'], () =>
@ -16,7 +16,9 @@ export function useApiVersion(environmentId: EnvironmentId) {
async function getApiVersion(environmentId: EnvironmentId) {
try {
const { headers } = await axios.get(buildUrl(environmentId, 'ping'));
const { headers } = await axios.get(
buildDockerProxyUrl(environmentId, 'ping')
);
return parseInt(headers['portainer-agent-api-version'], 10) || 1;
} catch (error) {
// 404 - agent is up - set version to 1

View File

@ -0,0 +1,21 @@
import { Config } from 'docker-types/generated/1.41';
import axios, { parseAxiosError } from '@/portainer/services/axios';
import { EnvironmentId } from '@/react/portainer/environments/types';
import { buildDockerProxyUrl } from '../../proxy/queries/buildDockerProxyUrl';
import { DockerConfig } from '../types';
export async function getConfig(
environmentId: EnvironmentId,
configId: DockerConfig['Id']
) {
try {
const { data } = await axios.get<Config>(
buildDockerProxyUrl(environmentId, 'configs', configId)
);
return data;
} catch (e) {
throw parseAxiosError(e, 'Unable to retrieve config');
}
}

View File

@ -0,0 +1,17 @@
import { Config } from 'docker-types/generated/1.41';
import axios, { parseAxiosError } from '@/portainer/services/axios';
import { EnvironmentId } from '@/react/portainer/environments/types';
import { buildDockerProxyUrl } from '../../proxy/queries/buildDockerProxyUrl';
export async function getConfigs(environmentId: EnvironmentId) {
try {
const { data } = await axios.get<Config[]>(
buildDockerProxyUrl(environmentId, 'configs')
);
return data;
} catch (e) {
throw parseAxiosError(e, 'Unable to retrieve configs');
}
}

View File

@ -0,0 +1,22 @@
import { ConfigSpec } from 'docker-types/generated/1.41';
import axios, { parseAxiosError } from '@/portainer/services/axios';
import { EnvironmentId } from '@/react/portainer/environments/types';
import { buildDockerProxyUrl } from '../../proxy/queries/buildDockerProxyUrl';
import { PortainerResponse } from '../../types';
export async function createConfig(
environmentId: EnvironmentId,
config: ConfigSpec
) {
try {
const { data } = await axios.post<PortainerResponse<{ Id: string }>>(
buildDockerProxyUrl(environmentId, 'configs', 'create'),
config
);
return data;
} catch (e) {
throw parseAxiosError(e, 'Unable to create config');
}
}

View File

@ -0,0 +1,16 @@
import axios, { parseAxiosError } from '@/portainer/services/axios';
import { EnvironmentId } from '@/react/portainer/environments/types';
import { DockerConfig } from '../types';
import { buildDockerProxyUrl } from '../../proxy/queries/buildDockerProxyUrl';
export async function deleteConfig(
environmentId: EnvironmentId,
id: DockerConfig['Id']
) {
try {
await axios.delete(buildDockerProxyUrl(environmentId, 'configs', id));
} catch (e) {
throw parseAxiosError(e, 'Unable to delete config');
}
}

View File

@ -3,13 +3,13 @@ import { ResourceControlOwnership } from '@/react/portainer/access-control/types
import { UserId } from '@/portainer/users/types';
import { getDefaultImageConfig } from '@/react/portainer/registries/utils/getImageConfig';
import { ContainerResponse } from '../../queries/container';
import { ContainerDetailsResponse } from '../../queries/useContainer';
import { toViewModel as toPortsMappingViewModel } from './PortsMappingField.viewModel';
import { Values } from './BaseForm';
export function toViewModel(
config: ContainerResponse,
config: ContainerDetailsResponse,
isPureAdmin: boolean,
currentUserId: UserId,
nodeName: string,

View File

@ -1,9 +1,9 @@
import { ContainerJSON } from '@/react/docker/containers/queries/container';
import { ContainerDetailsJSON } from '@/react/docker/containers/queries/useContainer';
import { capabilities } from './types';
import { Values } from './CapabilitiesTab';
export function toViewModel(config: ContainerJSON): Values {
export function toViewModel(config: ContainerDetailsJSON): Values {
const { CapAdd, CapDrop } = getDefaults(config);
const missingCaps = capabilities
@ -15,7 +15,7 @@ export function toViewModel(config: ContainerJSON): Values {
return [...CapAdd, ...missingCaps];
function getDefaults(config: ContainerJSON) {
function getDefaults(config: ContainerDetailsJSON) {
return {
CapAdd: config.HostConfig?.CapAdd || [],
CapDrop: config.HostConfig?.CapDrop || [],

View File

@ -2,7 +2,7 @@ import { HostConfig } from 'docker-types/generated/1.41';
import { commandArrayToString } from '@/docker/helpers/containers';
import { ContainerJSON } from '../../queries/container';
import { ContainerDetailsJSON } from '../../queries/useContainer';
import { ConsoleConfig, ConsoleSetting } from './ConsoleSettings';
import { LogConfig } from './LoggerConfig';
@ -19,7 +19,7 @@ export function getDefaultViewModel(): Values {
};
}
export function toViewModel(config: ContainerJSON): Values {
export function toViewModel(config: ContainerDetailsJSON): Values {
if (!config.Config) {
return getDefaultViewModel();
}

View File

@ -19,7 +19,7 @@ import { InformationPanel } from '@@/InformationPanel';
import { TextTip } from '@@/Tip/TextTip';
import { HelpLink } from '@@/HelpLink';
import { useContainers } from '../queries/containers';
import { useContainers } from '../queries/useContainers';
import { useSystemLimits, useIsWindows } from '../../proxy/queries/useInfo';
import { useCreateOrReplaceMutation } from './useCreateMutation';

View File

@ -1,11 +1,11 @@
import { parseArrayOfStrings } from '@@/form-components/EnvironmentVariablesFieldset/utils';
import { ContainerJSON } from '../../queries/container';
import { ContainerDetailsJSON } from '../../queries/useContainer';
export function getDefaultViewModel() {
return [];
}
export function toViewModel(container: ContainerJSON) {
export function toViewModel(container: ContainerDetailsJSON) {
return parseArrayOfStrings(container.Config?.Env);
}

View File

@ -1,8 +1,8 @@
import { ContainerJSON } from '../../queries/container';
import { ContainerDetailsJSON } from '../../queries/useContainer';
import { Values } from './types';
export function toViewModel(config: ContainerJSON): Values {
export function toViewModel(config: ContainerDetailsJSON): Values {
if (!config || !config.Config || !config.Config.Labels) {
return [];
}

View File

@ -2,7 +2,7 @@ import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
import { Option, PortainerSelect } from '@@/form-components/PortainerSelect';
import { useContainers } from '../../queries/containers';
import { useContainers } from '../../queries/useContainers';
import { ContainerStatus } from '../../types';
export function ContainerSelector({

View File

@ -1,7 +1,7 @@
import { DockerNetwork } from '@/react/docker/networks/types';
import { ContainerJSON } from '../../queries/container';
import { DockerContainer } from '../../types';
import { ContainerDetailsJSON } from '../../queries/useContainer';
import { ContainerListViewModel } from '../../types';
import { CONTAINER_MODE, Values } from './types';
@ -22,9 +22,9 @@ export function getDefaultViewModel(isWindows: boolean) {
}
export function toViewModel(
config: ContainerJSON,
config: ContainerDetailsJSON,
networks: Array<DockerNetwork>,
runningContainers: Array<DockerContainer> = []
runningContainers: Array<ContainerListViewModel> = []
): Values {
const dns = config.HostConfig?.Dns;
const [primaryDns = '', secondaryDns = ''] = dns || [];
@ -62,9 +62,9 @@ export function toViewModel(
}
function getNetworkMode(
config: ContainerJSON,
config: ContainerDetailsJSON,
networks: Array<DockerNetwork>,
runningContainers: Array<DockerContainer> = []
runningContainers: Array<ContainerListViewModel> = []
) {
let networkMode = config.HostConfig?.NetworkMode || '';
if (!networkMode) {

View File

@ -4,7 +4,7 @@ import { useCurrentStateAndParams } from '@uirouter/react';
import { useState } from 'react';
import { FormikHelpers } from 'formik/dist/types';
import { invalidateContainer } from '@/react/docker/containers/queries/container';
import { invalidateContainer } from '@/react/docker/containers/queries/useContainer';
import { notifySuccess } from '@/portainer/services/notifications';
import { mutationOptions, withError } from '@/react-tools/react-query';
import { useSystemLimits } from '@/react/docker/proxy/queries/useInfo';

View File

@ -1,11 +1,11 @@
import { ContainerJSON } from '../../queries/container';
import { ContainerDetailsJSON } from '../../queries/useContainer';
import { toDevicesViewModel } from './DevicesField';
import { gpuFieldsetUtils } from './GpuFieldset';
import { toViewModelCpu, toViewModelMemory } from './memory-utils';
import { Values } from './ResourcesTab';
export function toViewModel(config: ContainerJSON): Values {
export function toViewModel(config: ContainerDetailsJSON): Values {
return {
runtime: {
privileged: config.HostConfig?.Privileged || false,

View File

@ -1,8 +1,8 @@
import { ContainerJSON } from '../../queries/container';
import { ContainerDetailsJSON } from '../../queries/useContainer';
import { RestartPolicy } from './types';
export function toViewModel(config: ContainerJSON): RestartPolicy {
export function toViewModel(config: ContainerDetailsJSON): RestartPolicy {
switch (config.HostConfig?.RestartPolicy?.Name) {
case 'always':
return RestartPolicy.Always;

View File

@ -1,8 +1,8 @@
import { ContainerJSON } from '../../queries/container';
import { ContainerDetailsJSON } from '../../queries/useContainer';
import { VolumeType, Values } from './types';
export function toViewModel(config: ContainerJSON): Values {
export function toViewModel(config: ContainerDetailsJSON): Values {
return Object.values(config.Mounts || {}).map((mount) => ({
type: (mount.Type || 'volume') as VolumeType,
name: mount.Name || mount.Source || '',

View File

@ -31,13 +31,13 @@ import {
renameContainer,
startContainer,
stopContainer,
urlBuilder,
} from '../containers.service';
import { PortainerResponse } from '../../types';
import { connectContainer } from '../../networks/queries/useConnectContainer';
import { DockerContainer } from '../types';
import { connectContainer } from '../../networks/queries/useConnectContainerMutation';
import { ContainerListViewModel } from '../types';
import { queryKeys } from '../queries/query-keys';
import { addNodeHeader } from '../../proxy/addNodeHeader';
import { withAgentTargetHeader } from '../../proxy/queries/utils';
import { buildDockerProxyUrl } from '../../proxy/queries/buildDockerProxyUrl';
import { CreateContainerRequest } from './types';
import { Values } from './useInitialValues';
@ -75,7 +75,7 @@ interface CreateOptions {
}
interface ReplaceOptions extends CreateOptions {
oldContainer: DockerContainer;
oldContainer: ContainerListViewModel;
extraNetworks: Array<ExtraNetwork>;
}
@ -172,7 +172,7 @@ async function replace({
async function renameAndCreate(
environmentId: EnvironmentId,
name: string,
oldContainer: DockerContainer,
oldContainer: ContainerListViewModel,
config: CreateContainerRequest,
nodeName?: string
) {
@ -234,7 +234,7 @@ async function applyContainerSettings(
async function createAndStart(
environmentId: EnvironmentId,
config: CreateContainerRequest,
name: string,
name?: string,
nodeName?: string
) {
let containerId = '';
@ -290,12 +290,10 @@ async function createContainer(
{ nodeName }: { nodeName?: string } = {}
) {
try {
const headers = addNodeHeader(nodeName);
const { data } = await axios.post<
PortainerResponse<{ Id: string; Warnings: Array<string> }>
>(urlBuilder(environmentId, undefined, 'create'), config, {
headers,
>(buildDockerProxyUrl(environmentId, 'containers', 'create'), config, {
headers: { ...withAgentTargetHeader(nodeName) },
params: { name },
});
@ -349,7 +347,7 @@ function connectToExtraNetworks(
function stopContainerIfNeeded(
environmentId: EnvironmentId,
container: DockerContainer,
container: ContainerListViewModel,
nodeName?: string
) {
if (container.State !== 'running' || !container.Id) {

View File

@ -43,8 +43,8 @@ import { useEnvironmentRegistries } from '@/react/portainer/environments/queries
import { EnvVarValues } from '@@/form-components/EnvironmentVariablesFieldset';
import { useNetworksForSelector } from '../components/NetworkSelector';
import { useContainers } from '../queries/containers';
import { useContainer } from '../queries/container';
import { useContainers } from '../queries/useContainers';
import { useContainer } from '../queries/useContainer';
export interface Values extends BaseFormValues {
commands: CommandsTabValues;

View File

@ -3,7 +3,7 @@ import { SchemaOf, object, string } from 'yup';
import { useRouter } from '@uirouter/react';
import { useAuthorizations } from '@/react/hooks/useUser';
import { useConnectContainerMutation } from '@/react/docker/networks/queries/useConnectContainer';
import { useConnectContainerMutation } from '@/react/docker/networks/queries/useConnectContainerMutation';
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
import { FormControl } from '@@/form-components/FormControl';

Some files were not shown because too many files have changed in this diff Show More