Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb5b9ef069 | ||
|
|
a74c6dbd24 | ||
|
|
6451ccce94 | ||
|
|
6dd5150e23 | ||
|
|
441db15cfd | ||
|
|
b44fabaefe | ||
|
|
ddeddc723e | ||
|
|
e980ce3d6a | ||
|
|
123a138278 | ||
|
|
cc3ec3cebd | ||
|
|
5dab7a1df4 | ||
|
|
ed0cf4d79c | ||
|
|
aa4b8ad5e3 | ||
|
|
81811f669d | ||
|
|
3ae55d8c3e | ||
|
|
933c2a7002 | ||
|
|
1641642695 | ||
|
|
f80b1ed53a | ||
|
|
d04da7898d | ||
|
|
ec83d02afa | ||
|
|
05265dda47 | ||
|
|
74e1ff5e2d | ||
|
|
795d812652 | ||
|
|
46b1d5b528 | ||
|
|
cf7672d59e | ||
|
|
9c8a30693a | ||
|
|
023945cbd2 | ||
|
|
498ba46863 | ||
|
|
399ddaea3b | ||
|
|
13cee9975c | ||
|
|
f8927851e4 |
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
@@ -22,7 +22,7 @@ on:
|
||||
env:
|
||||
DOCKER_HUB_REPO: portainerci/portainer-ce
|
||||
EXTENSION_HUB_REPO: portainerci/portainer-docker-extension
|
||||
GO_VERSION: 1.21.6
|
||||
GO_VERSION: 1.21.9
|
||||
NODE_VERSION: 18.x
|
||||
|
||||
jobs:
|
||||
|
||||
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
@@ -18,7 +18,7 @@ on:
|
||||
- ready_for_review
|
||||
|
||||
env:
|
||||
GO_VERSION: 1.21.6
|
||||
GO_VERSION: 1.21.9
|
||||
NODE_VERSION: 18.x
|
||||
|
||||
jobs:
|
||||
|
||||
2
.github/workflows/nightly-security-scan.yml
vendored
2
.github/workflows/nightly-security-scan.yml
vendored
@@ -6,7 +6,7 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
GO_VERSION: 1.21.6
|
||||
GO_VERSION: 1.21.9
|
||||
|
||||
jobs:
|
||||
client-dependencies:
|
||||
|
||||
2
.github/workflows/pr-security.yml
vendored
2
.github/workflows/pr-security.yml
vendored
@@ -14,7 +14,7 @@ on:
|
||||
- '.github/workflows/pr-security.yml'
|
||||
|
||||
env:
|
||||
GO_VERSION: 1.21.6
|
||||
GO_VERSION: 1.21.9
|
||||
NODE_VERSION: 18.x
|
||||
|
||||
jobs:
|
||||
|
||||
2
.github/workflows/test.yaml
vendored
2
.github/workflows/test.yaml
vendored
@@ -1,7 +1,7 @@
|
||||
name: Test
|
||||
|
||||
env:
|
||||
GO_VERSION: 1.21.6
|
||||
GO_VERSION: 1.21.9
|
||||
NODE_VERSION: 18.x
|
||||
|
||||
on:
|
||||
|
||||
2
.github/workflows/validate-openapi-spec.yaml
vendored
2
.github/workflows/validate-openapi-spec.yaml
vendored
@@ -13,7 +13,7 @@ on:
|
||||
- ready_for_review
|
||||
|
||||
env:
|
||||
GO_VERSION: 1.21.6
|
||||
GO_VERSION: 1.21.9
|
||||
NODE_VERSION: 18.x
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -26,7 +26,7 @@ func RestoreArchive(archive io.Reader, password string, filestorePath string, ga
|
||||
if password != "" {
|
||||
archive, err = decrypt(archive, password)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to decrypt the archive")
|
||||
return errors.Wrap(err, "failed to decrypt the archive. Please ensure the password is correct and try again")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/datastore"
|
||||
"github.com/portainer/portainer/api/datastore/migrator"
|
||||
"github.com/portainer/portainer/api/datastore/postinit"
|
||||
"github.com/portainer/portainer/api/demo"
|
||||
"github.com/portainer/portainer/api/docker"
|
||||
dockerclient "github.com/portainer/portainer/api/docker/client"
|
||||
@@ -457,14 +458,6 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
authorizationService := authorization.NewService(dataStore)
|
||||
authorizationService.K8sClientFactory = kubernetesClientFactory
|
||||
|
||||
pendingActionsService := pendingactions.NewService(dataStore, kubernetesClientFactory, authorizationService, shutdownCtx)
|
||||
|
||||
snapshotService, err := initSnapshotService(*flags.SnapshotInterval, dataStore, dockerClientFactory, kubernetesClientFactory, shutdownCtx, pendingActionsService)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed initializing snapshot service")
|
||||
}
|
||||
snapshotService.Start()
|
||||
|
||||
kubernetesTokenCacheManager := kubeproxy.NewTokenCacheManager()
|
||||
|
||||
kubeClusterAccessService := kubernetes.NewKubeClusterAccessService(*flags.BaseURL, *flags.AddrHTTPS, sslSettings.CertPath)
|
||||
@@ -489,6 +482,14 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
|
||||
kubernetesDeployer := initKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, digitalSignatureService, proxyManager, *flags.Assets)
|
||||
|
||||
pendingActionsService := pendingactions.NewService(dataStore, kubernetesClientFactory, dockerClientFactory, authorizationService, shutdownCtx, *flags.Assets, kubernetesDeployer)
|
||||
|
||||
snapshotService, err := initSnapshotService(*flags.SnapshotInterval, dataStore, dockerClientFactory, kubernetesClientFactory, shutdownCtx, pendingActionsService)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed initializing snapshot service")
|
||||
}
|
||||
snapshotService.Start()
|
||||
|
||||
helmPackageManager, err := initHelmPackageManager(*flags.Assets)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed initializing helm package manager")
|
||||
@@ -578,10 +579,12 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
// but some more complex migrations require access to a kubernetes or docker
|
||||
// client. Therefore we run a separate migration process just before
|
||||
// starting the server.
|
||||
postInitMigrator := datastore.NewPostInitMigrator(
|
||||
postInitMigrator := postinit.NewPostInitMigrator(
|
||||
kubernetesClientFactory,
|
||||
dockerClientFactory,
|
||||
dataStore,
|
||||
*flags.Assets,
|
||||
kubernetesDeployer,
|
||||
)
|
||||
if err := postInitMigrator.PostInitMigrate(); err != nil {
|
||||
log.Fatal().Err(err).Msg("failure during post init migrations")
|
||||
@@ -650,6 +653,7 @@ func main() {
|
||||
Msg("starting Portainer")
|
||||
|
||||
err := server.Start()
|
||||
|
||||
log.Info().Err(err).Msg("HTTP server exited")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,52 +1,216 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"golang.org/x/crypto/argon2"
|
||||
"golang.org/x/crypto/scrypt"
|
||||
)
|
||||
|
||||
// NOTE: has to go with what is considered to be a simplistic in that it omits any
|
||||
// authentication of the encrypted data.
|
||||
// Person with better knowledge is welcomed to improve it.
|
||||
// sourced from https://golang.org/src/crypto/cipher/example_test.go
|
||||
const (
|
||||
// AES GCM settings
|
||||
aesGcmHeader = "AES256-GCM" // The encrypted file header
|
||||
aesGcmBlockSize = 1024 * 1024 // 1MB block for aes gcm
|
||||
|
||||
var emptySalt []byte = make([]byte, 0)
|
||||
// Argon2 settings
|
||||
// Recommded settings lower memory hardware according to current OWASP recommendations
|
||||
// Considering some people run portainer on a NAS I think it's prudent not to assume we're on server grade hardware
|
||||
// https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#argon2id
|
||||
argon2MemoryCost = 12 * 1024
|
||||
argon2TimeCost = 3
|
||||
argon2Threads = 1
|
||||
argon2KeyLength = 32
|
||||
)
|
||||
|
||||
// AesEncrypt reads from input, encrypts with AES-256 and writes to the output.
|
||||
// passphrase is used to generate an encryption key.
|
||||
// AesEncrypt reads from input, encrypts with AES-256 and writes to output. passphrase is used to generate an encryption key
|
||||
func AesEncrypt(input io.Reader, output io.Writer, passphrase []byte) error {
|
||||
// making a 32 bytes key that would correspond to AES-256
|
||||
// don't necessarily need a salt, so just kept in empty
|
||||
key, err := scrypt.Key(passphrase, emptySalt, 32768, 8, 1, 32)
|
||||
err := aesEncryptGCM(input, output, passphrase)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If the key is unique for each ciphertext, then it's ok to use a zero
|
||||
// IV.
|
||||
var iv [aes.BlockSize]byte
|
||||
stream := cipher.NewOFB(block, iv[:])
|
||||
|
||||
writer := &cipher.StreamWriter{S: stream, W: output}
|
||||
// Copy the input to the output, encrypting as we go.
|
||||
if _, err := io.Copy(writer, input); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("error encrypting file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AesDecrypt reads from input, decrypts with AES-256 and returns the reader to a read decrypted content from.
|
||||
// passphrase is used to generate an encryption key.
|
||||
// AesDecrypt reads from input, decrypts with AES-256 and returns the reader to read the decrypted content from
|
||||
func AesDecrypt(input io.Reader, passphrase []byte) (io.Reader, error) {
|
||||
// Read file header to determine how it was encrypted
|
||||
inputReader := bufio.NewReader(input)
|
||||
header, err := inputReader.Peek(len(aesGcmHeader))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading encrypted backup file header: %w", err)
|
||||
}
|
||||
|
||||
if string(header) == aesGcmHeader {
|
||||
reader, err := aesDecryptGCM(inputReader, passphrase)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error decrypting file: %w", err)
|
||||
}
|
||||
|
||||
return reader, nil
|
||||
}
|
||||
|
||||
// Use the previous decryption routine which has no header (to support older archives)
|
||||
reader, err := aesDecryptOFB(inputReader, passphrase)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error decrypting legacy file backup: %w", err)
|
||||
}
|
||||
|
||||
return reader, nil
|
||||
}
|
||||
|
||||
// aesEncryptGCM reads from input, encrypts with AES-256 and writes to output. passphrase is used to generate an encryption key.
|
||||
func aesEncryptGCM(input io.Reader, output io.Writer, passphrase []byte) error {
|
||||
// Derive key using argon2 with a random salt
|
||||
salt := make([]byte, 16) // 16 bytes salt
|
||||
if _, err := io.ReadFull(rand.Reader, salt); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
key := argon2.IDKey(passphrase, salt, argon2TimeCost, argon2MemoryCost, argon2Threads, 32)
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
aesgcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Generate nonce
|
||||
nonce, err := NewRandomNonce(aesgcm.NonceSize())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// write the header
|
||||
if _, err := output.Write([]byte(aesGcmHeader)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Write nonce and salt to the output file
|
||||
if _, err := output.Write(salt); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := output.Write(nonce.Value()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Buffer for reading plaintext blocks
|
||||
buf := make([]byte, aesGcmBlockSize) // Adjust buffer size as needed
|
||||
ciphertext := make([]byte, len(buf)+aesgcm.Overhead())
|
||||
|
||||
// Encrypt plaintext in blocks
|
||||
for {
|
||||
n, err := io.ReadFull(input, buf)
|
||||
if n == 0 {
|
||||
break // end of plaintext input
|
||||
}
|
||||
|
||||
if err != nil && !(errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF)) {
|
||||
return err
|
||||
}
|
||||
|
||||
// Seal encrypts the plaintext using the nonce returning the updated slice.
|
||||
ciphertext = aesgcm.Seal(ciphertext[:0], nonce.Value(), buf[:n], nil)
|
||||
|
||||
_, err = output.Write(ciphertext)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
nonce.Increment()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// aesDecryptGCM reads from input, decrypts with AES-256 and returns the reader to read the decrypted content from.
|
||||
func aesDecryptGCM(input io.Reader, passphrase []byte) (io.Reader, error) {
|
||||
// Reader & verify header
|
||||
header := make([]byte, len(aesGcmHeader))
|
||||
if _, err := io.ReadFull(input, header); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if string(header) != aesGcmHeader {
|
||||
return nil, fmt.Errorf("invalid header")
|
||||
}
|
||||
|
||||
// Read salt
|
||||
salt := make([]byte, 16) // Salt size
|
||||
if _, err := io.ReadFull(input, salt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
key := argon2.IDKey(passphrase, salt, argon2TimeCost, argon2MemoryCost, argon2Threads, 32)
|
||||
|
||||
// Initialize AES cipher block
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create GCM mode with the cipher block
|
||||
aesgcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Read nonce from the input reader
|
||||
nonce := NewNonce(aesgcm.NonceSize())
|
||||
if err := nonce.Read(input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Initialize a buffer to store decrypted data
|
||||
buf := bytes.Buffer{}
|
||||
plaintext := make([]byte, aesGcmBlockSize)
|
||||
|
||||
// Decrypt the ciphertext in blocks
|
||||
for {
|
||||
// Read a block of ciphertext from the input reader
|
||||
ciphertextBlock := make([]byte, aesGcmBlockSize+aesgcm.Overhead()) // Adjust block size as needed
|
||||
n, err := io.ReadFull(input, ciphertextBlock)
|
||||
if n == 0 {
|
||||
break // end of ciphertext
|
||||
}
|
||||
|
||||
if err != nil && !(errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF)) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Decrypt the block of ciphertext
|
||||
plaintext, err = aesgcm.Open(plaintext[:0], nonce.Value(), ciphertextBlock[:n], nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = buf.Write(plaintext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nonce.Increment()
|
||||
}
|
||||
|
||||
return &buf, nil
|
||||
}
|
||||
|
||||
// aesDecryptOFB reads from input, decrypts with AES-256 and returns the reader to a read decrypted content from.
|
||||
// passphrase is used to generate an encryption key.
|
||||
// note: This function used to decrypt files that were encrypted without a header i.e. old archives
|
||||
func aesDecryptOFB(input io.Reader, passphrase []byte) (io.Reader, error) {
|
||||
var emptySalt []byte = make([]byte, 0)
|
||||
|
||||
// making a 32 bytes key that would correspond to AES-256
|
||||
// don't necessarily need a salt, so just kept in empty
|
||||
key, err := scrypt.Key(passphrase, emptySalt, 32768, 8, 1, 32)
|
||||
@@ -59,11 +223,9 @@ func AesDecrypt(input io.Reader, passphrase []byte) (io.Reader, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If the key is unique for each ciphertext, then it's ok to use a zero
|
||||
// IV.
|
||||
// If the key is unique for each ciphertext, then it's ok to use a zero IV.
|
||||
var iv [aes.BlockSize]byte
|
||||
stream := cipher.NewOFB(block, iv[:])
|
||||
|
||||
reader := &cipher.StreamReader{S: stream, R: input}
|
||||
|
||||
return reader, nil
|
||||
|
||||
@@ -2,6 +2,7 @@ package crypto
|
||||
|
||||
import (
|
||||
"io"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
@@ -9,7 +10,19 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
|
||||
func randBytes(n int) []byte {
|
||||
b := make([]byte, n)
|
||||
for i := range b {
|
||||
b[i] = letterBytes[rand.Intn(len(letterBytes))]
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func Test_encryptAndDecrypt_withTheSamePassword(t *testing.T) {
|
||||
const passphrase = "passphrase"
|
||||
|
||||
tmpdir := t.TempDir()
|
||||
|
||||
var (
|
||||
@@ -18,17 +31,99 @@ func Test_encryptAndDecrypt_withTheSamePassword(t *testing.T) {
|
||||
decryptedFilePath = filepath.Join(tmpdir, "decrypted")
|
||||
)
|
||||
|
||||
content := []byte("content")
|
||||
content := randBytes(1024*1024*100 + 523)
|
||||
os.WriteFile(originFilePath, content, 0600)
|
||||
|
||||
originFile, _ := os.Open(originFilePath)
|
||||
defer originFile.Close()
|
||||
|
||||
encryptedFileWriter, _ := os.Create(encryptedFilePath)
|
||||
|
||||
err := AesEncrypt(originFile, encryptedFileWriter, []byte(passphrase))
|
||||
assert.Nil(t, err, "Failed to encrypt a file")
|
||||
encryptedFileWriter.Close()
|
||||
|
||||
encryptedContent, err := os.ReadFile(encryptedFilePath)
|
||||
assert.Nil(t, err, "Couldn't read encrypted file")
|
||||
assert.NotEqual(t, encryptedContent, content, "Content wasn't encrypted")
|
||||
|
||||
encryptedFileReader, _ := os.Open(encryptedFilePath)
|
||||
defer encryptedFileReader.Close()
|
||||
|
||||
decryptedFileWriter, _ := os.Create(decryptedFilePath)
|
||||
defer decryptedFileWriter.Close()
|
||||
|
||||
decryptedReader, err := AesDecrypt(encryptedFileReader, []byte(passphrase))
|
||||
assert.Nil(t, err, "Failed to decrypt file")
|
||||
|
||||
io.Copy(decryptedFileWriter, decryptedReader)
|
||||
|
||||
decryptedContent, _ := os.ReadFile(decryptedFilePath)
|
||||
assert.Equal(t, content, decryptedContent, "Original and decrypted content should match")
|
||||
}
|
||||
|
||||
func Test_encryptAndDecrypt_withStrongPassphrase(t *testing.T) {
|
||||
const passphrase = "A strong passphrase with special characters: !@#$%^&*()_+"
|
||||
tmpdir := t.TempDir()
|
||||
|
||||
var (
|
||||
originFilePath = filepath.Join(tmpdir, "origin2")
|
||||
encryptedFilePath = filepath.Join(tmpdir, "encrypted2")
|
||||
decryptedFilePath = filepath.Join(tmpdir, "decrypted2")
|
||||
)
|
||||
|
||||
content := randBytes(500)
|
||||
os.WriteFile(originFilePath, content, 0600)
|
||||
|
||||
originFile, _ := os.Open(originFilePath)
|
||||
defer originFile.Close()
|
||||
|
||||
encryptedFileWriter, _ := os.Create(encryptedFilePath)
|
||||
|
||||
err := AesEncrypt(originFile, encryptedFileWriter, []byte(passphrase))
|
||||
assert.Nil(t, err, "Failed to encrypt a file")
|
||||
encryptedFileWriter.Close()
|
||||
|
||||
encryptedContent, err := os.ReadFile(encryptedFilePath)
|
||||
assert.Nil(t, err, "Couldn't read encrypted file")
|
||||
assert.NotEqual(t, encryptedContent, content, "Content wasn't encrypted")
|
||||
|
||||
encryptedFileReader, _ := os.Open(encryptedFilePath)
|
||||
defer encryptedFileReader.Close()
|
||||
|
||||
decryptedFileWriter, _ := os.Create(decryptedFilePath)
|
||||
defer decryptedFileWriter.Close()
|
||||
|
||||
decryptedReader, err := AesDecrypt(encryptedFileReader, []byte(passphrase))
|
||||
assert.Nil(t, err, "Failed to decrypt file")
|
||||
|
||||
io.Copy(decryptedFileWriter, decryptedReader)
|
||||
|
||||
decryptedContent, _ := os.ReadFile(decryptedFilePath)
|
||||
assert.Equal(t, content, decryptedContent, "Original and decrypted content should match")
|
||||
}
|
||||
|
||||
func Test_encryptAndDecrypt_withTheSamePasswordSmallFile(t *testing.T) {
|
||||
tmpdir := t.TempDir()
|
||||
|
||||
var (
|
||||
originFilePath = filepath.Join(tmpdir, "origin2")
|
||||
encryptedFilePath = filepath.Join(tmpdir, "encrypted2")
|
||||
decryptedFilePath = filepath.Join(tmpdir, "decrypted2")
|
||||
)
|
||||
|
||||
content := randBytes(500)
|
||||
os.WriteFile(originFilePath, content, 0600)
|
||||
|
||||
originFile, _ := os.Open(originFilePath)
|
||||
defer originFile.Close()
|
||||
|
||||
encryptedFileWriter, _ := os.Create(encryptedFilePath)
|
||||
defer encryptedFileWriter.Close()
|
||||
|
||||
err := AesEncrypt(originFile, encryptedFileWriter, []byte("passphrase"))
|
||||
assert.Nil(t, err, "Failed to encrypt a file")
|
||||
encryptedFileWriter.Close()
|
||||
|
||||
encryptedContent, err := os.ReadFile(encryptedFilePath)
|
||||
assert.Nil(t, err, "Couldn't read encrypted file")
|
||||
assert.NotEqual(t, encryptedContent, content, "Content wasn't encrypted")
|
||||
@@ -57,7 +152,7 @@ func Test_encryptAndDecrypt_withEmptyPassword(t *testing.T) {
|
||||
decryptedFilePath = filepath.Join(tmpdir, "decrypted")
|
||||
)
|
||||
|
||||
content := []byte("content")
|
||||
content := randBytes(1024 * 50)
|
||||
os.WriteFile(originFilePath, content, 0600)
|
||||
|
||||
originFile, _ := os.Open(originFilePath)
|
||||
@@ -96,7 +191,7 @@ func Test_decryptWithDifferentPassphrase_shouldProduceWrongResult(t *testing.T)
|
||||
decryptedFilePath = filepath.Join(tmpdir, "decrypted")
|
||||
)
|
||||
|
||||
content := []byte("content")
|
||||
content := randBytes(1034)
|
||||
os.WriteFile(originFilePath, content, 0600)
|
||||
|
||||
originFile, _ := os.Open(originFilePath)
|
||||
@@ -117,11 +212,6 @@ func Test_decryptWithDifferentPassphrase_shouldProduceWrongResult(t *testing.T)
|
||||
decryptedFileWriter, _ := os.Create(decryptedFilePath)
|
||||
defer decryptedFileWriter.Close()
|
||||
|
||||
decryptedReader, err := AesDecrypt(encryptedFileReader, []byte("garbage"))
|
||||
assert.Nil(t, err, "Should allow to decrypt with wrong passphrase")
|
||||
|
||||
io.Copy(decryptedFileWriter, decryptedReader)
|
||||
|
||||
decryptedContent, _ := os.ReadFile(decryptedFilePath)
|
||||
assert.NotEqual(t, content, decryptedContent, "Original and decrypted content should NOT match")
|
||||
_, err = AesDecrypt(encryptedFileReader, []byte("garbage"))
|
||||
assert.NotNil(t, err, "Should not allow decrypt with wrong passphrase")
|
||||
}
|
||||
|
||||
61
api/crypto/nonce.go
Normal file
61
api/crypto/nonce.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"io"
|
||||
)
|
||||
|
||||
type Nonce struct {
|
||||
val []byte
|
||||
}
|
||||
|
||||
func NewNonce(size int) *Nonce {
|
||||
return &Nonce{val: make([]byte, size)}
|
||||
}
|
||||
|
||||
// NewRandomNonce generates a new initial nonce with the lower byte set to a random value
|
||||
// This ensures there are plenty of nonce values availble before rolling over
|
||||
// Based on ideas from the Secure Programming Cookbook for C and C++ by John Viega, Matt Messier
|
||||
// https://www.oreilly.com/library/view/secure-programming-cookbook/0596003943/ch04s09.html
|
||||
func NewRandomNonce(size int) (*Nonce, error) {
|
||||
randomBytes := 1
|
||||
if size <= randomBytes {
|
||||
return nil, errors.New("nonce size must be greater than the number of random bytes")
|
||||
}
|
||||
|
||||
randomPart := make([]byte, randomBytes)
|
||||
if _, err := rand.Read(randomPart); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
zeroPart := make([]byte, size-randomBytes)
|
||||
nonceVal := append(randomPart, zeroPart...)
|
||||
return &Nonce{val: nonceVal}, nil
|
||||
}
|
||||
|
||||
func (n *Nonce) Read(stream io.Reader) error {
|
||||
_, err := io.ReadFull(stream, n.val)
|
||||
return err
|
||||
}
|
||||
|
||||
func (n *Nonce) Value() []byte {
|
||||
return n.val
|
||||
}
|
||||
|
||||
func (n *Nonce) Increment() error {
|
||||
// Start incrementing from the least significant byte
|
||||
for i := len(n.val) - 1; i >= 0; i-- {
|
||||
// Increment the current byte
|
||||
n.val[i]++
|
||||
|
||||
// Check for overflow
|
||||
if n.val[i] != 0 {
|
||||
// No overflow, nonce is successfully incremented
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// If we reach here, it means the nonce has overflowed
|
||||
return errors.New("nonce overflow")
|
||||
}
|
||||
@@ -73,6 +73,7 @@ type (
|
||||
PendingActionsService interface {
|
||||
BaseCRUD[portainer.PendingActions, portainer.PendingActionsID]
|
||||
GetNextIdentifier() int
|
||||
DeleteByEndpointID(ID portainer.EndpointID) error
|
||||
}
|
||||
|
||||
// EdgeStackService represents a service to manage Edge stacks
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package pendingactions
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -45,6 +47,12 @@ func (s Service) Update(ID portainer.PendingActionsID, config *portainer.Pending
|
||||
})
|
||||
}
|
||||
|
||||
func (s Service) DeleteByEndpointID(ID portainer.EndpointID) error {
|
||||
return s.Connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||
return s.Tx(tx).DeleteByEndpointID(ID)
|
||||
})
|
||||
}
|
||||
|
||||
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
|
||||
return ServiceTx{
|
||||
BaseDataServiceTx: dataservices.BaseDataServiceTx[portainer.PendingActions, portainer.PendingActionsID]{
|
||||
@@ -68,6 +76,29 @@ func (s ServiceTx) Update(ID portainer.PendingActionsID, config *portainer.Pendi
|
||||
return s.BaseDataServiceTx.Update(ID, config)
|
||||
}
|
||||
|
||||
func (s ServiceTx) DeleteByEndpointID(ID portainer.EndpointID) error {
|
||||
log.Debug().Int("endpointId", int(ID)).Msg("deleting pending actions for endpoint")
|
||||
pendingActions, err := s.BaseDataServiceTx.ReadAll()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to retrieve pending-actions for endpoint (%d): %w", ID, err)
|
||||
}
|
||||
|
||||
for _, pendingAction := range pendingActions {
|
||||
if pendingAction.EndpointID == ID {
|
||||
err := s.BaseDataServiceTx.Delete(pendingAction.ID)
|
||||
if err != nil {
|
||||
log.Debug().Int("endpointId", int(ID)).Msgf("failed to delete pending action: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetNextIdentifier returns the next identifier for a custom template.
|
||||
func (service ServiceTx) GetNextIdentifier() int {
|
||||
return service.Tx.GetNextIdentifier(BucketName)
|
||||
}
|
||||
|
||||
// GetNextIdentifier returns the next identifier for a custom template.
|
||||
func (service *Service) GetNextIdentifier() int {
|
||||
return service.Connection.GetNextIdentifier(BucketName)
|
||||
|
||||
@@ -86,6 +86,7 @@ func (store *Store) newMigratorParameters(version *models.Version) *migrator.Mig
|
||||
EdgeStackService: store.EdgeStackService,
|
||||
EdgeJobService: store.EdgeJobService,
|
||||
TunnelServerService: store.TunnelServerService,
|
||||
PendingActionsService: store.PendingActionsService,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
package datastore
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
dockerclient "github.com/portainer/portainer/api/docker/client"
|
||||
"github.com/portainer/portainer/api/kubernetes/cli"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type PostInitMigrator struct {
|
||||
kubeFactory *cli.ClientFactory
|
||||
dockerFactory *dockerclient.ClientFactory
|
||||
dataStore dataservices.DataStore
|
||||
}
|
||||
|
||||
func NewPostInitMigrator(kubeFactory *cli.ClientFactory, dockerFactory *dockerclient.ClientFactory, dataStore dataservices.DataStore) *PostInitMigrator {
|
||||
return &PostInitMigrator{
|
||||
kubeFactory: kubeFactory,
|
||||
dockerFactory: dockerFactory,
|
||||
dataStore: dataStore,
|
||||
}
|
||||
}
|
||||
|
||||
func (migrator *PostInitMigrator) PostInitMigrate() error {
|
||||
if err := migrator.PostInitMigrateIngresses(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
migrator.PostInitMigrateGPUs()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (migrator *PostInitMigrator) PostInitMigrateIngresses() error {
|
||||
endpoints, err := migrator.dataStore.Endpoint().Endpoints()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i := range endpoints {
|
||||
// Early exit if we do not need to migrate!
|
||||
if !endpoints[i].PostInitMigrations.MigrateIngresses {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := migrator.kubeFactory.MigrateEndpointIngresses(&endpoints[i])
|
||||
if err != nil {
|
||||
log.Debug().Err(err).Msg("failure migrating endpoint ingresses")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// PostInitMigrateGPUs will check all docker endpoints for containers with GPUs and set EnableGPUManagement to true if any are found
|
||||
// If there's an error getting the containers, we'll log it and move on
|
||||
func (migrator *PostInitMigrator) PostInitMigrateGPUs() {
|
||||
environments, err := migrator.dataStore.Endpoint().Endpoints()
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failure getting endpoints")
|
||||
return
|
||||
}
|
||||
|
||||
for i := range environments {
|
||||
if environments[i].Type == portainer.DockerEnvironment {
|
||||
// // Early exit if we do not need to migrate!
|
||||
if !environments[i].PostInitMigrations.MigrateGPUs {
|
||||
return
|
||||
}
|
||||
|
||||
// set the MigrateGPUs flag to false so we don't run this again
|
||||
environments[i].PostInitMigrations.MigrateGPUs = false
|
||||
migrator.dataStore.Endpoint().UpdateEndpoint(environments[i].ID, &environments[i])
|
||||
|
||||
// create a docker client
|
||||
dockerClient, err := migrator.dockerFactory.CreateClient(&environments[i], "", nil)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failure creating docker client for environment: " + environments[i].Name)
|
||||
return
|
||||
}
|
||||
defer dockerClient.Close()
|
||||
|
||||
// get all containers
|
||||
containers, err := dockerClient.ContainerList(context.Background(), types.ContainerListOptions{All: true})
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to list containers")
|
||||
return
|
||||
}
|
||||
|
||||
// check for a gpu on each container. If even one GPU is found, set EnableGPUManagement to true for the whole endpoint
|
||||
containersLoop:
|
||||
for _, container := range containers {
|
||||
// https://www.sobyte.net/post/2022-10/go-docker/ has nice documentation on the docker client with GPUs
|
||||
containerDetails, err := dockerClient.ContainerInspect(context.Background(), container.ID)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to inspect container")
|
||||
return
|
||||
}
|
||||
|
||||
deviceRequests := containerDetails.HostConfig.Resources.DeviceRequests
|
||||
for _, deviceRequest := range deviceRequests {
|
||||
if deviceRequest.Driver == "nvidia" {
|
||||
environments[i].EnableGPUManagement = true
|
||||
migrator.dataStore.Endpoint().UpdateEndpoint(environments[i].ID, &environments[i])
|
||||
|
||||
break containersLoop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
32
api/datastore/migrator/migrate_dbversion111.go
Normal file
32
api/datastore/migrator/migrate_dbversion111.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func (migrator *Migrator) cleanPendingActionsForDeletedEndpointsForDB111() error {
|
||||
log.Info().Msg("cleaning up pending actions for deleted endpoints")
|
||||
|
||||
pendingActions, err := migrator.pendingActionsService.ReadAll()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
endpoints := make(map[portainer.EndpointID]struct{})
|
||||
for _, action := range pendingActions {
|
||||
endpoints[action.EndpointID] = struct{}{}
|
||||
}
|
||||
|
||||
for endpointId := range endpoints {
|
||||
_, err := migrator.endpointService.Endpoint(endpointId)
|
||||
if dataservices.IsErrObjectNotFound(err) {
|
||||
err := migrator.pendingActionsService.DeleteByEndpointID(endpointId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/portainer/portainer/api/dataservices/endpointrelation"
|
||||
"github.com/portainer/portainer/api/dataservices/extension"
|
||||
"github.com/portainer/portainer/api/dataservices/fdoprofile"
|
||||
"github.com/portainer/portainer/api/dataservices/pendingactions"
|
||||
"github.com/portainer/portainer/api/dataservices/registry"
|
||||
"github.com/portainer/portainer/api/dataservices/resourcecontrol"
|
||||
"github.com/portainer/portainer/api/dataservices/role"
|
||||
@@ -58,6 +59,7 @@ type (
|
||||
edgeStackService *edgestack.Service
|
||||
edgeJobService *edgejob.Service
|
||||
TunnelServerService *tunnelserver.Service
|
||||
pendingActionsService *pendingactions.Service
|
||||
}
|
||||
|
||||
// MigratorParameters represents the required parameters to create a new Migrator instance.
|
||||
@@ -85,6 +87,7 @@ type (
|
||||
EdgeStackService *edgestack.Service
|
||||
EdgeJobService *edgejob.Service
|
||||
TunnelServerService *tunnelserver.Service
|
||||
PendingActionsService *pendingactions.Service
|
||||
}
|
||||
)
|
||||
|
||||
@@ -114,6 +117,7 @@ func NewMigrator(parameters *MigratorParameters) *Migrator {
|
||||
edgeStackService: parameters.EdgeStackService,
|
||||
edgeJobService: parameters.EdgeJobService,
|
||||
TunnelServerService: parameters.TunnelServerService,
|
||||
pendingActionsService: parameters.PendingActionsService,
|
||||
}
|
||||
|
||||
migrator.initMigrations()
|
||||
@@ -232,6 +236,9 @@ func (m *Migrator) initMigrations() {
|
||||
m.updateAppTemplatesVersionForDB110,
|
||||
m.updateResourceOverCommitToDB110,
|
||||
)
|
||||
m.addMigrations("2.20.2",
|
||||
m.cleanPendingActionsForDeletedEndpointsForDB111,
|
||||
)
|
||||
|
||||
// Add new migrations below...
|
||||
// One function per migration, each versions migration funcs in the same file.
|
||||
|
||||
203
api/datastore/postinit/migrate_post_init.go
Normal file
203
api/datastore/postinit/migrate_post_init.go
Normal file
@@ -0,0 +1,203 @@
|
||||
package postinit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/client"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
dockerClient "github.com/portainer/portainer/api/docker/client"
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
"github.com/portainer/portainer/api/kubernetes/cli"
|
||||
"github.com/portainer/portainer/api/pendingactions/actions"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type PostInitMigrator struct {
|
||||
kubeFactory *cli.ClientFactory
|
||||
dockerFactory *dockerClient.ClientFactory
|
||||
dataStore dataservices.DataStore
|
||||
assetsPath string
|
||||
kubernetesDeployer portainer.KubernetesDeployer
|
||||
}
|
||||
|
||||
func NewPostInitMigrator(
|
||||
kubeFactory *cli.ClientFactory,
|
||||
dockerFactory *dockerClient.ClientFactory,
|
||||
dataStore dataservices.DataStore,
|
||||
assetsPath string,
|
||||
kubernetesDeployer portainer.KubernetesDeployer,
|
||||
) *PostInitMigrator {
|
||||
return &PostInitMigrator{
|
||||
kubeFactory: kubeFactory,
|
||||
dockerFactory: dockerFactory,
|
||||
dataStore: dataStore,
|
||||
assetsPath: assetsPath,
|
||||
kubernetesDeployer: kubernetesDeployer,
|
||||
}
|
||||
}
|
||||
|
||||
// PostInitMigrate will run all post-init migrations, which require docker/kube clients for all edge or non-edge environments
|
||||
func (postInitMigrator *PostInitMigrator) PostInitMigrate() error {
|
||||
environments, err := postInitMigrator.dataStore.Endpoint().Endpoints()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Error getting environments")
|
||||
return err
|
||||
}
|
||||
|
||||
for _, environment := range environments {
|
||||
// edge environments will run after the server starts, in pending actions
|
||||
if endpointutils.IsEdgeEndpoint(&environment) {
|
||||
log.Info().Msgf("Adding pending action 'PostInitMigrateEnvironment' for environment %d", environment.ID)
|
||||
err = postInitMigrator.createPostInitMigrationPendingAction(environment.ID)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Error creating pending action for environment %d", environment.ID)
|
||||
}
|
||||
} else {
|
||||
// non-edge environments will run before the server starts.
|
||||
err = postInitMigrator.MigrateEnvironment(&environment)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Error running post-init migrations for non-edge environment %d", environment.ID)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// try to create a post init migration pending action. If it already exists, do nothing
|
||||
// this function exists for readability, not reusability
|
||||
// TODO: This should be moved into pending actions as part of the pending action migration
|
||||
func (postInitMigrator *PostInitMigrator) createPostInitMigrationPendingAction(environmentID portainer.EndpointID) error {
|
||||
migrateEnvPendingAction := portainer.PendingActions{
|
||||
EndpointID: environmentID,
|
||||
Action: actions.PostInitMigrateEnvironment,
|
||||
}
|
||||
|
||||
// Get all pending actions and filter them by endpoint, action and action args that are equal to the migrateEnvPendingAction
|
||||
pendingActions, err := postInitMigrator.dataStore.PendingActions().ReadAll()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Error retrieving pending actions")
|
||||
return fmt.Errorf("failed to retrieve pending actions for environment %d: %w", environmentID, err)
|
||||
}
|
||||
for _, pendingAction := range pendingActions {
|
||||
if pendingAction.EndpointID == environmentID &&
|
||||
pendingAction.Action == migrateEnvPendingAction.Action &&
|
||||
reflect.DeepEqual(pendingAction.ActionData, migrateEnvPendingAction.ActionData) {
|
||||
log.Debug().Msgf("Migration pending action for environment %d already exists, skipping creating another", environmentID)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// If there are no pending actions for the given endpoint, create one
|
||||
err = postInitMigrator.dataStore.PendingActions().Create(&migrateEnvPendingAction)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Error creating pending action for environment %d", environmentID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MigrateEnvironment runs migrations on a single environment
|
||||
func (migrator *PostInitMigrator) MigrateEnvironment(environment *portainer.Endpoint) error {
|
||||
log.Info().Msgf("Executing post init migration for environment %d", environment.ID)
|
||||
|
||||
switch {
|
||||
case endpointutils.IsKubernetesEndpoint(environment):
|
||||
// get the kubeclient for the environment, and skip all kube migrations if there's an error
|
||||
kubeclient, err := migrator.kubeFactory.GetKubeClient(environment)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Error creating kubeclient for environment: %d", environment.ID)
|
||||
return err
|
||||
}
|
||||
// if one environment fails, it is logged and the next migration runs. The error is returned at the end and handled by pending actions
|
||||
err = migrator.MigrateIngresses(*environment, kubeclient)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
case endpointutils.IsDockerEndpoint(environment):
|
||||
// get the docker client for the environment, and skip all docker migrations if there's an error
|
||||
dockerClient, err := migrator.dockerFactory.CreateClient(environment, "", nil)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Error creating docker client for environment: %d", environment.ID)
|
||||
return err
|
||||
}
|
||||
defer dockerClient.Close()
|
||||
migrator.MigrateGPUs(*environment, dockerClient)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (migrator *PostInitMigrator) MigrateIngresses(environment portainer.Endpoint, kubeclient *cli.KubeClient) error {
|
||||
// Early exit if we do not need to migrate!
|
||||
if !environment.PostInitMigrations.MigrateIngresses {
|
||||
return nil
|
||||
}
|
||||
log.Debug().Msgf("Migrating ingresses for environment %d", environment.ID)
|
||||
|
||||
err := migrator.kubeFactory.MigrateEndpointIngresses(&environment, migrator.dataStore, kubeclient)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Error migrating ingresses for environment %d", environment.ID)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MigrateGPUs will check all docker endpoints for containers with GPUs and set EnableGPUManagement to true if any are found
|
||||
// If there's an error getting the containers, we'll log it and move on
|
||||
func (migrator *PostInitMigrator) MigrateGPUs(e portainer.Endpoint, dockerClient *client.Client) error {
|
||||
return migrator.dataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
environment, err := tx.Endpoint().Endpoint(e.ID)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Error getting environment %d", environment.ID)
|
||||
return err
|
||||
}
|
||||
// Early exit if we do not need to migrate!
|
||||
if !environment.PostInitMigrations.MigrateGPUs {
|
||||
return nil
|
||||
}
|
||||
log.Debug().Msgf("Migrating GPUs for environment %d", e.ID)
|
||||
|
||||
// get all containers
|
||||
containers, err := dockerClient.ContainerList(context.Background(), container.ListOptions{All: true})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("failed to list containers for environment %d", environment.ID)
|
||||
return err
|
||||
}
|
||||
|
||||
// check for a gpu on each container. If even one GPU is found, set EnableGPUManagement to true for the whole environment
|
||||
containersLoop:
|
||||
for _, container := range containers {
|
||||
// https://www.sobyte.net/post/2022-10/go-docker/ has nice documentation on the docker client with GPUs
|
||||
containerDetails, err := dockerClient.ContainerInspect(context.Background(), container.ID)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to inspect container")
|
||||
continue
|
||||
}
|
||||
|
||||
deviceRequests := containerDetails.HostConfig.Resources.DeviceRequests
|
||||
for _, deviceRequest := range deviceRequests {
|
||||
if deviceRequest.Driver == "nvidia" {
|
||||
environment.EnableGPUManagement = true
|
||||
break containersLoop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// set the MigrateGPUs flag to false so we don't run this again
|
||||
environment.PostInitMigrations.MigrateGPUs = false
|
||||
err = tx.Endpoint().UpdateEndpoint(environment.ID, environment)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Error updating EnableGPUManagement flag for environment %d", environment.ID)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -16,7 +16,9 @@ func (tx *StoreTx) IsErrObjectNotFound(err error) bool {
|
||||
|
||||
func (tx *StoreTx) CustomTemplate() dataservices.CustomTemplateService { return nil }
|
||||
|
||||
func (tx *StoreTx) PendingActions() dataservices.PendingActionsService { return nil }
|
||||
func (tx *StoreTx) PendingActions() dataservices.PendingActionsService {
|
||||
return tx.store.PendingActionsService.Tx(tx.tx)
|
||||
}
|
||||
|
||||
func (tx *StoreTx) EdgeGroup() dataservices.EdgeGroupService {
|
||||
return tx.store.EdgeGroupService.Tx(tx.tx)
|
||||
|
||||
@@ -631,6 +631,7 @@
|
||||
"LogoURL": "",
|
||||
"OAuthSettings": {
|
||||
"AccessTokenURI": "",
|
||||
"AuthStyle": 0,
|
||||
"AuthorizationURI": "",
|
||||
"ClientID": "",
|
||||
"DefaultTeamID": 0,
|
||||
@@ -677,6 +678,7 @@
|
||||
"Architecture": "",
|
||||
"BridgeNfIp6tables": false,
|
||||
"BridgeNfIptables": false,
|
||||
"CDISpecDirs": null,
|
||||
"CPUSet": false,
|
||||
"CPUShares": false,
|
||||
"CgroupDriver": "",
|
||||
@@ -939,6 +941,6 @@
|
||||
}
|
||||
],
|
||||
"version": {
|
||||
"VERSION": "{\"SchemaVersion\":\"2.20.1\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
|
||||
"VERSION": "{\"SchemaVersion\":\"2.20.2\",\"MigratorCount\":1,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/crypto"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/segmentio/encoding/json"
|
||||
)
|
||||
@@ -93,11 +93,17 @@ func createTCPClient(endpoint *portainer.Endpoint, timeout *time.Duration) (*cli
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client.NewClientWithOpts(
|
||||
opts := []client.Opt{
|
||||
client.WithHost(endpoint.URL),
|
||||
client.WithAPIVersionNegotiation(),
|
||||
client.WithHTTPClient(httpCli),
|
||||
)
|
||||
}
|
||||
|
||||
if nnTransport, ok := httpCli.Transport.(*NodeNameTransport); ok && nnTransport.TLSClientConfig != nil {
|
||||
opts = append(opts, client.WithScheme("https"))
|
||||
}
|
||||
|
||||
return client.NewClientWithOpts(opts...)
|
||||
}
|
||||
|
||||
func createAgentClient(endpoint *portainer.Endpoint, endpointURL string, signatureService portainer.DigitalSignatureService, nodeName string, timeout *time.Duration) (*client.Client, error) {
|
||||
@@ -159,7 +165,7 @@ func (t *NodeNameTransport) RoundTrip(req *http.Request) (*http.Response, error)
|
||||
resp.Body = io.NopCloser(bytes.NewReader(body))
|
||||
|
||||
var rs []struct {
|
||||
types.ImageSummary
|
||||
image.Summary
|
||||
Portainer struct {
|
||||
Agent struct {
|
||||
NodeName string
|
||||
|
||||
@@ -119,7 +119,7 @@ func (c *ContainerService) Recreate(ctx context.Context, endpoint *portainer.End
|
||||
for _, network := range container.NetworkSettings.Networks {
|
||||
cli.NetworkConnect(ctx, network.NetworkID, containerId, network)
|
||||
}
|
||||
cli.ContainerStart(ctx, containerId, types.ContainerStartOptions{})
|
||||
cli.ContainerStart(ctx, containerId, dockercontainer.StartOptions{})
|
||||
})
|
||||
|
||||
log.Debug().Str("container", strings.Split(container.Name, "/")[1]).Msg("starting to create a new container")
|
||||
@@ -135,7 +135,7 @@ func (c *ContainerService) Recreate(ctx context.Context, endpoint *portainer.End
|
||||
c.sr.push(func() {
|
||||
log.Debug().Str("container_id", create.ID).Msg("removing the new container")
|
||||
cli.ContainerStop(ctx, create.ID, dockercontainer.StopOptions{})
|
||||
cli.ContainerRemove(ctx, create.ID, types.ContainerRemoveOptions{})
|
||||
cli.ContainerRemove(ctx, create.ID, dockercontainer.RemoveOptions{})
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
@@ -164,14 +164,14 @@ func (c *ContainerService) Recreate(ctx context.Context, endpoint *portainer.End
|
||||
|
||||
// 8. start the new container
|
||||
log.Debug().Str("container_id", newContainerId).Msg("starting the new container")
|
||||
err = cli.ContainerStart(ctx, newContainerId, types.ContainerStartOptions{})
|
||||
err = cli.ContainerStart(ctx, newContainerId, dockercontainer.StartOptions{})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "start container error")
|
||||
}
|
||||
|
||||
// 9. delete the old container
|
||||
log.Debug().Str("container_id", containerId).Msg("starting to remove the old container")
|
||||
_ = cli.ContainerRemove(ctx, containerId, types.ContainerRemoveOptions{})
|
||||
_ = cli.ContainerRemove(ctx, containerId, dockercontainer.RemoveOptions{})
|
||||
|
||||
c.sr.disable()
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
consts "github.com/portainer/portainer/api/docker/consts"
|
||||
@@ -157,7 +158,7 @@ func (c *DigestClient) ServiceImageStatus(ctx context.Context, serviceID string,
|
||||
return Error, nil
|
||||
}
|
||||
|
||||
containers, err := cli.ContainerList(ctx, types.ContainerListOptions{
|
||||
containers, err := cli.ContainerList(ctx, container.ListOptions{
|
||||
All: true,
|
||||
Filters: filters.NewArgs(filters.Arg("label", consts.SwarmServiceIdLabel+"="+serviceID)),
|
||||
})
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
_container "github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/volume"
|
||||
"github.com/docker/docker/client"
|
||||
@@ -147,7 +148,7 @@ func snapshotSwarmServices(snapshot *portainer.DockerSnapshot, cli *client.Clien
|
||||
}
|
||||
|
||||
func snapshotContainers(snapshot *portainer.DockerSnapshot, cli *client.Client) error {
|
||||
containers, err := cli.ContainerList(context.Background(), types.ContainerListOptions{All: true})
|
||||
containers, err := cli.ContainerList(context.Background(), container.ListOptions{All: true})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -934,7 +934,7 @@ func FileExists(filePath string) (bool, error) {
|
||||
func (service *Service) SafeMoveDirectory(originalPath, newPath string) error {
|
||||
// 1. Backup the source directory to a different folder
|
||||
backupDir := fmt.Sprintf("%s-%s", filepath.Dir(originalPath), "backup")
|
||||
err := MoveDirectory(originalPath, backupDir)
|
||||
err := MoveDirectory(originalPath, backupDir, false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to backup source directory: %w", err)
|
||||
}
|
||||
@@ -973,14 +973,14 @@ func restoreBackup(src, backupDir string) error {
|
||||
return fmt.Errorf("failed to delete destination directory: %w", err)
|
||||
}
|
||||
|
||||
err = MoveDirectory(backupDir, src)
|
||||
err = MoveDirectory(backupDir, src, false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to restore backup directory: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func MoveDirectory(originalPath, newPath string) error {
|
||||
func MoveDirectory(originalPath, newPath string, overwriteTargetPath bool) error {
|
||||
if _, err := os.Stat(originalPath); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -991,7 +991,13 @@ func MoveDirectory(originalPath, newPath string) error {
|
||||
}
|
||||
|
||||
if alreadyExists {
|
||||
return errors.New("Target path already exists")
|
||||
if !overwriteTargetPath {
|
||||
return fmt.Errorf("Target path already exists")
|
||||
}
|
||||
|
||||
if err = os.RemoveAll(newPath); err != nil {
|
||||
return fmt.Errorf("failed to overwrite path %s: %s", newPath, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
return os.Rename(originalPath, newPath)
|
||||
|
||||
@@ -16,7 +16,7 @@ func Test_movePath_shouldFailIfSourceDirDoesNotExist(t *testing.T) {
|
||||
file1 := addFile(destinationDir, "dir", "file")
|
||||
file2 := addFile(destinationDir, "file")
|
||||
|
||||
err := MoveDirectory(sourceDir, destinationDir)
|
||||
err := MoveDirectory(sourceDir, destinationDir, false)
|
||||
assert.Error(t, err, "move directory should fail when source path is missing")
|
||||
assert.FileExists(t, file1, "destination dir contents should remain")
|
||||
assert.FileExists(t, file2, "destination dir contents should remain")
|
||||
@@ -30,7 +30,7 @@ func Test_movePath_shouldFailIfDestinationDirExists(t *testing.T) {
|
||||
file3 := addFile(destinationDir, "dir", "file")
|
||||
file4 := addFile(destinationDir, "file")
|
||||
|
||||
err := MoveDirectory(sourceDir, destinationDir)
|
||||
err := MoveDirectory(sourceDir, destinationDir, false)
|
||||
assert.Error(t, err, "move directory should fail when destination directory already exists")
|
||||
assert.FileExists(t, file1, "source dir contents should remain")
|
||||
assert.FileExists(t, file2, "source dir contents should remain")
|
||||
@@ -38,6 +38,22 @@ func Test_movePath_shouldFailIfDestinationDirExists(t *testing.T) {
|
||||
assert.FileExists(t, file4, "destination dir contents should remain")
|
||||
}
|
||||
|
||||
func Test_movePath_succesIfOverwriteSetWhenDestinationDirExists(t *testing.T) {
|
||||
sourceDir := t.TempDir()
|
||||
file1 := addFile(sourceDir, "dir", "file")
|
||||
file2 := addFile(sourceDir, "file")
|
||||
destinationDir := t.TempDir()
|
||||
file3 := addFile(destinationDir, "dir", "file")
|
||||
file4 := addFile(destinationDir, "file")
|
||||
|
||||
err := MoveDirectory(sourceDir, destinationDir, true)
|
||||
assert.NoError(t, err)
|
||||
assert.NoFileExists(t, file1, "source dir contents should be moved")
|
||||
assert.NoFileExists(t, file2, "source dir contents should be moved")
|
||||
assert.FileExists(t, file3, "destination dir contents should remain")
|
||||
assert.FileExists(t, file4, "destination dir contents should remain")
|
||||
}
|
||||
|
||||
func Test_movePath_successWhenSourceExistsAndDestinationIsMissing(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
sourceDir := path.Join(tmp, "source")
|
||||
@@ -46,7 +62,7 @@ func Test_movePath_successWhenSourceExistsAndDestinationIsMissing(t *testing.T)
|
||||
file2 := addFile(sourceDir, "file")
|
||||
destinationDir := path.Join(tmp, "destination")
|
||||
|
||||
err := MoveDirectory(sourceDir, destinationDir)
|
||||
err := MoveDirectory(sourceDir, destinationDir, false)
|
||||
assert.NoError(t, err)
|
||||
assert.NoFileExists(t, file1, "source dir contents should be moved")
|
||||
assert.NoFileExists(t, file2, "source dir contents should be moved")
|
||||
|
||||
@@ -38,7 +38,7 @@ func CloneWithBackup(gitService portainer.GitService, fileService portainer.File
|
||||
}
|
||||
}
|
||||
|
||||
err = filesystem.MoveDirectory(options.ProjectPath, backupProjectPath)
|
||||
err = filesystem.MoveDirectory(options.ProjectPath, backupProjectPath, true)
|
||||
if err != nil {
|
||||
return cleanFn, errors.WithMessage(err, "Unable to move git repository directory")
|
||||
}
|
||||
@@ -48,7 +48,7 @@ func CloneWithBackup(gitService portainer.GitService, fileService portainer.File
|
||||
err = gitService.CloneRepository(options.ProjectPath, options.URL, options.ReferenceName, options.Username, options.Password, options.TLSSkipVerify)
|
||||
if err != nil {
|
||||
cleanUp = false
|
||||
restoreError := filesystem.MoveDirectory(backupProjectPath, options.ProjectPath)
|
||||
restoreError := filesystem.MoveDirectory(backupProjectPath, options.ProjectPath, false)
|
||||
if restoreError != nil {
|
||||
log.Warn().Err(restoreError).Msg("failed restoring backup folder")
|
||||
}
|
||||
|
||||
@@ -75,7 +75,12 @@ func (handler *Handler) authenticate(rw http.ResponseWriter, r *http.Request) *h
|
||||
if settings.AuthenticationMethod == portainer.AuthenticationInternal ||
|
||||
settings.AuthenticationMethod == portainer.AuthenticationOAuth ||
|
||||
(settings.AuthenticationMethod == portainer.AuthenticationLDAP && !settings.LDAPSettings.AutoCreateUsers) {
|
||||
return httperror.NewError(http.StatusUnprocessableEntity, "Invalid credentials", httperrors.ErrUnauthorized)
|
||||
// avoid username enumeration timing attack by creating a fake user
|
||||
// https://en.wikipedia.org/wiki/Timing_attack
|
||||
user = &portainer.User{
|
||||
Username: "portainer-fake-username",
|
||||
Password: "$2a$10$abcdefghijklmnopqrstuvwx..ABCDEFGHIJKLMNOPQRSTUVWXYZ12", // fake but valid format bcrypt hash
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +117,11 @@ func (handler *Handler) authenticateInternal(w http.ResponseWriter, user *portai
|
||||
func (handler *Handler) authenticateLDAP(w http.ResponseWriter, user *portainer.User, username, password string, ldapSettings *portainer.LDAPSettings) *httperror.HandlerError {
|
||||
err := handler.LDAPService.AuthenticateUser(username, password, ldapSettings)
|
||||
if err != nil {
|
||||
return httperror.Forbidden("Only initial admin is allowed to login without oauth", err)
|
||||
if errors.Is(err, httperrors.ErrUnauthorized) {
|
||||
return httperror.NewError(http.StatusUnprocessableEntity, "Invalid credentials", httperrors.ErrUnauthorized)
|
||||
}
|
||||
|
||||
return httperror.InternalServerError("Unable to authenticate user against LDAP", err)
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
)
|
||||
|
||||
type ImageResponse struct {
|
||||
@@ -63,7 +64,7 @@ func (handler *Handler) imagesList(w http.ResponseWriter, r *http.Request) *http
|
||||
|
||||
imageUsageSet := set.Set[string]{}
|
||||
if withUsage {
|
||||
containers, err := cli.ContainerList(r.Context(), types.ContainerListOptions{})
|
||||
containers, err := cli.ContainerList(r.Context(), container.ListOptions{})
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve Docker containers", err)
|
||||
}
|
||||
|
||||
@@ -179,6 +179,12 @@ func (handler *Handler) deleteEndpoint(tx dataservices.DataStoreTx, endpointID p
|
||||
}
|
||||
}
|
||||
|
||||
// delete the pending actions
|
||||
err = tx.PendingActions().DeleteByEndpointID(endpoint.ID)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Int("endpointId", int(endpoint.ID)).Msgf("Unable to delete pending actions")
|
||||
}
|
||||
|
||||
err = tx.Endpoint().DeleteEndpoint(portainer.EndpointID(endpointID))
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to delete the environment from the database", err)
|
||||
|
||||
@@ -12,8 +12,8 @@ import (
|
||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
dockertypes "github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
)
|
||||
|
||||
@@ -39,7 +39,7 @@ func (payload *forceUpdateServicePayload) Validate(r *http.Request) error {
|
||||
// @produce json
|
||||
// @param id path int true "endpoint identifier"
|
||||
// @param body body forceUpdateServicePayload true "details"
|
||||
// @success 200 {object} dockertypes.ServiceUpdateResponse "Success"
|
||||
// @success 200 {object} swarm.ServiceUpdateResponse "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 403 "Permission denied"
|
||||
// @failure 404 "endpoint not found"
|
||||
@@ -94,7 +94,7 @@ func (handler *Handler) endpointForceUpdateService(w http.ResponseWriter, r *htt
|
||||
go func() {
|
||||
images.EvictImageStatus(payload.ServiceID)
|
||||
images.EvictImageStatus(service.Spec.Labels[consts.SwarmStackNameLabel])
|
||||
containers, _ := dockerClient.ContainerList(context.TODO(), types.ContainerListOptions{
|
||||
containers, _ := dockerClient.ContainerList(context.TODO(), container.ListOptions{
|
||||
All: true,
|
||||
Filters: filters.NewArgs(filters.Arg("label", consts.SwarmServiceIdLabel+"="+payload.ServiceID)),
|
||||
})
|
||||
|
||||
@@ -622,6 +622,7 @@ func getEdgeStackStatusParam(r *http.Request) (*portainer.EdgeStackStatusType, e
|
||||
portainer.EdgeStackStatusRunning,
|
||||
portainer.EdgeStackStatusDeploying,
|
||||
portainer.EdgeStackStatusRemoving,
|
||||
portainer.EdgeStackStatusCompleted,
|
||||
}, edgeStackStatus) {
|
||||
return nil, errors.New("invalid edgeStackStatus parameter")
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ type Handler struct {
|
||||
}
|
||||
|
||||
// @title PortainerCE API
|
||||
// @version 2.20.1
|
||||
// @version 2.20.2
|
||||
// @description.markdown api-description.md
|
||||
// @termsOfService
|
||||
|
||||
|
||||
@@ -61,8 +61,7 @@ func (handler *Handler) helmInstall(w http.ResponseWriter, r *http.Request) *htt
|
||||
return httperror.InternalServerError("Unable to install a chart", err)
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
return response.JSON(w, release)
|
||||
return response.JSONWithStatus(w, release, http.StatusCreated)
|
||||
}
|
||||
|
||||
func (p *installChartPayload) Validate(_ *http.Request) error {
|
||||
|
||||
@@ -155,7 +155,7 @@ func pullImage(ctx context.Context, docker *client.Client, imageName string) err
|
||||
// runContainer should be used to run a short command that returns information to stdout
|
||||
// TODO: add k8s support
|
||||
func runContainer(ctx context.Context, docker *client.Client, imageName, containerName string, cmdLine []string) (output string, err error) {
|
||||
opts := types.ContainerListOptions{All: true}
|
||||
opts := container.ListOptions{All: true}
|
||||
opts.Filters = filters.NewArgs()
|
||||
opts.Filters.Add("name", containerName)
|
||||
existingContainers, err := docker.ContainerList(ctx, opts)
|
||||
@@ -170,7 +170,7 @@ func runContainer(ctx context.Context, docker *client.Client, imageName, contain
|
||||
}
|
||||
|
||||
if len(existingContainers) > 0 {
|
||||
err = docker.ContainerRemove(ctx, existingContainers[0].ID, types.ContainerRemoveOptions{Force: true})
|
||||
err = docker.ContainerRemove(ctx, existingContainers[0].ID, container.RemoveOptions{Force: true})
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Str("image_name", imageName).
|
||||
@@ -211,7 +211,7 @@ func runContainer(ctx context.Context, docker *client.Client, imageName, contain
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = docker.ContainerStart(ctx, created.ID, types.ContainerStartOptions{})
|
||||
err = docker.ContainerStart(ctx, created.ID, container.StartOptions{})
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Str("image_name", imageName).
|
||||
@@ -243,14 +243,14 @@ func runContainer(ctx context.Context, docker *client.Client, imageName, contain
|
||||
|
||||
log.Debug().Int64("status", statusCode).Msg("container wait status")
|
||||
|
||||
out, err := docker.ContainerLogs(ctx, created.ID, types.ContainerLogsOptions{ShowStdout: true})
|
||||
out, err := docker.ContainerLogs(ctx, created.ID, container.LogsOptions{ShowStdout: true})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("image_name", imageName).Str("container_name", containerName).Msg("getting container log")
|
||||
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = docker.ContainerRemove(ctx, created.ID, types.ContainerRemoveOptions{})
|
||||
err = docker.ContainerRemove(ctx, created.ID, container.RemoveOptions{})
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Str("image_name", imageName).
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/pendingactions"
|
||||
"github.com/portainer/portainer/api/pendingactions/actions"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||
@@ -91,7 +92,7 @@ func (handler *Handler) deleteKubernetesSecrets(registry *portainer.Registry) er
|
||||
if len(failedNamespaces) > 0 {
|
||||
handler.PendingActionsService.Create(portainer.PendingActions{
|
||||
EndpointID: endpointId,
|
||||
Action: pendingactions.DeletePortainerK8sRegistrySecrets,
|
||||
Action: actions.DeletePortainerK8sRegistrySecrets,
|
||||
|
||||
// When extracting the data, this is the type we need to pull out
|
||||
// i.e. pendingactions.DeletePortainerK8sRegistrySecretsData
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/pkg/errors"
|
||||
@@ -95,6 +96,11 @@ func (payload *settingsUpdatePayload) Validate(r *http.Request) error {
|
||||
}
|
||||
}
|
||||
|
||||
if payload.OAuthSettings != nil {
|
||||
if payload.OAuthSettings.AuthStyle < oauth2.AuthStyleAutoDetect || payload.OAuthSettings.AuthStyle > oauth2.AuthStyleInHeader {
|
||||
return errors.New("Invalid OAuth AuthStyle")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -225,6 +231,7 @@ func (handler *Handler) updateSettings(tx dataservices.DataStoreTx, payload sett
|
||||
settings.OAuthSettings = *payload.OAuthSettings
|
||||
settings.OAuthSettings.ClientSecret = clientSecret
|
||||
settings.OAuthSettings.KubeSecretKey = kubeSecret
|
||||
settings.OAuthSettings.AuthStyle = payload.OAuthSettings.AuthStyle
|
||||
}
|
||||
|
||||
if payload.EnableEdgeComputeFeatures != nil {
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
@@ -190,7 +191,7 @@ func (handler *Handler) checkUniqueStackNameInDocker(endpoint *portainer.Endpoin
|
||||
}
|
||||
}
|
||||
|
||||
containers, err := dockerClient.ContainerList(context.Background(), types.ContainerListOptions{All: true})
|
||||
containers, err := dockerClient.ContainerList(context.Background(), container.ListOptions{All: true})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package users
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
@@ -20,9 +21,6 @@ type userAccessTokenCreatePayload struct {
|
||||
}
|
||||
|
||||
func (payload *userAccessTokenCreatePayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.Password) {
|
||||
return errors.New("invalid password: cannot be empty")
|
||||
}
|
||||
if govalidator.IsNull(payload.Description) {
|
||||
return errors.New("invalid description: cannot be empty")
|
||||
}
|
||||
@@ -44,6 +42,7 @@ type accessTokenResponse struct {
|
||||
// @summary Generate an API key for a user
|
||||
// @description Generates an API key for a user.
|
||||
// @description Only the calling user can generate a token for themselves.
|
||||
// @description Password is required only for internal authentication.
|
||||
// @description **Access policy**: restricted
|
||||
// @tags users
|
||||
// @security jwt
|
||||
@@ -51,7 +50,7 @@ type accessTokenResponse struct {
|
||||
// @produce json
|
||||
// @param id path int true "User identifier"
|
||||
// @param body body userAccessTokenCreatePayload true "details"
|
||||
// @success 201 {object} accessTokenResponse "Created"
|
||||
// @success 200 {object} accessTokenResponse "Created"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 401 "Unauthorized"
|
||||
// @failure 403 "Permission denied"
|
||||
@@ -94,9 +93,21 @@ func (handler *Handler) userCreateAccessToken(w http.ResponseWriter, r *http.Req
|
||||
return httperror.InternalServerError("Unable to find a user with the specified identifier inside the database", err)
|
||||
}
|
||||
|
||||
err = handler.CryptoService.CompareHashAndData(user.Password, payload.Password)
|
||||
internalAuth, err := handler.usesInternalAuthentication(portainer.UserID(userID))
|
||||
if err != nil {
|
||||
return httperror.Forbidden("Current password doesn't match", errors.New("Current password does not match the password provided. Please try again"))
|
||||
return httperror.InternalServerError("Unable to determine the authentication method", err)
|
||||
}
|
||||
|
||||
if internalAuth {
|
||||
// Internal auth requires the password field and must not be empty
|
||||
if govalidator.IsNull(payload.Password) {
|
||||
return httperror.BadRequest("Invalid request payload", errors.New("invalid password: cannot be empty"))
|
||||
}
|
||||
|
||||
err = handler.CryptoService.CompareHashAndData(user.Password, payload.Password)
|
||||
if err != nil {
|
||||
return httperror.Forbidden("Current password doesn't match", errors.New("Current password does not match the password provided. Please try again"))
|
||||
}
|
||||
}
|
||||
|
||||
rawAPIKey, apiKey, err := handler.apiKeyService.GenerateApiKey(*user, payload.Description)
|
||||
@@ -104,6 +115,20 @@ func (handler *Handler) userCreateAccessToken(w http.ResponseWriter, r *http.Req
|
||||
return httperror.InternalServerError("Internal Server Error", err)
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
return response.JSON(w, accessTokenResponse{rawAPIKey, *apiKey})
|
||||
return response.JSONWithStatus(w, accessTokenResponse{rawAPIKey, *apiKey}, http.StatusCreated)
|
||||
}
|
||||
|
||||
func (handler *Handler) usesInternalAuthentication(userid portainer.UserID) (bool, error) {
|
||||
// userid 1 is the admin user and always uses internal auth
|
||||
if userid == 1 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// otherwise determine the auth method from the settings
|
||||
settings, err := handler.DataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("unable to retrieve the settings from the database: %w", err)
|
||||
}
|
||||
|
||||
return settings.AuthenticationMethod == portainer.AuthenticationInternal, nil
|
||||
}
|
||||
|
||||
@@ -61,7 +61,6 @@ import (
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
edgestackservice "github.com/portainer/portainer/api/internal/edge/edgestacks"
|
||||
"github.com/portainer/portainer/api/internal/snapshot"
|
||||
"github.com/portainer/portainer/api/internal/ssl"
|
||||
"github.com/portainer/portainer/api/internal/upgrade"
|
||||
k8s "github.com/portainer/portainer/api/kubernetes"
|
||||
@@ -382,7 +381,8 @@ func (server *Server) Start() error {
|
||||
|
||||
go shutdown(server.ShutdownCtx, httpsServer)
|
||||
|
||||
go snapshot.NewBackgroundSnapshotter(server.DataStore, server.ReverseTunnelService)
|
||||
// Temporarily disable for EE-6905 until we have a solution for the snapshotter
|
||||
// go snapshot.NewBackgroundSnapshotter(server.DataStore, server.ReverseTunnelService)
|
||||
|
||||
return httpsServer.ListenAndServeTLS("", "")
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/patrickmn/go-cache"
|
||||
"github.com/pkg/errors"
|
||||
@@ -286,106 +287,111 @@ func buildLocalConfig() (*rest.Config, error) {
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (factory *ClientFactory) MigrateEndpointIngresses(e *portainer.Endpoint) error {
|
||||
// classes is a list of controllers which have been manually added to the
|
||||
// cluster setup view. These need to all be allowed globally, but then
|
||||
// blocked in specific namespaces which they were not previously allowed in.
|
||||
classes := e.Kubernetes.Configuration.IngressClasses
|
||||
|
||||
// We need a kube client to gather namespace level permissions. In pre-2.16
|
||||
// versions of portainer, the namespace level permissions were stored by
|
||||
// creating an actual ingress rule in the cluster with a particular
|
||||
// annotation indicating that it's name (the class name) should be allowed.
|
||||
cli, err := factory.GetKubeClient(e)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
detected, err := cli.GetIngressControllers()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// newControllers is a set of all currently detected controllers.
|
||||
newControllers := make(map[string]struct{})
|
||||
for _, controller := range detected {
|
||||
newControllers[controller.ClassName] = struct{}{}
|
||||
}
|
||||
|
||||
namespaces, err := cli.GetNamespaces()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set of namespaces, if any, in which "allow none" should be true.
|
||||
allow := make(map[string]map[string]struct{})
|
||||
for _, c := range classes {
|
||||
allow[c.Name] = make(map[string]struct{})
|
||||
}
|
||||
allow["none"] = make(map[string]struct{})
|
||||
|
||||
for namespace := range namespaces {
|
||||
// Compare old annotations with currently detected controllers.
|
||||
ingresses, err := cli.GetIngresses(namespace)
|
||||
func (factory *ClientFactory) MigrateEndpointIngresses(e *portainer.Endpoint, datastore dataservices.DataStore, cli *KubeClient) error {
|
||||
return datastore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
environment, err := tx.Endpoint().Endpoint(e.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failure getting ingresses during migration")
|
||||
log.Error().Err(err).Msgf("Error retrieving environment %d", e.ID)
|
||||
return err
|
||||
}
|
||||
for _, ingress := range ingresses {
|
||||
oldController, ok := ingress.Annotations["ingress.portainer.io/ingress-type"]
|
||||
if !ok {
|
||||
// Skip rules without our old annotation.
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := newControllers[oldController]; ok {
|
||||
// Skip rules which match a detected controller.
|
||||
// TODO: Allow this particular controller.
|
||||
allow[oldController][ingress.Namespace] = struct{}{}
|
||||
continue
|
||||
}
|
||||
// classes is a list of controllers which have been manually added to the
|
||||
// cluster setup view. These need to all be allowed globally, but then
|
||||
// blocked in specific namespaces which they were not previously allowed in.
|
||||
classes := environment.Kubernetes.Configuration.IngressClasses
|
||||
|
||||
allow["none"][ingress.Namespace] = struct{}{}
|
||||
// In pre-2.16 versions of portainer, the namespace level permissions were stored by
|
||||
// creating an actual ingress rule in the cluster with a particular
|
||||
// annotation indicating that it's name (the class name) should be allowed.
|
||||
detected, err := cli.GetIngressControllers()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Error getting ingress controllers in environment %d", environment.ID)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Locally, disable "allow none" for namespaces not inside shouldAllowNone.
|
||||
var newClasses []portainer.KubernetesIngressClassConfig
|
||||
for _, c := range classes {
|
||||
var blocked []string
|
||||
// newControllers is a set of all currently detected controllers.
|
||||
newControllers := make(map[string]struct{})
|
||||
for _, controller := range detected {
|
||||
newControllers[controller.ClassName] = struct{}{}
|
||||
}
|
||||
|
||||
namespaces, err := cli.GetNamespaces()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Error getting namespaces in environment %d", environment.ID)
|
||||
return err
|
||||
}
|
||||
|
||||
// Set of namespaces, if any, in which "allow none" should be true.
|
||||
allow := make(map[string]map[string]struct{})
|
||||
for _, c := range classes {
|
||||
allow[c.Name] = make(map[string]struct{})
|
||||
}
|
||||
allow["none"] = make(map[string]struct{})
|
||||
|
||||
for namespace := range namespaces {
|
||||
if _, ok := allow[c.Name][namespace]; ok {
|
||||
continue
|
||||
// Compare old annotations with currently detected controllers.
|
||||
ingresses, err := cli.GetIngresses(namespace)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Error getting ingresses in environment %d", environment.ID)
|
||||
return err
|
||||
}
|
||||
for _, ingress := range ingresses {
|
||||
oldController, ok := ingress.Annotations["ingress.portainer.io/ingress-type"]
|
||||
if !ok {
|
||||
// Skip rules without our old annotation.
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := newControllers[oldController]; ok {
|
||||
// Skip rules which match a detected controller.
|
||||
// TODO: Allow this particular controller.
|
||||
allow[oldController][ingress.Namespace] = struct{}{}
|
||||
continue
|
||||
}
|
||||
|
||||
allow["none"][ingress.Namespace] = struct{}{}
|
||||
}
|
||||
blocked = append(blocked, namespace)
|
||||
}
|
||||
|
||||
newClasses = append(newClasses, portainer.KubernetesIngressClassConfig{
|
||||
Name: c.Name,
|
||||
Type: c.Type,
|
||||
GloballyBlocked: false,
|
||||
BlockedNamespaces: blocked,
|
||||
})
|
||||
}
|
||||
|
||||
// Handle "none".
|
||||
if len(allow["none"]) != 0 {
|
||||
e.Kubernetes.Configuration.AllowNoneIngressClass = true
|
||||
var disallowNone []string
|
||||
for namespace := range namespaces {
|
||||
if _, ok := allow["none"][namespace]; ok {
|
||||
continue
|
||||
// Locally, disable "allow none" for namespaces not inside shouldAllowNone.
|
||||
var newClasses []portainer.KubernetesIngressClassConfig
|
||||
for _, c := range classes {
|
||||
var blocked []string
|
||||
for namespace := range namespaces {
|
||||
if _, ok := allow[c.Name][namespace]; ok {
|
||||
continue
|
||||
}
|
||||
blocked = append(blocked, namespace)
|
||||
}
|
||||
disallowNone = append(disallowNone, namespace)
|
||||
}
|
||||
newClasses = append(newClasses, portainer.KubernetesIngressClassConfig{
|
||||
Name: "none",
|
||||
Type: "custom",
|
||||
GloballyBlocked: false,
|
||||
BlockedNamespaces: disallowNone,
|
||||
})
|
||||
}
|
||||
|
||||
e.Kubernetes.Configuration.IngressClasses = newClasses
|
||||
e.PostInitMigrations.MigrateIngresses = false
|
||||
return factory.dataStore.Endpoint().UpdateEndpoint(e.ID, e)
|
||||
newClasses = append(newClasses, portainer.KubernetesIngressClassConfig{
|
||||
Name: c.Name,
|
||||
Type: c.Type,
|
||||
GloballyBlocked: false,
|
||||
BlockedNamespaces: blocked,
|
||||
})
|
||||
}
|
||||
|
||||
// Handle "none".
|
||||
if len(allow["none"]) != 0 {
|
||||
environment.Kubernetes.Configuration.AllowNoneIngressClass = true
|
||||
var disallowNone []string
|
||||
for namespace := range namespaces {
|
||||
if _, ok := allow["none"][namespace]; ok {
|
||||
continue
|
||||
}
|
||||
disallowNone = append(disallowNone, namespace)
|
||||
}
|
||||
newClasses = append(newClasses, portainer.KubernetesIngressClassConfig{
|
||||
Name: "none",
|
||||
Type: "custom",
|
||||
GloballyBlocked: false,
|
||||
BlockedNamespaces: disallowNone,
|
||||
})
|
||||
}
|
||||
|
||||
environment.Kubernetes.Configuration.IngressClasses = newClasses
|
||||
environment.PostInitMigrations.MigrateIngresses = false
|
||||
return tx.Endpoint().UpdateEndpoint(environment.ID, environment)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -75,7 +75,14 @@ func (*Service) AuthenticateUser(username, password string, settings *portainer.
|
||||
|
||||
userDN, err := searchUser(username, connection, settings.SearchSettings)
|
||||
if err != nil {
|
||||
return err
|
||||
if errors.Is(err, errUserNotFound) {
|
||||
// prevent user enumeration timing attack by attempting the bind with a fake user
|
||||
// and whatever password was provided should definately fail
|
||||
// https://en.wikipedia.org/wiki/Timing_attack
|
||||
userDN = "portainer-fake-ldap-username"
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = connection.Bind(userDN, password)
|
||||
|
||||
@@ -172,8 +172,9 @@ func getResource(token string, configuration *portainer.OAuthSettings) (map[stri
|
||||
|
||||
func buildConfig(configuration *portainer.OAuthSettings) *oauth2.Config {
|
||||
endpoint := oauth2.Endpoint{
|
||||
AuthURL: configuration.AuthorizationURI,
|
||||
TokenURL: configuration.AccessTokenURI,
|
||||
AuthURL: configuration.AuthorizationURI,
|
||||
TokenURL: configuration.AccessTokenURI,
|
||||
AuthStyle: configuration.AuthStyle,
|
||||
}
|
||||
|
||||
return &oauth2.Config{
|
||||
|
||||
7
api/pendingactions/actions/actions.go
Normal file
7
api/pendingactions/actions/actions.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package actions
|
||||
|
||||
const (
|
||||
CleanNAPWithOverridePolicies = "CleanNAPWithOverridePolicies"
|
||||
DeletePortainerK8sRegistrySecrets = "DeletePortainerK8sRegistrySecrets"
|
||||
PostInitMigrateEnvironment = "PostInitMigrateEnvironment"
|
||||
)
|
||||
@@ -17,7 +17,7 @@ func (service *PendingActionsService) DeleteKubernetesRegistrySecrets(endpoint *
|
||||
return nil
|
||||
}
|
||||
|
||||
kubeClient, err := service.clientFactory.GetKubeClient(endpoint)
|
||||
kubeClient, err := service.kubeFactory.GetKubeClient(endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -7,22 +7,24 @@ import (
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/datastore/postinit"
|
||||
dockerClient "github.com/portainer/portainer/api/docker/client"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
kubecli "github.com/portainer/portainer/api/kubernetes/cli"
|
||||
"github.com/portainer/portainer/api/pendingactions/actions"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const (
|
||||
CleanNAPWithOverridePolicies = "CleanNAPWithOverridePolicies"
|
||||
DeletePortainerK8sRegistrySecrets = "DeletePortainerK8sRegistrySecrets"
|
||||
)
|
||||
|
||||
type (
|
||||
PendingActionsService struct {
|
||||
authorizationService *authorization.Service
|
||||
clientFactory *kubecli.ClientFactory
|
||||
kubeFactory *kubecli.ClientFactory
|
||||
dockerFactory *dockerClient.ClientFactory
|
||||
dataStore dataservices.DataStore
|
||||
shutdownCtx context.Context
|
||||
assetsPath string
|
||||
kubernetesDeployer portainer.KubernetesDeployer
|
||||
|
||||
mu sync.Mutex
|
||||
}
|
||||
@@ -30,15 +32,21 @@ type (
|
||||
|
||||
func NewService(
|
||||
dataStore dataservices.DataStore,
|
||||
clientFactory *kubecli.ClientFactory,
|
||||
kubeFactory *kubecli.ClientFactory,
|
||||
dockerFactory *dockerClient.ClientFactory,
|
||||
authorizationService *authorization.Service,
|
||||
shutdownCtx context.Context,
|
||||
assetsPath string,
|
||||
kubernetesDeployer portainer.KubernetesDeployer,
|
||||
) *PendingActionsService {
|
||||
return &PendingActionsService{
|
||||
dataStore: dataStore,
|
||||
shutdownCtx: shutdownCtx,
|
||||
authorizationService: authorizationService,
|
||||
clientFactory: clientFactory,
|
||||
kubeFactory: kubeFactory,
|
||||
dockerFactory: dockerFactory,
|
||||
assetsPath: assetsPath,
|
||||
kubernetesDeployer: kubernetesDeployer,
|
||||
mu: sync.Mutex{},
|
||||
}
|
||||
}
|
||||
@@ -57,9 +65,22 @@ func (service *PendingActionsService) Execute(id portainer.EndpointID) error {
|
||||
return fmt.Errorf("failed to retrieve environment %d: %w", id, err)
|
||||
}
|
||||
|
||||
if endpoint.Status != portainer.EndpointStatusUp {
|
||||
isKubernetesEndpoint := endpointutils.IsKubernetesEndpoint(endpoint) && !endpointutils.IsEdgeEndpoint(endpoint)
|
||||
|
||||
// EndpointStatusUp is only relevant for non-Kubernetes endpoints
|
||||
// Sometimes the endpoint is UP but the status is not updated in the database
|
||||
if !isKubernetesEndpoint && endpoint.Status != portainer.EndpointStatusUp {
|
||||
log.Debug().Msgf("Environment %q (id: %d) is not up", endpoint.Name, id)
|
||||
return fmt.Errorf("environment %q (id: %d) is not up: %w", endpoint.Name, id, err)
|
||||
return fmt.Errorf("environment %q (id: %d) is not up", endpoint.Name, id)
|
||||
}
|
||||
|
||||
// For Kubernetes endpoints, we need to check if the endpoint is up by creating a kube client
|
||||
if isKubernetesEndpoint {
|
||||
_, err := service.kubeFactory.GetKubeClient(endpoint)
|
||||
if err != nil {
|
||||
log.Debug().Err(err).Msgf("Environment %q (id: %d) is not up", endpoint.Name, id)
|
||||
return fmt.Errorf("environment %q (id: %d) is not up", endpoint.Name, id)
|
||||
}
|
||||
}
|
||||
|
||||
pendingActions, err := service.dataStore.PendingActions().ReadAll()
|
||||
@@ -95,7 +116,7 @@ func (service *PendingActionsService) executePendingAction(pendingAction portain
|
||||
}()
|
||||
|
||||
switch pendingAction.Action {
|
||||
case CleanNAPWithOverridePolicies:
|
||||
case actions.CleanNAPWithOverridePolicies:
|
||||
if (pendingAction.ActionData == nil) || (pendingAction.ActionData.(portainer.EndpointGroupID) == 0) {
|
||||
service.authorizationService.CleanNAPWithOverridePolicies(service.dataStore, endpoint, nil)
|
||||
return nil
|
||||
@@ -114,7 +135,7 @@ func (service *PendingActionsService) executePendingAction(pendingAction portain
|
||||
}
|
||||
|
||||
return nil
|
||||
case DeletePortainerK8sRegistrySecrets:
|
||||
case actions.DeletePortainerK8sRegistrySecrets:
|
||||
if pendingAction.ActionData == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -130,6 +151,22 @@ func (service *PendingActionsService) executePendingAction(pendingAction portain
|
||||
return fmt.Errorf("failed to delete kubernetes registry secrets for environment %d: %w", endpoint.ID, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
case actions.PostInitMigrateEnvironment:
|
||||
postInitMigrator := postinit.NewPostInitMigrator(
|
||||
service.kubeFactory,
|
||||
service.dockerFactory,
|
||||
service.dataStore,
|
||||
service.assetsPath,
|
||||
service.kubernetesDeployer,
|
||||
)
|
||||
err := postInitMigrator.MigrateEnvironment(endpoint)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Error running post-init migrations for edge environment %d", endpoint.ID)
|
||||
return fmt.Errorf("failed running post-init migrations for edge environment %d: %w", endpoint.ID, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -6,10 +6,13 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/api/types/system"
|
||||
"github.com/docker/docker/api/types/volume"
|
||||
gittypes "github.com/portainer/portainer/api/git/types"
|
||||
models "github.com/portainer/portainer/api/http/models/kubernetes"
|
||||
"github.com/portainer/portainer/pkg/featureflags"
|
||||
"golang.org/x/oauth2"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
@@ -242,8 +245,8 @@ type (
|
||||
Containers []DockerContainerSnapshot `json:"Containers" swaggerignore:"true"`
|
||||
Volumes volume.ListResponse `json:"Volumes" swaggerignore:"true"`
|
||||
Networks []types.NetworkResource `json:"Networks" swaggerignore:"true"`
|
||||
Images []types.ImageSummary `json:"Images" swaggerignore:"true"`
|
||||
Info types.Info `json:"Info" swaggerignore:"true"`
|
||||
Images []image.Summary `json:"Images" swaggerignore:"true"`
|
||||
Info system.Info `json:"Info" swaggerignore:"true"`
|
||||
Version types.Version `json:"Version" swaggerignore:"true"`
|
||||
}
|
||||
|
||||
@@ -756,19 +759,20 @@ type (
|
||||
|
||||
// OAuthSettings represents the settings used to authorize with an authorization server
|
||||
OAuthSettings struct {
|
||||
ClientID string `json:"ClientID"`
|
||||
ClientSecret string `json:"ClientSecret,omitempty"`
|
||||
AccessTokenURI string `json:"AccessTokenURI"`
|
||||
AuthorizationURI string `json:"AuthorizationURI"`
|
||||
ResourceURI string `json:"ResourceURI"`
|
||||
RedirectURI string `json:"RedirectURI"`
|
||||
UserIdentifier string `json:"UserIdentifier"`
|
||||
Scopes string `json:"Scopes"`
|
||||
OAuthAutoCreateUsers bool `json:"OAuthAutoCreateUsers"`
|
||||
DefaultTeamID TeamID `json:"DefaultTeamID"`
|
||||
SSO bool `json:"SSO"`
|
||||
LogoutURI string `json:"LogoutURI"`
|
||||
KubeSecretKey []byte `json:"KubeSecretKey"`
|
||||
ClientID string `json:"ClientID"`
|
||||
ClientSecret string `json:"ClientSecret,omitempty"`
|
||||
AccessTokenURI string `json:"AccessTokenURI"`
|
||||
AuthorizationURI string `json:"AuthorizationURI"`
|
||||
ResourceURI string `json:"ResourceURI"`
|
||||
RedirectURI string `json:"RedirectURI"`
|
||||
UserIdentifier string `json:"UserIdentifier"`
|
||||
Scopes string `json:"Scopes"`
|
||||
OAuthAutoCreateUsers bool `json:"OAuthAutoCreateUsers"`
|
||||
DefaultTeamID TeamID `json:"DefaultTeamID"`
|
||||
SSO bool `json:"SSO"`
|
||||
LogoutURI string `json:"LogoutURI"`
|
||||
KubeSecretKey []byte `json:"KubeSecretKey"`
|
||||
AuthStyle oauth2.AuthStyle `json:"AuthStyle"`
|
||||
}
|
||||
|
||||
// Pair defines a key/value string pair
|
||||
@@ -1595,7 +1599,7 @@ type (
|
||||
|
||||
const (
|
||||
// APIVersion is the version number of the Portainer API
|
||||
APIVersion = "2.20.1"
|
||||
APIVersion = "2.20.2"
|
||||
// Edition is what this edition of Portainer is called
|
||||
Edition = PortainerCE
|
||||
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax
|
||||
@@ -1724,6 +1728,8 @@ const (
|
||||
EdgeStackStatusRollingBack
|
||||
// EdgeStackStatusRolledBack represents an Edge stack which has rolled back
|
||||
EdgeStackStatusRolledBack
|
||||
// EdgeStackStatusCompleted represents a completed Edge stack
|
||||
EdgeStackStatusCompleted
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/api/types/system"
|
||||
dockerclient "github.com/docker/docker/client"
|
||||
"github.com/docker/docker/pkg/stdcopy"
|
||||
"github.com/pkg/errors"
|
||||
@@ -24,7 +25,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
defaultUnpackerImage = "portainer/compose-unpacker:latest"
|
||||
defaultUnpackerImage = "portainer/compose-unpacker:" + portainer.APIVersion
|
||||
composeUnpackerImageEnvVar = "COMPOSE_UNPACKER_IMAGE"
|
||||
composePathPrefix = "portainer-compose-unpacker"
|
||||
)
|
||||
@@ -211,9 +212,9 @@ func (d *stackDeployer) remoteStack(stack *portainer.Stack, endpoint *portainer.
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to create unpacker container")
|
||||
}
|
||||
defer cli.ContainerRemove(ctx, unpackerContainer.ID, types.ContainerRemoveOptions{})
|
||||
defer cli.ContainerRemove(ctx, unpackerContainer.ID, container.RemoveOptions{})
|
||||
|
||||
if err := cli.ContainerStart(ctx, unpackerContainer.ID, types.ContainerStartOptions{}); err != nil {
|
||||
if err := cli.ContainerStart(ctx, unpackerContainer.ID, container.StartOptions{}); err != nil {
|
||||
return errors.Wrap(err, "start unpacker container error")
|
||||
}
|
||||
|
||||
@@ -228,7 +229,7 @@ func (d *stackDeployer) remoteStack(stack *portainer.Stack, endpoint *portainer.
|
||||
|
||||
stdErr := &bytes.Buffer{}
|
||||
|
||||
out, err := cli.ContainerLogs(ctx, unpackerContainer.ID, types.ContainerLogsOptions{ShowStdout: true, ShowStderr: true})
|
||||
out, err := cli.ContainerLogs(ctx, unpackerContainer.ID, container.LogsOptions{ShowStdout: true, ShowStderr: true})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("unable to get logs from unpacker container")
|
||||
} else {
|
||||
@@ -335,6 +336,6 @@ func getTargetSocketBind(osType string) string {
|
||||
|
||||
// Per https://stackoverflow.com/a/50590287 and Docker's LocalNodeState possible values
|
||||
// `LocalNodeStateInactive` means the node is not in a swarm cluster
|
||||
func isNotInASwarm(info *types.Info) bool {
|
||||
func isNotInASwarm(info *system.Info) bool {
|
||||
return info.Swarm.LocalNodeState == swarm.LocalNodeStateInactive
|
||||
}
|
||||
|
||||
@@ -65,13 +65,12 @@
|
||||
<relative-path-fieldset value="$ctrl.stack" git-model="$ctrl.stack" is-editing="true" hide-edge-configs="true"></relative-path-fieldset>
|
||||
</div>
|
||||
|
||||
<environment-variables-panel
|
||||
<stack-environment-variables-panel
|
||||
values="$ctrl.formValues.Env"
|
||||
explanation="'These values will be used as substitutions in the stack file. To reference the .env file in your compose file, use ‘stack.env’.'"
|
||||
on-change="($ctrl.onChangeEnvVar)"
|
||||
show-help-message="true"
|
||||
is-foldable="true"
|
||||
></environment-variables-panel>
|
||||
></stack-environment-variables-panel>
|
||||
|
||||
<option-panel ng-if="$ctrl.stack.Type === 1 && $ctrl.endpoint.apiVersion >= 1.27" ng-model="$ctrl.formValues.Option" on-change="($ctrl.onChangeOption)"></option-panel>
|
||||
|
||||
|
||||
@@ -78,6 +78,7 @@ export function OAuthSettingsViewModel(data) {
|
||||
this.DefaultTeamID = data.DefaultTeamID;
|
||||
this.SSO = data.SSO;
|
||||
this.LogoutURI = data.LogoutURI;
|
||||
this.AuthStyle = data.AuthStyle;
|
||||
}
|
||||
|
||||
export function EdgeSettingsViewModel(data = {}) {
|
||||
|
||||
@@ -4,7 +4,6 @@ import { isLimitedToBE } from '@/react/portainer/feature-flags/feature-flags.ser
|
||||
import { ModalType } from '@@/modals';
|
||||
import { confirm } from '@@/modals/confirm';
|
||||
import { buildConfirmButton } from '@@/modals/utils';
|
||||
|
||||
import providers, { getProviderByUrl } from './providers';
|
||||
|
||||
const MS_TENANT_ID_PLACEHOLDER = 'TENANT_ID';
|
||||
@@ -31,6 +30,7 @@ export default class OAuthSettingsController {
|
||||
this.addTeamMembershipMapping = this.addTeamMembershipMapping.bind(this);
|
||||
this.removeTeamMembership = this.removeTeamMembership.bind(this);
|
||||
this.onToggleAutoTeamMembership = this.onToggleAutoTeamMembership.bind(this);
|
||||
this.onChangeAuthStyle = this.onChangeAuthStyle.bind(this);
|
||||
}
|
||||
|
||||
onMicrosoftTenantIDChange() {
|
||||
@@ -54,6 +54,7 @@ export default class OAuthSettingsController {
|
||||
this.settings.LogoutURI = provider.logoutUrl;
|
||||
this.settings.UserIdentifier = provider.userIdentifier;
|
||||
this.settings.Scopes = provider.scopes;
|
||||
this.settings.AuthStyle = provider.authStyle;
|
||||
|
||||
if (providerId === 'microsoft' && this.state.microsoftTenantID !== '') {
|
||||
this.onMicrosoftTenantIDChange();
|
||||
@@ -77,6 +78,12 @@ export default class OAuthSettingsController {
|
||||
});
|
||||
}
|
||||
|
||||
onChangeAuthStyle(val) {
|
||||
this.$scope.$evalAsync(() => {
|
||||
this.settings.AuthStyle = val;
|
||||
});
|
||||
}
|
||||
|
||||
async onChangeHideInternalAuth(checked) {
|
||||
this.$async(async () => {
|
||||
if (this.isLimitedToBE) {
|
||||
|
||||
@@ -341,6 +341,8 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<oauth-auth-style value="$ctrl.settings.AuthStyle" on-change="($ctrl.onChangeAuthStyle)"></oauth-auth-style>
|
||||
<save-auth-settings-button
|
||||
on-save-settings="($ctrl.onSaveSettings)"
|
||||
save-button-state="($ctrl.saveButtonState)"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { baseHref } from '@/portainer/helpers/pathHelper';
|
||||
import { OAuthStyle } from '@/react/portainer/settings/types';
|
||||
|
||||
export default {
|
||||
microsoft: {
|
||||
@@ -8,6 +9,7 @@ export default {
|
||||
logoutUrl: `https://login.microsoftonline.com/TENANT_ID/oauth2/v2.0/logout`,
|
||||
userIdentifier: 'userPrincipalName',
|
||||
scopes: 'profile openid',
|
||||
authStyle: OAuthStyle.InParams,
|
||||
},
|
||||
google: {
|
||||
authUrl: 'https://accounts.google.com/o/oauth2/auth',
|
||||
@@ -16,6 +18,7 @@ export default {
|
||||
logoutUrl: `https://www.google.com/accounts/Logout?continue=https://appengine.google.com/_ah/logout?continue=${window.location.origin}${baseHref()}#!/auth`,
|
||||
userIdentifier: 'email',
|
||||
scopes: 'profile email',
|
||||
authStyle: OAuthStyle.InParams,
|
||||
},
|
||||
github: {
|
||||
authUrl: 'https://github.com/login/oauth/authorize',
|
||||
@@ -24,8 +27,9 @@ export default {
|
||||
logoutUrl: `https://github.com/logout`,
|
||||
userIdentifier: 'login',
|
||||
scopes: 'id email name',
|
||||
authStyle: OAuthStyle.AutoDetect,
|
||||
},
|
||||
custom: { authUrl: '', accessTokenUrl: '', resourceUrl: '', logoutUrl: '', userIdentifier: '', scopes: '' },
|
||||
custom: { authUrl: '', accessTokenUrl: '', resourceUrl: '', logoutUrl: '', userIdentifier: '', scopes: '', authStyle: OAuthStyle.AutoDetect },
|
||||
};
|
||||
|
||||
export function getProviderByUrl(providerAuthURL = '') {
|
||||
|
||||
@@ -14,6 +14,7 @@ import { withControlledInput } from '@/react-tools/withControlledInput';
|
||||
import {
|
||||
EnvironmentVariablesFieldset,
|
||||
EnvironmentVariablesPanel,
|
||||
StackEnvironmentVariablesPanel,
|
||||
envVarValidation,
|
||||
} from '@@/form-components/EnvironmentVariablesFieldset';
|
||||
import { Icon } from '@@/Icon';
|
||||
@@ -263,3 +264,13 @@ withFormValidation(
|
||||
['explanation', 'showHelpMessage', 'isFoldable'],
|
||||
envVarValidation
|
||||
);
|
||||
|
||||
withFormValidation(
|
||||
ngModule,
|
||||
withUIRouter(
|
||||
withControlledInput(StackEnvironmentVariablesPanel, { values: 'onChange' })
|
||||
),
|
||||
'stackEnvironmentVariablesPanel',
|
||||
['showHelpMessage', 'isFoldable'],
|
||||
envVarValidation
|
||||
);
|
||||
|
||||
@@ -11,6 +11,7 @@ import { KubeSettingsPanel } from '@/react/portainer/settings/SettingsView/KubeS
|
||||
import { HelmCertPanel } from '@/react/portainer/settings/SettingsView/HelmCertPanel';
|
||||
import { HiddenContainersPanel } from '@/react/portainer/settings/SettingsView/HiddenContainersPanel/HiddenContainersPanel';
|
||||
import { SSLSettingsPanelWrapper } from '@/react/portainer/settings/SettingsView/SSLSettingsPanel/SSLSettingsPanel';
|
||||
import { AuthStyleField } from '@/react/portainer/settings/AuthenticationView/OAuth';
|
||||
|
||||
export const settingsModule = angular
|
||||
.module('portainer.app.react.components.settings', [])
|
||||
@@ -39,4 +40,15 @@ export const settingsModule = angular
|
||||
.component(
|
||||
'kubeSettingsPanel',
|
||||
r2a(withUIRouter(withReactQuery(KubeSettingsPanel)), ['settings'])
|
||||
)
|
||||
.component(
|
||||
'oauthAuthStyle',
|
||||
r2a(AuthStyleField, [
|
||||
'value',
|
||||
'onChange',
|
||||
'label',
|
||||
'tooltip',
|
||||
'readonly',
|
||||
'size',
|
||||
])
|
||||
).name;
|
||||
|
||||
@@ -152,12 +152,7 @@
|
||||
</div>
|
||||
|
||||
<!-- environment-variables -->
|
||||
<environment-variables-panel
|
||||
values="formValues.Env"
|
||||
explanation="'These values will be used as substitutions in the stack file. To reference the .env file in your compose file, use ‘stack.env’'"
|
||||
on-change="(handleEnvVarChange)"
|
||||
>
|
||||
</environment-variables-panel>
|
||||
<stack-environment-variables-panel values="formValues.Env" on-change="(handleEnvVarChange)" show-alert="true"> </stack-environment-variables-panel>
|
||||
<!-- !environment-variables -->
|
||||
<por-access-control-form form-data="formValues.AccessControlData"></por-access-control-form>
|
||||
<!-- actions -->
|
||||
|
||||
@@ -169,13 +169,12 @@
|
||||
|
||||
<!-- environment-variables -->
|
||||
<div ng-if="stack">
|
||||
<environment-variables-panel
|
||||
<stack-environment-variables-panel
|
||||
values="formValues.Env"
|
||||
explanation="'These values will be used as substitutions in the stack file. To reference the .env file in your compose file, use ‘stack.env’.'"
|
||||
on-change="(handleEnvVarChange)"
|
||||
show-help-message="true"
|
||||
is-foldable="true"
|
||||
></environment-variables-panel>
|
||||
></stack-environment-variables-panel>
|
||||
</div>
|
||||
<!-- !environment-variables -->
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ComponentProps } from 'react';
|
||||
import React, { ComponentProps } from 'react';
|
||||
|
||||
import { FormSection } from '@@/form-components/FormSection';
|
||||
import { TextTip } from '@@/Tip/TextTip';
|
||||
@@ -14,16 +14,19 @@ export function EnvironmentVariablesPanel({
|
||||
showHelpMessage,
|
||||
errors,
|
||||
isFoldable = false,
|
||||
alertMessage,
|
||||
}: {
|
||||
explanation?: string;
|
||||
explanation?: React.ReactNode;
|
||||
showHelpMessage?: boolean;
|
||||
isFoldable?: boolean;
|
||||
alertMessage?: React.ReactNode;
|
||||
} & FieldsetProps) {
|
||||
return (
|
||||
<FormSection
|
||||
title="Environment variables"
|
||||
isFoldable={isFoldable}
|
||||
defaultFolded={isFoldable}
|
||||
className="flex flex-col w-full"
|
||||
>
|
||||
<div className="form-group">
|
||||
{!!explanation && (
|
||||
@@ -32,6 +35,8 @@ export function EnvironmentVariablesPanel({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{alertMessage}
|
||||
|
||||
<div className="col-sm-12">
|
||||
<EnvironmentVariablesFieldset
|
||||
values={values}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { ComponentProps } from 'react';
|
||||
|
||||
import { Alert } from '@@/Alert';
|
||||
|
||||
import { EnvironmentVariablesFieldset } from './EnvironmentVariablesFieldset';
|
||||
import { EnvironmentVariablesPanel } from './EnvironmentVariablesPanel';
|
||||
|
||||
type FieldsetProps = ComponentProps<typeof EnvironmentVariablesFieldset>;
|
||||
|
||||
export function StackEnvironmentVariablesPanel({
|
||||
onChange,
|
||||
values,
|
||||
errors,
|
||||
isFoldable = false,
|
||||
showHelpMessage,
|
||||
}: {
|
||||
isFoldable?: boolean;
|
||||
showHelpMessage?: boolean;
|
||||
} & FieldsetProps) {
|
||||
return (
|
||||
<EnvironmentVariablesPanel
|
||||
explanation={
|
||||
<div>
|
||||
You may use{' '}
|
||||
<a
|
||||
href="https://docs.portainer.io/v/2.20/user/docker/stacks/add#environment-variables"
|
||||
target="_blank"
|
||||
data-cy="stack-env-vars-help-link"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
environment variables in your compose file
|
||||
</a>
|
||||
. The environment variable values set below will be used as
|
||||
substitutions in the compose file. Note that you may also reference a
|
||||
stack.env file in your compose file. A stack.env file contains the
|
||||
environment variables and their values (e.g. TAG=v1.5).
|
||||
</div>
|
||||
}
|
||||
onChange={onChange}
|
||||
values={values}
|
||||
errors={errors}
|
||||
isFoldable={isFoldable}
|
||||
showHelpMessage={showHelpMessage}
|
||||
alertMessage={
|
||||
<div className="flex p-4">
|
||||
<Alert color="info" className="col-sm-12">
|
||||
<div>
|
||||
<p>
|
||||
<strong>stack.env file operation</strong>
|
||||
</p>
|
||||
<div>
|
||||
When deploying via <strong>Repository</strong>, the stack.env
|
||||
file must already reside in the Git repo.
|
||||
</div>
|
||||
<div>
|
||||
When deploying via <strong>Web editor</strong>,{' '}
|
||||
<strong>Upload</strong> or{' '}
|
||||
<strong>Custom template deployment</strong>, the stack.env file
|
||||
is auto created from what you set below.
|
||||
</div>
|
||||
</div>
|
||||
</Alert>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -4,5 +4,6 @@ export {
|
||||
} from './EnvironmentVariablesFieldset';
|
||||
|
||||
export { EnvironmentVariablesPanel } from './EnvironmentVariablesPanel';
|
||||
export { StackEnvironmentVariablesPanel } from './StackEnvironmentVariablesPanel';
|
||||
|
||||
export { type Values as EnvVarValues } from './types';
|
||||
|
||||
@@ -11,6 +11,7 @@ interface Props {
|
||||
isFoldable?: boolean;
|
||||
defaultFolded?: boolean;
|
||||
titleClassName?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function FormSection({
|
||||
@@ -20,11 +21,12 @@ export function FormSection({
|
||||
isFoldable = false,
|
||||
defaultFolded = isFoldable,
|
||||
titleClassName,
|
||||
className,
|
||||
}: PropsWithChildren<Props>) {
|
||||
const [isExpanded, setIsExpanded] = useState(!defaultFolded);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={className}>
|
||||
<FormSectionTitle
|
||||
htmlFor={isFoldable ? `foldingButton${title}` : ''}
|
||||
titleSize={titleSize}
|
||||
@@ -52,6 +54,6 @@ export function FormSection({
|
||||
</FormSectionTitle>
|
||||
|
||||
{isExpanded && children}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -66,6 +66,8 @@ function getTooltip(count: number, total: number, type?: StatusType) {
|
||||
switch (type) {
|
||||
case StatusType.Running:
|
||||
return 'deployments running';
|
||||
case StatusType.Completed:
|
||||
return 'deployments completed';
|
||||
case StatusType.DeploymentReceived:
|
||||
return 'deployments received';
|
||||
case StatusType.Error:
|
||||
|
||||
@@ -84,6 +84,16 @@ function getStatus(
|
||||
};
|
||||
}
|
||||
|
||||
const allCompleted = envStatus.every((s) => s.Type === StatusType.Completed);
|
||||
|
||||
if (allCompleted) {
|
||||
return {
|
||||
label: 'Completed',
|
||||
icon: CheckCircle,
|
||||
mode: 'success',
|
||||
};
|
||||
}
|
||||
|
||||
const allRunning = envStatus.every(
|
||||
(s) =>
|
||||
s.Type === StatusType.Running ||
|
||||
|
||||
@@ -44,6 +44,8 @@ export enum StatusType {
|
||||
RollingBack,
|
||||
/** PausedRemoving represents an Edge stack which has been rolled back */
|
||||
RolledBack,
|
||||
/** Completed represents a completed Edge stack */
|
||||
Completed,
|
||||
}
|
||||
|
||||
export interface DeploymentStatus {
|
||||
|
||||
@@ -34,7 +34,8 @@ export function DashboardView() {
|
||||
);
|
||||
const { data: services, ...servicesQuery } = useServicesForCluster(
|
||||
environmentId,
|
||||
namespaceNames
|
||||
namespaceNames,
|
||||
{ lookupApplications: false }
|
||||
);
|
||||
const { data: ingresses, ...ingressesQuery } = useIngresses(
|
||||
environmentId,
|
||||
|
||||
@@ -42,6 +42,7 @@ export function ServicesDatatable() {
|
||||
namespaceNames,
|
||||
{
|
||||
autoRefreshRate: tableState.autoRefreshRate * 1000,
|
||||
lookupApplications: true,
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ export const queryKeys = {
|
||||
export function useServicesForCluster(
|
||||
environmentId: EnvironmentId,
|
||||
namespaceNames?: string[],
|
||||
options?: { autoRefreshRate?: number }
|
||||
options?: { autoRefreshRate?: number; lookupApplications?: boolean }
|
||||
) {
|
||||
return useQuery(
|
||||
queryKeys.clusterServices(environmentId),
|
||||
@@ -32,7 +32,7 @@ export function useServicesForCluster(
|
||||
}
|
||||
const settledServicesPromise = await Promise.allSettled(
|
||||
namespaceNames.map((namespace) =>
|
||||
getServices(environmentId, namespace, true)
|
||||
getServices(environmentId, namespace, options?.lookupApplications)
|
||||
)
|
||||
);
|
||||
return compact(
|
||||
@@ -87,14 +87,14 @@ export function useMutationDeleteServices(environmentId: EnvironmentId) {
|
||||
export async function getServices(
|
||||
environmentId: EnvironmentId,
|
||||
namespace: string,
|
||||
lookupApps: boolean
|
||||
lookupApplications?: boolean
|
||||
) {
|
||||
try {
|
||||
const { data: services } = await axios.get<Array<Service>>(
|
||||
`kubernetes/${environmentId}/namespaces/${namespace}/services`,
|
||||
{
|
||||
params: {
|
||||
lookupapplications: lookupApps,
|
||||
lookupapplications: lookupApplications,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -2,11 +2,22 @@ import { SchemaOf, object, string } from 'yup';
|
||||
|
||||
import { ApiKeyFormValues } from './types';
|
||||
|
||||
export function getAPITokenValidationSchema(): SchemaOf<ApiKeyFormValues> {
|
||||
export function getAPITokenValidationSchema(
|
||||
requirePassword: boolean
|
||||
): SchemaOf<ApiKeyFormValues> {
|
||||
if (requirePassword) {
|
||||
return object({
|
||||
password: string().required('Password is required.'),
|
||||
description: string()
|
||||
.max(128, 'Description must be at most 128 characters')
|
||||
.required('Description is required.'),
|
||||
});
|
||||
}
|
||||
|
||||
return object({
|
||||
password: string().required('Password is required.'),
|
||||
password: string().optional(),
|
||||
description: string()
|
||||
.max(128, 'Description must be at most 128 characters')
|
||||
.required('Description is required'),
|
||||
.required('Description is required.'),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -33,7 +33,10 @@ test('the button is disabled when all fields are blank and enabled when all fiel
|
||||
});
|
||||
|
||||
function renderComponent() {
|
||||
const user = new UserViewModel({ Username: 'user' });
|
||||
const user = new UserViewModel({
|
||||
Username: 'admin',
|
||||
Id: 1,
|
||||
});
|
||||
|
||||
const Wrapped = withTestQueryProvider(
|
||||
withUserProvider(withTestRouter(CreateUserAccessToken), user)
|
||||
|
||||
@@ -3,10 +3,13 @@ import { useState } from 'react';
|
||||
|
||||
import { useCurrentUser } from '@/react/hooks/useUser';
|
||||
import { useAnalytics } from '@/react/hooks/useAnalytics';
|
||||
import { AuthenticationMethod } from '@/react/portainer/settings/types';
|
||||
|
||||
import { Widget } from '@@/Widget';
|
||||
import { PageHeader } from '@@/PageHeader';
|
||||
|
||||
import { usePublicSettings } from '../../settings/queries/usePublicSettings';
|
||||
|
||||
import { ApiKeyFormValues } from './types';
|
||||
import { getAPITokenValidationSchema } from './CreateUserAcccessToken.validation';
|
||||
import { useCreateUserAccessTokenMutation } from './useCreateUserAccessTokenMutation';
|
||||
@@ -23,6 +26,11 @@ export function CreateUserAccessToken() {
|
||||
const { user } = useCurrentUser();
|
||||
const [newAPIToken, setNewAPIToken] = useState('');
|
||||
const { trackEvent } = useAnalytics();
|
||||
const settings = usePublicSettings();
|
||||
|
||||
const requirePassword =
|
||||
settings.data?.AuthenticationMethod === AuthenticationMethod.Internal ||
|
||||
user.Id === 1;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -43,12 +51,16 @@ export function CreateUserAccessToken() {
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
onSubmit={onSubmit}
|
||||
validationSchema={getAPITokenValidationSchema}
|
||||
validationSchema={getAPITokenValidationSchema(
|
||||
requirePassword
|
||||
)}
|
||||
>
|
||||
<CreateUserAccessTokenInnerForm />
|
||||
<CreateUserAccessTokenInnerForm
|
||||
showAuthentication={requirePassword}
|
||||
/>
|
||||
</Formik>
|
||||
) : (
|
||||
DisplayUserAccessToken(newAPIToken)
|
||||
<DisplayUserAccessToken apikey={newAPIToken} />
|
||||
)}
|
||||
</Widget.Body>
|
||||
</Widget>
|
||||
|
||||
@@ -6,7 +6,11 @@ import { LoadingButton } from '@@/buttons';
|
||||
|
||||
import { ApiKeyFormValues } from './types';
|
||||
|
||||
export function CreateUserAccessTokenInnerForm() {
|
||||
interface Props {
|
||||
showAuthentication: boolean;
|
||||
}
|
||||
|
||||
export function CreateUserAccessTokenInnerForm({ showAuthentication }: Props) {
|
||||
const { errors, values, handleSubmit, isValid, dirty } =
|
||||
useFormikContext<ApiKeyFormValues>();
|
||||
|
||||
@@ -16,21 +20,23 @@ export function CreateUserAccessTokenInnerForm() {
|
||||
onSubmit={handleSubmit}
|
||||
autoComplete="off"
|
||||
>
|
||||
<FormControl
|
||||
inputId="password"
|
||||
label="Current password"
|
||||
required
|
||||
errors={errors.password}
|
||||
>
|
||||
<Field
|
||||
as={Input}
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
value={values.password}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</FormControl>
|
||||
{showAuthentication && (
|
||||
<FormControl
|
||||
inputId="password"
|
||||
label="Current password"
|
||||
required
|
||||
errors={errors.password}
|
||||
>
|
||||
<Field
|
||||
as={Input}
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
value={values.password}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
<FormControl
|
||||
inputId="description"
|
||||
label="Description"
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Button, CopyButton } from '@@/buttons';
|
||||
import { FormSectionTitle } from '@@/form-components/FormSectionTitle';
|
||||
import { TextTip } from '@@/Tip/TextTip';
|
||||
|
||||
export function DisplayUserAccessToken(apikey: string) {
|
||||
export function DisplayUserAccessToken({ apikey }: { apikey: string }) {
|
||||
const router = useRouter();
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export interface ApiKeyFormValues {
|
||||
password: string;
|
||||
password?: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Options } from '@/react/edge/components/useIntervalOptions';
|
||||
import { OAuthStyle } from '@/react/portainer/settings/types';
|
||||
|
||||
import { FormControl, Size } from '@@/form-components/FormControl';
|
||||
import { Select } from '@@/form-components/Input';
|
||||
|
||||
interface Props {
|
||||
value: OAuthStyle;
|
||||
onChange(value: OAuthStyle): void;
|
||||
label?: string;
|
||||
tooltip?: string;
|
||||
readonly?: boolean;
|
||||
size?: Size;
|
||||
}
|
||||
|
||||
// The options are based on oauth2 lib definition @https://pkg.go.dev/golang.org/x/oauth2#AuthStyle
|
||||
export const authStyleOptions: Options = [
|
||||
{ label: 'Auto Detect', value: OAuthStyle.AutoDetect, isDefault: true },
|
||||
{ label: 'In Params', value: OAuthStyle.InParams },
|
||||
{ label: 'In Header', value: OAuthStyle.InHeader },
|
||||
];
|
||||
|
||||
export function AuthStyleField({
|
||||
value,
|
||||
readonly = false,
|
||||
onChange,
|
||||
label = 'Auth Style',
|
||||
tooltip = 'Auth Style specifies how the endpoint wants the client ID & client secret sent.',
|
||||
size = 'small',
|
||||
}: Props) {
|
||||
return (
|
||||
<FormControl
|
||||
inputId="oauth_authstyle"
|
||||
label={label}
|
||||
tooltip={tooltip}
|
||||
size={size}
|
||||
>
|
||||
<Select
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
onChange(parseInt(e.currentTarget.value, 10));
|
||||
}}
|
||||
options={authStyleOptions}
|
||||
disabled={readonly}
|
||||
id="oauth_authstyle"
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { AuthStyleField } from './AuthStyleField';
|
||||
@@ -72,11 +72,11 @@ export interface OAuthSettings {
|
||||
KubeSecretKey: string;
|
||||
}
|
||||
|
||||
enum AuthenticationMethod {
|
||||
export enum AuthenticationMethod {
|
||||
/**
|
||||
* Internal represents the internal authentication method (authentication against Portainer API)
|
||||
*/
|
||||
Internal,
|
||||
Internal = 1,
|
||||
/**
|
||||
* LDAP represents the LDAP authentication method (authentication against a LDAP server)
|
||||
*/
|
||||
@@ -87,6 +87,15 @@ enum AuthenticationMethod {
|
||||
OAuth,
|
||||
}
|
||||
|
||||
/**
|
||||
* The definition are based on oauth2 lib definition @https://pkg.go.dev/golang.org/x/oauth2#AuthStyle
|
||||
*/
|
||||
export enum OAuthStyle {
|
||||
AutoDetect = 0,
|
||||
InParams,
|
||||
InHeader,
|
||||
}
|
||||
|
||||
type Feature = string;
|
||||
|
||||
export interface DefaultRegistry {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"docker": "v24.0.7",
|
||||
"dockerCompose": "v2.24.6",
|
||||
"helm": "v3.13.3",
|
||||
"kubectl": "v1.29.0",
|
||||
"mingit": "2.42.0.2"
|
||||
"docker": "v24.0.9",
|
||||
"dockerCompose": "v2.26.1",
|
||||
"helm": "v3.14.4",
|
||||
"kubectl": "v1.29.4",
|
||||
"mingit": "2.44.0.1"
|
||||
}
|
||||
|
||||
@@ -13,6 +13,6 @@ if [[ ${PLATFORM} == "windows" ]]; then
|
||||
GIT_VERSION=$(echo $MINGIT_VERSION | cut -d "." -f 1-3)
|
||||
GIT_PATCH_VERSION=$(echo $MINGIT_VERSION | cut -d "." -f 4)
|
||||
|
||||
wget --tries=3 --waitretry=30 --quiet "https://github.com/git-for-windows/git/releases/download/v$GIT_VERSION.windows.$GIT_PATCH_VERSION/MinGit-$GIT_VERSION.$GIT_PATCH_VERSION-busybox-64-bit.zip"
|
||||
unzip "MinGit-$GIT_VERSION.$GIT_PATCH_VERSION-busybox-64-bit.zip" -d dist/mingit
|
||||
fi
|
||||
wget --tries=3 --waitretry=30 --quiet "https://github.com/git-for-windows/git/releases/download/v$GIT_VERSION.windows.$GIT_PATCH_VERSION/MinGit-$GIT_VERSION-busybox-64-bit.zip"
|
||||
unzip "MinGit-$GIT_VERSION-busybox-64-bit.zip" -d dist/mingit
|
||||
fi
|
||||
|
||||
54
go.mod
54
go.mod
@@ -2,7 +2,7 @@ module github.com/portainer/portainer
|
||||
|
||||
go 1.21
|
||||
|
||||
toolchain go1.21.6
|
||||
toolchain go1.21.9
|
||||
|
||||
require (
|
||||
github.com/Masterminds/semver v1.5.0
|
||||
@@ -16,8 +16,8 @@ require (
|
||||
github.com/containers/image/v5 v5.29.0
|
||||
github.com/coreos/go-semver v0.3.0
|
||||
github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5
|
||||
github.com/docker/cli v24.0.7+incompatible
|
||||
github.com/docker/docker v24.0.7+incompatible
|
||||
github.com/docker/cli v26.0.1+incompatible
|
||||
github.com/docker/docker v26.0.1+incompatible
|
||||
github.com/fvbommel/sortorder v1.0.2
|
||||
github.com/fxamacker/cbor/v2 v2.4.0
|
||||
github.com/g07cha/defender v0.0.0-20180505193036-5665c627c814
|
||||
@@ -42,15 +42,15 @@ require (
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/rs/zerolog v1.29.0
|
||||
github.com/segmentio/encoding v0.3.6
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/urfave/negroni v1.0.0
|
||||
github.com/viney-shih/go-lock v1.1.1
|
||||
go.etcd.io/bbolt v1.3.8
|
||||
golang.org/x/crypto v0.17.0
|
||||
golang.org/x/crypto v0.21.0
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d
|
||||
golang.org/x/mod v0.14.0
|
||||
golang.org/x/oauth2 v0.14.0
|
||||
golang.org/x/sync v0.5.0
|
||||
golang.org/x/oauth2 v0.17.0
|
||||
golang.org/x/sync v0.6.0
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
k8s.io/api v0.27.4
|
||||
@@ -76,15 +76,11 @@ require (
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.19 // indirect
|
||||
github.com/aws/smithy-go v1.13.4 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/checkpoint-restore/go-criu/v5 v5.3.0 // indirect
|
||||
github.com/cilium/ebpf v0.7.0 // indirect
|
||||
github.com/cloudflare/circl v1.3.7 // indirect
|
||||
github.com/containerd/console v1.0.3 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 // indirect
|
||||
github.com/containers/ocicrypt v1.1.9 // indirect
|
||||
github.com/containers/storage v1.51.0 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/distribution/reference v0.5.0 // indirect
|
||||
@@ -95,26 +91,26 @@ require (
|
||||
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
|
||||
github.com/felixge/httpsnoop v1.0.3 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.1 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.5.0 // indirect
|
||||
github.com/go-logr/logr v1.2.4 // indirect
|
||||
github.com/go-logr/logr v1.4.1 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.6 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||
github.com/go-openapi/swag v0.22.4 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/godbus/dbus/v5 v5.0.6 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/gnostic v0.5.7-v3refs // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/google/uuid v1.5.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/imdario/mergo v0.3.15 // indirect
|
||||
@@ -133,20 +129,17 @@ require (
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/spdystream v0.2.0 // indirect
|
||||
github.com/moby/sys/mountinfo v0.7.1 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/mrunalp/fileutils v0.5.1 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0-rc5 // indirect
|
||||
github.com/opencontainers/runc v1.1.12 // indirect
|
||||
github.com/opencontainers/runtime-spec v1.1.0 // indirect
|
||||
github.com/opencontainers/selinux v1.11.0 // indirect
|
||||
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/seccomp/libseccomp-golang v0.9.2-0.20220502022130-f33da4d89646 // indirect
|
||||
github.com/segmentio/asm v1.1.3 // indirect
|
||||
github.com/sergi/go-diff v1.1.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
@@ -155,23 +148,28 @@ require (
|
||||
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 // indirect
|
||||
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce // indirect
|
||||
github.com/ulikunitz/xz v0.5.11 // indirect
|
||||
github.com/urfave/cli v1.22.12 // indirect
|
||||
github.com/vbatts/tar-split v0.11.5 // indirect
|
||||
github.com/vishvananda/netlink v1.1.0 // indirect
|
||||
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||
golang.org/x/net v0.19.0 // indirect
|
||||
golang.org/x/sys v0.15.0 // indirect
|
||||
golang.org/x/term v0.15.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.50.0 // indirect
|
||||
go.opentelemetry.io/otel v1.25.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.25.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.25.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.25.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.25.0 // indirect
|
||||
golang.org/x/net v0.23.0 // indirect
|
||||
golang.org/x/sys v0.18.0 // indirect
|
||||
golang.org/x/term v0.18.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
golang.org/x/tools v0.16.1 // indirect
|
||||
google.golang.org/appengine v1.6.8 // indirect
|
||||
google.golang.org/protobuf v1.32.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240401170217-c3f982113cda // indirect
|
||||
google.golang.org/grpc v1.63.2 // indirect
|
||||
google.golang.org/protobuf v1.33.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
|
||||
123
go.sum
123
go.sum
@@ -6,7 +6,6 @@ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg6
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c h1:/IBSNwUN8+eKzUzbJPqhK839ygXJ82sde8x3ogr6R28=
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
|
||||
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
|
||||
@@ -58,20 +57,18 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
|
||||
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||
github.com/cbroglie/mustache v1.4.0 h1:Azg0dVhxTml5me+7PsZ7WPrQq1Gkf3WApcHMjMprYoU=
|
||||
github.com/cbroglie/mustache v1.4.0/go.mod h1:SS1FTIghy0sjse4DUVGV1k/40B1qE1XkD9DtDsHo9iM=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/checkpoint-restore/go-criu/v5 v5.3.0 h1:wpFFOoomK3389ue2lAb0Boag6XPht5QYpipxmSNL4d8=
|
||||
github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E=
|
||||
github.com/cilium/ebpf v0.7.0 h1:1k/q3ATgxSXRdrmPfH8d7YK0GfqVsEKZAX9dQZvs56k=
|
||||
github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
|
||||
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
|
||||
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
|
||||
github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
|
||||
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
|
||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||
github.com/containers/image/v5 v5.29.0 h1:9+nhS/ZM7c4Kuzu5tJ0NMpxrgoryOJ2HAYTgG8Ny7j4=
|
||||
github.com/containers/image/v5 v5.29.0/go.mod h1:kQ7qcDsps424ZAz24thD+x7+dJw1vgur3A9tTDsj97E=
|
||||
github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 h1:Qzk5C6cYglewc+UyGf6lc8Mj2UaPTHy/iF2De0/77CA=
|
||||
@@ -82,10 +79,7 @@ github.com/containers/storage v1.51.0 h1:AowbcpiWXzAjHosKz7MKvPEqpyX+ryZA/ZurytR
|
||||
github.com/containers/storage v1.51.0/go.mod h1:ybl8a3j1PPtpyaEi/5A6TOFs+5TrEyObeKJzVtkUlfc=
|
||||
github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=
|
||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534 h1:rtAn27wIbmOGUs7RIbVgPEjb31ehTVniDwPGXyMxm5U=
|
||||
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg=
|
||||
github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
|
||||
@@ -96,12 +90,12 @@ github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5 h1:RAV05c0xOkJ3dZGS0
|
||||
github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4=
|
||||
github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0=
|
||||
github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/docker/cli v24.0.7+incompatible h1:wa/nIwYFW7BVTGa7SWPVyyXU9lgORqUb1xfI36MSkFg=
|
||||
github.com/docker/cli v24.0.7+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/cli v26.0.1+incompatible h1:eZDuplk2jYqgUkNLDYwTBxqmY9cM3yHnmN6OIUEjL3U=
|
||||
github.com/docker/cli v26.0.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
|
||||
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker v24.0.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig1seQen0cKYlM=
|
||||
github.com/docker/docker v24.0.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker v26.0.1+incompatible h1:t39Hm6lpXuXtgkF0dm1t9a5HkbUfdGy6XbWexmGr+hA=
|
||||
github.com/docker/docker v26.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker-credential-helpers v0.8.0 h1:YQFtbBQb4VrpoPxhFuzEBPQ9E16qz5SpHLS+uswaCp8=
|
||||
github.com/docker/docker-credential-helpers v0.8.0/go.mod h1:UGFXcuoQ5TxPiB54nHOZ32AWRqQdECoh/Mg0AlEYb40=
|
||||
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
|
||||
@@ -122,9 +116,8 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
|
||||
github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84=
|
||||
github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
||||
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
|
||||
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||
github.com/fvbommel/sortorder v1.0.2 h1:mV4o8B2hKboCdkJm+a7uX/SIpZob4JzUpc5GGnM45eo=
|
||||
@@ -148,8 +141,11 @@ github.com/go-git/go-git/v5 v5.11.0/go.mod h1:6GFcX2P3NM7FPBfpePbpLd21XxsgdAt+lK
|
||||
github.com/go-ldap/ldap/v3 v3.4.1 h1:fU/0xli6HY02ocbMuozHAYsaHLcnkLjvho2r5a34BUU=
|
||||
github.com/go-ldap/ldap/v3 v3.4.1/go.mod h1:iYS1MdmrmceOJ1QOTnRXrIs7i3kloqtmGQjRvjKpyMg=
|
||||
github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
|
||||
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
|
||||
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
|
||||
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
|
||||
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
|
||||
@@ -168,8 +164,6 @@ github.com/go-playground/validator/v10 v10.12.0/go.mod h1:hCAPuzYvKdP33pxWa+2+6A
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/godbus/dbus/v5 v5.0.6 h1:mkgN1ofwASrYnJ5W6U/BxG15eXXXjirgZc7CLqkcaro=
|
||||
github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0=
|
||||
github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
@@ -190,8 +184,8 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54=
|
||||
@@ -213,8 +207,8 @@ github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJY
|
||||
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
||||
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/csrf v1.7.1 h1:Ir3o2c1/Uzj6FBxMlAUB6SivgVMy1ONXwYgXn+/aHPE=
|
||||
github.com/gorilla/csrf v1.7.1/go.mod h1:+a/4tCmqhG6/w4oafeAZ9pEa3/NZOWYVbD9fV0FwIQA=
|
||||
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
|
||||
@@ -226,6 +220,8 @@ github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
@@ -289,6 +285,8 @@ github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zk
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8=
|
||||
github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c=
|
||||
github.com/moby/sys/mountinfo v0.7.1 h1:/tTvQaSJRr2FshkhXiIpux6fQ2Zvc4j7tAhMTStAG2g=
|
||||
@@ -302,8 +300,6 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/mrunalp/fileutils v0.5.1 h1:F+S7ZlNKnrwHfSwdlgNSkKo67ReVf8o9fel6C3dkm/Q=
|
||||
github.com/mrunalp/fileutils v0.5.1/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/onsi/ginkgo/v2 v2.9.1 h1:zie5Ly042PD3bsCvsSOPvRnFwyo3rKe64TJlD6nu0mk=
|
||||
@@ -314,14 +310,10 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.0-rc5 h1:Ygwkfw9bpDvs+c9E34SdgGOj41dX/cbdlwvlWt0pnFI=
|
||||
github.com/opencontainers/image-spec v1.1.0-rc5/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8=
|
||||
github.com/opencontainers/runc v1.1.10 h1:EaL5WeO9lv9wmS6SASjszOeQdSctvpbu0DdBQBizE40=
|
||||
github.com/opencontainers/runc v1.1.10/go.mod h1:+/R6+KmDlh+hOO8NkjmgkG9Qzvypzk0yXxAPYYR65+M=
|
||||
github.com/opencontainers/runc v1.1.12 h1:BOIssBaW1La0/qbNZHXOOa71dZfZEQOzW7dqQf3phss=
|
||||
github.com/opencontainers/runc v1.1.12/go.mod h1:S+lQwSfncpBha7XTy/5lBwWgm5+y5Ma/O44Ekby9FK8=
|
||||
github.com/opencontainers/runtime-spec v1.1.0 h1:HHUyrt9mwHUjtasSbXSMvs4cyFxh+Bll4AjJ9odEGpg=
|
||||
github.com/opencontainers/runtime-spec v1.1.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
|
||||
github.com/opencontainers/selinux v1.11.0 h1:+5Zbo97w3Lbmb3PeqQtpmTkMwsW5nRI3YaLpt7tQ7oU=
|
||||
github.com/opencontainers/selinux v1.11.0/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec=
|
||||
github.com/orcaman/concurrent-map v1.0.0 h1:I/2A2XPCb4IuQWcQhBhSwGfiuybl/J0ev9HDbW65HOY=
|
||||
github.com/orcaman/concurrent-map v1.0.0/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||
@@ -348,12 +340,7 @@ github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUz
|
||||
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w=
|
||||
github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
|
||||
github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/rwtodd/Go.Sed v0.0.0-20210816025313-55464686f9ef/go.mod h1:8AEUvGVi2uQ5b24BIhcr0GCcpd/RNAFWaN2CJFrWIIQ=
|
||||
github.com/seccomp/libseccomp-golang v0.9.2-0.20220502022130-f33da4d89646 h1:RpforrEYXWkmGwJHIGnLZ3tTWStkjVVstwzNGqxX2Ds=
|
||||
github.com/seccomp/libseccomp-golang v0.9.2-0.20220502022130-f33da4d89646/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg=
|
||||
github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc=
|
||||
github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg=
|
||||
github.com/segmentio/encoding v0.3.6 h1:E6lVLyDPseWEulBmCmAKPanDd3jiyGDo5gMcugCRwZQ=
|
||||
@@ -380,26 +367,20 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI=
|
||||
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
|
||||
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce h1:fb190+cK2Xz/dvi9Hv8eCYJYvIGUTN2/KLq1pT6CjEc=
|
||||
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4=
|
||||
github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
|
||||
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/urfave/cli v1.22.12 h1:igJgVw1JdKH+trcLWLeLwZjU9fEfPesQ+9/e4MQ44S8=
|
||||
github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8=
|
||||
github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc=
|
||||
github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4=
|
||||
github.com/vbatts/tar-split v0.11.5 h1:3bHCTIheBm1qFTcgh9oPu+nNBtX+XJIupG/vacinCts=
|
||||
github.com/vbatts/tar-split v0.11.5/go.mod h1:yZbwRsSeGjusneWgA781EKej9HF8vme8okylkAeNKLk=
|
||||
github.com/viney-shih/go-lock v1.1.1 h1:SwzDPPAiHpcwGCr5k8xD15d2gQSo8d4roRYd7TDV2eI=
|
||||
github.com/viney-shih/go-lock v1.1.1/go.mod h1:Yijm78Ljteb3kRiJrbLAxVntkUukGu5uzSxq/xV7OO8=
|
||||
github.com/vishvananda/netlink v1.1.0 h1:1iyaYNBLmP6L0220aDnYQpo1QEV4t4hJ+xEEhhJH8j0=
|
||||
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
|
||||
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df h1:OviZH7qLw/7ZovXvuNyL3XQl8UFofeikI1NW1Gypu7k=
|
||||
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
||||
@@ -416,6 +397,22 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA=
|
||||
go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.50.0 h1:cEPbyTSEHlQR89XVlyo78gqluF8Y3oMeBkXGWzQsfXY=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.50.0/go.mod h1:DKdbWcT4GH1D0Y3Sqt/PFXt2naRKDWtU+eE6oLdFNA8=
|
||||
go.opentelemetry.io/otel v1.25.0 h1:gldB5FfhRl7OJQbUHt/8s0a7cE8fbsPAtdpRaApKy4k=
|
||||
go.opentelemetry.io/otel v1.25.0/go.mod h1:Wa2ds5NOXEMkCmUou1WA7ZBfLTHWIsp034OVD7AO+Vg=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.25.0 h1:dT33yIHtmsqpixFsSQPwNeY5drM9wTcoL8h0FWF4oGM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.25.0/go.mod h1:h95q0LBGh7hlAC08X2DhSeyIG02YQ0UyioTCVAqRPmc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.25.0 h1:Mbi5PKN7u322woPa85d7ebZ+SOvEoPvoiBu+ryHWgfA=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.25.0/go.mod h1:e7ciERRhZaOZXVjx5MiL8TK5+Xv7G5Gv5PA2ZDEJdL8=
|
||||
go.opentelemetry.io/otel/metric v1.25.0 h1:LUKbS7ArpFL/I2jJHdJcqMGxkRdxpPHE0VU/D4NuEwA=
|
||||
go.opentelemetry.io/otel/metric v1.25.0/go.mod h1:rkDLUSd2lC5lq2dFNrX9LGAbINP5B7WBkC78RXCpH5s=
|
||||
go.opentelemetry.io/otel/sdk v1.25.0 h1:PDryEJPC8YJZQSyLY5eqLeafHtG+X7FWnf3aXMtxbqo=
|
||||
go.opentelemetry.io/otel/sdk v1.25.0/go.mod h1:oFgzCM2zdsxKzz6zwpTZYLLQsFwc+K0daArPdIhuxkw=
|
||||
go.opentelemetry.io/otel/trace v1.25.0 h1:tqukZGLwQYRIFtSQM2u2+yfMVTgGVeqRLPUYx1Dq6RM=
|
||||
go.opentelemetry.io/otel/trace v1.25.0/go.mod h1:hCCs70XM/ljO+BeQkyFnbK28SBIJ/Emuha+ccrCRT7I=
|
||||
go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI=
|
||||
go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
@@ -425,8 +422,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
|
||||
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||
@@ -453,11 +450,11 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
|
||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
|
||||
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
||||
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
|
||||
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.14.0 h1:P0Vrf/2538nmC0H+pEQ3MNFRRnVR7RlqyVw+bvm26z0=
|
||||
golang.org/x/oauth2 v0.14.0/go.mod h1:lAtNWgaWfL4cm7j2OV8TxGi9Qb7ECORx8DktCY74OwM=
|
||||
golang.org/x/oauth2 v0.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ=
|
||||
golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -466,12 +463,11 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
||||
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -480,7 +476,6 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211110154304-99a53858aa08/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -493,16 +488,16 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
|
||||
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
|
||||
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
|
||||
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
@@ -539,9 +534,16 @@ google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoA
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240401170217-c3f982113cda h1:b6F6WIV4xHHD0FA4oIyzU6mHWg2WI2X1RBehwa5QN38=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240401170217-c3f982113cda/go.mod h1:AHcE/gZH76Bk/ROZhQphlRoWo5xKDEtz3eVEO1LfA8c=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda h1:LI5DOvAxUPMv/50agcLLoo+AdWc1irS9Rzz4vPuD1V4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM=
|
||||
google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
@@ -552,9 +554,8 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD
|
||||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
|
||||
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"author": "Portainer.io",
|
||||
"name": "portainer",
|
||||
"homepage": "http://portainer.io",
|
||||
"version": "2.20.1",
|
||||
"version": "2.20.2",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git@github.com:portainer/portainer.git"
|
||||
|
||||
@@ -14,7 +14,14 @@ import (
|
||||
// JSON encodes data to rw in JSON format. Returns a pointer to a
|
||||
// HandlerError if encoding fails.
|
||||
func JSON(rw http.ResponseWriter, data interface{}) *httperror.HandlerError {
|
||||
return JSONWithStatus(rw, data, http.StatusOK)
|
||||
}
|
||||
|
||||
// JSONWithStatus encodes data to rw in JSON format with a specific status code.
|
||||
// Returns a pointer to a HandlerError if encoding fails.
|
||||
func JSONWithStatus(rw http.ResponseWriter, data interface{}, status int) *httperror.HandlerError {
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
rw.WriteHeader(status)
|
||||
|
||||
enc := json.NewEncoder(rw)
|
||||
enc.SetSortMapKeys(false)
|
||||
|
||||
143
pkg/libhttp/response/response_test.go
Normal file
143
pkg/libhttp/response/response_test.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package response
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestJSONWithStatus(t *testing.T) {
|
||||
type TestData struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
data interface{}
|
||||
status int
|
||||
}{
|
||||
{
|
||||
name: "Success",
|
||||
data: TestData{Message: "Hello, World!"},
|
||||
status: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "Internal Server Error",
|
||||
data: TestData{Message: "Internal Server Error"},
|
||||
status: http.StatusInternalServerError,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
httpErr := JSONWithStatus(recorder, test.data, test.status)
|
||||
|
||||
assert.Nil(t, httpErr)
|
||||
assert.Equal(t, test.status, recorder.Code)
|
||||
assert.Equal(t, "application/json", recorder.Header().Get("Content-Type"))
|
||||
|
||||
var response TestData
|
||||
err := json.Unmarshal(recorder.Body.Bytes(), &response)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, test.data, response)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSON(t *testing.T) {
|
||||
type TestData struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
data interface{}
|
||||
status int
|
||||
}{
|
||||
{
|
||||
name: "Success",
|
||||
data: TestData{Message: "Hello, World!"},
|
||||
status: http.StatusOK,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
httpErr := JSONWithStatus(recorder, test.data, test.status)
|
||||
|
||||
assert.Nil(t, httpErr)
|
||||
assert.Equal(t, test.status, recorder.Code)
|
||||
assert.Equal(t, "application/json", recorder.Header().Get("Content-Type"))
|
||||
|
||||
var response TestData
|
||||
err := json.Unmarshal(recorder.Body.Bytes(), &response)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, test.data, response)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestYAML(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
data interface{}
|
||||
expected string
|
||||
invalid bool
|
||||
}{
|
||||
{
|
||||
name: "Success",
|
||||
data: "key: value",
|
||||
expected: "key: value",
|
||||
},
|
||||
{
|
||||
name: "Invalid Data",
|
||||
data: 123,
|
||||
expected: "",
|
||||
invalid: true,
|
||||
},
|
||||
{
|
||||
name: "doesn't support an Object",
|
||||
data: map[string]interface{}{
|
||||
"key": "value",
|
||||
},
|
||||
expected: "",
|
||||
invalid: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
httpErr := YAML(recorder, test.data)
|
||||
|
||||
if test.invalid {
|
||||
assert.NotNil(t, httpErr)
|
||||
assert.Equal(t, http.StatusInternalServerError, httpErr.StatusCode)
|
||||
return
|
||||
}
|
||||
|
||||
assert.Nil(t, httpErr)
|
||||
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||
assert.Equal(t, "text/yaml", recorder.Header().Get("Content-Type"))
|
||||
assert.Equal(t, test.expected, recorder.Body.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmpty(t *testing.T) {
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
httpErr := Empty(recorder)
|
||||
|
||||
assert.Nil(t, httpErr)
|
||||
assert.Equal(t, http.StatusNoContent, recorder.Code)
|
||||
}
|
||||
@@ -51,7 +51,12 @@ func getServiceStatus(service service) (libstack.Status, string) {
|
||||
return libstack.StatusRunning, ""
|
||||
case "removing":
|
||||
return libstack.StatusRemoving, ""
|
||||
case "exited", "dead":
|
||||
case "exited":
|
||||
if service.ExitCode != 0 {
|
||||
return libstack.StatusError, fmt.Sprintf("service %s exited with code %d", service.Name, service.ExitCode)
|
||||
}
|
||||
return libstack.StatusCompleted, ""
|
||||
case "dead":
|
||||
if service.ExitCode != 0 {
|
||||
return libstack.StatusError, fmt.Sprintf("service %s exited with code %d", service.Name, service.ExitCode)
|
||||
}
|
||||
@@ -94,7 +99,9 @@ func aggregateStatuses(services []service) (libstack.Status, string) {
|
||||
return libstack.StatusStarting, ""
|
||||
case statusCounts[libstack.StatusRemoving] > 0:
|
||||
return libstack.StatusRemoving, ""
|
||||
case statusCounts[libstack.StatusRunning] == servicesCount:
|
||||
case statusCounts[libstack.StatusCompleted] == servicesCount:
|
||||
return libstack.StatusCompleted, ""
|
||||
case statusCounts[libstack.StatusRunning]+statusCounts[libstack.StatusCompleted] == servicesCount:
|
||||
return libstack.StatusRunning, ""
|
||||
case statusCounts[libstack.StatusStopped] == servicesCount:
|
||||
return libstack.StatusStopped, ""
|
||||
@@ -106,15 +113,19 @@ func aggregateStatuses(services []service) (libstack.Status, string) {
|
||||
|
||||
}
|
||||
|
||||
func (wrapper *PluginWrapper) WaitForStatus(ctx context.Context, name string, status libstack.Status) <-chan string {
|
||||
errorMessageCh := make(chan string)
|
||||
func (wrapper *PluginWrapper) WaitForStatus(ctx context.Context, name string, status libstack.Status) <-chan libstack.WaitResult {
|
||||
waitResultCh := make(chan libstack.WaitResult)
|
||||
waitResult := libstack.WaitResult{
|
||||
Status: status,
|
||||
}
|
||||
|
||||
go func() {
|
||||
OUTER:
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
errorMessageCh <- fmt.Sprintf("failed to wait for status: %s", ctx.Err().Error())
|
||||
waitResult.ErrorMsg = fmt.Sprintf("failed to wait for status: %s", ctx.Err().Error())
|
||||
waitResultCh <- waitResult
|
||||
default:
|
||||
}
|
||||
|
||||
@@ -129,7 +140,7 @@ func (wrapper *PluginWrapper) WaitForStatus(ctx context.Context, name string, st
|
||||
Msg("no output from docker compose ps")
|
||||
|
||||
if status == libstack.StatusRemoved {
|
||||
errorMessageCh <- ""
|
||||
waitResultCh <- waitResult
|
||||
return
|
||||
}
|
||||
|
||||
@@ -165,18 +176,25 @@ func (wrapper *PluginWrapper) WaitForStatus(ctx context.Context, name string, st
|
||||
}
|
||||
|
||||
if len(services) == 0 && status == libstack.StatusRemoved {
|
||||
errorMessageCh <- ""
|
||||
waitResultCh <- waitResult
|
||||
return
|
||||
}
|
||||
|
||||
aggregateStatus, errorMessage := aggregateStatuses(services)
|
||||
if aggregateStatus == status {
|
||||
errorMessageCh <- ""
|
||||
waitResultCh <- waitResult
|
||||
return
|
||||
}
|
||||
|
||||
if status == libstack.StatusRunning && aggregateStatus == libstack.StatusCompleted {
|
||||
waitResult.Status = libstack.StatusCompleted
|
||||
waitResultCh <- waitResult
|
||||
return
|
||||
}
|
||||
|
||||
if errorMessage != "" {
|
||||
errorMessageCh <- errorMessage
|
||||
waitResult.ErrorMsg = errorMessage
|
||||
waitResultCh <- waitResult
|
||||
return
|
||||
}
|
||||
|
||||
@@ -188,5 +206,5 @@ func (wrapper *PluginWrapper) WaitForStatus(ctx context.Context, name string, st
|
||||
}
|
||||
}()
|
||||
|
||||
return errorMessageCh
|
||||
return waitResultCh
|
||||
}
|
||||
|
||||
@@ -108,9 +108,9 @@ func waitForStatus(deployer libstack.Deployer, ctx context.Context, stackName st
|
||||
|
||||
statusCh := deployer.WaitForStatus(ctx, stackName, requiredStatus)
|
||||
result := <-statusCh
|
||||
if result == "" {
|
||||
return requiredStatus, "", nil
|
||||
if result.ErrorMsg == "" {
|
||||
return result.Status, "", nil
|
||||
}
|
||||
|
||||
return libstack.StatusError, result, nil
|
||||
return libstack.StatusError, result.ErrorMsg, nil
|
||||
}
|
||||
|
||||
@@ -13,21 +13,27 @@ type Deployer interface {
|
||||
Remove(ctx context.Context, projectName string, filePaths []string, options Options) error
|
||||
Pull(ctx context.Context, filePaths []string, options Options) error
|
||||
Validate(ctx context.Context, filePaths []string, options Options) error
|
||||
WaitForStatus(ctx context.Context, name string, status Status) <-chan string
|
||||
WaitForStatus(ctx context.Context, name string, status Status) <-chan WaitResult
|
||||
}
|
||||
|
||||
type Status string
|
||||
|
||||
const (
|
||||
StatusUnknown Status = "unknown"
|
||||
StatusStarting Status = "starting"
|
||||
StatusRunning Status = "running"
|
||||
StatusStopped Status = "stopped"
|
||||
StatusError Status = "error"
|
||||
StatusRemoving Status = "removing"
|
||||
StatusRemoved Status = "removed"
|
||||
StatusUnknown Status = "unknown"
|
||||
StatusStarting Status = "starting"
|
||||
StatusRunning Status = "running"
|
||||
StatusStopped Status = "stopped"
|
||||
StatusError Status = "error"
|
||||
StatusRemoving Status = "removing"
|
||||
StatusRemoved Status = "removed"
|
||||
StatusCompleted Status = "completed"
|
||||
)
|
||||
|
||||
type WaitResult struct {
|
||||
Status Status
|
||||
ErrorMsg string
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
WorkingDir string
|
||||
Host string
|
||||
|
||||
Reference in New Issue
Block a user