Compare commits

..

15 Commits

Author SHA1 Message Date
James Carppe b2f6f43a25 Update template to include lifecycle policy link (#156) 2024-12-16 11:16:17 +13:00
James Carppe 4ad3d70739 Update bug report template for 2.24.0 (#153) 2024-11-20 13:15:56 +13:00
andres-portainer e6a1c29655 fix(compose): fix support for ECR BE-11392 (#151) 2024-11-18 16:42:53 -03:00
Yajith Dayarathna 333dfe1ebf refactor(edge/update): choose images from registry [BE-10964] (#6)
Co-authored-by: oscarzhou <oscar.zhou@portainer.io>
2024-11-18 14:11:26 +13:00
andres-portainer c59872553a fix(stacks): pass the registry credentials to Compose stacks BE-11388 (#147)
Co-authored-by: andres-portainer <andres-portainer@users.noreply.github.com>
2024-11-18 08:39:13 +13:00
andres-portainer 1a39370f5b fix(libstack): add missing private registry credentials BE-11388 (#143) 2024-11-15 17:38:55 -03:00
Oscar Zhou bc44056815 fix(swarm): failed to deploy app template [BE-11385] (#138) 2024-11-15 11:53:22 +13:00
andres-portainer 17c92343e0 fix(compose): avoid leftovers in Run() BE-11381 (#129) 2024-11-13 20:24:20 -03:00
andres-portainer cd6935b07a feat(edgestacks): add a retry period to edge stack deployments BE-11155 (#109)
Co-authored-by: andres-portainer <andres-portainer@users.noreply.github.com>
Co-authored-by: LP B <xAt0mZ@users.noreply.github.com>
2024-11-13 20:13:30 -03:00
andres-portainer 47d428f3eb fix(libstack): fix compose run BE-11381 (#126) 2024-11-13 14:38:53 -03:00
LP B 2baae7072f fix(edge/stacks): use default namespace when none is specified in manifest (#124) 2024-11-13 16:30:08 +13:00
andres-portainer 2e9e459aa3 fix(libstack): add a different timeout for WaitForStatus BE-11376 (#120) 2024-11-12 19:31:44 -03:00
andres-portainer 7444e2c1c7 fix(compose): provide the project name for proper validation BE-11375 (#118) 2024-11-12 17:18:40 -03:00
Oscar Zhou d6469eb33d fix(libstack): empty project name [BE-11375] (#116) 2024-11-12 10:20:45 -03:00
Ali a2da6f1827 fix(configmap): create portainer configmap if it doesn't exist [r8s-141] (#113) 2024-11-12 18:23:00 +13:00
16 changed files with 137 additions and 242 deletions
+4 -4
View File
@@ -11,6 +11,8 @@ body:
The issue tracker is for reporting bugs. If you have an [idea for a new feature](https://github.com/orgs/portainer/discussions/categories/ideas) or a [general question about Portainer](https://github.com/orgs/portainer/discussions/categories/help) please post in our [GitHub Discussions](https://github.com/orgs/portainer/discussions).
You can also ask for help in our [community Slack channel](https://join.slack.com/t/portainer/shared_invite/zt-txh3ljab-52QHTyjCqbe5RibC2lcjKA).
Please note that we only provide support for current versions of Portainer. You can find a list of supported versions in our [lifecycle policy](https://docs.portainer.io/start/lifecycle).
**DO NOT FILE ISSUES FOR GENERAL SUPPORT QUESTIONS**.
@@ -90,9 +92,10 @@ body:
- type: dropdown
attributes:
label: Portainer version
description: We only provide support for the most recent version of Portainer and the previous 3 versions. If you are on an older version of Portainer we recommend [upgrading first](https://docs.portainer.io/start/upgrade) in case your bug has already been fixed.
description: We only provide support for current versions of Portainer as per the lifecycle policy linked above. If you are on an older version of Portainer we recommend [upgrading first](https://docs.portainer.io/start/upgrade) in case your bug has already been fixed.
multiple: false
options:
- '2.24.0'
- '2.23.0'
- '2.22.0'
- '2.21.4'
@@ -116,9 +119,6 @@ body:
- '2.18.1'
- '2.17.1'
- '2.17.0'
- '2.16.2'
- '2.16.1'
- '2.16.0'
validations:
required: true
@@ -610,7 +610,7 @@
"RequiredPasswordLength": 12
},
"KubeconfigExpiry": "0",
"KubectlShellImage": "portainer/kubectl-shell:2.24.1",
"KubectlShellImage": "portainer/kubectl-shell:2.24.0",
"LDAPSettings": {
"AnonymousMode": true,
"AutoCreateUsers": true,
@@ -942,7 +942,7 @@
}
],
"version": {
"VERSION": "{\"SchemaVersion\":\"2.24.1\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
"VERSION": "{\"SchemaVersion\":\"2.24.0\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
},
"webhooks": null
}
+1 -1
View File
@@ -83,7 +83,7 @@ type Handler struct {
}
// @title PortainerCE API
// @version 2.24.1
// @version 2.24.0
// @description.markdown api-description.md
// @termsOfService
+1 -1
View File
@@ -1617,7 +1617,7 @@ type (
const (
// APIVersion is the version number of the Portainer API
APIVersion = "2.24.1"
APIVersion = "2.24.0"
// Edition is what this edition of Portainer is called
Edition = PortainerCE
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax
@@ -5,110 +5,82 @@
library css for buttons is overriden by `.widget .widget-body button`
so we have to force margin: 0
*/
.react-daterange-picker__calendar .react-calendar button {
.react-datetime-picker .react-calendar button {
margin: 0 !important;
}
/*
Extending Calendar.css from react-daterange-picker__calendar
Extending Calendar.css from react-datetime-picker
*/
.react-daterange-picker__calendar .react-calendar {
.react-datetime-picker .react-calendar {
background: var(--bg-calendar-color);
color: var(--text-main-color);
}
/* calendar nav buttons */
.react-daterange-picker__calendar .react-calendar__navigation button:disabled {
background: var(--bg-calendar-color);
.react-datetime-picker .react-calendar__navigation button:disabled {
background-color: var(--bg-calendar-color);
@apply opacity-60;
@apply brightness-95 th-dark:brightness-110;
}
.react-daterange-picker__calendar .react-calendar__navigation button:enabled:hover,
.react-daterange-picker__calendar .react-calendar__navigation button:enabled:focus {
background: var(--bg-daterangepicker-color);
.react-datetime-picker .react-calendar__navigation button:enabled:hover,
.react-datetime-picker .react-calendar__navigation button:enabled:focus {
background-color: var(--bg-daterangepicker-color);
}
/* date tile */
.react-daterange-picker__calendar .react-calendar__tile:disabled {
background: var(--bg-calendar-color);
.react-datetime-picker .react-calendar__tile:disabled {
background-color: var(--bg-calendar-color);
@apply opacity-60;
@apply brightness-95 th-dark:brightness-110;
}
.react-daterange-picker__calendar .react-calendar__tile:enabled:hover,
.react-daterange-picker__calendar .react-calendar__tile:enabled:focus {
background: var(--bg-daterangepicker-hover);
.react-datetime-picker .react-calendar__tile:enabled:hover,
.react-datetime-picker .react-calendar__tile:enabled:focus {
background-color: var(--bg-daterangepicker-hover);
}
/* today's date tile */
.react-daterange-picker__calendar .react-calendar__tile--now {
.react-datetime-picker .react-calendar__tile--now {
/* use background color to avoid white on yellow in dark/high contrast modes */
@apply th-highcontrast:text-[color:var(--bg-calendar-color)] th-dark:text-[color:var(--bg-calendar-color)];
border-radius: 0.25rem !important;
}
.react-daterange-picker__calendar .react-calendar__tile--now:enabled:hover,
.react-daterange-picker__calendar .react-calendar__tile--now:enabled:focus {
.react-datetime-picker .react-calendar__tile--now:enabled:hover,
.react-datetime-picker .react-calendar__tile--now:enabled:focus {
background: var(--bg-daterangepicker-hover);
color: var(--text-daterangepicker-hover);
}
/* probably date tile in range */
.react-daterange-picker__calendar .react-calendar__tile--hasActive {
.react-datetime-picker .react-calendar__tile--hasActive {
background: var(--bg-daterangepicker-end-date);
color: var(--text-daterangepicker-end-date);
}
.react-daterange-picker__calendar .react-calendar__tile--hasActive:enabled:hover,
.react-daterange-picker__calendar .react-calendar__tile--hasActive:enabled:focus {
.react-datetime-picker .react-calendar__tile--hasActive:enabled:hover,
.react-datetime-picker .react-calendar__tile--hasActive:enabled:focus {
background: var(--bg-daterangepicker-hover);
color: var(--text-daterangepicker-hover);
}
.react-daterange-picker__calendar .react-calendar__tile--active:enabled:hover,
.react-daterange-picker__calendar .react-calendar__tile--active:enabled:focus {
/* selected date tile */
.react-datetime-picker .react-calendar__tile--active {
background: var(--bg-daterangepicker-active);
color: var(--text-daterangepicker-active);
}
.react-datetime-picker .react-calendar__tile--active:enabled:hover,
.react-datetime-picker .react-calendar__tile--active:enabled:focus {
background: var(--bg-daterangepicker-hover);
color: var(--text-daterangepicker-hover);
}
.react-daterange-picker__calendar
.react-calendar__month-view__days__day:hover:not(.react-daterange-picker__calendar .react-calendar__tile--hoverEnd):not(
.react-daterange-picker__calendar .react-calendar__tile--hoverStart
):not(.react-calendar__tile--active) {
border-radius: 0.25rem !important;
}
/* on range select hover */
.react-daterange-picker__calendar .react-calendar--selectRange .react-calendar__tile--hover {
background: var(--bg-daterangepicker-in-range);
.react-datetime-picker .react-calendar--selectRange .react-calendar__tile--hover {
background-color: var(--bg-daterangepicker-in-range);
color: var(--text-daterangepicker-in-range);
}
/*
Extending DateTimePicker.css from react-daterange-picker__calendar
Extending DateTimePicker.css from react-datetime-picker
*/
.react-daterange-picker__calendar .react-daterange-picker__calendar--disabled {
.react-datetime-picker .react-datetime-picker--disabled {
@apply opacity-40;
}
/* selected date tile */
.react-daterange-picker__calendar .react-calendar__tile--active {
background: var(--bg-daterangepicker-active) !important;
color: var(--text-daterangepicker-active) !important;
}
.react-daterange-picker__calendar .react-calendar__tile--rangeStart:not(.react-calendar__tile--rangeEnd),
.react-daterange-picker__calendar .react-calendar__tile--hoverStart {
border-top-left-radius: 0.25rem;
border-bottom-left-radius: 0.25rem;
}
.react-daterange-picker__calendar .react-calendar__tile--rangeEnd:not(.react-calendar__tile--rangeStart),
.react-daterange-picker__calendar .react-calendar__tile--hoverEnd {
border-top-right-radius: 0.25rem;
border-bottom-right-radius: 0.25rem;
}
.react-daterange-picker__calendar .react-calendar__month-view__days__day--weekend {
color: inherit;
}
.react-calendar__tile--active.react-calendar__month-view__days__day--weekend {
color: var(--text-daterangepicker-active);
}
@@ -14,8 +14,13 @@ type StringPortBinding = {
containerPort: number;
};
type NumericPortBinding = {
hostPort: number;
protocol: Protocol;
containerPort: number;
};
type RangePortBinding = {
hostIp: string;
hostPort: Range;
protocol: Protocol;
containerPort: Range;
@@ -37,7 +42,9 @@ export function toViewModel(portBindings: PortMap): Values {
return value === 'tcp' || value === 'udp';
}
function parsePorts(portBindings: PortMap): Array<StringPortBinding> {
function parsePorts(
portBindings: PortMap
): Array<StringPortBinding | NumericPortBinding> {
return Object.entries(portBindings).flatMap(([key, bindings]) => {
const [containerPort, protocol] = key.split('/');
@@ -56,24 +63,15 @@ export function toViewModel(portBindings: PortMap): Values {
}
return bindings.map((binding) => {
let port = '';
if (binding.HostPort) {
port = binding.HostPort;
}
if (binding.HostIp) {
port = `${binding.HostIp}:${port}`;
}
if (binding.HostPort?.includes('-')) {
// Range port
return {
hostPort: port,
hostPort: binding.HostPort,
protocol,
containerPort: containerPortNumber,
};
}
return {
hostPort: port,
hostPort: parseInt(binding.HostPort || '0', 10),
protocol,
containerPort: containerPortNumber,
};
@@ -81,9 +79,9 @@ export function toViewModel(portBindings: PortMap): Values {
});
}
function sortPorts(ports: Array<StringPortBinding>) {
const rangePorts = ports.filter(isRangePortBinding);
const nonRangePorts = ports.filter((port) => !isRangePortBinding(port));
function sortPorts(ports: Array<StringPortBinding | NumericPortBinding>) {
const rangePorts = ports.filter(isStringPortBinding);
const nonRangePorts = ports.filter(isNumericPortBinding);
return {
rangePorts,
@@ -95,40 +93,27 @@ export function toViewModel(portBindings: PortMap): Values {
};
}
function combinePorts(ports: Array<StringPortBinding>) {
function combinePorts(ports: Array<NumericPortBinding>) {
return ports
.reduce((acc, port) => {
let hostIp = '';
let hostPort = 0;
if (port.hostPort.includes(':')) {
const [ipStr, portStr] = port.hostPort.split(':');
hostIp = ipStr;
hostPort = parseInt(portStr || '0', 10);
} else {
hostPort = parseInt(port.hostPort || '0', 10);
}
const lastPort = acc[acc.length - 1];
if (
lastPort &&
lastPort.hostIp === hostIp &&
lastPort.containerPort.end === port.containerPort - 1 &&
lastPort.hostPort.end === hostPort - 1 &&
lastPort.hostPort.end === port.hostPort - 1 &&
lastPort.protocol === port.protocol
) {
lastPort.hostIp = hostIp;
lastPort.containerPort.end = port.containerPort;
lastPort.hostPort.end = hostPort;
lastPort.hostPort.end = port.hostPort;
return acc;
}
return [
...acc,
{
hostIp,
hostPort: {
start: hostPort,
end: hostPort,
start: port.hostPort,
end: port.hostPort,
},
containerPort: {
start: port.containerPort,
@@ -138,32 +123,34 @@ export function toViewModel(portBindings: PortMap): Values {
},
];
}, [] as Array<RangePortBinding>)
.map(({ protocol, containerPort, hostPort, hostIp }) => ({
hostPort: getRange(hostPort.start, hostPort.end, hostIp),
.map(({ protocol, containerPort, hostPort }) => ({
hostPort: getRange(hostPort.start, hostPort.end),
containerPort: getRange(containerPort.start, containerPort.end),
protocol,
}));
function getRange(start: number, end: number, hostIp?: string): string {
function getRange(start: number, end: number): string {
if (start === end) {
if (start === 0) {
return '';
}
if (hostIp) {
return `${hostIp}:${start}`;
}
return start.toString();
}
if (hostIp) {
return `${hostIp}:${start}-${end}`;
}
return `${start}-${end}`;
}
}
}
function isRangePortBinding(port: StringPortBinding): boolean {
return port.hostPort.includes('-');
function isNumericPortBinding(
port: StringPortBinding | NumericPortBinding
): port is NumericPortBinding {
return port.hostPort !== 'string';
}
function isStringPortBinding(
port: StringPortBinding | NumericPortBinding
): port is StringPortBinding {
return port.hostPort === 'string';
}
@@ -57,15 +57,10 @@ export async function buildImageFromDockerfileContentAndFiles(
const dockerfile = new Blob([content], { type: 'text/plain' });
const uploadFiles = [dockerfile, ...files];
const formData = new FormData();
uploadFiles.forEach((file, index) => {
formData.append(`file${index}`, file);
});
return buildImage(
environmentId,
{ t: names },
formData,
{ file: uploadFiles },
'multipart/form-data'
);
}
@@ -76,6 +76,10 @@ function CreateView() {
Edge Groups
</Link>{' '}
page to assign environments and create groups.
<br />
You can upgrade from any agent version to 2.17 or later only.
You can not upgrade to an agent version prior to 2.17 . The
ability to rollback to originating version is for 2.15.0+ only.
</TextTip>
<Formik
@@ -96,13 +100,6 @@ function CreateView() {
error={errors.groupIds}
/>
<TextTip color="blue">
You can upgrade from any agent version to 2.17 or later
only. You can not upgrade to an agent version prior to
2.17 . The ability to rollback to originating version is
for 2.15.0+ only.
</TextTip>
<div className="mt-2">
<ScheduleTypeSelector />
</div>
@@ -91,7 +91,7 @@ function ItemView() {
<Widget>
<Widget.Title title="Update & Rollback Scheduler" icon={Settings} />
<Widget.Body>
<TextTip color="blue">
<TextTip color="blue" className="mb-2">
Devices need to be allocated to an Edge group, visit the{' '}
<Link
to="edge.groups"
@@ -100,6 +100,10 @@ function ItemView() {
Edge Groups
</Link>{' '}
page to assign environments and create groups.
<br />
You can upgrade from any agent version to 2.17 or later only.
You can not upgrade to an agent version prior to 2.17 . The
ability to rollback to originating version is for 2.15.0+ only.
</TextTip>
<Formik
@@ -44,5 +44,19 @@ export function validation(
otherwise: (schema) => schema.required('No rollback options available'),
}),
registryId: number().default(0),
agentImage: string()
.default('')
.when('registryId', {
is: 0,
then: (schema) => schema.optional(),
otherwise: (schema) => schema.required('Agent image is required'),
}),
updaterImage: string()
.default('')
.when('registryId', {
is: 0,
then: (schema) => schema.optional(),
otherwise: (schema) => schema.required('Updater image is required'),
}),
});
}
@@ -1,19 +1,8 @@
interface BaseActivityLog {
export interface ActivityLog {
timestamp: number;
action: string;
context: string;
id: number;
payload: object;
username: string;
}
export interface ActivityLogResponse extends BaseActivityLog {
payload: string;
}
export interface ActivityLog extends BaseActivityLog {
payload: string | object;
}
export interface ActivityLogsResponse {
logs: Array<ActivityLogResponse>;
totalCount: number;
}
@@ -4,7 +4,7 @@ import axios, { parseAxiosError } from '@/portainer/services/axios';
import { isBE } from '../../feature-flags/feature-flags.service';
import { ActivityLogResponse, ActivityLogsResponse } from './types';
import { ActivityLog } from './types';
export const sortKeys = ['Context', 'Action', 'Timestamp', 'Username'] as const;
export type SortKey = (typeof sortKeys)[number];
@@ -30,18 +30,19 @@ export function useActivityLogs(query: Query) {
queryKey: ['activityLogs', query] as const,
queryFn: () => fetchActivityLogs(query),
keepPreviousData: true,
select: (data) => ({
...data,
logs: decorateLogs(data.logs),
}),
});
}
interface ActivityLogsResponse {
logs: Array<ActivityLog>;
totalCount: number;
}
async function fetchActivityLogs(query: Query): Promise<ActivityLogsResponse> {
try {
if (!isBE) {
return {
logs: [{}, {}, {}, {}, {}] as Array<ActivityLogResponse>,
logs: [{}, {}, {}, {}, {}] as Array<ActivityLog>,
totalCount: 5,
};
}
@@ -55,40 +56,3 @@ async function fetchActivityLogs(query: Query): Promise<ActivityLogsResponse> {
throw parseAxiosError(err, 'Failed loading user activity logs csv');
}
}
/**
* Decorates logs with the payload parsed from base64
*/
function decorateLogs(logs?: ActivityLogResponse[]) {
if (!logs || logs.length === 0) {
return [];
}
return logs.map((log) => ({
...log,
payload: parseBase64AsObject(log.payload),
}));
}
function parseBase64AsObject(value: string): string | object {
if (!value) {
return value;
}
try {
return JSON.parse(safeAtob(value));
} catch (err) {
return safeAtob(value);
}
}
function safeAtob(value: string) {
if (!value) {
return value;
}
try {
return window.atob(value);
} catch (err) {
// If the payload is not base64 encoded, return the original value
return value;
}
}
@@ -78,7 +78,6 @@ export function useGenericRegistriesQuery<T = Registry[]>(
export async function getRegistries() {
try {
const { data } = await axios.get<Registry[]>('/registries');
return data;
} catch (e) {
throw parseAxiosError(e as Error, 'Unable to retrieve registries');
+1 -1
View File
@@ -2,7 +2,7 @@
"author": "Portainer.io",
"name": "portainer",
"homepage": "http://portainer.io",
"version": "2.24.1",
"version": "2.24.0",
"repository": {
"type": "git",
"url": "git@github.com:portainer/portainer.git"
+32 -55
View File
@@ -70,24 +70,32 @@ func withComposeService(
return withCli(ctx, options, func(ctx context.Context, cli *command.DockerCli) error {
composeService := compose.NewComposeService(cli)
if len(filePaths) == 0 {
return composeFn(composeService, nil)
}
env, err := parseEnvironment(options)
if err != nil {
return err
}
configDetails := types.ConfigDetails{
Environment: env,
WorkingDir: filepath.Dir(filePaths[0]),
WorkingDir: options.WorkingDir,
Environment: make(map[string]string),
}
for _, p := range filePaths {
configDetails.ConfigFiles = append(configDetails.ConfigFiles, types.ConfigFile{Filename: p})
}
envFile := make(map[string]string)
if options.EnvFilePath != "" {
env, err := dotenv.GetEnvFromFile(make(map[string]string), []string{options.EnvFilePath})
if err != nil {
return fmt.Errorf("unable to get the environment from the env file: %w", err)
}
maps.Copy(envFile, env)
configDetails.Environment = env
}
if len(configDetails.ConfigFiles) == 0 {
return composeFn(composeService, nil)
}
project, err := loader.LoadWithContext(ctx, configDetails,
func(o *loader.Options) {
o.SkipResolveEnvironment = true
@@ -102,20 +110,21 @@ func withComposeService(
return fmt.Errorf("failed to load the compose file: %w", err)
}
// Work around compose path handling
for i, service := range project.Services {
for j, envFile := range service.EnvFiles {
if !filepath.IsAbs(envFile.Path) {
project.Services[i].EnvFiles[j].Path = filepath.Join(configDetails.WorkingDir, envFile.Path)
if options.EnvFilePath != "" {
// Work around compose path handling
for i, service := range project.Services {
for j, envFile := range service.EnvFiles {
if !filepath.IsAbs(envFile.Path) {
project.Services[i].EnvFiles[j].Path = filepath.Join(project.WorkingDir, envFile.Path)
}
}
}
}
// Set the services environment variables
if p, err := project.WithServicesEnvironmentResolved(true); err == nil {
project = p
} else {
return fmt.Errorf("failed to resolve services environment: %w", err)
if p, err := project.WithServicesEnvironmentResolved(true); err == nil {
project = p
} else {
return fmt.Errorf("failed to resolve services environment: %w", err)
}
}
return composeFn(composeService, project)
@@ -127,8 +136,6 @@ func (c *ComposeDeployer) Deploy(ctx context.Context, filePaths []string, option
return withComposeService(ctx, filePaths, options.Options, func(composeService api.Service, project *types.Project) error {
addServiceLabels(project, false)
project = project.WithoutUnnecessaryResources()
var opts api.UpOptions
if options.ForceRecreate {
opts.Create.Recreate = api.RecreateForce
@@ -137,10 +144,6 @@ func (c *ComposeDeployer) Deploy(ctx context.Context, filePaths []string, option
opts.Create.RemoveOrphans = options.RemoveOrphans
opts.Start.CascadeStop = options.AbortOnContainerExit
if err := composeService.Build(ctx, project, api.BuildOptions{}); err != nil {
return fmt.Errorf("compose build operation failed: %w", err)
}
if err := composeService.Up(ctx, project, opts); err != nil {
return fmt.Errorf("compose up operation failed: %w", err)
}
@@ -253,36 +256,10 @@ func addServiceLabels(project *types.Project, oneOff bool) {
api.ProjectLabel: project.Name,
api.ServiceLabel: s.Name,
api.VersionLabel: api.ComposeVersion,
api.WorkingDirLabel: project.WorkingDir,
api.WorkingDirLabel: "/",
api.ConfigFilesLabel: strings.Join(project.ComposeFiles, ","),
api.OneoffLabel: oneOffLabel,
}
project.Services[i] = s
}
}
func parseEnvironment(options libstack.Options) (map[string]string, error) {
env := make(map[string]string)
for _, envLine := range options.Env {
e, err := dotenv.UnmarshalWithLookup(envLine, nil)
if err != nil {
return nil, fmt.Errorf("unable to parse environment variables: %w", err)
}
maps.Copy(env, e)
}
if options.EnvFilePath == "" {
return env, nil
}
e, err := dotenv.GetEnvFromFile(make(map[string]string), []string{options.EnvFilePath})
if err != nil {
return nil, fmt.Errorf("unable to get the environment from the env file: %w", err)
}
maps.Copy(env, e)
return env, nil
}
-3
View File
@@ -14,9 +14,6 @@ export default defineConfig({
},
bail: 2,
include: ['./app/**/*.test.ts', './app/**/*.test.tsx'],
env: {
PORTAINER_EDITION: 'CE',
},
},
plugins: [svgr({ include: /\?c$/ }), tsconfigPaths()],
});