Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion api/dev/Unraid.net/myservers.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[api]
version="4.4.1"
version="4.1.3"
extraOrigins="https://google.com,https://test.com"
[local]
sandbox="yes"
Expand Down
8 changes: 5 additions & 3 deletions api/src/core/modules/docker/get-docker-containers.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import fs from 'fs';

import camelCaseKeys from 'camelcase-keys';
import { ContainerInfo } from 'dockerode';

import type { ContainerPort, Docker, DockerContainer } from '@app/graphql/generated/api/types.js';
import { dockerLogger } from '@app/core/log.js';
Expand All @@ -11,13 +10,16 @@ import { ContainerPortType, ContainerState } from '@app/graphql/generated/api/ty
import { getters, store } from '@app/store/index.js';
import { updateDockerState } from '@app/store/modules/docker.js';

export interface ContainerListingOptions {
useCache?: boolean;
}

/**
* Get all Docker containers.
* @returns All the in/active Docker containers on the system.
*/

export const getDockerContainers = async (
{ useCache } = { useCache: true }
{ useCache }: ContainerListingOptions = { useCache: true }
): Promise<Array<DockerContainer>> => {
const dockerState = getters.docker();
if (useCache && dockerState.containers) {
Expand Down
23 changes: 22 additions & 1 deletion api/src/graphql/generated/api/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import * as Types from '@app/graphql/generated/api/types.js';

import { z } from 'zod'
import { AccessUrl, AccessUrlInput, AddPermissionInput, AddRoleForApiKeyInput, AddRoleForUserInput, AllowedOriginInput, ApiKey, ApiKeyResponse, ApiKeyWithSecret, ApiSettingsInput, ArrayType, ArrayCapacity, ArrayDisk, ArrayDiskFsColor, ArrayDiskInput, ArrayDiskStatus, ArrayDiskType, ArrayMutations, ArrayMutationsaddDiskToArrayArgs, ArrayMutationsclearArrayDiskStatisticsArgs, ArrayMutationsmountArrayDiskArgs, ArrayMutationsremoveDiskFromArrayArgs, ArrayMutationssetStateArgs, ArrayMutationsunmountArrayDiskArgs, ArrayPendingState, ArrayState, ArrayStateInput, ArrayStateInputState, Baseboard, Capacity, Case, Cloud, CloudResponse, Config, ConfigErrorState, Connect, ConnectSettings, ConnectSettingsValues, ConnectSignInInput, ConnectUserInfoInput, ContainerHostConfig, ContainerMount, ContainerPort, ContainerPortType, ContainerState, CreateApiKeyInput, Devices, Disk, DiskFsType, DiskInterfaceType, DiskPartition, DiskSmartStatus, Display, Docker, DockerContainer, DockerNetwork, DynamicRemoteAccessStatus, DynamicRemoteAccessType, EnableDynamicRemoteAccessInput, Flash, Gpu, Importance, Info, InfoApps, InfoCpu, InfoMemory, KeyFile, LogFile, LogFileContent, Me, MemoryFormFactor, MemoryLayout, MemoryType, MinigraphStatus, MinigraphqlResponse, Mount, Network, Node, Notification, NotificationCounts, NotificationData, NotificationFilter, NotificationOverview, NotificationType, Notifications, NotificationslistArgs, Os, Owner, ParityCheck, Partition, Pci, Permission, ProfileModel, Registration, RegistrationState, RelayResponse, RemoteAccess, RemoveRoleFromApiKeyInput, Resource, Role, Server, ServerStatus, Service, SetupRemoteAccessInput, Share, System, Temperature, Theme, URL_TYPE, UnassignedDevice, Uptime, Usb, User, UserAccount, Vars, Versions, VmDomain, VmState, Vms, WAN_ACCESS_TYPE, WAN_FORWARD_TYPE, Welcome, addUserInput, deleteUserInput, mdState, registrationType, usersInput } from '@app/graphql/generated/api/types.js'
import { AccessUrl, AccessUrlInput, AddPermissionInput, AddRoleForApiKeyInput, AddRoleForUserInput, AllowedOriginInput, ApiKey, ApiKeyResponse, ApiKeyWithSecret, ApiSettingsInput, ArrayType, ArrayCapacity, ArrayDisk, ArrayDiskFsColor, ArrayDiskInput, ArrayDiskStatus, ArrayDiskType, ArrayMutations, ArrayMutationsaddDiskToArrayArgs, ArrayMutationsclearArrayDiskStatisticsArgs, ArrayMutationsmountArrayDiskArgs, ArrayMutationsremoveDiskFromArrayArgs, ArrayMutationssetStateArgs, ArrayMutationsunmountArrayDiskArgs, ArrayPendingState, ArrayState, ArrayStateInput, ArrayStateInputState, Baseboard, Capacity, Case, Cloud, CloudResponse, Config, ConfigErrorState, Connect, ConnectSettings, ConnectSettingsValues, ConnectSignInInput, ConnectUserInfoInput, ContainerHostConfig, ContainerMount, ContainerPort, ContainerPortType, ContainerState, CreateApiKeyInput, Devices, Disk, DiskFsType, DiskInterfaceType, DiskPartition, DiskSmartStatus, Display, Docker, DockerContainer, DockerMutations, DockerMutationsstartContainerArgs, DockerMutationsstopContainerArgs, DockerNetwork, DynamicRemoteAccessStatus, DynamicRemoteAccessType, EnableDynamicRemoteAccessInput, Flash, Gpu, Importance, Info, InfoApps, InfoCpu, InfoMemory, KeyFile, LogFile, LogFileContent, Me, MemoryFormFactor, MemoryLayout, MemoryType, MinigraphStatus, MinigraphqlResponse, Mount, Network, Node, Notification, NotificationCounts, NotificationData, NotificationFilter, NotificationOverview, NotificationType, Notifications, NotificationslistArgs, Os, Owner, ParityCheck, Partition, Pci, Permission, ProfileModel, Registration, RegistrationState, RelayResponse, RemoteAccess, RemoveRoleFromApiKeyInput, Resource, Role, Server, ServerStatus, Service, SetupRemoteAccessInput, Share, System, Temperature, Theme, URL_TYPE, UnassignedDevice, Uptime, Usb, User, UserAccount, Vars, Versions, VmDomain, VmState, Vms, WAN_ACCESS_TYPE, WAN_FORWARD_TYPE, Welcome, addUserInput, deleteUserInput, mdState, registrationType, usersInput } from '@app/graphql/generated/api/types.js'
import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';

type Properties<T> = Required<{
Expand Down Expand Up @@ -501,6 +501,7 @@ export function DockerSchema(): z.ZodObject<Properties<Docker>> {
__typename: z.literal('Docker').optional(),
containers: z.array(DockerContainerSchema()).nullish(),
id: z.string(),
mutations: DockerMutationsSchema(),
networks: z.array(DockerNetworkSchema()).nullish()
})
}
Expand All @@ -526,6 +527,26 @@ export function DockerContainerSchema(): z.ZodObject<Properties<DockerContainer>
})
}

export function DockerMutationsSchema(): z.ZodObject<Properties<DockerMutations>> {
return z.object({
__typename: z.literal('DockerMutations').optional(),
startContainer: DockerContainerSchema(),
stopContainer: DockerContainerSchema()
})
}

export function DockerMutationsstartContainerArgsSchema(): z.ZodObject<Properties<DockerMutationsstartContainerArgs>> {
return z.object({
id: z.string()
})
}

export function DockerMutationsstopContainerArgsSchema(): z.ZodObject<Properties<DockerMutationsstopContainerArgs>> {
return z.object({
id: z.string()
})
}

export function DockerNetworkSchema(): z.ZodObject<Properties<DockerNetwork>> {
return z.object({
__typename: z.literal('DockerNetwork').optional(),
Expand Down
27 changes: 27 additions & 0 deletions api/src/graphql/generated/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,7 @@ export type Docker = Node & {
__typename?: 'Docker';
containers?: Maybe<Array<DockerContainer>>;
id: Scalars['ID']['output'];
mutations: DockerMutations;
networks?: Maybe<Array<DockerNetwork>>;
};

Expand All @@ -578,6 +579,22 @@ export type DockerContainer = {
status: Scalars['String']['output'];
};

export type DockerMutations = {
__typename?: 'DockerMutations';
startContainer: DockerContainer;
stopContainer: DockerContainer;
};


export type DockerMutationsstartContainerArgs = {
id: Scalars['ID']['input'];
};


export type DockerMutationsstopContainerArgs = {
id: Scalars['ID']['input'];
};

export type DockerNetwork = {
__typename?: 'DockerNetwork';
attachable: Scalars['Boolean']['output'];
Expand Down Expand Up @@ -2002,6 +2019,7 @@ export type ResolversTypes = ResolversObject<{
Display: ResolverTypeWrapper<Display>;
Docker: ResolverTypeWrapper<Docker>;
DockerContainer: ResolverTypeWrapper<DockerContainer>;
DockerMutations: ResolverTypeWrapper<DockerMutations>;
DockerNetwork: ResolverTypeWrapper<DockerNetwork>;
DynamicRemoteAccessStatus: ResolverTypeWrapper<DynamicRemoteAccessStatus>;
DynamicRemoteAccessType: DynamicRemoteAccessType;
Expand Down Expand Up @@ -2128,6 +2146,7 @@ export type ResolversParentTypes = ResolversObject<{
Display: Display;
Docker: Docker;
DockerContainer: DockerContainer;
DockerMutations: DockerMutations;
DockerNetwork: DockerNetwork;
DynamicRemoteAccessStatus: DynamicRemoteAccessStatus;
EnableDynamicRemoteAccessInput: EnableDynamicRemoteAccessInput;
Expand Down Expand Up @@ -2452,6 +2471,7 @@ export type DisplayResolvers<ContextType = Context, ParentType extends Resolvers
export type DockerResolvers<ContextType = Context, ParentType extends ResolversParentTypes['Docker'] = ResolversParentTypes['Docker']> = ResolversObject<{
containers?: Resolver<Maybe<Array<ResolversTypes['DockerContainer']>>, ParentType, ContextType>;
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
mutations?: Resolver<ResolversTypes['DockerMutations'], ParentType, ContextType>;
networks?: Resolver<Maybe<Array<ResolversTypes['DockerNetwork']>>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
}>;
Expand All @@ -2475,6 +2495,12 @@ export type DockerContainerResolvers<ContextType = Context, ParentType extends R
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
}>;

export type DockerMutationsResolvers<ContextType = Context, ParentType extends ResolversParentTypes['DockerMutations'] = ResolversParentTypes['DockerMutations']> = ResolversObject<{
startContainer?: Resolver<ResolversTypes['DockerContainer'], ParentType, ContextType, RequireFields<DockerMutationsstartContainerArgs, 'id'>>;
stopContainer?: Resolver<ResolversTypes['DockerContainer'], ParentType, ContextType, RequireFields<DockerMutationsstopContainerArgs, 'id'>>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
}>;

export type DockerNetworkResolvers<ContextType = Context, ParentType extends ResolversParentTypes['DockerNetwork'] = ResolversParentTypes['DockerNetwork']> = ResolversObject<{
attachable?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
configFrom?: Resolver<Maybe<ResolversTypes['JSON']>, ParentType, ContextType>;
Expand Down Expand Up @@ -3322,6 +3348,7 @@ export type Resolvers<ContextType = Context> = ResolversObject<{
Display?: DisplayResolvers<ContextType>;
Docker?: DockerResolvers<ContextType>;
DockerContainer?: DockerContainerResolvers<ContextType>;
DockerMutations?: DockerMutationsResolvers<ContextType>;
DockerNetwork?: DockerNetworkResolvers<ContextType>;
DynamicRemoteAccessStatus?: DynamicRemoteAccessStatusResolvers<ContextType>;
Flash?: FlashResolvers<ContextType>;
Expand Down
9 changes: 9 additions & 0 deletions api/src/graphql/schema/types/docker/docker.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,13 @@ type Docker implements Node {

type Query {
docker: Docker!
}

type DockerMutations {
startContainer(id: ID!): DockerContainer!
stopContainer(id: ID!): DockerContainer!
}

extend type Docker {
mutations: DockerMutations!
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import type { TestingModule } from '@nestjs/testing';
import { Test } from '@nestjs/testing';

import { beforeEach, describe, expect, it, vi } from 'vitest';

import type { DockerContainer } from '@app/graphql/generated/api/types.js';
import { ContainerState } from '@app/graphql/generated/api/types.js';
import { DockerMutationsResolver } from '@app/unraid-api/graph/resolvers/docker/docker.mutations.resolver.js';
import { DockerService } from '@app/unraid-api/graph/resolvers/docker/docker.service.js';

describe('DockerMutationsResolver', () => {
let resolver: DockerMutationsResolver;
let dockerService: DockerService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
DockerMutationsResolver,
{
provide: DockerService,
useValue: {
startContainer: vi.fn(),
stopContainer: vi.fn(),
},
},
],
}).compile();

resolver = module.get<DockerMutationsResolver>(DockerMutationsResolver);
dockerService = module.get<DockerService>(DockerService);
});

it('should be defined', () => {
expect(resolver).toBeDefined();
});

it('should start container', async () => {
const mockContainer: DockerContainer = {
id: '1',
autoStart: false,
command: 'test',
created: 1234567890,
image: 'test-image',
imageId: 'test-image-id',
ports: [],
state: ContainerState.RUNNING,
status: 'Up 2 hours',
};
vi.mocked(dockerService.startContainer).mockResolvedValue(mockContainer);

const result = await resolver.startContainer('1');
expect(result).toEqual(mockContainer);
expect(dockerService.startContainer).toHaveBeenCalledWith('1');
});

it('should stop container', async () => {
const mockContainer: DockerContainer = {
id: '1',
autoStart: false,
command: 'test',
created: 1234567890,
image: 'test-image',
imageId: 'test-image-id',
ports: [],
state: ContainerState.EXITED,
status: 'Exited',
};
vi.mocked(dockerService.stopContainer).mockResolvedValue(mockContainer);

const result = await resolver.stopContainer('1');
expect(result).toEqual(mockContainer);
expect(dockerService.stopContainer).toHaveBeenCalledWith('1');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Args, ResolveField, Resolver } from '@nestjs/graphql';

import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz';

import { Resource } from '@app/graphql/generated/api/types.js';
import { DockerService } from '@app/unraid-api/graph/resolvers/docker/docker.service.js';

@Resolver('DockerMutations')
export class DockerMutationsResolver {
constructor(private readonly dockerService: DockerService) {}

@ResolveField('startContainer')
@UsePermissions({
action: AuthActionVerb.UPDATE,
resource: Resource.DOCKER,
possession: AuthPossession.ANY,
})
public async startContainer(@Args('id') id: string) {
return this.dockerService.startContainer(id);
}

@ResolveField('stopContainer')
@UsePermissions({
action: AuthActionVerb.UPDATE,
resource: Resource.DOCKER,
possession: AuthPossession.ANY,
})
public async stopContainer(@Args('id') id: string) {
return this.dockerService.stopContainer(id);
}
}
59 changes: 57 additions & 2 deletions api/src/unraid-api/graph/resolvers/docker/docker.resolver.spec.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,77 @@
import type { TestingModule } from '@nestjs/testing';
import { Test } from '@nestjs/testing';

import { beforeEach, describe, expect, it } from 'vitest';
import { beforeEach, describe, expect, it, vi } from 'vitest';

import type { DockerContainer } from '@app/graphql/generated/api/types.js';
import { ContainerState } from '@app/graphql/generated/api/types.js';
import { DockerResolver } from '@app/unraid-api/graph/resolvers/docker/docker.resolver.js';
import { DockerService } from '@app/unraid-api/graph/resolvers/docker/docker.service.js';

describe('DockerResolver', () => {
let resolver: DockerResolver;
let dockerService: DockerService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [DockerResolver],
providers: [
DockerResolver,
{
provide: DockerService,
useValue: {
getContainers: vi.fn(),
},
},
],
}).compile();

resolver = module.get<DockerResolver>(DockerResolver);
dockerService = module.get<DockerService>(DockerService);
});

it('should be defined', () => {
expect(resolver).toBeDefined();
});

it('should return docker object with id', () => {
const result = resolver.docker();
expect(result).toEqual({ id: 'docker' });
});

it('should return containers from service', async () => {
const mockContainers: DockerContainer[] = [
{
id: '1',
autoStart: false,
command: 'test',
created: 1234567890,
image: 'test-image',
imageId: 'test-image-id',
ports: [],
state: ContainerState.EXITED,
status: 'Exited',
},
{
id: '2',
autoStart: true,
command: 'test2',
created: 1234567891,
image: 'test-image2',
imageId: 'test-image-id2',
ports: [],
state: ContainerState.RUNNING,
status: 'Up 2 hours',
},
];
vi.mocked(dockerService.getContainers).mockResolvedValue(mockContainers);

const result = await resolver.containers();
expect(result).toEqual(mockContainers);
expect(dockerService.getContainers).toHaveBeenCalledWith({ useCache: false });
});

it('should return mutations object with id', () => {
const result = resolver.mutations();
expect(result).toEqual({ id: 'docker-mutations' });
});
});
14 changes: 10 additions & 4 deletions api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ import { Query, ResolveField, Resolver } from '@nestjs/graphql';
import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz';

import { Resource } from '@app/graphql/generated/api/types.js';
import { DockerService } from '@app/unraid-api/graph/resolvers/docker/docker.service.js';

@Resolver('Docker')
export class DockerResolver {
constructor(private readonly dockerService: DockerService) {}

@UsePermissions({
action: AuthActionVerb.READ,
resource: Resource.DOCKER,
Expand All @@ -25,10 +28,13 @@ export class DockerResolver {
})
@ResolveField()
public async containers() {
const { getDockerContainers } = await import(
'@app/core/modules/docker/get-docker-containers.js'
);
return this.dockerService.getContainers({ useCache: false });
}

return getDockerContainers({ useCache: false });
@ResolveField()
public mutations() {
return {
id: 'docker-mutations',
};
}
}
Loading
Loading