Compare commits

...

3 Commits

Author SHA1 Message Date
andres-portainer
b46bff06c6 fix: 2.24 regressions (#190)
Co-authored-by: andres-portainer <andres-portainer@users.noreply.github.com>
Co-authored-by: LP B <xAt0mZ@users.noreply.github.com>
Co-authored-by: testA113 <aliharriss1995@gmail.com>
Co-authored-by: oscarzhou <oscar.zhou@portainer.io>
2024-12-03 15:25:53 +13:00
Oscar Zhou
5d311031e3 version: bump version to 2.24.1 (#187) 2024-12-03 09:10:19 +13:00
andres-portainer
99de11894c fix(compose): fix support for ECR BE-11392 (#150) 2024-11-18 16:42:49 -03:00
18 changed files with 307 additions and 191 deletions

View File

@@ -49,7 +49,6 @@ import (
"github.com/portainer/portainer/api/stacks/deployments"
"github.com/portainer/portainer/pkg/featureflags"
"github.com/portainer/portainer/pkg/libhelm"
"github.com/portainer/portainer/pkg/libstack"
"github.com/portainer/portainer/pkg/libstack/compose"
"github.com/gofrs/uuid"
@@ -166,26 +165,6 @@ func checkDBSchemaServerVersionMatch(dbStore dataservices.DataStore, serverVersi
return v.SchemaVersion == serverVersion && v.Edition == serverEdition
}
func initComposeStackManager(composeDeployer libstack.Deployer, proxyManager *proxy.Manager) portainer.ComposeStackManager {
composeWrapper, err := exec.NewComposeStackManager(composeDeployer, proxyManager)
if err != nil {
log.Fatal().Err(err).Msg("failed creating compose manager")
}
return composeWrapper
}
func initSwarmStackManager(
assetsPath string,
configPath string,
signatureService portainer.DigitalSignatureService,
fileService portainer.FileService,
reverseTunnelService portainer.ReverseTunnelService,
dataStore dataservices.DataStore,
) (portainer.SwarmStackManager, error) {
return exec.NewSwarmStackManager(assetsPath, configPath, signatureService, fileService, reverseTunnelService, dataStore)
}
func initKubernetesDeployer(kubernetesTokenCacheManager *kubeproxy.TokenCacheManager, kubernetesClientFactory *kubecli.ClientFactory, dataStore dataservices.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, proxyManager *proxy.Manager, assetsPath string) portainer.KubernetesDeployer {
return exec.NewKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, signatureService, proxyManager, assetsPath)
}
@@ -435,9 +414,9 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
composeDeployer := compose.NewComposeDeployer()
composeStackManager := initComposeStackManager(composeDeployer, proxyManager)
composeStackManager := exec.NewComposeStackManager(composeDeployer, proxyManager, dataStore)
swarmStackManager, err := initSwarmStackManager(*flags.Assets, dockerConfigPath, signatureService, fileService, reverseTunnelService, dataStore)
swarmStackManager, err := exec.NewSwarmStackManager(*flags.Assets, dockerConfigPath, signatureService, fileService, reverseTunnelService, dataStore)
if err != nil {
log.Fatal().Err(err).Msg("failed initializing swarm stack manager")
}

View File

@@ -610,7 +610,7 @@
"RequiredPasswordLength": 12
},
"KubeconfigExpiry": "0",
"KubectlShellImage": "portainer/kubectl-shell:2.24.0",
"KubectlShellImage": "portainer/kubectl-shell:2.24.1",
"LDAPSettings": {
"AnonymousMode": true,
"AutoCreateUsers": true,
@@ -942,7 +942,7 @@
}
],
"version": {
"VERSION": "{\"SchemaVersion\":\"2.24.0\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
"VERSION": "{\"SchemaVersion\":\"2.24.1\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
},
"webhooks": null
}

View File

@@ -9,27 +9,32 @@ import (
"strings"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/http/proxy"
"github.com/portainer/portainer/api/http/proxy/factory"
"github.com/portainer/portainer/api/internal/registryutils"
"github.com/portainer/portainer/api/stacks/stackutils"
"github.com/portainer/portainer/pkg/libstack"
"github.com/docker/cli/cli/config/types"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
)
// ComposeStackManager is a wrapper for docker-compose binary
type ComposeStackManager struct {
deployer libstack.Deployer
proxyManager *proxy.Manager
dataStore dataservices.DataStore
}
// NewComposeStackManager returns a docker-compose wrapper if corresponding binary present, otherwise nil
func NewComposeStackManager(deployer libstack.Deployer, proxyManager *proxy.Manager) (*ComposeStackManager, error) {
// NewComposeStackManager returns a Compose stack manager
func NewComposeStackManager(deployer libstack.Deployer, proxyManager *proxy.Manager, dataStore dataservices.DataStore) *ComposeStackManager {
return &ComposeStackManager{
deployer: deployer,
proxyManager: proxyManager,
}, nil
dataStore: dataStore,
}
}
// ComposeSyntaxMaxVersion returns the maximum supported version of the docker compose syntax
@@ -60,7 +65,7 @@ func (manager *ComposeStackManager) Up(ctx context.Context, stack *portainer.Sta
EnvFilePath: envFilePath,
Host: url,
ProjectName: stack.Name,
Registries: portainerRegistriesToAuthConfigs(options.Registries),
Registries: portainerRegistriesToAuthConfigs(manager.dataStore, options.Registries),
},
ForceRecreate: options.ForceRecreate,
AbortOnContainerExit: options.AbortOnContainerExit,
@@ -91,7 +96,7 @@ func (manager *ComposeStackManager) Run(ctx context.Context, stack *portainer.St
EnvFilePath: envFilePath,
Host: url,
ProjectName: stack.Name,
Registries: portainerRegistriesToAuthConfigs(options.Registries),
Registries: portainerRegistriesToAuthConfigs(manager.dataStore, options.Registries),
},
Remove: options.Remove,
Args: options.Args,
@@ -140,7 +145,7 @@ func (manager *ComposeStackManager) Pull(ctx context.Context, stack *portainer.S
EnvFilePath: envFilePath,
Host: url,
ProjectName: stack.Name,
Registries: portainerRegistriesToAuthConfigs(options.Registries),
Registries: portainerRegistriesToAuthConfigs(manager.dataStore, options.Registries),
})
return errors.Wrap(err, "failed to pull images of the stack")
}
@@ -221,16 +226,48 @@ func copyConfigEnvVars(w io.Writer, envs []portainer.Pair) error {
return nil
}
func portainerRegistriesToAuthConfigs(registries []portainer.Registry) []types.AuthConfig {
func portainerRegistriesToAuthConfigs(tx dataservices.DataStoreTx, registries []portainer.Registry) []types.AuthConfig {
var authConfigs []types.AuthConfig
for _, r := range registries {
authConfigs = append(authConfigs, types.AuthConfig{
ac := types.AuthConfig{
Username: r.Username,
Password: r.Password,
ServerAddress: r.URL,
})
}
if r.Authentication {
var err error
ac.Username, ac.Password, err = getEffectiveRegUsernamePassword(tx, &r)
if err != nil {
continue
}
}
authConfigs = append(authConfigs, ac)
}
return authConfigs
}
func getEffectiveRegUsernamePassword(tx dataservices.DataStoreTx, registry *portainer.Registry) (string, string, error) {
if err := registryutils.EnsureRegTokenValid(tx, registry); err != nil {
log.Warn().
Err(err).
Str("RegistryName", registry.Name).
Msg("Failed to validate registry token. Skip logging with this registry.")
return "", "", err
}
username, password, err := registryutils.GetRegEffectiveCredential(registry)
if err != nil {
log.Warn().
Err(err).
Str("RegistryName", registry.Name).
Msg("Failed to get effective credential. Skip logging with this registry.")
}
return username, password, err
}

View File

@@ -48,10 +48,7 @@ func Test_UpAndDown(t *testing.T) {
deployer := compose.NewComposeDeployer()
w, err := NewComposeStackManager(deployer, nil)
if err != nil {
t.Fatalf("Failed creating manager: %s", err)
}
w := NewComposeStackManager(deployer, nil, nil)
ctx := context.TODO()

View File

@@ -11,7 +11,6 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/internal/registryutils"
"github.com/portainer/portainer/api/stacks/stackutils"
"github.com/rs/zerolog/log"
@@ -62,22 +61,8 @@ func (manager *SwarmStackManager) Login(registries []portainer.Registry, endpoin
for _, registry := range registries {
if registry.Authentication {
if err := registryutils.EnsureRegTokenValid(manager.dataStore, &registry); err != nil {
log.Warn().
Err(err).
Str("RegistryName", registry.Name).
Msg("Failed to validate registry token. Skip logging with this registry.")
continue
}
username, password, err := registryutils.GetRegEffectiveCredential(&registry)
username, password, err := getEffectiveRegUsernamePassword(manager.dataStore, &registry)
if err != nil {
log.Warn().
Err(err).
Str("RegistryName", registry.Name).
Msg("Failed to get effective credential. Skip logging with this registry.")
continue
}

View File

@@ -83,7 +83,7 @@ type Handler struct {
}
// @title PortainerCE API
// @version 2.24.0
// @version 2.24.1
// @description.markdown api-description.md
// @termsOfService

View File

@@ -14,47 +14,51 @@ func isRegTokenValid(registry *portainer.Registry) (valid bool) {
return registry.AccessToken != "" && registry.AccessTokenExpiry > time.Now().Unix()
}
func doGetRegToken(dataStore dataservices.DataStore, registry *portainer.Registry) (err error) {
func doGetRegToken(tx dataservices.DataStoreTx, registry *portainer.Registry) error {
ecrClient := ecr.NewService(registry.Username, registry.Password, registry.Ecr.Region)
accessToken, expiryAt, err := ecrClient.GetAuthorizationToken()
if err != nil {
return
return err
}
registry.AccessToken = *accessToken
registry.AccessTokenExpiry = expiryAt.Unix()
err = dataStore.Registry().Update(registry.ID, registry)
return
return tx.Registry().Update(registry.ID, registry)
}
func parseRegToken(registry *portainer.Registry) (username, password string, err error) {
ecrClient := ecr.NewService(registry.Username, registry.Password, registry.Ecr.Region)
return ecrClient.ParseAuthorizationToken(registry.AccessToken)
return ecr.NewService(registry.Username, registry.Password, registry.Ecr.Region).
ParseAuthorizationToken(registry.AccessToken)
}
func EnsureRegTokenValid(dataStore dataservices.DataStore, registry *portainer.Registry) (err error) {
if registry.Type == portainer.EcrRegistry {
if isRegTokenValid(registry) {
log.Debug().Msg("current ECR token is still valid")
} else {
err = doGetRegToken(dataStore, registry)
if err != nil {
log.Debug().Msg("refresh ECR token")
}
}
func EnsureRegTokenValid(tx dataservices.DataStoreTx, registry *portainer.Registry) error {
if registry.Type != portainer.EcrRegistry {
return nil
}
return
if isRegTokenValid(registry) {
log.Debug().Msg("current ECR token is still valid")
return nil
}
if err := doGetRegToken(tx, registry); err != nil {
log.Debug().Msg("refresh ECR token")
return err
}
return nil
}
func GetRegEffectiveCredential(registry *portainer.Registry) (username, password string, err error) {
username = registry.Username
password = registry.Password
if registry.Type == portainer.EcrRegistry {
username, password, err = parseRegToken(registry)
} else {
username = registry.Username
password = registry.Password
}
return
}

View File

@@ -1617,7 +1617,7 @@ type (
const (
// APIVersion is the version number of the Portainer API
APIVersion = "2.24.0"
APIVersion = "2.24.1"
// Edition is what this edition of Portainer is called
Edition = PortainerCE
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax

View File

@@ -5,82 +5,110 @@
library css for buttons is overriden by `.widget .widget-body button`
so we have to force margin: 0
*/
.react-datetime-picker .react-calendar button {
.react-daterange-picker__calendar .react-calendar button {
margin: 0 !important;
}
/*
Extending Calendar.css from react-datetime-picker
Extending Calendar.css from react-daterange-picker__calendar
*/
.react-datetime-picker .react-calendar {
.react-daterange-picker__calendar .react-calendar {
background: var(--bg-calendar-color);
color: var(--text-main-color);
}
/* calendar nav buttons */
.react-datetime-picker .react-calendar__navigation button:disabled {
background-color: var(--bg-calendar-color);
.react-daterange-picker__calendar .react-calendar__navigation button:disabled {
background: var(--bg-calendar-color);
@apply opacity-60;
@apply brightness-95 th-dark:brightness-110;
}
.react-datetime-picker .react-calendar__navigation button:enabled:hover,
.react-datetime-picker .react-calendar__navigation button:enabled:focus {
background-color: var(--bg-daterangepicker-color);
.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);
}
/* date tile */
.react-datetime-picker .react-calendar__tile:disabled {
background-color: var(--bg-calendar-color);
.react-daterange-picker__calendar .react-calendar__tile:disabled {
background: var(--bg-calendar-color);
@apply opacity-60;
@apply brightness-95 th-dark:brightness-110;
}
.react-datetime-picker .react-calendar__tile:enabled:hover,
.react-datetime-picker .react-calendar__tile:enabled:focus {
background-color: var(--bg-daterangepicker-hover);
.react-daterange-picker__calendar .react-calendar__tile:enabled:hover,
.react-daterange-picker__calendar .react-calendar__tile:enabled:focus {
background: var(--bg-daterangepicker-hover);
}
/* today's date tile */
.react-datetime-picker .react-calendar__tile--now {
/* use background color to avoid white on yellow in dark/high contrast modes */
.react-daterange-picker__calendar .react-calendar__tile--now {
@apply th-highcontrast:text-[color:var(--bg-calendar-color)] th-dark:text-[color:var(--bg-calendar-color)];
border-radius: 0.25rem !important;
}
.react-datetime-picker .react-calendar__tile--now:enabled:hover,
.react-datetime-picker .react-calendar__tile--now:enabled:focus {
.react-daterange-picker__calendar .react-calendar__tile--now:enabled:hover,
.react-daterange-picker__calendar .react-calendar__tile--now:enabled:focus {
background: var(--bg-daterangepicker-hover);
color: var(--text-daterangepicker-hover);
}
/* probably date tile in range */
.react-datetime-picker .react-calendar__tile--hasActive {
.react-daterange-picker__calendar .react-calendar__tile--hasActive {
background: var(--bg-daterangepicker-end-date);
color: var(--text-daterangepicker-end-date);
}
.react-datetime-picker .react-calendar__tile--hasActive:enabled:hover,
.react-datetime-picker .react-calendar__tile--hasActive:enabled:focus {
.react-daterange-picker__calendar .react-calendar__tile--hasActive:enabled:hover,
.react-daterange-picker__calendar .react-calendar__tile--hasActive:enabled:focus {
background: var(--bg-daterangepicker-hover);
color: var(--text-daterangepicker-hover);
}
/* 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 {
.react-daterange-picker__calendar .react-calendar__tile--active:enabled:hover,
.react-daterange-picker__calendar .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-datetime-picker .react-calendar--selectRange .react-calendar__tile--hover {
background-color: var(--bg-daterangepicker-in-range);
.react-daterange-picker__calendar .react-calendar--selectRange .react-calendar__tile--hover {
background: var(--bg-daterangepicker-in-range);
color: var(--text-daterangepicker-in-range);
}
/*
Extending DateTimePicker.css from react-datetime-picker
Extending DateTimePicker.css from react-daterange-picker__calendar
*/
.react-datetime-picker .react-datetime-picker--disabled {
.react-daterange-picker__calendar .react-daterange-picker__calendar--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);
}

View File

@@ -14,13 +14,8 @@ type StringPortBinding = {
containerPort: number;
};
type NumericPortBinding = {
hostPort: number;
protocol: Protocol;
containerPort: number;
};
type RangePortBinding = {
hostIp: string;
hostPort: Range;
protocol: Protocol;
containerPort: Range;
@@ -42,9 +37,7 @@ export function toViewModel(portBindings: PortMap): Values {
return value === 'tcp' || value === 'udp';
}
function parsePorts(
portBindings: PortMap
): Array<StringPortBinding | NumericPortBinding> {
function parsePorts(portBindings: PortMap): Array<StringPortBinding> {
return Object.entries(portBindings).flatMap(([key, bindings]) => {
const [containerPort, protocol] = key.split('/');
@@ -63,15 +56,24 @@ 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: binding.HostPort,
hostPort: port,
protocol,
containerPort: containerPortNumber,
};
}
return {
hostPort: parseInt(binding.HostPort || '0', 10),
hostPort: port,
protocol,
containerPort: containerPortNumber,
};
@@ -79,9 +81,9 @@ export function toViewModel(portBindings: PortMap): Values {
});
}
function sortPorts(ports: Array<StringPortBinding | NumericPortBinding>) {
const rangePorts = ports.filter(isStringPortBinding);
const nonRangePorts = ports.filter(isNumericPortBinding);
function sortPorts(ports: Array<StringPortBinding>) {
const rangePorts = ports.filter(isRangePortBinding);
const nonRangePorts = ports.filter((port) => !isRangePortBinding(port));
return {
rangePorts,
@@ -93,27 +95,40 @@ export function toViewModel(portBindings: PortMap): Values {
};
}
function combinePorts(ports: Array<NumericPortBinding>) {
function combinePorts(ports: Array<StringPortBinding>) {
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 === port.hostPort - 1 &&
lastPort.hostPort.end === hostPort - 1 &&
lastPort.protocol === port.protocol
) {
lastPort.hostIp = hostIp;
lastPort.containerPort.end = port.containerPort;
lastPort.hostPort.end = port.hostPort;
lastPort.hostPort.end = hostPort;
return acc;
}
return [
...acc,
{
hostIp,
hostPort: {
start: port.hostPort,
end: port.hostPort,
start: hostPort,
end: hostPort,
},
containerPort: {
start: port.containerPort,
@@ -123,34 +138,32 @@ export function toViewModel(portBindings: PortMap): Values {
},
];
}, [] as Array<RangePortBinding>)
.map(({ protocol, containerPort, hostPort }) => ({
hostPort: getRange(hostPort.start, hostPort.end),
.map(({ protocol, containerPort, hostPort, hostIp }) => ({
hostPort: getRange(hostPort.start, hostPort.end, hostIp),
containerPort: getRange(containerPort.start, containerPort.end),
protocol,
}));
function getRange(start: number, end: number): string {
function getRange(start: number, end: number, hostIp?: string): 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 isNumericPortBinding(
port: StringPortBinding | NumericPortBinding
): port is NumericPortBinding {
return port.hostPort !== 'string';
}
function isStringPortBinding(
port: StringPortBinding | NumericPortBinding
): port is StringPortBinding {
return port.hostPort === 'string';
function isRangePortBinding(port: StringPortBinding): boolean {
return port.hostPort.includes('-');
}

View File

@@ -57,10 +57,15 @@ 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 },
{ file: uploadFiles },
formData,
'multipart/form-data'
);
}

View File

@@ -1,8 +1,19 @@
export interface ActivityLog {
interface BaseActivityLog {
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;
}

View File

@@ -4,7 +4,7 @@ import axios, { parseAxiosError } from '@/portainer/services/axios';
import { isBE } from '../../feature-flags/feature-flags.service';
import { ActivityLog } from './types';
import { ActivityLogResponse, ActivityLogsResponse } from './types';
export const sortKeys = ['Context', 'Action', 'Timestamp', 'Username'] as const;
export type SortKey = (typeof sortKeys)[number];
@@ -30,19 +30,18 @@ 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<ActivityLog>,
logs: [{}, {}, {}, {}, {}] as Array<ActivityLogResponse>,
totalCount: 5,
};
}
@@ -56,3 +55,40 @@ 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;
}
}

2
go.mod
View File

@@ -9,7 +9,7 @@ require (
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2
github.com/aws/aws-sdk-go-v2 v1.24.1
github.com/aws/aws-sdk-go-v2/credentials v1.16.16
github.com/aws/aws-sdk-go-v2/service/ecr v1.14.0
github.com/aws/aws-sdk-go-v2/service/ecr v1.24.1
github.com/cbroglie/mustache v1.4.0
github.com/compose-spec/compose-go/v2 v2.0.2
github.com/containers/image/v5 v5.30.1

9
go.sum
View File

@@ -57,7 +57,6 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/aws/aws-sdk-go-v2 v1.13.0/go.mod h1:L6+ZpqHaLbAaxsqV0L4cvxZY7QupWJB4fhkf8LXvC7w=
github.com/aws/aws-sdk-go-v2 v1.24.1 h1:xAojnj+ktS95YZlDf0zxWBkbFtymPeDP+rvUQIH3uAU=
github.com/aws/aws-sdk-go-v2 v1.24.1/go.mod h1:LNh45Br1YAkEKaAqvmE1m8FUx6a5b/V0oAKV7of29b4=
github.com/aws/aws-sdk-go-v2/config v1.26.6 h1:Z/7w9bUqlRI0FFQpetVuFYEsjzE3h7fpU6HuGmfPL/o=
@@ -66,16 +65,14 @@ github.com/aws/aws-sdk-go-v2/credentials v1.16.16 h1:8q6Rliyv0aUFAVtzaldUEcS+T5g
github.com/aws/aws-sdk-go-v2/credentials v1.16.16/go.mod h1:UHVZrdUsv63hPXFo1H7c5fEneoVo9UXiz36QG1GEPi0=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 h1:c5I5iH+DZcH3xOIMlz3/tCKJDaHFwYEmxvlh2fAcFo8=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11/go.mod h1:cRrYDYAMUohBJUtUnOhydaMHtiK/1NZ0Otc9lIb6O0Y=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.4/go.mod h1:XHgQ7Hz2WY2GAn//UXHofLfPXWh+s62MbMOijrg12Lw=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 h1:vF+Zgd9s+H4vOXd5BMaPWykta2a6Ih0AKLq/X6NYKn4=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10/go.mod h1:6BkRjejp/GR4411UGqkX8+wFMbFbqsUIimfK4XjOKR4=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.2.0/go.mod h1:BsCSJHx5DnDXIrOcqB8KN1/B+hXLG/bi4Y6Vjcx/x9E=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 h1:nYPe006ktcqUji8S2mqXf9c/7NdiKriOwMvWQHgYztw=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10/go.mod h1:6UV4SZkVvmODfXKql4LCbaZUpF7HO2BX38FgBf9ZOLw=
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.3 h1:n3GDfwqF2tzEkXlv5cuy4iy7LpKDtqDMcNLfZDu9rls=
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.3/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY=
github.com/aws/aws-sdk-go-v2/service/ecr v1.14.0 h1:AAZJJAENsQ4yYbnfvqPZT8Nc1YlEd5CZ4usymlC2b4U=
github.com/aws/aws-sdk-go-v2/service/ecr v1.14.0/go.mod h1:a3WUi3JjM3MFtIYenSYPJ7UZPXsw7U7vzebnynxucks=
github.com/aws/aws-sdk-go-v2/service/ecr v1.24.1 h1:zqXEIhuR7RcHob2gxB/Xf1X4XuMS0vapn7xr+wCPrpg=
github.com/aws/aws-sdk-go-v2/service/ecr v1.24.1/go.mod h1:+rWYJfms9p+D/wUN599tx3FtWvxoXCP25b8Porlrxcc=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 h1:/b31bi3YVNlkzkBrm9LfpaKoaYZUxIAj4sHfOTmLfqw=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4/go.mod h1:2aGXHFmbInwgP9ZfpmdIfOELL79zhdNYNmReK8qDfdQ=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 h1:DBYTXwIGQSGs9w4jKm60F5dmCQ3EEruxdc0MFh+3EY4=
@@ -86,7 +83,6 @@ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7 h1:QPMJf+Jw8E1l7zqhZmMlFw6w
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7/go.mod h1:ykf3COxYI0UJmxcfcxcVuz7b6uADi1FkiUz6Eb7AgM8=
github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 h1:NzO4Vrau795RkUdSHKEwiR01FaGzGOH1EETJ+5QHnm0=
github.com/aws/aws-sdk-go-v2/service/sts v1.26.7/go.mod h1:6h2YuIoxaMSCFf5fi1EgZAwdfkGMgDY+DVfa61uLe4U=
github.com/aws/smithy-go v1.10.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM=
github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE=
github.com/beorn7/perks v0.0.0-20150223135152-b965b613227f/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
@@ -315,7 +311,6 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=

View File

@@ -2,7 +2,7 @@
"author": "Portainer.io",
"name": "portainer",
"homepage": "http://portainer.io",
"version": "2.24.0",
"version": "2.24.1",
"repository": {
"type": "git",
"url": "git@github.com:portainer/portainer.git"

View File

@@ -70,32 +70,24 @@ 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{
WorkingDir: options.WorkingDir,
Environment: make(map[string]string),
Environment: env,
WorkingDir: filepath.Dir(filePaths[0]),
}
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
@@ -110,21 +102,20 @@ func withComposeService(
return fmt.Errorf("failed to load the compose file: %w", err)
}
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)
}
// 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 p, err := project.WithServicesEnvironmentResolved(true); err == nil {
project = p
} else {
return fmt.Errorf("failed to resolve services environment: %w", err)
}
// 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)
}
return composeFn(composeService, project)
@@ -136,6 +127,8 @@ 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
@@ -144,6 +137,10 @@ 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)
}
@@ -256,10 +253,36 @@ func addServiceLabels(project *types.Project, oneOff bool) {
api.ProjectLabel: project.Name,
api.ServiceLabel: s.Name,
api.VersionLabel: api.ComposeVersion,
api.WorkingDirLabel: "/",
api.WorkingDirLabel: project.WorkingDir,
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
}

View File

@@ -14,6 +14,9 @@ export default defineConfig({
},
bail: 2,
include: ['./app/**/*.test.ts', './app/**/*.test.tsx'],
env: {
PORTAINER_EDITION: 'CE',
},
},
plugins: [svgr({ include: /\?c$/ }), tsconfigPaths()],
});