1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-02 20:35:25 +02:00

feat(app): limit the docker API version supported by the frontend (#11855)
Some checks are pending
ci / build_images (map[arch:amd64 platform:linux version:]) (push) Waiting to run
ci / build_images (map[arch:amd64 platform:windows version:1809]) (push) Waiting to run
ci / build_images (map[arch:amd64 platform:windows version:ltsc2022]) (push) Waiting to run
ci / build_images (map[arch:arm platform:linux version:]) (push) Waiting to run
ci / build_images (map[arch:arm64 platform:linux version:]) (push) Waiting to run
ci / build_images (map[arch:ppc64le platform:linux version:]) (push) Waiting to run
ci / build_images (map[arch:s390x platform:linux version:]) (push) Waiting to run
ci / build_manifests (push) Blocked by required conditions
/ triage (push) Waiting to run
Lint / Run linters (push) Waiting to run
Test / test-client (push) Waiting to run
Test / test-server (map[arch:amd64 platform:linux]) (push) Waiting to run
Test / test-server (map[arch:amd64 platform:windows version:1809]) (push) Waiting to run
Test / test-server (map[arch:amd64 platform:windows version:ltsc2022]) (push) Waiting to run
Test / test-server (map[arch:arm64 platform:linux]) (push) Waiting to run

This commit is contained in:
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

@ -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;