Compare commits

..

10 Commits

Author SHA1 Message Date
Chaim Lev-Ari
a4439e90ec fix(settings): return the right value for public 2024-06-10 18:15:45 +03:00
Chaim Lev-Ari
7a8d39eab6 fix(settings): return the right value for public 2024-06-10 16:29:49 +03:00
Chaim Lev-Ari
6f048fb82d fix(settings): use getSettings 2024-06-10 16:05:24 +03:00
Chaim Lev-Ari
d0e9ea17c5 docs(settings): update api docs 2024-06-10 15:44:23 +03:00
Chaim Lev-Ari
d8c503a7f6 fix(settings): fix test 2024-06-09 16:54:43 +03:00
Chaim Lev-Ari
0944861d68 fix(settings): change access policy 2024-06-09 16:10:05 +03:00
Chaim Lev-Ari
f13351ec12 style(docker): fix imports 2024-06-09 16:09:49 +03:00
Chaim Lev-Ari
f1adc5515b fix(state): remove updateSnapshotInterval 2024-06-09 16:04:12 +03:00
Chaim Lev-Ari
5039b497bb fix(settings): remove deprecated fields 2024-06-09 16:01:05 +03:00
Chaim Lev-Ari
3386b1a18a refactor(settings): allow all to use settings api [EE-6923]
fix [EE-6923]
2024-06-09 15:51:28 +03:00
1137 changed files with 15673 additions and 22389 deletions

View File

@@ -93,11 +93,6 @@ body:
description: We only provide support for the most recent version of Portainer and the previous 3 versions. If you are on an older version of Portainer we recommend [upgrading first](https://docs.portainer.io/start/upgrade) in case your bug has already been fixed.
multiple: false
options:
- '2.22.0'
- '2.21.3'
- '2.21.2'
- '2.21.1'
- '2.21.0'
- '2.20.3'
- '2.20.2'
- '2.20.1'

View File

@@ -22,6 +22,7 @@ on:
env:
DOCKER_HUB_REPO: portainerci/portainer-ce
EXTENSION_HUB_REPO: portainerci/portainer-docker-extension
GO_VERSION: 1.21.9
NODE_VERSION: 18.x
jobs:
@@ -33,6 +34,7 @@ jobs:
- { platform: linux, arch: arm64, version: "" }
- { platform: linux, arch: arm, version: "" }
- { platform: linux, arch: ppc64le, version: "" }
- { platform: linux, arch: s390x, version: "" }
- { platform: windows, arch: amd64, version: 1809 }
- { platform: windows, arch: amd64, version: ltsc2022 }
runs-on: ubuntu-latest
@@ -45,7 +47,7 @@ jobs:
- name: '[preparation] set up golang'
uses: actions/setup-go@v5.0.0
with:
go-version-file: go.mod
go-version: ${{ env.GO_VERSION }}
- name: '[preparation] set up node.js'
uses: actions/setup-node@v4.0.1
with:
@@ -101,14 +103,14 @@ jobs:
run: |
if [ "${{ matrix.config.platform }}" == "windows" ]; then
mv dist/portainer dist/portainer.exe
docker buildx build --output=type=registry --attest type=provenance,mode=max --attest type=sbom,disabled=false --platform ${{ matrix.config.platform }}/${{ matrix.config.arch }} --build-arg OSVERSION=${{ matrix.config.version }} -t "${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}" -f build/${{ matrix.config.platform }}/Dockerfile .
docker buildx build --output=type=registry --platform ${{ matrix.config.platform }}/${{ matrix.config.arch }} --build-arg OSVERSION=${{ matrix.config.version }} -t "${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}" -f build/${{ matrix.config.platform }}/Dockerfile .
else
docker buildx build --output=type=registry --attest type=provenance,mode=max --attest type=sbom,disabled=false --platform ${{ matrix.config.platform }}/${{ matrix.config.arch }} -t "${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}" -f build/${{ matrix.config.platform }}/Dockerfile .
docker buildx build --output=type=registry --attest type=provenance,mode=max --attest type=sbom,disabled=false --platform ${{ matrix.config.platform }}/${{ matrix.config.arch }} -t "${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-alpine" -f build/${{ matrix.config.platform }}/alpine.Dockerfile .
docker buildx build --output=type=registry --platform ${{ matrix.config.platform }}/${{ matrix.config.arch }} -t "${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}" -f build/${{ matrix.config.platform }}/Dockerfile .
docker buildx build --output=type=registry --platform ${{ matrix.config.platform }}/${{ matrix.config.arch }} -t "${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-alpine" -f build/${{ matrix.config.platform }}/alpine.Dockerfile .
if [[ "${GITHUB_REF_NAME}" =~ ^release/.*$ ]]; then
docker buildx build --output=type=registry --attest type=provenance,mode=max --attest type=sbom,disabled=false --platform ${{ matrix.config.platform }}/${{ matrix.config.arch }} -t "${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}" -f build/${{ matrix.config.platform }}/Dockerfile .
docker buildx build --output=type=registry --attest type=provenance,mode=max --attest type=sbom,disabled=false --platform ${{ matrix.config.platform }}/${{ matrix.config.arch }} -t "${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-alpine" -f build/${{ matrix.config.platform }}/alpine.Dockerfile .
docker buildx build --output=type=registry --platform ${{ matrix.config.platform }}/${{ matrix.config.arch }} -t "${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}" -f build/${{ matrix.config.platform }}/Dockerfile .
docker buildx build --output=type=registry --platform ${{ matrix.config.platform }}/${{ matrix.config.arch }} -t "${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-alpine" -f build/${{ matrix.config.platform }}/alpine.Dockerfile .
fi
fi
env:
@@ -150,17 +152,25 @@ jobs:
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm64" \
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm" \
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-ppc64le" \
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-s390x" \
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-windows1809-amd64" \
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-windowsltsc2022-amd64"
docker buildx imagetools create -t "${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-alpine" \
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-amd64-alpine" \
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm64-alpine" \
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm-alpine" \
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-ppc64le-alpine"
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm-alpine"
if [[ "${GITHUB_REF_NAME}" =~ ^release/.*$ ]]; then
docker buildx imagetools create -t "${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}" \
"${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-amd64" \
"${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm64"
"${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm64" \
"${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm" \
"${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-ppc64le" \
"${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-s390x"
docker buildx imagetools create -t "${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-alpine" \
"${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-amd64-alpine" \
"${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm64-alpine" \
"${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm-alpine"
fi

View File

@@ -18,7 +18,7 @@ on:
- ready_for_review
env:
GO_VERSION: 1.22.5
GO_VERSION: 1.21.9
NODE_VERSION: 18.x
jobs:
@@ -51,5 +51,5 @@ jobs:
- name: GolangCI-Lint
uses: golangci/golangci-lint-action@v3
with:
version: v1.59.1
version: v1.55.2
args: --timeout=10m -c .golangci.yaml

View File

@@ -6,9 +6,7 @@ on:
workflow_dispatch:
env:
GO_VERSION: 1.22.5
DOCKER_HUB_REPO: portainerci/portainer-ce
DOCKER_HUB_IMAGE_TAG: develop
GO_VERSION: 1.21.9
jobs:
client-dependencies:
@@ -114,7 +112,7 @@ jobs:
uses: docker://docker.io/aquasec/trivy:latest
continue-on-error: true
with:
args: image --ignore-unfixed=true --vuln-type="os,library" --exit-code=1 --format="json" --output="image-trivy.json" --no-progress ${{ env.DOCKER_HUB_REPO }}:${{ env.DOCKER_HUB_IMAGE_TAG }}
args: image --ignore-unfixed=true --vuln-type="os,library" --exit-code=1 --format="json" --output="image-trivy.json" --no-progress portainerci/portainer:develop
- name: upload Trivy image security scan result as artifact
uses: actions/upload-artifact@v3
@@ -143,7 +141,7 @@ jobs:
continue-on-error: true
with:
command: cves
image: ${{ env.DOCKER_HUB_REPO }}:${{ env.DOCKER_HUB_IMAGE_TAG }}
image: portainerci/portainer:develop
sarif-file: image-docker-scout.json
dockerhub-user: ${{ secrets.DOCKER_HUB_USERNAME }}
dockerhub-password: ${{ secrets.DOCKER_HUB_PASSWORD }}

View File

@@ -14,7 +14,7 @@ on:
- '.github/workflows/pr-security.yml'
env:
GO_VERSION: 1.22.5
GO_VERSION: 1.21.9
NODE_VERSION: 18.x
jobs:

View File

@@ -1,11 +1,10 @@
name: Test
env:
GO_VERSION: 1.22.5
GO_VERSION: 1.21.9
NODE_VERSION: 18.x
on:
workflow_dispatch:
pull_request:
branches:
- master
@@ -28,22 +27,15 @@ jobs:
if: github.event.pull_request.draft == false
steps:
- name: 'checkout the current branch'
uses: actions/checkout@v4.1.1
with:
ref: ${{ github.event.inputs.branch }}
- name: 'set up node.js'
uses: actions/setup-node@v4.0.1
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'yarn'
- run: yarn --frozen-lockfile
- name: Run tests
run: make test-client ARGS="--maxWorkers=2 --minWorkers=1"
test-server:
strategy:
matrix:
@@ -56,21 +48,9 @@ jobs:
if: github.event.pull_request.draft == false
steps:
- name: 'checkout the current branch'
uses: actions/checkout@v4.1.1
with:
ref: ${{ github.event.inputs.branch }}
- name: 'set up golang'
uses: actions/setup-go@v5.0.0
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: ${{ env.GO_VERSION }}
- name: 'install dependencies'
run: make test-deps PLATFORM=linux ARCH=amd64
- name: 'update $PATH'
run: echo "$(pwd)/dist" >> $GITHUB_PATH
- name: 'run tests'
- name: Run tests
run: make test-server

View File

@@ -13,7 +13,7 @@ on:
- ready_for_review
env:
GO_VERSION: 1.22.5
GO_VERSION: 1.21.9
NODE_VERSION: 18.x
jobs:

View File

@@ -9,6 +9,7 @@ linters:
- gosimple
- govet
- errorlint
- exportloopref
linters-settings:
depguard:

View File

@@ -30,7 +30,7 @@ build-server: init-dist ## Build the server binary
./build/build_binary.sh "$(PLATFORM)" "$(ARCH)"
build-image: build-all ## Build the Portainer image locally
docker buildx build --load -t portainerci/portainer-ce:$(TAG) -f build/linux/Dockerfile .
docker buildx build --load -t portainerci/portainer:$(TAG) -f build/linux/Dockerfile .
build-storybook: ## Build and serve the storybook files
yarn storybook:build
@@ -64,9 +64,6 @@ clean: ## Remove all build and download artifacts
.PHONY: test test-client test-server
test: test-server test-client ## Run all tests
test-deps: init-dist
./build/download_docker_compose_binary.sh $(PLATFORM) $(ARCH) $(shell jq -r '.dockerCompose' < "./binary-version.json")
test-client: ## Run client tests
yarn test $(ARGS)
@@ -85,8 +82,6 @@ dev-client: ## Run the client in development mode
dev-server: build-server ## Run the server in development mode
@./dev/run_container.sh
dev-server-podman: build-server ## Run the server in development mode
@./dev/run_container_podman.sh
##@ Format
.PHONY: format format-client format-server

View File

@@ -10,7 +10,7 @@ import (
"time"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/url"
"github.com/portainer/portainer/api/internal/url"
)
// GetAgentVersionAndPlatform returns the agent version and platform

View File

@@ -3,6 +3,7 @@ package apikey
import (
"testing"
"github.com/portainer/portainer/api/internal/securecookie"
"github.com/stretchr/testify/assert"
)
@@ -33,19 +34,17 @@ func Test_generateRandomKey(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := GenerateRandomKey(tt.wantLenth)
got := securecookie.GenerateRandomKey(tt.wantLenth)
is.Equal(tt.wantLenth, len(got))
})
}
t.Run("Generated keys are unique", func(t *testing.T) {
keys := make(map[string]bool)
for range 100 {
key := GenerateRandomKey(8)
for i := 0; i < 100; i++ {
key := securecookie.GenerateRandomKey(8)
_, ok := keys[string(key)]
is.False(ok)
keys[string(key)] = true
}
})

View File

@@ -1,79 +1,69 @@
package apikey
import (
portainer "github.com/portainer/portainer/api"
lru "github.com/hashicorp/golang-lru"
portainer "github.com/portainer/portainer/api"
)
const DefaultAPIKeyCacheSize = 1024
const defaultAPIKeyCacheSize = 1024
// entry is a tuple containing the user and API key associated to an API key digest
type entry[T any] struct {
user T
type entry struct {
user portainer.User
apiKey portainer.APIKey
}
type UserCompareFn[T any] func(T, portainer.UserID) bool
// ApiKeyCache is a concurrency-safe, in-memory cache which primarily exists for to reduce database roundtrips.
// apiKeyCache is a concurrency-safe, in-memory cache which primarily exists for to reduce database roundtrips.
// We store the api-key digest (keys) and the associated user and key-data (values) in the cache.
// This is required because HTTP requests will contain only the api-key digest in the x-api-key request header;
// digest value must be mapped to a portainer user (and respective key data) for validation.
// This cache is used to avoid multiple database queries to retrieve these user/key associated to the digest.
type ApiKeyCache[T any] struct {
type apiKeyCache struct {
// cache type [string]entry cache (key: string(digest), value: user/key entry)
// note: []byte keys are not supported by golang-lru Cache
cache *lru.Cache
userCmpFn UserCompareFn[T]
cache *lru.Cache
}
// NewAPIKeyCache creates a new cache for API keys
func NewAPIKeyCache[T any](cacheSize int, userCompareFn UserCompareFn[T]) *ApiKeyCache[T] {
func NewAPIKeyCache(cacheSize int) *apiKeyCache {
cache, _ := lru.New(cacheSize)
return &ApiKeyCache[T]{cache: cache, userCmpFn: userCompareFn}
return &apiKeyCache{cache: cache}
}
// Get returns the user/key associated to an api-key's digest
// This is required because HTTP requests will contain the digest of the API key in header,
// the digest value must be mapped to a portainer user.
func (c *ApiKeyCache[T]) Get(digest string) (T, portainer.APIKey, bool) {
func (c *apiKeyCache) Get(digest string) (portainer.User, portainer.APIKey, bool) {
val, ok := c.cache.Get(digest)
if !ok {
var t T
return t, portainer.APIKey{}, false
return portainer.User{}, portainer.APIKey{}, false
}
tuple := val.(entry[T])
tuple := val.(entry)
return tuple.user, tuple.apiKey, true
}
// Set persists a user/key entry to the cache
func (c *ApiKeyCache[T]) Set(digest string, user T, apiKey portainer.APIKey) {
c.cache.Add(digest, entry[T]{
func (c *apiKeyCache) Set(digest string, user portainer.User, apiKey portainer.APIKey) {
c.cache.Add(digest, entry{
user: user,
apiKey: apiKey,
})
}
// Delete evicts a digest's user/key entry key from the cache
func (c *ApiKeyCache[T]) Delete(digest string) {
func (c *apiKeyCache) Delete(digest string) {
c.cache.Remove(digest)
}
// InvalidateUserKeyCache loops through all the api-keys associated to a user and removes them from the cache
func (c *ApiKeyCache[T]) InvalidateUserKeyCache(userId portainer.UserID) bool {
func (c *apiKeyCache) InvalidateUserKeyCache(userId portainer.UserID) bool {
present := false
for _, k := range c.cache.Keys() {
user, _, _ := c.Get(k.(string))
if c.userCmpFn(user, userId) {
if user.ID == userId {
present = c.cache.Remove(k)
}
}
return present
}

View File

@@ -10,11 +10,11 @@ import (
func Test_apiKeyCacheGet(t *testing.T) {
is := assert.New(t)
keyCache := NewAPIKeyCache(10, compareUser)
keyCache := NewAPIKeyCache(10)
// pre-populate cache
keyCache.cache.Add(string("foo"), entry[portainer.User]{user: portainer.User{}, apiKey: portainer.APIKey{}})
keyCache.cache.Add(string(""), entry[portainer.User]{user: portainer.User{}, apiKey: portainer.APIKey{}})
keyCache.cache.Add(string("foo"), entry{user: portainer.User{}, apiKey: portainer.APIKey{}})
keyCache.cache.Add(string(""), entry{user: portainer.User{}, apiKey: portainer.APIKey{}})
tests := []struct {
digest string
@@ -35,7 +35,7 @@ func Test_apiKeyCacheGet(t *testing.T) {
}
for _, test := range tests {
t.Run(test.digest, func(t *testing.T) {
t.Run(string(test.digest), func(t *testing.T) {
_, _, found := keyCache.Get(test.digest)
is.Equal(test.found, found)
})
@@ -45,7 +45,7 @@ func Test_apiKeyCacheGet(t *testing.T) {
func Test_apiKeyCacheSet(t *testing.T) {
is := assert.New(t)
keyCache := NewAPIKeyCache(10, compareUser)
keyCache := NewAPIKeyCache(10)
// pre-populate cache
keyCache.Set("bar", portainer.User{ID: 2}, portainer.APIKey{})
@@ -57,23 +57,23 @@ func Test_apiKeyCacheSet(t *testing.T) {
val, ok := keyCache.cache.Get(string("bar"))
is.True(ok)
tuple := val.(entry[portainer.User])
tuple := val.(entry)
is.Equal(portainer.User{ID: 2}, tuple.user)
val, ok = keyCache.cache.Get(string("foo"))
is.True(ok)
tuple = val.(entry[portainer.User])
tuple = val.(entry)
is.Equal(portainer.User{ID: 3}, tuple.user)
}
func Test_apiKeyCacheDelete(t *testing.T) {
is := assert.New(t)
keyCache := NewAPIKeyCache(10, compareUser)
keyCache := NewAPIKeyCache(10)
t.Run("Delete an existing entry", func(t *testing.T) {
keyCache.cache.Add(string("foo"), entry[portainer.User]{user: portainer.User{ID: 1}, apiKey: portainer.APIKey{}})
keyCache.cache.Add(string("foo"), entry{user: portainer.User{ID: 1}, apiKey: portainer.APIKey{}})
keyCache.Delete("foo")
_, ok := keyCache.cache.Get(string("foo"))
@@ -128,7 +128,7 @@ func Test_apiKeyCacheLRU(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
keyCache := NewAPIKeyCache(test.cacheLen, compareUser)
keyCache := NewAPIKeyCache(test.cacheLen)
for _, key := range test.key {
keyCache.Set(key, portainer.User{ID: 1}, portainer.APIKey{})
@@ -150,10 +150,10 @@ func Test_apiKeyCacheLRU(t *testing.T) {
func Test_apiKeyCacheInvalidateUserKeyCache(t *testing.T) {
is := assert.New(t)
keyCache := NewAPIKeyCache(10, compareUser)
keyCache := NewAPIKeyCache(10)
t.Run("Removes users keys from cache", func(t *testing.T) {
keyCache.cache.Add(string("foo"), entry[portainer.User]{user: portainer.User{ID: 1}, apiKey: portainer.APIKey{}})
keyCache.cache.Add(string("foo"), entry{user: portainer.User{ID: 1}, apiKey: portainer.APIKey{}})
ok := keyCache.InvalidateUserKeyCache(1)
is.True(ok)
@@ -163,8 +163,8 @@ func Test_apiKeyCacheInvalidateUserKeyCache(t *testing.T) {
})
t.Run("Does not affect other keys", func(t *testing.T) {
keyCache.cache.Add(string("foo"), entry[portainer.User]{user: portainer.User{ID: 1}, apiKey: portainer.APIKey{}})
keyCache.cache.Add(string("bar"), entry[portainer.User]{user: portainer.User{ID: 2}, apiKey: portainer.APIKey{}})
keyCache.cache.Add(string("foo"), entry{user: portainer.User{ID: 1}, apiKey: portainer.APIKey{}})
keyCache.cache.Add(string("bar"), entry{user: portainer.User{ID: 2}, apiKey: portainer.APIKey{}})
ok := keyCache.InvalidateUserKeyCache(1)
is.True(ok)

View File

@@ -1,15 +1,14 @@
package apikey
import (
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"fmt"
"io"
"time"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/internal/securecookie"
"github.com/pkg/errors"
)
@@ -21,45 +20,30 @@ var ErrInvalidAPIKey = errors.New("Invalid API key")
type apiKeyService struct {
apiKeyRepository dataservices.APIKeyRepository
userRepository dataservices.UserService
cache *ApiKeyCache[portainer.User]
}
// GenerateRandomKey generates a random key of specified length
// source: https://github.com/gorilla/securecookie/blob/master/securecookie.go#L515
func GenerateRandomKey(length int) []byte {
k := make([]byte, length)
if _, err := io.ReadFull(rand.Reader, k); err != nil {
return nil
}
return k
}
func compareUser(u portainer.User, id portainer.UserID) bool {
return u.ID == id
cache *apiKeyCache
}
func NewAPIKeyService(apiKeyRepository dataservices.APIKeyRepository, userRepository dataservices.UserService) *apiKeyService {
return &apiKeyService{
apiKeyRepository: apiKeyRepository,
userRepository: userRepository,
cache: NewAPIKeyCache(DefaultAPIKeyCacheSize, compareUser),
cache: NewAPIKeyCache(defaultAPIKeyCacheSize),
}
}
// HashRaw computes a hash digest of provided raw API key.
func (a *apiKeyService) HashRaw(rawKey string) string {
hashDigest := sha256.Sum256([]byte(rawKey))
return base64.StdEncoding.EncodeToString(hashDigest[:])
}
// GenerateApiKey generates a raw API key for a user (for one-time display).
// The generated API key is stored in the cache and database.
func (a *apiKeyService) GenerateApiKey(user portainer.User, description string) (string, *portainer.APIKey, error) {
randKey := GenerateRandomKey(32)
randKey := securecookie.GenerateRandomKey(32)
encodedRawAPIKey := base64.StdEncoding.EncodeToString(randKey)
prefixedAPIKey := portainerAPIKeyPrefix + encodedRawAPIKey
hashDigest := a.HashRaw(prefixedAPIKey)
apiKey := &portainer.APIKey{
@@ -70,7 +54,8 @@ func (a *apiKeyService) GenerateApiKey(user portainer.User, description string)
Digest: hashDigest,
}
if err := a.apiKeyRepository.Create(apiKey); err != nil {
err := a.apiKeyRepository.Create(apiKey)
if err != nil {
return "", nil, errors.Wrap(err, "Unable to create API key")
}
@@ -93,6 +78,7 @@ func (a *apiKeyService) GetAPIKeys(userID portainer.UserID) ([]portainer.APIKey,
// GetDigestUserAndKey returns the user and api-key associated to a specified hash digest.
// A cache lookup is performed first; if the user/api-key is not found in the cache, respective database lookups are performed.
func (a *apiKeyService) GetDigestUserAndKey(digest string) (portainer.User, portainer.APIKey, error) {
// get api key from cache if possible
cachedUser, cachedKey, ok := a.cache.Get(digest)
if ok {
return cachedUser, cachedKey, nil
@@ -120,21 +106,20 @@ func (a *apiKeyService) UpdateAPIKey(apiKey *portainer.APIKey) error {
if err != nil {
return errors.Wrap(err, "Unable to retrieve API key")
}
a.cache.Set(apiKey.Digest, user, *apiKey)
return a.apiKeyRepository.Update(apiKey.ID, apiKey)
}
// DeleteAPIKey deletes an API key and removes the digest/api-key entry from the cache.
func (a *apiKeyService) DeleteAPIKey(apiKeyID portainer.APIKeyID) error {
// get api-key digest to remove from cache
apiKey, err := a.apiKeyRepository.Read(apiKeyID)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("Unable to retrieve API key: %d", apiKeyID))
}
// delete the user/api-key from cache
a.cache.Delete(apiKey.Digest)
return a.apiKeyRepository.Delete(apiKeyID)
}

View File

@@ -287,7 +287,7 @@ func (service *Service) checkTunnels() {
Msg("unable to snapshot Edge environment")
}
service.close(endpointID)
service.close(portainer.EndpointID(endpointID))
return
}

View File

@@ -15,10 +15,8 @@ import (
func TestPingAgentPanic(t *testing.T) {
endpoint := &portainer.Endpoint{
ID: 1,
EdgeID: "test-edge-id",
Type: portainer.EdgeAgentOnDockerEnvironment,
UserTrusted: true,
ID: 1,
Type: portainer.EdgeAgentOnDockerEnvironment,
}
_, store := datastore.MustNewTestStore(t, true, true)
@@ -44,8 +42,7 @@ func TestPingAgentPanic(t *testing.T) {
errCh <- srv.Serve(ln)
}()
err = s.Open(endpoint)
require.NoError(t, err)
s.Open(endpoint)
s.activeTunnels[endpoint.ID].Port = ln.Addr().(*net.TCPAddr).Port
require.Error(t, s.pingAgent(endpoint.ID))

View File

@@ -24,24 +24,14 @@ const (
maxAvailablePort = 65535
)
var (
ErrNonEdgeEnv = errors.New("cannot open a tunnel for non-edge environments")
ErrAsyncEnv = errors.New("cannot open a tunnel for async edge environments")
ErrInvalidEnv = errors.New("cannot open a tunnel for an invalid environment")
)
// Open will mark the tunnel as REQUIRED so the agent opens it
func (s *Service) Open(endpoint *portainer.Endpoint) error {
if !endpointutils.IsEdgeEndpoint(endpoint) {
return ErrNonEdgeEnv
return errors.New("cannot open a tunnel for non-edge environments")
}
if endpoint.Edge.AsyncMode {
return ErrAsyncEnv
}
if endpoint.ID == 0 || endpoint.EdgeID == "" || !endpoint.UserTrusted {
return ErrInvalidEnv
return errors.New("cannot open a tunnel for async edge environments")
}
s.mu.Lock()

View File

@@ -17,14 +17,17 @@ import (
type Service struct{}
var (
ErrInvalidEndpointProtocol = errors.New("Invalid environment protocol: Portainer only supports unix://, npipe:// or tcp://")
ErrSocketOrNamedPipeNotFound = errors.New("Unable to locate Unix socket or named pipe")
ErrInvalidSnapshotInterval = errors.New("Invalid snapshot interval")
ErrAdminPassExcludeAdminPassFile = errors.New("Cannot use --admin-password with --admin-password-file")
errInvalidEndpointProtocol = errors.New("Invalid environment protocol: Portainer only supports unix://, npipe:// or tcp://")
errSocketOrNamedPipeNotFound = errors.New("Unable to locate Unix socket or named pipe")
errInvalidSnapshotInterval = errors.New("Invalid snapshot interval")
errAdminPassExcludeAdminPassFile = errors.New("Cannot use --admin-password with --admin-password-file")
)
func CLIFlags() *portainer.CLIFlags {
return &portainer.CLIFlags{
// ParseFlags parse the CLI flags and return a portainer.Flags struct
func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
kingpin.Version(version)
flags := &portainer.CLIFlags{
Addr: kingpin.Flag("bind", "Address and port to serve Portainer").Default(defaultBindAddress).Short('p').String(),
AddrHTTPS: kingpin.Flag("bind-https", "Address and port to serve Portainer via https").Default(defaultHTTPSBindAddress).String(),
TunnelAddr: kingpin.Flag("tunnel-addr", "Address to serve the tunnel server").Default(defaultTunnelServerAddress).String(),
@@ -60,13 +63,6 @@ func CLIFlags() *portainer.CLIFlags {
LogLevel: kingpin.Flag("log-level", "Set the minimum logging level to show").Default("INFO").Enum("DEBUG", "INFO", "WARN", "ERROR"),
LogMode: kingpin.Flag("log-mode", "Set the logging output mode").Default("PRETTY").Enum("NOCOLOR", "PRETTY", "JSON"),
}
}
// ParseFlags parse the CLI flags and return a portainer.Flags struct
func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
kingpin.Version(version)
flags := CLIFlags()
kingpin.Parse()
@@ -86,16 +82,18 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
displayDeprecationWarnings(flags)
if err := validateEndpointURL(*flags.EndpointURL); err != nil {
err := validateEndpointURL(*flags.EndpointURL)
if err != nil {
return err
}
if err := validateSnapshotInterval(*flags.SnapshotInterval); err != nil {
err = validateSnapshotInterval(*flags.SnapshotInterval)
if err != nil {
return err
}
if *flags.AdminPassword != "" && *flags.AdminPasswordFile != "" {
return ErrAdminPassExcludeAdminPassFile
return errAdminPassExcludeAdminPassFile
}
return nil
@@ -117,16 +115,15 @@ func validateEndpointURL(endpointURL string) error {
}
if !strings.HasPrefix(endpointURL, "unix://") && !strings.HasPrefix(endpointURL, "tcp://") && !strings.HasPrefix(endpointURL, "npipe://") {
return ErrInvalidEndpointProtocol
return errInvalidEndpointProtocol
}
if strings.HasPrefix(endpointURL, "unix://") || strings.HasPrefix(endpointURL, "npipe://") {
socketPath := strings.TrimPrefix(endpointURL, "unix://")
socketPath = strings.TrimPrefix(socketPath, "npipe://")
if _, err := os.Stat(socketPath); err != nil {
if os.IsNotExist(err) {
return ErrSocketOrNamedPipeNotFound
return errSocketOrNamedPipeNotFound
}
return err
@@ -141,8 +138,9 @@ func validateSnapshotInterval(snapshotInterval string) error {
return nil
}
if _, err := time.ParseDuration(snapshotInterval); err != nil {
return ErrInvalidSnapshotInterval
_, err := time.ParseDuration(snapshotInterval)
if err != nil {
return errInvalidSnapshotInterval
}
return nil

View File

@@ -54,7 +54,7 @@ func setLoggingMode(mode string) {
}
}
func formatMessage(i any) string {
func formatMessage(i interface{}) string {
if i == nil {
return ""
}

View File

@@ -1,7 +1,6 @@
package main
import (
"cmp"
"context"
"crypto/sha256"
"os"
@@ -45,7 +44,6 @@ import (
"github.com/portainer/portainer/api/pendingactions"
"github.com/portainer/portainer/api/pendingactions/actions"
"github.com/portainer/portainer/api/pendingactions/handlers"
"github.com/portainer/portainer/api/platform"
"github.com/portainer/portainer/api/scheduler"
"github.com/portainer/portainer/api/stacks/deployments"
"github.com/portainer/portainer/pkg/featureflags"
@@ -58,14 +56,14 @@ import (
)
func initCLI() *portainer.CLIFlags {
cliService := &cli.Service{}
var cliService portainer.CLIService = &cli.Service{}
flags, err := cliService.ParseFlags(portainer.APIVersion)
if err != nil {
log.Fatal().Err(err).Msg("failed parsing flags")
}
if err := cliService.ValidateFlags(flags); err != nil {
err = cliService.ValidateFlags(flags)
if err != nil {
log.Fatal().Err(err).Msg("failed validating flags")
}
@@ -96,14 +94,14 @@ func initDataStore(flags *portainer.CLIFlags, secretKey []byte, fileService port
}
store := datastore.NewStore(*flags.Data, fileService, connection)
isNew, err := store.Open()
if err != nil {
log.Fatal().Err(err).Msg("failed opening store")
}
if *flags.Rollback {
if err := store.Rollback(false); err != nil {
err := store.Rollback(false)
if err != nil {
log.Fatal().Err(err).Msg("failed rolling back")
}
@@ -112,7 +110,8 @@ func initDataStore(flags *portainer.CLIFlags, secretKey []byte, fileService port
}
// Init sets some defaults - it's basically a migration
if err := store.Init(); err != nil {
err = store.Init()
if err != nil {
log.Fatal().Err(err).Msg("failed initializing data store")
}
@@ -134,23 +133,25 @@ func initDataStore(flags *portainer.CLIFlags, secretKey []byte, fileService port
}
store.VersionService.UpdateVersion(&v)
if err := updateSettingsFromFlags(store, flags); err != nil {
err = updateSettingsFromFlags(store, flags)
if err != nil {
log.Fatal().Err(err).Msg("failed updating settings from flags")
}
} else {
if err := store.MigrateData(); err != nil {
err = store.MigrateData()
if err != nil {
log.Fatal().Err(err).Msg("failed migration")
}
}
if err := updateSettingsFromFlags(store, flags); err != nil {
err = updateSettingsFromFlags(store, flags)
if err != nil {
log.Fatal().Err(err).Msg("failed updating settings from flags")
}
// this is for the db restore functionality - needs more tests.
go func() {
<-shutdownCtx.Done()
defer connection.Close()
}()
@@ -204,16 +205,36 @@ func initJWTService(userSessionTimeout string, dataStore dataservices.DataStore)
userSessionTimeout = portainer.DefaultUserSessionTimeout
}
return jwt.NewService(userSessionTimeout, dataStore)
jwtService, err := jwt.NewService(userSessionTimeout, dataStore)
if err != nil {
return nil, err
}
return jwtService, nil
}
func initDigitalSignatureService() portainer.DigitalSignatureService {
return crypto.NewECDSAService(os.Getenv("AGENT_SECRET"))
}
func initCryptoService() portainer.CryptoService {
return &crypto.Service{}
}
func initLDAPService() portainer.LDAPService {
return &ldap.Service{}
}
func initOAuthService() portainer.OAuthService {
return oauth.NewService()
}
func initGitService(ctx context.Context) portainer.GitService {
return git.NewService(ctx)
}
func initSSLService(addr, certPath, keyPath string, fileService portainer.FileService, dataStore dataservices.DataStore, shutdownTrigger context.CancelFunc) (*ssl.Service, error) {
slices := strings.Split(addr, ":")
host := slices[0]
if host == "" {
host = "0.0.0.0"
@@ -221,13 +242,22 @@ func initSSLService(addr, certPath, keyPath string, fileService portainer.FileSe
sslService := ssl.NewService(fileService, dataStore, shutdownTrigger)
if err := sslService.Init(host, certPath, keyPath); err != nil {
err := sslService.Init(host, certPath, keyPath)
if err != nil {
return nil, err
}
return sslService, nil
}
func initDockerClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService) *dockerclient.ClientFactory {
return dockerclient.NewClientFactory(signatureService, reverseTunnelService)
}
func initKubernetesClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService, dataStore dataservices.DataStore, instanceID, addrHTTPS, userSessionTimeout string) (*kubecli.ClientFactory, error) {
return kubecli.NewClientFactory(signatureService, reverseTunnelService, dataStore, instanceID, addrHTTPS, userSessionTimeout)
}
func initSnapshotService(
snapshotIntervalFromFlag string,
dataStore dataservices.DataStore,
@@ -260,21 +290,34 @@ func updateSettingsFromFlags(dataStore dataservices.DataStore, flags *portainer.
return err
}
settings.SnapshotInterval = *cmp.Or(flags.SnapshotInterval, &settings.SnapshotInterval)
settings.LogoURL = *cmp.Or(flags.Logo, &settings.LogoURL)
settings.EnableEdgeComputeFeatures = *cmp.Or(flags.EnableEdgeComputeFeatures, &settings.EnableEdgeComputeFeatures)
settings.TemplatesURL = *cmp.Or(flags.Templates, &settings.TemplatesURL)
if *flags.SnapshotInterval != "" {
settings.SnapshotInterval = *flags.SnapshotInterval
}
if *flags.Logo != "" {
settings.LogoURL = *flags.Logo
}
if *flags.EnableEdgeComputeFeatures {
settings.EnableEdgeComputeFeatures = *flags.EnableEdgeComputeFeatures
}
if *flags.Templates != "" {
settings.TemplatesURL = *flags.Templates
}
if *flags.Labels != nil {
settings.BlackListedLabels = *flags.Labels
}
settings.AgentSecret = ""
if agentKey, ok := os.LookupEnv("AGENT_SECRET"); ok {
settings.AgentSecret = agentKey
} else {
settings.AgentSecret = ""
}
if err := dataStore.Settings().UpdateSettings(settings); err != nil {
err = dataStore.Settings().UpdateSettings(settings)
if err != nil {
return err
}
@@ -297,7 +340,6 @@ func loadAndParseKeyPair(fileService portainer.FileService, signatureService por
if err != nil {
return err
}
return signatureService.ParseKeyPair(private, public)
}
@@ -306,9 +348,7 @@ func generateAndStoreKeyPair(fileService portainer.FileService, signatureService
if err != nil {
return err
}
privateHeader, publicHeader := signatureService.PEMHeaders()
return fileService.StoreKeyPair(private, public, privateHeader, publicHeader)
}
@@ -321,7 +361,6 @@ func initKeyPair(fileService portainer.FileService, signatureService portainer.D
if existingKeyPair {
return loadAndParseKeyPair(fileService, signatureService)
}
return generateAndStoreKeyPair(fileService, signatureService)
}
@@ -339,7 +378,6 @@ func loadEncryptionSecretKey(keyfilename string) []byte {
// return a 32 byte hash of the secret (required for AES)
hash := sha256.Sum256(content)
return hash[:]
}
@@ -384,17 +422,17 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
log.Fatal().Err(err).Msg("failed initializing JWT service")
}
ldapService := &ldap.Service{}
ldapService := initLDAPService()
oauthService := oauth.NewService()
oauthService := initOAuthService()
gitService := git.NewService(shutdownCtx)
gitService := initGitService(shutdownCtx)
openAMTService := openamt.NewService()
cryptoService := &crypto.Service{}
cryptoService := initCryptoService()
signatureService := initDigitalSignatureService()
digitalSignatureService := initDigitalSignatureService()
edgeStacksService := edgestacks.NewService(dataStore)
@@ -408,18 +446,15 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
log.Fatal().Err(err).Msg("failed to get SSL settings")
}
if err := initKeyPair(fileService, signatureService); err != nil {
err = initKeyPair(fileService, digitalSignatureService)
if err != nil {
log.Fatal().Err(err).Msg("failed initializing key pair")
}
reverseTunnelService := chisel.NewService(dataStore, shutdownCtx, fileService)
dockerClientFactory := dockerclient.NewClientFactory(signatureService, reverseTunnelService)
kubernetesClientFactory, err := kubecli.NewClientFactory(signatureService, reverseTunnelService, dataStore, instanceID, *flags.AddrHTTPS, settings.UserSessionTimeout)
if err != nil {
log.Fatal().Err(err).Msg("failed initializing Kubernetes Client Factory service")
}
dockerClientFactory := initDockerClientFactory(digitalSignatureService, reverseTunnelService)
kubernetesClientFactory, err := initKubernetesClientFactory(digitalSignatureService, reverseTunnelService, dataStore, instanceID, *flags.AddrHTTPS, settings.UserSessionTimeout)
authorizationService := authorization.NewService(dataStore)
authorizationService.K8sClientFactory = kubernetesClientFactory
@@ -441,12 +476,12 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
composeStackManager := initComposeStackManager(composeDeployer, proxyManager)
swarmStackManager, err := initSwarmStackManager(*flags.Assets, dockerConfigPath, signatureService, fileService, reverseTunnelService, dataStore)
swarmStackManager, err := initSwarmStackManager(*flags.Assets, dockerConfigPath, digitalSignatureService, fileService, reverseTunnelService, dataStore)
if err != nil {
log.Fatal().Err(err).Msg("failed initializing swarm stack manager")
}
kubernetesDeployer := initKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, signatureService, proxyManager, *flags.Assets)
kubernetesDeployer := initKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, digitalSignatureService, proxyManager, *flags.Assets)
pendingActionsService := pendingactions.NewService(dataStore, kubernetesClientFactory)
pendingActionsService.RegisterHandler(actions.CleanNAPWithOverridePolicies, handlers.NewHandlerCleanNAPWithOverridePolicies(authorizationService, dataStore))
@@ -457,17 +492,17 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
if err != nil {
log.Fatal().Err(err).Msg("failed initializing snapshot service")
}
snapshotService.Start()
proxyManager.NewProxyFactory(dataStore, signatureService, reverseTunnelService, dockerClientFactory, kubernetesClientFactory, kubernetesTokenCacheManager, gitService, snapshotService)
proxyManager.NewProxyFactory(dataStore, digitalSignatureService, reverseTunnelService, dockerClientFactory, kubernetesClientFactory, kubernetesTokenCacheManager, gitService, snapshotService)
helmPackageManager, err := initHelmPackageManager(*flags.Assets)
if err != nil {
log.Fatal().Err(err).Msg("failed initializing helm package manager")
}
if err := edge.LoadEdgeJobs(dataStore, reverseTunnelService); err != nil {
err = edge.LoadEdgeJobs(dataStore, reverseTunnelService)
if err != nil {
log.Fatal().Err(err).Msg("failed loading edge jobs from database")
}
@@ -479,7 +514,6 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
go endpointutils.InitEndpoint(shutdownCtx, adminCreationDone, flags, dataStore, snapshotService)
adminPasswordHash := ""
if *flags.AdminPasswordFile != "" {
content, err := fileService.GetFileContent(*flags.AdminPasswordFile, "")
if err != nil {
@@ -502,14 +536,14 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
if len(users) == 0 {
log.Info().Msg("created admin user with the given password.")
user := &portainer.User{
Username: "admin",
Role: portainer.AdministratorRole,
Password: adminPasswordHash,
}
if err := dataStore.User().Create(user); err != nil {
err := dataStore.User().Create(user)
if err != nil {
log.Fatal().Err(err).Msg("failed creating admin user")
}
@@ -520,7 +554,8 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
}
}
if err := reverseTunnelService.StartTunnelServer(*flags.TunnelAddr, *flags.TunnelPort, snapshotService); err != nil {
err = reverseTunnelService.StartTunnelServer(*flags.TunnelAddr, *flags.TunnelPort, snapshotService)
if err != nil {
log.Fatal().Err(err).Msg("failed starting tunnel server")
}
@@ -533,20 +568,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
log.Fatal().Msg("failed to fetch SSL settings from DB")
}
platformService, err := platform.NewService(dataStore)
if err != nil {
log.Fatal().Err(err).Msg("failed initializing platform service")
}
upgradeService, err := upgrade.NewService(
*flags.Assets,
kubernetesClientFactory,
dockerClientFactory,
composeStackManager,
dataStore,
fileService,
stackDeployer,
)
upgradeService, err := upgrade.NewService(*flags.Assets, composeDeployer, kubernetesClientFactory)
if err != nil {
log.Fatal().Err(err).Msg("failed initializing upgrade service")
}
@@ -591,7 +613,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
ProxyManager: proxyManager,
KubernetesTokenCacheManager: kubernetesTokenCacheManager,
KubeClusterAccessService: kubeClusterAccessService,
SignatureService: signatureService,
SignatureService: digitalSignatureService,
SnapshotService: snapshotService,
SSLService: sslService,
DockerClientFactory: dockerClientFactory,
@@ -603,7 +625,6 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
UpgradeService: upgradeService,
AdminCreationDone: adminCreationDone,
PendingActionsService: pendingActionsService,
PlatformService: platformService,
}
}
@@ -618,7 +639,6 @@ func main() {
for {
server := buildServer(flags)
log.Info().
Str("version", portainer.APIVersion).
Str("build_number", build.BuildNumber).

View File

@@ -5,21 +5,22 @@ import (
)
type ReadTransaction interface {
GetObject(bucketName string, key []byte, object any) error
GetAll(bucketName string, obj any, append func(o any) (any, error)) error
GetAllWithKeyPrefix(bucketName string, keyPrefix []byte, obj any, append func(o any) (any, error)) error
GetObject(bucketName string, key []byte, object interface{}) error
GetAll(bucketName string, obj interface{}, append func(o interface{}) (interface{}, error)) error
GetAllWithJsoniter(bucketName string, obj interface{}, append func(o interface{}) (interface{}, error)) error
GetAllWithKeyPrefix(bucketName string, keyPrefix []byte, obj interface{}, append func(o interface{}) (interface{}, error)) error
}
type Transaction interface {
ReadTransaction
SetServiceName(bucketName string) error
UpdateObject(bucketName string, key []byte, object any) error
UpdateObject(bucketName string, key []byte, object interface{}) error
DeleteObject(bucketName string, key []byte) error
CreateObject(bucketName string, fn func(uint64) (int, any)) error
CreateObjectWithId(bucketName string, id int, obj any) error
CreateObjectWithStringId(bucketName string, id []byte, obj any) error
DeleteAllObjects(bucketName string, obj any, matching func(o any) (id int, ok bool)) error
CreateObject(bucketName string, fn func(uint64) (int, interface{})) error
CreateObjectWithId(bucketName string, id int, obj interface{}) error
CreateObjectWithStringId(bucketName string, id []byte, obj interface{}) error
DeleteAllObjects(bucketName string, obj interface{}, matching func(o interface{}) (id int, ok bool)) error
GetNextIdentifier(bucketName string) int
}
@@ -45,8 +46,8 @@ type Connection interface {
NeedsEncryptionMigration() (bool, error)
SetEncrypted(encrypted bool)
BackupMetadata() (map[string]any, error)
RestoreMetadata(s map[string]any) error
BackupMetadata() (map[string]interface{}, error)
RestoreMetadata(s map[string]interface{}) error
UpdateObjectFunc(bucketName string, key []byte, object any, updateFn func()) error
ConvertToKey(v int) []byte

View File

@@ -8,7 +8,6 @@ import (
"math"
"os"
"path"
"strconv"
"time"
portainer "github.com/portainer/portainer/api"
@@ -74,6 +73,7 @@ func (connection *DbConnection) IsEncryptedStore() bool {
// NeedsEncryptionMigration returns true if database encryption is enabled and
// we have an un-encrypted DB that requires migration to an encrypted DB
func (connection *DbConnection) NeedsEncryptionMigration() (bool, error) {
// Cases: Note, we need to check both portainer.db and portainer.edb
// to determine if it's a new store. We only need to differentiate between cases 2,3 and 5
@@ -121,11 +121,11 @@ func (connection *DbConnection) NeedsEncryptionMigration() (bool, error) {
// Open opens and initializes the BoltDB database.
func (connection *DbConnection) Open() error {
log.Info().Str("filename", connection.GetDatabaseFileName()).Msg("loading PortainerDB")
// Now we open the db
databasePath := connection.GetDatabaseFilePath()
db, err := bolt.Open(databasePath, 0600, &bolt.Options{
Timeout: 1 * time.Second,
InitialMmapSize: connection.InitialMmapSize,
@@ -178,7 +178,6 @@ func (connection *DbConnection) ViewTx(fn func(portainer.Transaction) error) err
func (connection *DbConnection) BackupTo(w io.Writer) error {
return connection.View(func(tx *bolt.Tx) error {
_, err := tx.WriteTo(w)
return err
})
}
@@ -193,7 +192,6 @@ func (connection *DbConnection) ExportRaw(filename string) error {
if err != nil {
return err
}
return os.WriteFile(filename, b, 0600)
}
@@ -203,7 +201,6 @@ func (connection *DbConnection) ExportRaw(filename string) error {
func (connection *DbConnection) ConvertToKey(v int) []byte {
b := make([]byte, 8)
binary.BigEndian.PutUint64(b, uint64(v))
return b
}
@@ -215,7 +212,7 @@ func keyToString(b []byte) string {
v := binary.BigEndian.Uint64(b)
if v <= math.MaxInt32 {
return strconv.FormatUint(v, 10)
return fmt.Sprintf("%d", v)
}
return string(b)
@@ -229,7 +226,7 @@ func (connection *DbConnection) SetServiceName(bucketName string) error {
}
// GetObject is a generic function used to retrieve an unmarshalled object from a database.
func (connection *DbConnection) GetObject(bucketName string, key []byte, object any) error {
func (connection *DbConnection) GetObject(bucketName string, key []byte, object interface{}) error {
return connection.ViewTx(func(tx portainer.Transaction) error {
return tx.GetObject(bucketName, key, object)
})
@@ -244,7 +241,7 @@ func (connection *DbConnection) getEncryptionKey() []byte {
}
// UpdateObject is a generic function used to update an object inside a database.
func (connection *DbConnection) UpdateObject(bucketName string, key []byte, object any) error {
func (connection *DbConnection) UpdateObject(bucketName string, key []byte, object interface{}) error {
return connection.UpdateTx(func(tx portainer.Transaction) error {
return tx.UpdateObject(bucketName, key, object)
})
@@ -285,7 +282,7 @@ func (connection *DbConnection) DeleteObject(bucketName string, key []byte) erro
// DeleteAllObjects delete all objects where matching() returns (id, ok).
// TODO: think about how to return the error inside (maybe change ok to type err, and use "notfound"?
func (connection *DbConnection) DeleteAllObjects(bucketName string, obj any, matching func(o any) (id int, ok bool)) error {
func (connection *DbConnection) DeleteAllObjects(bucketName string, obj interface{}, matching func(o interface{}) (id int, ok bool)) error {
return connection.UpdateTx(func(tx portainer.Transaction) error {
return tx.DeleteAllObjects(bucketName, obj, matching)
})
@@ -304,64 +301,71 @@ func (connection *DbConnection) GetNextIdentifier(bucketName string) int {
}
// CreateObject creates a new object in the bucket, using the next bucket sequence id
func (connection *DbConnection) CreateObject(bucketName string, fn func(uint64) (int, any)) error {
func (connection *DbConnection) CreateObject(bucketName string, fn func(uint64) (int, interface{})) error {
return connection.UpdateTx(func(tx portainer.Transaction) error {
return tx.CreateObject(bucketName, fn)
})
}
// CreateObjectWithId creates a new object in the bucket, using the specified id
func (connection *DbConnection) CreateObjectWithId(bucketName string, id int, obj any) error {
func (connection *DbConnection) CreateObjectWithId(bucketName string, id int, obj interface{}) error {
return connection.UpdateTx(func(tx portainer.Transaction) error {
return tx.CreateObjectWithId(bucketName, id, obj)
})
}
// CreateObjectWithStringId creates a new object in the bucket, using the specified id
func (connection *DbConnection) CreateObjectWithStringId(bucketName string, id []byte, obj any) error {
func (connection *DbConnection) CreateObjectWithStringId(bucketName string, id []byte, obj interface{}) error {
return connection.UpdateTx(func(tx portainer.Transaction) error {
return tx.CreateObjectWithStringId(bucketName, id, obj)
})
}
func (connection *DbConnection) GetAll(bucketName string, obj any, appendFn func(o any) (any, error)) error {
func (connection *DbConnection) GetAll(bucketName string, obj interface{}, append func(o interface{}) (interface{}, error)) error {
return connection.ViewTx(func(tx portainer.Transaction) error {
return tx.GetAll(bucketName, obj, appendFn)
return tx.GetAll(bucketName, obj, append)
})
}
func (connection *DbConnection) GetAllWithKeyPrefix(bucketName string, keyPrefix []byte, obj any, appendFn func(o any) (any, error)) error {
// TODO: decide which Unmarshal to use, and use one...
func (connection *DbConnection) GetAllWithJsoniter(bucketName string, obj interface{}, append func(o interface{}) (interface{}, error)) error {
return connection.ViewTx(func(tx portainer.Transaction) error {
return tx.GetAllWithKeyPrefix(bucketName, keyPrefix, obj, appendFn)
return tx.GetAllWithJsoniter(bucketName, obj, append)
})
}
func (connection *DbConnection) GetAllWithKeyPrefix(bucketName string, keyPrefix []byte, obj interface{}, append func(o interface{}) (interface{}, error)) error {
return connection.ViewTx(func(tx portainer.Transaction) error {
return tx.GetAllWithKeyPrefix(bucketName, keyPrefix, obj, append)
})
}
// BackupMetadata will return a copy of the boltdb sequence numbers for all buckets.
func (connection *DbConnection) BackupMetadata() (map[string]any, error) {
buckets := map[string]any{}
func (connection *DbConnection) BackupMetadata() (map[string]interface{}, error) {
buckets := map[string]interface{}{}
err := connection.View(func(tx *bolt.Tx) error {
return tx.ForEach(func(name []byte, bucket *bolt.Bucket) error {
err := tx.ForEach(func(name []byte, bucket *bolt.Bucket) error {
bucketName := string(name)
seqId := bucket.Sequence()
buckets[bucketName] = int(seqId)
return nil
})
return err
})
return buckets, err
}
// RestoreMetadata will restore the boltdb sequence numbers for all buckets.
func (connection *DbConnection) RestoreMetadata(s map[string]any) error {
func (connection *DbConnection) RestoreMetadata(s map[string]interface{}) error {
var err error
for bucketName, v := range s {
id, ok := v.(float64) // JSON ints are unmarshalled to interface as float64. See: https://pkg.go.dev/encoding/json#Decoder.Decode
if !ok {
log.Error().Str("bucket", bucketName).Msg("failed to restore metadata to bucket, skipped")
continue
}

View File

@@ -87,7 +87,10 @@ func Test_NeedsEncryptionMigration(t *testing.T) {
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
connection := DbConnection{Path: dir}
if tc.dbname == "both" {

View File

@@ -8,8 +8,8 @@ import (
bolt "go.etcd.io/bbolt"
)
func backupMetadata(connection *bolt.DB) (map[string]any, error) {
buckets := map[string]any{}
func backupMetadata(connection *bolt.DB) (map[string]interface{}, error) {
buckets := map[string]interface{}{}
err := connection.View(func(tx *bolt.Tx) error {
err := tx.ForEach(func(name []byte, bucket *bolt.Bucket) error {
@@ -39,7 +39,7 @@ func (c *DbConnection) ExportJSON(databasePath string, metadata bool) ([]byte, e
}
defer connection.Close()
backup := make(map[string]any)
backup := make(map[string]interface{})
if metadata {
meta, err := backupMetadata(connection)
if err != nil {
@@ -52,7 +52,7 @@ func (c *DbConnection) ExportJSON(databasePath string, metadata bool) ([]byte, e
err = connection.View(func(tx *bolt.Tx) error {
err = tx.ForEach(func(name []byte, bucket *bolt.Bucket) error {
bucketName := string(name)
var list []any
var list []interface{}
version := make(map[string]string)
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
@@ -60,7 +60,7 @@ func (c *DbConnection) ExportJSON(databasePath string, metadata bool) ([]byte, e
continue
}
var obj any
var obj interface{}
err := c.UnmarshalObject(v, &obj)
if err != nil {
log.Error().

View File

@@ -5,16 +5,17 @@ import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"fmt"
"io"
"github.com/pkg/errors"
"github.com/segmentio/encoding/json"
)
var errEncryptedStringTooShort = errors.New("encrypted string too short")
var errEncryptedStringTooShort = fmt.Errorf("encrypted string too short")
// MarshalObject encodes an object to binary format
func (connection *DbConnection) MarshalObject(object any) ([]byte, error) {
func (connection *DbConnection) MarshalObject(object interface{}) ([]byte, error) {
buf := &bytes.Buffer{}
// Special case for the VERSION bucket. Here we're not using json
@@ -38,7 +39,7 @@ func (connection *DbConnection) MarshalObject(object any) ([]byte, error) {
}
// UnmarshalObject decodes an object from binary data
func (connection *DbConnection) UnmarshalObject(data []byte, object any) error {
func (connection *DbConnection) UnmarshalObject(data []byte, object interface{}) error {
var err error
if connection.getEncryptionKey() != nil {
data, err = decrypt(data, connection.getEncryptionKey())
@@ -46,8 +47,8 @@ func (connection *DbConnection) UnmarshalObject(data []byte, object any) error {
return errors.Wrap(err, "Failed decrypting object")
}
}
if e := json.Unmarshal(data, object); e != nil {
e := json.Unmarshal(data, object)
if e != nil {
// Special case for the VERSION bucket. Here we're not using json
// So we need to return it as a string
s, ok := object.(*string)
@@ -57,7 +58,6 @@ func (connection *DbConnection) UnmarshalObject(data []byte, object any) error {
*s = string(data)
}
return err
}
@@ -70,20 +70,22 @@ func encrypt(plaintext []byte, passphrase []byte) (encrypted []byte, err error)
if err != nil {
return encrypted, err
}
nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
return encrypted, err
}
return gcm.Seal(nonce, nonce, plaintext, nil), nil
ciphertextByte := gcm.Seal(
nonce,
nonce,
plaintext,
nil)
return ciphertextByte, nil
}
func decrypt(encrypted []byte, passphrase []byte) (plaintextByte []byte, err error) {
if string(encrypted) == "false" {
return []byte("false"), nil
}
block, err := aes.NewCipher(passphrase)
if err != nil {
return encrypted, errors.Wrap(err, "Error creating cypher block")
@@ -100,8 +102,11 @@ func decrypt(encrypted []byte, passphrase []byte) (plaintextByte []byte, err err
}
nonce, ciphertextByteClean := encrypted[:nonceSize], encrypted[nonceSize:]
plaintextByte, err = gcm.Open(nil, nonce, ciphertextByteClean, nil)
plaintextByte, err = gcm.Open(
nil,
nonce,
ciphertextByteClean,
nil)
if err != nil {
return encrypted, errors.Wrap(err, "Error decrypting text")
}

View File

@@ -25,7 +25,7 @@ func Test_MarshalObjectUnencrypted(t *testing.T) {
uuid := uuid.Must(uuid.NewV4())
tests := []struct {
object any
object interface{}
expected string
}{
{
@@ -57,7 +57,7 @@ func Test_MarshalObjectUnencrypted(t *testing.T) {
expected: uuid.String(),
},
{
object: map[string]any{"key": "value"},
object: map[string]interface{}{"key": "value"},
expected: `{"key":"value"}`,
},
{
@@ -73,11 +73,11 @@ func Test_MarshalObjectUnencrypted(t *testing.T) {
expected: `["1","2","3"]`,
},
{
object: []map[string]any{{"key1": "value1"}, {"key2": "value2"}},
object: []map[string]interface{}{{"key1": "value1"}, {"key2": "value2"}},
expected: `[{"key1":"value1"},{"key2":"value2"}]`,
},
{
object: []any{1, "2", false, map[string]any{"key1": "value1"}},
object: []interface{}{1, "2", false, map[string]interface{}{"key1": "value1"}},
expected: `[1,"2",false,{"key1":"value1"}]`,
},
}

View File

@@ -20,7 +20,7 @@ func (tx *DbTransaction) SetServiceName(bucketName string) error {
return err
}
func (tx *DbTransaction) GetObject(bucketName string, key []byte, object any) error {
func (tx *DbTransaction) GetObject(bucketName string, key []byte, object interface{}) error {
bucket := tx.tx.Bucket([]byte(bucketName))
value := bucket.Get(key)
@@ -31,7 +31,7 @@ func (tx *DbTransaction) GetObject(bucketName string, key []byte, object any) er
return tx.conn.UnmarshalObject(value, object)
}
func (tx *DbTransaction) UpdateObject(bucketName string, key []byte, object any) error {
func (tx *DbTransaction) UpdateObject(bucketName string, key []byte, object interface{}) error {
data, err := tx.conn.MarshalObject(object)
if err != nil {
return err
@@ -46,7 +46,7 @@ func (tx *DbTransaction) DeleteObject(bucketName string, key []byte) error {
return bucket.Delete(key)
}
func (tx *DbTransaction) DeleteAllObjects(bucketName string, obj any, matchingFn func(o any) (id int, ok bool)) error {
func (tx *DbTransaction) DeleteAllObjects(bucketName string, obj interface{}, matchingFn func(o interface{}) (id int, ok bool)) error {
var ids []int
bucket := tx.tx.Bucket([]byte(bucketName))
@@ -74,18 +74,16 @@ func (tx *DbTransaction) DeleteAllObjects(bucketName string, obj any, matchingFn
func (tx *DbTransaction) GetNextIdentifier(bucketName string) int {
bucket := tx.tx.Bucket([]byte(bucketName))
id, err := bucket.NextSequence()
if err != nil {
log.Error().Err(err).Str("bucket", bucketName).Msg("failed to get the next identifier")
log.Error().Err(err).Str("bucket", bucketName).Msg("failed to get the next identifer")
return 0
}
return int(id)
}
func (tx *DbTransaction) CreateObject(bucketName string, fn func(uint64) (int, any)) error {
func (tx *DbTransaction) CreateObject(bucketName string, fn func(uint64) (int, interface{})) error {
bucket := tx.tx.Bucket([]byte(bucketName))
seqId, _ := bucket.NextSequence()
@@ -99,7 +97,7 @@ func (tx *DbTransaction) CreateObject(bucketName string, fn func(uint64) (int, a
return bucket.Put(tx.conn.ConvertToKey(id), data)
}
func (tx *DbTransaction) CreateObjectWithId(bucketName string, id int, obj any) error {
func (tx *DbTransaction) CreateObjectWithId(bucketName string, id int, obj interface{}) error {
bucket := tx.tx.Bucket([]byte(bucketName))
data, err := tx.conn.MarshalObject(obj)
if err != nil {
@@ -109,7 +107,7 @@ func (tx *DbTransaction) CreateObjectWithId(bucketName string, id int, obj any)
return bucket.Put(tx.conn.ConvertToKey(id), data)
}
func (tx *DbTransaction) CreateObjectWithStringId(bucketName string, id []byte, obj any) error {
func (tx *DbTransaction) CreateObjectWithStringId(bucketName string, id []byte, obj interface{}) error {
bucket := tx.tx.Bucket([]byte(bucketName))
data, err := tx.conn.MarshalObject(obj)
if err != nil {
@@ -119,7 +117,7 @@ func (tx *DbTransaction) CreateObjectWithStringId(bucketName string, id []byte,
return bucket.Put(id, data)
}
func (tx *DbTransaction) GetAll(bucketName string, obj any, appendFn func(o any) (any, error)) error {
func (tx *DbTransaction) GetAll(bucketName string, obj interface{}, appendFn func(o interface{}) (interface{}, error)) error {
bucket := tx.tx.Bucket([]byte(bucketName))
return bucket.ForEach(func(k []byte, v []byte) error {
@@ -132,7 +130,20 @@ func (tx *DbTransaction) GetAll(bucketName string, obj any, appendFn func(o any)
})
}
func (tx *DbTransaction) GetAllWithKeyPrefix(bucketName string, keyPrefix []byte, obj any, appendFn func(o any) (any, error)) error {
func (tx *DbTransaction) GetAllWithJsoniter(bucketName string, obj interface{}, appendFn func(o interface{}) (interface{}, error)) error {
bucket := tx.tx.Bucket([]byte(bucketName))
return bucket.ForEach(func(k []byte, v []byte) error {
err := tx.conn.UnmarshalObject(v, obj)
if err == nil {
obj, err = appendFn(obj)
}
return err
})
}
func (tx *DbTransaction) GetAllWithKeyPrefix(bucketName string, keyPrefix []byte, obj interface{}, appendFn func(o interface{}) (interface{}, error)) error {
cursor := tx.tx.Bucket([]byte(bucketName)).Cursor()
for k, v := cursor.Seek(keyPrefix); k != nil && bytes.HasPrefix(k, keyPrefix); k, v = cursor.Next() {

View File

@@ -41,7 +41,7 @@ func (service *Service) GetAPIKeysByUserID(userID portainer.UserID) ([]portainer
err := service.Connection.GetAll(
BucketName,
&portainer.APIKey{},
func(obj any) (any, error) {
func(obj interface{}) (interface{}, error) {
record, ok := obj.(*portainer.APIKey)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to APIKey object")
@@ -66,7 +66,7 @@ func (service *Service) GetAPIKeyByDigest(digest string) (*portainer.APIKey, err
err := service.Connection.GetAll(
BucketName,
&portainer.APIKey{},
func(obj any) (any, error) {
func(obj interface{}) (interface{}, error) {
key, ok := obj.(*portainer.APIKey)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to APIKey object")
@@ -95,7 +95,7 @@ func (service *Service) GetAPIKeyByDigest(digest string) (*portainer.APIKey, err
func (service *Service) Create(record *portainer.APIKey) error {
return service.Connection.CreateObject(
BucketName,
func(id uint64) (int, any) {
func(id uint64) (int, interface{}) {
record.ID = portainer.APIKeyID(id)
return int(record.ID), record

View File

@@ -31,7 +31,7 @@ func (service BaseDataServiceTx[T, I]) Read(ID I) (*T, error) {
func (service BaseDataServiceTx[T, I]) ReadAll() ([]T, error) {
var collection = make([]T, 0)
return collection, service.Tx.GetAll(
return collection, service.Tx.GetAllWithJsoniter(
service.Bucket,
new(T),
AppendFn(&collection),

View File

@@ -19,7 +19,7 @@ func (service ServiceTx) UpdateEdgeGroupFunc(ID portainer.EdgeGroupID, updateFun
func (service ServiceTx) Create(group *portainer.EdgeGroup) error {
return service.Tx.CreateObject(
BucketName,
func(id uint64) (int, any) {
func(id uint64) (int, interface{}) {
group.ID = portainer.EdgeGroupID(id)
return int(group.ID), group
},

View File

@@ -24,7 +24,7 @@ func (service ServiceTx) EdgeStacks() ([]portainer.EdgeStack, error) {
err := service.tx.GetAll(
BucketName,
&portainer.EdgeStack{},
func(obj any) (any, error) {
func(obj interface{}) (interface{}, error) {
stack, ok := obj.(*portainer.EdgeStack)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to EdgeStack object")

View File

@@ -6,8 +6,6 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/rs/zerolog/log"
)
// BucketName represents the name of the bucket where this service stores data.
@@ -159,7 +157,6 @@ func (service *Service) EndpointsByTeamID(teamID portainer.TeamID) ([]portainer.
return true
}
}
return false
}),
)
@@ -169,13 +166,11 @@ func (service *Service) EndpointsByTeamID(teamID portainer.TeamID) ([]portainer.
func (service *Service) GetNextIdentifier() int {
var identifier int
if err := service.connection.UpdateTx(func(tx portainer.Transaction) error {
service.connection.UpdateTx(func(tx portainer.Transaction) error {
identifier = service.Tx(tx).GetNextIdentifier()
return nil
}); err != nil {
log.Error().Err(err).Str("bucket", BucketName).Msg("could not get the next identifier")
}
})
return identifier
}

View File

@@ -20,10 +20,10 @@ func (service ServiceTx) BucketName() string {
// Endpoint returns an environment(endpoint) by ID.
func (service ServiceTx) Endpoint(ID portainer.EndpointID) (*portainer.Endpoint, error) {
var endpoint portainer.Endpoint
identifier := service.service.connection.ConvertToKey(int(ID))
if err := service.tx.GetObject(BucketName, identifier, &endpoint); err != nil {
err := service.tx.GetObject(BucketName, identifier, &endpoint)
if err != nil {
return nil, err
}
@@ -36,7 +36,8 @@ func (service ServiceTx) Endpoint(ID portainer.EndpointID) (*portainer.Endpoint,
func (service ServiceTx) UpdateEndpoint(ID portainer.EndpointID, endpoint *portainer.Endpoint) error {
identifier := service.service.connection.ConvertToKey(int(ID))
if err := service.tx.UpdateObject(BucketName, identifier, endpoint); err != nil {
err := service.tx.UpdateObject(BucketName, identifier, endpoint)
if err != nil {
return err
}
@@ -44,7 +45,6 @@ func (service ServiceTx) UpdateEndpoint(ID portainer.EndpointID, endpoint *porta
if len(endpoint.EdgeID) > 0 {
service.service.idxEdgeID[endpoint.EdgeID] = ID
}
service.service.heartbeats.Store(ID, endpoint.LastCheckInDate)
service.service.mu.Unlock()
@@ -57,7 +57,8 @@ func (service ServiceTx) UpdateEndpoint(ID portainer.EndpointID, endpoint *porta
func (service ServiceTx) DeleteEndpoint(ID portainer.EndpointID) error {
identifier := service.service.connection.ConvertToKey(int(ID))
if err := service.tx.DeleteObject(BucketName, identifier); err != nil {
err := service.tx.DeleteObject(BucketName, identifier)
if err != nil {
return err
}
@@ -69,7 +70,6 @@ func (service ServiceTx) DeleteEndpoint(ID portainer.EndpointID) error {
break
}
}
service.service.heartbeats.Delete(ID)
service.service.mu.Unlock()
@@ -82,7 +82,7 @@ func (service ServiceTx) DeleteEndpoint(ID portainer.EndpointID) error {
func (service ServiceTx) Endpoints() ([]portainer.Endpoint, error) {
var endpoints = make([]portainer.Endpoint, 0)
return endpoints, service.tx.GetAll(
return endpoints, service.tx.GetAllWithJsoniter(
BucketName,
&portainer.Endpoint{},
dataservices.AppendFn(&endpoints),
@@ -107,7 +107,8 @@ func (service ServiceTx) UpdateHeartbeat(endpointID portainer.EndpointID) {
// CreateEndpoint assign an ID to a new environment(endpoint) and saves it.
func (service ServiceTx) Create(endpoint *portainer.Endpoint) error {
if err := service.tx.CreateObjectWithId(BucketName, int(endpoint.ID), endpoint); err != nil {
err := service.tx.CreateObjectWithId(BucketName, int(endpoint.ID), endpoint)
if err != nil {
return err
}
@@ -115,7 +116,6 @@ func (service ServiceTx) Create(endpoint *portainer.Endpoint) error {
if len(endpoint.EdgeID) > 0 {
service.service.idxEdgeID[endpoint.EdgeID] = endpoint.ID
}
service.service.heartbeats.Store(endpoint.ID, endpoint.LastCheckInDate)
service.service.mu.Unlock()
@@ -134,7 +134,6 @@ func (service ServiceTx) EndpointsByTeamID(teamID portainer.TeamID) ([]portainer
return true
}
}
return false
}),
)

View File

@@ -41,7 +41,7 @@ func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
func (service *Service) Create(endpointGroup *portainer.EndpointGroup) error {
return service.Connection.CreateObject(
BucketName,
func(id uint64) (int, any) {
func(id uint64) (int, interface{}) {
endpointGroup.ID = portainer.EndpointGroupID(id)
return int(endpointGroup.ID), endpointGroup
},

View File

@@ -13,7 +13,7 @@ type ServiceTx struct {
func (service ServiceTx) Create(endpointGroup *portainer.EndpointGroup) error {
return service.Tx.CreateObject(
BucketName,
func(id uint64) (int, any) {
func(id uint64) (int, interface{}) {
endpointGroup.ID = portainer.EndpointGroupID(id)
return int(endpointGroup.ID), endpointGroup
},

View File

@@ -32,7 +32,8 @@ func (service *Service) RegisterUpdateStackFunction(
// NewService creates a new instance of a service.
func NewService(connection portainer.Connection) (*Service, error) {
if err := connection.SetServiceName(BucketName); err != nil {
err := connection.SetServiceName(BucketName)
if err != nil {
return nil, err
}
@@ -64,7 +65,8 @@ func (service *Service) EndpointRelation(endpointID portainer.EndpointID) (*port
var endpointRelation portainer.EndpointRelation
identifier := service.connection.ConvertToKey(int(endpointID))
if err := service.connection.GetObject(BucketName, identifier, &endpointRelation); err != nil {
err := service.connection.GetObject(BucketName, identifier, &endpointRelation)
if err != nil {
return nil, err
}
@@ -159,24 +161,19 @@ func (service *Service) updateEdgeStacksAfterRelationChange(previousRelationStat
// list how many time this stack is referenced in all relations
// in order to update the stack deployments count
for refStackId, refStackEnabled := range stacksToUpdate {
if !refStackEnabled {
continue
}
numDeployments := 0
for _, r := range relations {
for sId, enabled := range r.EdgeStacks {
if enabled && sId == refStackId {
numDeployments += 1
if refStackEnabled {
numDeployments := 0
for _, r := range relations {
for sId, enabled := range r.EdgeStacks {
if enabled && sId == refStackId {
numDeployments += 1
}
}
}
}
if err := service.updateStackFn(refStackId, func(edgeStack *portainer.EdgeStack) {
edgeStack.NumDeployments = numDeployments
}); err != nil {
log.Error().Err(err).Msg("could not update the number of deployments")
service.updateStackFn(refStackId, func(edgeStack *portainer.EdgeStack) {
edgeStack.NumDeployments = numDeployments
})
}
}
}

View File

@@ -33,7 +33,8 @@ func (service ServiceTx) EndpointRelation(endpointID portainer.EndpointID) (*por
var endpointRelation portainer.EndpointRelation
identifier := service.service.connection.ConvertToKey(int(endpointID))
if err := service.tx.GetObject(BucketName, identifier, &endpointRelation); err != nil {
err := service.tx.GetObject(BucketName, identifier, &endpointRelation)
if err != nil {
return nil, err
}
@@ -128,23 +129,19 @@ func (service ServiceTx) updateEdgeStacksAfterRelationChange(previousRelationSta
// list how many time this stack is referenced in all relations
// in order to update the stack deployments count
for refStackId, refStackEnabled := range stacksToUpdate {
if !refStackEnabled {
continue
}
numDeployments := 0
for _, r := range relations {
for sId, enabled := range r.EdgeStacks {
if enabled && sId == refStackId {
numDeployments += 1
if refStackEnabled {
numDeployments := 0
for _, r := range relations {
for sId, enabled := range r.EdgeStacks {
if enabled && sId == refStackId {
numDeployments += 1
}
}
}
}
if err := service.service.updateStackFnTx(service.tx, refStackId, func(edgeStack *portainer.EdgeStack) {
edgeStack.NumDeployments = numDeployments
}); err != nil {
log.Error().Err(err).Msg("could not update the number of deployments")
service.service.updateStackFnTx(service.tx, refStackId, func(edgeStack *portainer.EdgeStack) {
edgeStack.NumDeployments = numDeployments
})
}
}
}

View File

@@ -0,0 +1,43 @@
package fdoprofile
import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
)
// BucketName represents the name of the bucket where this service stores data.
const BucketName = "fdo_profiles"
// Service represents a service for managingFDO Profiles data.
type Service struct {
dataservices.BaseDataService[portainer.FDOProfile, portainer.FDOProfileID]
}
// NewService creates a new instance of a service.
func NewService(connection portainer.Connection) (*Service, error) {
err := connection.SetServiceName(BucketName)
if err != nil {
return nil, err
}
return &Service{
BaseDataService: dataservices.BaseDataService[portainer.FDOProfile, portainer.FDOProfileID]{
Bucket: BucketName,
Connection: connection,
},
}, nil
}
// Create assign an ID to a new FDO Profile and saves it.
func (service *Service) Create(FDOProfile *portainer.FDOProfile) error {
return service.Connection.CreateObjectWithId(
BucketName,
int(FDOProfile.ID),
FDOProfile,
)
}
// GetNextIdentifier returns the next identifier for a FDO Profile.
func (service *Service) GetNextIdentifier() int {
return service.Connection.GetNextIdentifier(BucketName)
}

View File

@@ -45,7 +45,7 @@ func (service *Service) HelmUserRepositoryByUserID(userID portainer.UserID) ([]p
func (service *Service) Create(record *portainer.HelmUserRepository) error {
return service.Connection.CreateObject(
BucketName,
func(id uint64) (int, any) {
func(id uint64) (int, interface{}) {
record.ID = portainer.HelmUserRepositoryID(id)
return int(record.ID), record
},

View File

@@ -17,8 +17,8 @@ func IsErrObjectNotFound(e error) bool {
}
// AppendFn appends elements to the given collection slice
func AppendFn[T any](collection *[]T) func(obj any) (any, error) {
return func(obj any) (any, error) {
func AppendFn[T any](collection *[]T) func(obj interface{}) (interface{}, error) {
return func(obj interface{}) (interface{}, error) {
element, ok := obj.(*T)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("type assertion failed")
@@ -32,8 +32,8 @@ func AppendFn[T any](collection *[]T) func(obj any) (any, error) {
}
// FilterFn appends elements to the given collection when the predicate is true
func FilterFn[T any](collection *[]T, predicate func(T) bool) func(obj any) (any, error) {
return func(obj any) (any, error) {
func FilterFn[T any](collection *[]T, predicate func(T) bool) func(obj interface{}) (interface{}, error) {
return func(obj interface{}) (interface{}, error) {
element, ok := obj.(*T)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("type assertion failed")
@@ -50,8 +50,8 @@ func FilterFn[T any](collection *[]T, predicate func(T) bool) func(obj any) (any
// FirstFn sets the element to the first one that satisfies the predicate and stops the computation, returns ErrStop on
// success
func FirstFn[T any](element *T, predicate func(T) bool) func(obj any) (any, error) {
return func(obj any) (any, error) {
func FirstFn[T any](element *T, predicate func(T) bool) func(obj interface{}) (interface{}, error) {
return func(obj interface{}) (interface{}, error) {
e, ok := obj.(*T)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("type assertion failed")

View File

@@ -15,6 +15,7 @@ type (
Endpoint() EndpointService
EndpointGroup() EndpointGroupService
EndpointRelation() EndpointRelationService
FDOProfile() FDOProfileService
HelmUserRepository() HelmUserRepositoryService
Registry() RegistryService
ResourceControl() ResourceControlService
@@ -119,6 +120,12 @@ type (
BucketName() string
}
// FDOProfileService represents a service to manage FDO Profiles
FDOProfileService interface {
BaseCRUD[portainer.FDOProfile, portainer.FDOProfileID]
GetNextIdentifier() int
}
// HelmUserRepositoryService represents a service to manage HelmUserRepositories
HelmUserRepositoryService interface {
BaseCRUD[portainer.HelmUserRepository, portainer.HelmUserRepositoryID]

View File

@@ -64,7 +64,7 @@ func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
}
func (s ServiceTx) Create(config *portainer.PendingAction) error {
return s.Tx.CreateObject(BucketName, func(id uint64) (int, any) {
return s.Tx.CreateObject(BucketName, func(id uint64) (int, interface{}) {
config.ID = portainer.PendingActionID(id)
config.CreatedAt = time.Now().Unix()

View File

@@ -42,7 +42,7 @@ func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
func (service *Service) Create(registry *portainer.Registry) error {
return service.Connection.CreateObject(
BucketName,
func(id uint64) (int, any) {
func(id uint64) (int, interface{}) {
registry.ID = portainer.RegistryID(id)
return int(registry.ID), registry
},

View File

@@ -13,7 +13,7 @@ type ServiceTx struct {
func (service ServiceTx) Create(registry *portainer.Registry) error {
return service.Tx.CreateObject(
BucketName,
func(id uint64) (int, any) {
func(id uint64) (int, interface{}) {
registry.ID = portainer.RegistryID(id)
return int(registry.ID), registry
},

View File

@@ -52,7 +52,7 @@ func (service *Service) ResourceControlByResourceIDAndType(resourceID string, re
err := service.Connection.GetAll(
BucketName,
&portainer.ResourceControl{},
func(obj any) (any, error) {
func(obj interface{}) (interface{}, error) {
rc, ok := obj.(*portainer.ResourceControl)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to ResourceControl object")
@@ -84,7 +84,7 @@ func (service *Service) ResourceControlByResourceIDAndType(resourceID string, re
func (service *Service) Create(resourceControl *portainer.ResourceControl) error {
return service.Connection.CreateObject(
BucketName,
func(id uint64) (int, any) {
func(id uint64) (int, interface{}) {
resourceControl.ID = portainer.ResourceControlID(id)
return int(resourceControl.ID), resourceControl
},

View File

@@ -23,7 +23,7 @@ func (service ServiceTx) ResourceControlByResourceIDAndType(resourceID string, r
err := service.Tx.GetAll(
BucketName,
&portainer.ResourceControl{},
func(obj any) (any, error) {
func(obj interface{}) (interface{}, error) {
rc, ok := obj.(*portainer.ResourceControl)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to ResourceControl object")
@@ -55,7 +55,7 @@ func (service ServiceTx) ResourceControlByResourceIDAndType(resourceID string, r
func (service ServiceTx) Create(resourceControl *portainer.ResourceControl) error {
return service.Tx.CreateObject(
BucketName,
func(id uint64) (int, any) {
func(id uint64) (int, interface{}) {
resourceControl.ID = portainer.ResourceControlID(id)
return int(resourceControl.ID), resourceControl
},

View File

@@ -42,7 +42,7 @@ func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
func (service *Service) Create(role *portainer.Role) error {
return service.Connection.CreateObject(
BucketName,
func(id uint64) (int, any) {
func(id uint64) (int, interface{}) {
role.ID = portainer.RoleID(id)
return int(role.ID), role
},

View File

@@ -13,7 +13,7 @@ type ServiceTx struct {
func (service ServiceTx) Create(role *portainer.Role) error {
return service.Tx.CreateObject(
BucketName,
func(id uint64) (int, any) {
func(id uint64) (int, interface{}) {
role.ID = portainer.RoleID(id)
return int(role.ID), role
},

View File

@@ -33,7 +33,7 @@ func TestService_StackByWebhookID(t *testing.T) {
b := stackBuilder{t: t, store: store}
b.createNewStack(newGuidString(t))
for range 10 {
for i := 0; i < 10; i++ {
b.createNewStack("")
}
webhookID := newGuidString(t)

View File

@@ -42,7 +42,7 @@ func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
func (service *Service) Create(tag *portainer.Tag) error {
return service.Connection.CreateObject(
BucketName,
func(id uint64) (int, any) {
func(id uint64) (int, interface{}) {
tag.ID = portainer.TagID(id)
return int(tag.ID), tag
},

View File

@@ -15,7 +15,7 @@ type ServiceTx struct {
func (service ServiceTx) Create(tag *portainer.Tag) error {
return service.Tx.CreateObject(
BucketName,
func(id uint64) (int, any) {
func(id uint64) (int, interface{}) {
tag.ID = portainer.TagID(id)
return int(tag.ID), tag
},

View File

@@ -19,7 +19,8 @@ type Service struct {
// NewService creates a new instance of a service.
func NewService(connection portainer.Connection) (*Service, error) {
if err := connection.SetServiceName(BucketName); err != nil {
err := connection.SetServiceName(BucketName)
if err != nil {
return nil, err
}
@@ -31,16 +32,6 @@ func NewService(connection portainer.Connection) (*Service, error) {
}, nil
}
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
return ServiceTx{
BaseDataServiceTx: dataservices.BaseDataServiceTx[portainer.Team, portainer.TeamID]{
Bucket: BucketName,
Connection: service.Connection,
Tx: tx,
},
}
}
// TeamByName returns a team by name.
func (service *Service) TeamByName(name string) (*portainer.Team, error) {
var t portainer.Team
@@ -68,7 +59,7 @@ func (service *Service) TeamByName(name string) (*portainer.Team, error) {
func (service *Service) Create(team *portainer.Team) error {
return service.Connection.CreateObject(
BucketName,
func(id uint64) (int, any) {
func(id uint64) (int, interface{}) {
team.ID = portainer.TeamID(id)
return int(team.ID), team
},

View File

@@ -1,48 +0,0 @@
package team
import (
"errors"
"strings"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
dserrors "github.com/portainer/portainer/api/dataservices/errors"
)
type ServiceTx struct {
dataservices.BaseDataServiceTx[portainer.Team, portainer.TeamID]
}
// TeamByName returns a team by name.
func (service ServiceTx) TeamByName(name string) (*portainer.Team, error) {
var t portainer.Team
err := service.Tx.GetAll(
BucketName,
&portainer.Team{},
dataservices.FirstFn(&t, func(e portainer.Team) bool {
return strings.EqualFold(e.Name, name)
}),
)
if errors.Is(err, dataservices.ErrStop) {
return &t, nil
}
if err == nil {
return nil, dserrors.ErrObjectNotFound
}
return nil, err
}
// CreateTeam creates a new Team.
func (service ServiceTx) Create(team *portainer.Team) error {
return service.Tx.CreateObject(
BucketName,
func(id uint64) (int, any) {
team.ID = portainer.TeamID(id)
return int(team.ID), team
},
)
}

View File

@@ -72,7 +72,7 @@ func (service *Service) TeamMembershipsByTeamID(teamID portainer.TeamID) ([]port
func (service *Service) Create(membership *portainer.TeamMembership) error {
return service.Connection.CreateObject(
BucketName,
func(id uint64) (int, any) {
func(id uint64) (int, interface{}) {
membership.ID = portainer.TeamMembershipID(id)
return int(membership.ID), membership
},
@@ -84,8 +84,8 @@ func (service *Service) DeleteTeamMembershipByUserID(userID portainer.UserID) er
return service.Connection.DeleteAllObjects(
BucketName,
&portainer.TeamMembership{},
func(obj any) (id int, ok bool) {
membership, ok := obj.(*portainer.TeamMembership)
func(obj interface{}) (id int, ok bool) {
membership, ok := obj.(portainer.TeamMembership)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to TeamMembership object")
//return fmt.Errorf("Failed to convert to TeamMembership object: %s", obj)
@@ -105,8 +105,8 @@ func (service *Service) DeleteTeamMembershipByTeamID(teamID portainer.TeamID) er
return service.Connection.DeleteAllObjects(
BucketName,
&portainer.TeamMembership{},
func(obj any) (id int, ok bool) {
membership, ok := obj.(*portainer.TeamMembership)
func(obj interface{}) (id int, ok bool) {
membership, ok := obj.(portainer.TeamMembership)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to TeamMembership object")
//return fmt.Errorf("Failed to convert to TeamMembership object: %s", obj)
@@ -125,8 +125,8 @@ func (service *Service) DeleteTeamMembershipByTeamIDAndUserID(teamID portainer.T
return service.Connection.DeleteAllObjects(
BucketName,
&portainer.TeamMembership{},
func(obj any) (id int, ok bool) {
membership, ok := obj.(*portainer.TeamMembership)
func(obj interface{}) (id int, ok bool) {
membership, ok := obj.(portainer.TeamMembership)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to TeamMembership object")
//return fmt.Errorf("Failed to convert to TeamMembership object: %s", obj)

View File

@@ -43,7 +43,7 @@ func (service ServiceTx) TeamMembershipsByTeamID(teamID portainer.TeamID) ([]por
func (service ServiceTx) Create(membership *portainer.TeamMembership) error {
return service.Tx.CreateObject(
BucketName,
func(id uint64) (int, any) {
func(id uint64) (int, interface{}) {
membership.ID = portainer.TeamMembershipID(id)
return int(membership.ID), membership
},
@@ -55,7 +55,7 @@ func (service ServiceTx) DeleteTeamMembershipByUserID(userID portainer.UserID) e
return service.Tx.DeleteAllObjects(
BucketName,
&portainer.TeamMembership{},
func(obj any) (id int, ok bool) {
func(obj interface{}) (id int, ok bool) {
membership, ok := obj.(portainer.TeamMembership)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to TeamMembership object")
@@ -76,7 +76,7 @@ func (service ServiceTx) DeleteTeamMembershipByTeamID(teamID portainer.TeamID) e
return service.Tx.DeleteAllObjects(
BucketName,
&portainer.TeamMembership{},
func(obj any) (id int, ok bool) {
func(obj interface{}) (id int, ok bool) {
membership, ok := obj.(portainer.TeamMembership)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to TeamMembership object")
@@ -96,7 +96,7 @@ func (service ServiceTx) DeleteTeamMembershipByTeamIDAndUserID(teamID portainer.
return service.Tx.DeleteAllObjects(
BucketName,
&portainer.TeamMembership{},
func(obj any) (id int, ok bool) {
func(obj interface{}) (id int, ok bool) {
membership, ok := obj.(portainer.TeamMembership)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to TeamMembership object")

View File

@@ -53,7 +53,7 @@ func (service ServiceTx) UsersByRole(role portainer.UserRole) ([]portainer.User,
func (service ServiceTx) Create(user *portainer.User) error {
return service.Tx.CreateObject(
BucketName,
func(id uint64) (int, any) {
func(id uint64) (int, interface{}) {
user.ID = portainer.UserID(id)
user.Username = strings.ToLower(user.Username)

View File

@@ -82,7 +82,7 @@ func (service *Service) UsersByRole(role portainer.UserRole) ([]portainer.User,
func (service *Service) Create(user *portainer.User) error {
return service.Connection.CreateObject(
BucketName,
func(id uint64) (int, any) {
func(id uint64) (int, interface{}) {
user.ID = portainer.UserID(id)
user.Username = strings.ToLower(user.Username)

View File

@@ -81,7 +81,7 @@ func (service *Service) WebhookByToken(token string) (*portainer.Webhook, error)
func (service *Service) Create(webhook *portainer.Webhook) error {
return service.Connection.CreateObject(
BucketName,
func(id uint64) (int, any) {
func(id uint64) (int, interface{}) {
webhook.ID = portainer.WebhookID(id)
return int(webhook.ID), webhook
},

View File

@@ -321,7 +321,7 @@ func migrateDBTestHelper(t *testing.T, srcPath, wantPath string, overrideInstanc
// importJSON reads input JSON and commits it to a portainer datastore.Store.
// Errors are logged with the testing package.
func importJSON(t *testing.T, r io.Reader, store *Store) error {
objects := make(map[string]any)
objects := make(map[string]interface{})
// Parse json into map of objects.
d := json.NewDecoder(r)
@@ -337,9 +337,9 @@ func importJSON(t *testing.T, r io.Reader, store *Store) error {
for k, v := range objects {
switch k {
case "version":
versions, ok := v.(map[string]any)
versions, ok := v.(map[string]interface{})
if !ok {
t.Logf("failed casting %s to map[string]any", k)
t.Logf("failed casting %s to map[string]interface{}", k)
}
// New format db
@@ -404,9 +404,9 @@ func importJSON(t *testing.T, r io.Reader, store *Store) error {
}
case "dockerhub":
obj, ok := v.([]any)
obj, ok := v.([]interface{})
if !ok {
t.Logf("failed to cast %s to []any", k)
t.Logf("failed to cast %s to []interface{}", k)
}
err := con.CreateObjectWithStringId(
k,
@@ -418,9 +418,9 @@ func importJSON(t *testing.T, r io.Reader, store *Store) error {
}
case "ssl":
obj, ok := v.(map[string]any)
obj, ok := v.(map[string]interface{})
if !ok {
t.Logf("failed to case %s to map[string]any", k)
t.Logf("failed to case %s to map[string]interface{}", k)
}
err := con.CreateObjectWithStringId(
k,
@@ -432,9 +432,9 @@ func importJSON(t *testing.T, r io.Reader, store *Store) error {
}
case "settings":
obj, ok := v.(map[string]any)
obj, ok := v.(map[string]interface{})
if !ok {
t.Logf("failed to case %s to map[string]any", k)
t.Logf("failed to case %s to map[string]interface{}", k)
}
err := con.CreateObjectWithStringId(
k,
@@ -446,9 +446,9 @@ func importJSON(t *testing.T, r io.Reader, store *Store) error {
}
case "tunnel_server":
obj, ok := v.(map[string]any)
obj, ok := v.(map[string]interface{})
if !ok {
t.Logf("failed to case %s to map[string]any", k)
t.Logf("failed to case %s to map[string]interface{}", k)
}
err := con.CreateObjectWithStringId(
k,
@@ -462,18 +462,18 @@ func importJSON(t *testing.T, r io.Reader, store *Store) error {
continue
default:
objlist, ok := v.([]any)
objlist, ok := v.([]interface{})
if !ok {
t.Logf("failed to cast %s to []any", k)
t.Logf("failed to cast %s to []interface{}", k)
}
for _, obj := range objlist {
value, ok := obj.(map[string]any)
value, ok := obj.(map[string]interface{})
if !ok {
t.Logf("failed to cast %v to map[string]any", obj)
t.Logf("failed to cast %v to map[string]interface{}", obj)
} else {
var ok bool
var id any
var id interface{}
switch k {
case "endpoint_relations":
// TODO: need to make into an int, then do that weird

View File

@@ -12,13 +12,13 @@ const dummyLogoURL = "example.com"
// initTestingDBConn creates a settings service with raw database DB connection
// for unit testing usage only since using NewStore will cause cycle import inside migrator pkg
func initTestingSettingsService(dbConn portainer.Connection, preSetObj map[string]any) error {
func initTestingSettingsService(dbConn portainer.Connection, preSetObj map[string]interface{}) error {
//insert a obj
return dbConn.UpdateObject("settings", []byte("SETTINGS"), preSetObj)
}
func setup(store *Store) error {
dummySettingsObj := map[string]any{
dummySettingsObj := map[string]interface{}{
"LogoURL": dummyLogoURL,
}

View File

@@ -99,7 +99,7 @@ func (store *Store) getOrMigrateLegacyVersion() (*models.Version, error) {
return &models.Version{
SchemaVersion: dbVersionToSemanticVersion(dbVersion),
Edition: edition,
InstanceID: instanceId,
InstanceID: string(instanceId),
}, nil
}
@@ -111,6 +111,5 @@ func (store *Store) finishMigrateLegacyVersion(versionToWrite *models.Version) e
store.connection.DeleteObject(bucketName, []byte(legacyDBVersionKey))
store.connection.DeleteObject(bucketName, []byte(legacyEditionKey))
store.connection.DeleteObject(bucketName, []byte(legacyInstanceKey))
return err
}

View File

@@ -15,7 +15,7 @@ func migrationError(err error, context string) error {
return errors.Wrap(err, "failed in "+context)
}
func GetFunctionName(i any) string {
func GetFunctionName(i interface{}) string {
return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
}
@@ -39,19 +39,20 @@ func (m *Migrator) Migrate() error {
latestMigrations := m.LatestMigrations()
if latestMigrations.Version.Equal(schemaVersion) &&
version.MigratorCount != len(latestMigrations.MigrationFuncs) {
if err := runMigrations(latestMigrations.MigrationFuncs); err != nil {
err := runMigrations(latestMigrations.MigrationFuncs)
if err != nil {
return err
}
newMigratorCount = len(latestMigrations.MigrationFuncs)
}
} else {
// regular path when major/minor/patch versions differ
for _, migration := range m.migrations {
if schemaVersion.LessThan(migration.Version) {
log.Info().Msgf("migrating data to %s", migration.Version.String())
if err := runMigrations(migration.MigrationFuncs); err != nil {
log.Info().Msgf("migrating data to %s", migration.Version.String())
err := runMigrations(migration.MigrationFuncs)
if err != nil {
return err
}
}
@@ -62,14 +63,16 @@ func (m *Migrator) Migrate() error {
}
}
if err := m.Always(); err != nil {
err = m.Always()
if err != nil {
return migrationError(err, "Always migrations returned error")
}
version.SchemaVersion = portainer.APIVersion
version.MigratorCount = newMigratorCount
if err := m.versionService.UpdateVersion(version); err != nil {
err = m.versionService.UpdateVersion(version)
if err != nil {
return migrationError(err, "StoreDBVersion")
}
@@ -96,7 +99,6 @@ func (m *Migrator) NeedsMigration() bool {
// In this particular instance we should log a fatal error
if m.CurrentDBEdition() != portainer.PortainerCE {
log.Fatal().Msg("the Portainer database is set for Portainer Business Edition, please follow the instructions in our documentation to downgrade it: https://documentation.portainer.io/v2.0-be/downgrade/be-to-ce/")
return false
}

View File

@@ -7,7 +7,6 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/chisel/crypto"
"github.com/portainer/portainer/api/dataservices"
"github.com/rs/zerolog/log"
)
@@ -38,11 +37,9 @@ func (m *Migrator) convertSeedToPrivateKeyForDB100() error {
log.Info().Msg("ServerInfo object not found")
return nil
}
log.Error().
Err(err).
Msg("Failed to read ServerInfo from DB")
return err
}
@@ -52,15 +49,14 @@ func (m *Migrator) convertSeedToPrivateKeyForDB100() error {
log.Error().
Err(err).
Msg("Failed to read ServerInfo from DB")
return err
}
if err := m.fileService.StoreChiselPrivateKey(key); err != nil {
err = m.fileService.StoreChiselPrivateKey(key)
if err != nil {
log.Error().
Err(err).
Msg("Failed to save Chisel private key to disk")
return err
}
} else {
@@ -68,14 +64,14 @@ func (m *Migrator) convertSeedToPrivateKeyForDB100() error {
}
serverInfo.PrivateKeySeed = ""
if err := m.TunnelServerService.UpdateInfo(serverInfo); err != nil {
err = m.TunnelServerService.UpdateInfo(serverInfo)
if err != nil {
log.Error().
Err(err).
Msg("Failed to clean private key seed in DB")
} else {
log.Info().Msg("Success to migrate private key seed to private key file")
}
return err
}
@@ -88,8 +84,9 @@ func (m *Migrator) updateEdgeStackStatusForDB100() error {
}
for _, edgeStack := range edgeStacks {
for environmentID, environmentStatus := range edgeStack.Status {
// Skip if status is already updated
// skip if status is already updated
if len(environmentStatus.Status) > 0 {
continue
}
@@ -149,7 +146,8 @@ func (m *Migrator) updateEdgeStackStatusForDB100() error {
edgeStack.Status[environmentID] = environmentStatus
}
if err := m.edgeStackService.UpdateEdgeStack(edgeStack.ID, &edgeStack); err != nil {
err = m.edgeStackService.UpdateEdgeStack(edgeStack.ID, &edgeStack)
if err != nil {
return err
}
}

View File

@@ -32,8 +32,8 @@ func (m *Migrator) updateStacksToDB24() error {
for idx := range stacks {
stack := &stacks[idx]
stack.Status = portainer.StackStatusActive
if err := m.stackService.Update(stack.ID, stack); err != nil {
err := m.stackService.Update(stack.ID, stack)
if err != nil {
return err
}
}

View File

@@ -123,7 +123,7 @@ func (m *Migrator) updateDockerhubToDB32() error {
migrated = true
} else {
// delete subsequent duplicates
m.registryService.Delete(r.ID)
m.registryService.Delete(portainer.RegistryID(r.ID))
}
}
}

View File

@@ -13,6 +13,7 @@ import (
"github.com/portainer/portainer/api/dataservices/endpointgroup"
"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"
@@ -40,6 +41,7 @@ type (
endpointService *endpoint.Service
endpointRelationService *endpointrelation.Service
extensionService *extension.Service
fdoProfilesService *fdoprofile.Service
registryService *registry.Service
resourceControlService *resourcecontrol.Service
roleService *role.Service
@@ -67,6 +69,7 @@ type (
EndpointService *endpoint.Service
EndpointRelationService *endpointrelation.Service
ExtensionService *extension.Service
FDOProfilesService *fdoprofile.Service
RegistryService *registry.Service
ResourceControlService *resourcecontrol.Service
RoleService *role.Service
@@ -96,6 +99,7 @@ func NewMigrator(parameters *MigratorParameters) *Migrator {
endpointService: parameters.EndpointService,
endpointRelationService: parameters.EndpointRelationService,
extensionService: parameters.ExtensionService,
fdoProfilesService: parameters.FDOProfilesService,
registryService: parameters.RegistryService,
resourceControlService: parameters.ResourceControlService,
roleService: parameters.RoleService,

View File

@@ -90,7 +90,7 @@ func (migrator *PostInitMigrator) MigrateEnvironment(environment *portainer.Endp
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.GetPrivilegedKubeClient(environment)
kubeclient, err := migrator.kubeFactory.GetKubeClient(environment)
if err != nil {
log.Error().Err(err).Msgf("Error creating kubeclient for environment: %d", environment.ID)
return err

View File

@@ -17,6 +17,7 @@ import (
"github.com/portainer/portainer/api/dataservices/endpointgroup"
"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/helmuserrepository"
"github.com/portainer/portainer/api/dataservices/pendingactions"
"github.com/portainer/portainer/api/dataservices/registry"
@@ -54,6 +55,7 @@ type Store struct {
EndpointService *endpoint.Service
EndpointRelationService *endpointrelation.Service
ExtensionService *extension.Service
FDOProfilesService *fdoprofile.Service
HelmUserRepositoryService *helmuserrepository.Service
RegistryService *registry.Service
ResourceControlService *resourcecontrol.Service
@@ -136,6 +138,12 @@ func (store *Store) initServices() error {
}
store.ExtensionService = extensionService
fdoProfilesService, err := fdoprofile.NewService(store.connection)
if err != nil {
return err
}
store.FDOProfilesService = fdoProfilesService
helmUserRepositoryService, err := helmuserrepository.NewService(store.connection)
if err != nil {
return err
@@ -281,6 +289,11 @@ func (store *Store) EndpointRelation() dataservices.EndpointRelationService {
return store.EndpointRelationService
}
// FDOProfile gives access to the FDOProfile data management layer
func (store *Store) FDOProfile() dataservices.FDOProfileService {
return store.FDOProfilesService
}
// HelmUserRepository access the helm user repository settings
func (store *Store) HelmUserRepository() dataservices.HelmUserRepositoryService {
return store.HelmUserRepositoryService
@@ -385,10 +398,11 @@ type storeExport struct {
User []portainer.User `json:"users,omitempty"`
Version models.Version `json:"version,omitempty"`
Webhook []portainer.Webhook `json:"webhooks,omitempty"`
Metadata map[string]any `json:"metadata,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
func (store *Store) Export(filename string) (err error) {
backup := storeExport{}
if c, err := store.CustomTemplate().ReadAll(); err != nil {
@@ -592,7 +606,6 @@ func (store *Store) Export(filename string) (err error) {
if err != nil {
return err
}
return os.WriteFile(filename, b, 0600)
}
@@ -603,7 +616,7 @@ func (store *Store) Import(filename string) (err error) {
if err != nil {
return err
}
err = json.Unmarshal(s, &backup)
err = json.Unmarshal([]byte(s), &backup)
if err != nil {
return err
}

View File

@@ -44,6 +44,7 @@ func (tx *StoreTx) EndpointRelation() dataservices.EndpointRelationService {
return tx.store.EndpointRelationService.Tx(tx.tx)
}
func (tx *StoreTx) FDOProfile() dataservices.FDOProfileService { return nil }
func (tx *StoreTx) HelmUserRepository() dataservices.HelmUserRepositoryService { return nil }
func (tx *StoreTx) Registry() dataservices.RegistryService {
@@ -82,9 +83,7 @@ func (tx *StoreTx) TeamMembership() dataservices.TeamMembershipService {
return tx.store.TeamMembershipService.Tx(tx.tx)
}
func (tx *StoreTx) Team() dataservices.TeamService {
return tx.store.TeamService.Tx(tx.tx)
}
func (tx *StoreTx) Team() dataservices.TeamService { return nil }
func (tx *StoreTx) TunnelServer() dataservices.TunnelServerService { return nil }

View File

@@ -38,7 +38,6 @@
"TenantID": ""
},
"ComposeSyntaxMaxVersion": "",
"ContainerEngine": "",
"Edge": {
"AsyncMode": false,
"CommandInterval": 0,
@@ -584,6 +583,7 @@
"AuthenticationMethod": 1,
"BlackListedLabels": [],
"Edge": {
"AsyncMode": false,
"CommandInterval": 0,
"PingInterval": 0,
"SnapshotInterval": 0
@@ -602,7 +602,7 @@
"RequiredPasswordLength": 12
},
"KubeconfigExpiry": "0",
"KubectlShellImage": "portainer/kubectl-shell:2.22.0",
"KubectlShellImage": "portainer/kubectl-shell",
"LDAPSettings": {
"AnonymousMode": true,
"AutoCreateUsers": true,
@@ -644,10 +644,17 @@
"Scopes": "",
"UserIdentifier": ""
},
"ShowKomposeBuildOption": false,
"SnapshotInterval": "5m",
"TemplatesURL": "",
"TrustOnFirstConnect": false,
"UserSessionTimeout": "8h",
"fdoConfiguration": {
"enabled": false,
"ownerPassword": "",
"ownerURL": "",
"ownerUsername": ""
},
"openAMTConfiguration": {
"certFileContent": "",
"certFileName": "",
@@ -769,7 +776,6 @@
"GpuUseList": null,
"HealthyContainerCount": 0,
"ImageCount": 9,
"IsPodman": false,
"NodeCount": 0,
"RunningContainerCount": 5,
"ServiceCount": 0,
@@ -804,6 +810,7 @@
"FromAppTemplate": false,
"GitConfig": null,
"Id": 2,
"IsComposeFormat": false,
"Name": "alpine",
"Namespace": "",
"Option": null,
@@ -826,6 +833,7 @@
"FromAppTemplate": false,
"GitConfig": null,
"Id": 5,
"IsComposeFormat": false,
"Name": "redis",
"Namespace": "",
"Option": null,
@@ -848,6 +856,7 @@
"FromAppTemplate": false,
"GitConfig": null,
"Id": 6,
"IsComposeFormat": false,
"Name": "nginx",
"Namespace": "",
"Option": null,

View File

@@ -52,24 +52,27 @@ func NewTestStore(t testing.TB, init, secure bool) (bool, *Store, func(), error)
}
if init {
if err := store.Init(); err != nil {
err = store.Init()
if err != nil {
return newStore, nil, nil, err
}
}
if newStore {
// From MigrateData
// from MigrateData
v := models.Version{
SchemaVersion: portainer.APIVersion,
Edition: int(portainer.PortainerCE),
}
if err := store.VersionService.UpdateVersion(&v); err != nil {
err = store.VersionService.UpdateVersion(&v)
if err != nil {
return newStore, nil, nil, err
}
}
teardown := func() {
if err := store.Close(); err != nil {
err := store.Close()
if err != nil {
log.Fatal().Err(err).Msg("")
}
}

View File

@@ -3,7 +3,7 @@ package consts
const (
ComposeStackNameLabel = "com.docker.compose.project"
SwarmStackNameLabel = "com.docker.stack.namespace"
SwarmServiceIDLabel = "com.docker.swarm.service.id"
SwarmNodeIDLabel = "com.docker.swarm.node.id"
SwarmServiceIdLabel = "com.docker.swarm.service.id"
SwarmNodeIdLabel = "com.docker.swarm.node.id"
HideStackLabel = "io.portainer.hideStack"
)

View File

@@ -4,16 +4,15 @@ import (
"context"
"strings"
"github.com/docker/docker/api/types"
dockercontainer "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
"github.com/pkg/errors"
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/docker/images"
"github.com/Masterminds/semver"
"github.com/docker/docker/api/types"
dockercontainer "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
)
@@ -31,51 +30,16 @@ func NewContainerService(factory *dockerclient.ClientFactory, dataStore dataserv
}
}
// applyVersionConstraint uses the version to apply a transformation function to
// the value when the constraint is satisfied
func applyVersionConstraint[T any](currentVersion, versionConstraint string, value T, transform func(T) T) (T, error) {
newValue := value
constraint, err := semver.NewConstraint(versionConstraint)
if err != nil {
return newValue, errors.New("invalid version constraint specified")
}
currentVer, err := semver.NewVersion(currentVersion)
if err != nil {
log.Warn().Err(err).Msg("Unable to parse the Docker client version")
return newValue, nil
}
if satisfiesConstraint, _ := constraint.Validate(currentVer); satisfiesConstraint {
newValue = transform(value)
}
return newValue, nil
}
func clearMacAddrs(n network.NetworkingConfig) network.NetworkingConfig {
netConfig := network.NetworkingConfig{
EndpointsConfig: make(map[string]*network.EndpointSettings),
}
for k := range n.EndpointsConfig {
endpointConfig := n.EndpointsConfig[k].Copy()
endpointConfig.MacAddress = ""
netConfig.EndpointsConfig[k] = endpointConfig
}
return netConfig
}
// Recreate a container
func (c *ContainerService) Recreate(ctx context.Context, endpoint *portainer.Endpoint, containerId string, forcePullImage bool, imageTag, nodeName string) (*types.ContainerJSON, error) {
cli, err := c.factory.CreateClient(endpoint, nodeName, nil)
if err != nil {
return nil, errors.Wrap(err, "create client error")
}
defer cli.Close()
defer func(cli *client.Client) {
cli.Close()
}(cli)
log.Debug().Str("container_id", containerId).Msg("starting to fetch container information")
@@ -93,7 +57,8 @@ func (c *ContainerService) Recreate(ctx context.Context, endpoint *portainer.End
}
if imageTag != "" {
if err := img.WithTag(imageTag); err != nil {
err = img.WithTag(imageTag)
if err != nil {
return nil, errors.Wrapf(err, "set image tag error %s", imageTag)
}
@@ -105,39 +70,43 @@ func (c *ContainerService) Recreate(ctx context.Context, endpoint *portainer.End
// 1. pull image if you need force pull
if forcePullImage {
puller := images.NewPuller(cli, images.NewRegistryClient(c.dataStore), c.dataStore)
if err := puller.Pull(ctx, img); err != nil {
err = puller.Pull(ctx, img)
if err != nil {
return nil, errors.Wrapf(err, "pull image error %s", img.FullName())
}
}
// 2. stop the current container
log.Debug().Str("container_id", containerId).Msg("starting to stop the container")
if err := cli.ContainerStop(ctx, containerId, dockercontainer.StopOptions{}); err != nil {
err = cli.ContainerStop(ctx, containerId, dockercontainer.StopOptions{})
if err != nil {
return nil, errors.Wrap(err, "stop container error")
}
// 3. rename the current container
log.Debug().Str("container_id", containerId).Msg("starting to rename the container")
if err := cli.ContainerRename(ctx, containerId, container.Name+"-old"); err != nil {
err = cli.ContainerRename(ctx, containerId, container.Name+"-old")
if err != nil {
return nil, errors.Wrap(err, "rename container error")
}
initialNetwork := network.NetworkingConfig{
networkWithCreation := network.NetworkingConfig{
EndpointsConfig: make(map[string]*network.EndpointSettings),
}
// 4. disconnect all networks from the current container
for name, network := range container.NetworkSettings.Networks {
// This allows new container to use the same IP address if specified
if err := cli.NetworkDisconnect(ctx, network.NetworkID, containerId, true); err != nil {
err = cli.NetworkDisconnect(ctx, network.NetworkID, containerId, true)
if err != nil {
return nil, errors.Wrap(err, "disconnect network from old container error")
}
// 5. get the first network attached to the current container
if len(initialNetwork.EndpointsConfig) == 0 {
if len(networkWithCreation.EndpointsConfig) == 0 {
// Retrieve the first network that is linked to the present container, which
// will be utilized when creating the container.
initialNetwork.EndpointsConfig[name] = network
networkWithCreation.EndpointsConfig[name] = network
}
}
c.sr.enable()
@@ -147,11 +116,9 @@ func (c *ContainerService) Recreate(ctx context.Context, endpoint *portainer.End
c.sr.push(func() {
log.Debug().Str("container_id", containerId).Str("container", container.Name).Msg("restoring the container")
cli.ContainerRename(ctx, containerId, container.Name)
for _, network := range container.NetworkSettings.Networks {
cli.NetworkConnect(ctx, network.NetworkID, containerId, network)
}
cli.ContainerStart(ctx, containerId, dockercontainer.StartOptions{})
})
@@ -163,15 +130,7 @@ func (c *ContainerService) Recreate(ctx context.Context, endpoint *portainer.End
// to retain the same network settings we have to connect on creation to one of the old
// container's networks, and connect to the other networks after creation.
// see: https://portainer.atlassian.net/browse/EE-5448
// Docker API < 1.44 does not support specifying MAC addresses
// https://github.com/moby/moby/blob/6aea26b431ea152a8b085e453da06ea403f89886/client/container_create.go#L44-L46
initialNetwork, err = applyVersionConstraint(cli.ClientVersion(), "< 1.44", initialNetwork, clearMacAddrs)
if err != nil {
return nil, err
}
create, err := cli.ContainerCreate(ctx, container.Config, container.HostConfig, &initialNetwork, nil, container.Name)
create, err := cli.ContainerCreate(ctx, container.Config, container.HostConfig, &networkWithCreation, nil, container.Name)
c.sr.push(func() {
log.Debug().Str("container_id", create.ID).Msg("removing the new container")
@@ -191,19 +150,22 @@ func (c *ContainerService) Recreate(ctx context.Context, endpoint *portainer.End
log.Debug().Str("container_id", newContainerId).Msg("connecting networks to container")
networks := container.NetworkSettings.Networks
for key, network := range networks {
if _, ok := initialNetwork.EndpointsConfig[key]; ok {
_, ok := networkWithCreation.EndpointsConfig[key]
if ok {
// skip the network that is used during container creation
continue
}
if err := cli.NetworkConnect(ctx, network.NetworkID, newContainerId, network); err != nil {
err = cli.NetworkConnect(ctx, network.NetworkID, newContainerId, network)
if err != nil {
return nil, errors.Wrap(err, "connect container network error")
}
}
// 8. start the new container
log.Debug().Str("container_id", newContainerId).Msg("starting the new container")
if err := cli.ContainerStart(ctx, newContainerId, dockercontainer.StartOptions{}); err != nil {
err = cli.ContainerStart(ctx, newContainerId, dockercontainer.StartOptions{})
if err != nil {
return nil, errors.Wrap(err, "start container error")
}

View File

@@ -1,52 +0,0 @@
package docker
import (
"testing"
"github.com/docker/docker/api/types/network"
"github.com/stretchr/testify/require"
)
func TestApplyVersionConstraint(t *testing.T) {
initialNet := network.NetworkingConfig{
EndpointsConfig: map[string]*network.EndpointSettings{
"key1": {
MacAddress: "mac1",
EndpointID: "endpointID1",
},
"key2": {
MacAddress: "mac2",
EndpointID: "endpointID2",
},
},
}
f := func(currentVer string, constraint string, success, emptyMac bool) {
t.Helper()
transformedNet, err := applyVersionConstraint(currentVer, constraint, initialNet, clearMacAddrs)
if success {
require.NoError(t, err)
} else {
require.Error(t, err)
}
require.Len(t, transformedNet.EndpointsConfig, len(initialNet.EndpointsConfig))
for k := range initialNet.EndpointsConfig {
if emptyMac {
require.NotEqual(t, initialNet.EndpointsConfig[k], transformedNet.EndpointsConfig[k])
require.Empty(t, transformedNet.EndpointsConfig[k].MacAddress)
continue
}
require.Equal(t, initialNet.EndpointsConfig[k], transformedNet.EndpointsConfig[k])
}
}
f("1.45", "< 1.44", true, false) // No transformation
f("1.43", "< 1.44", true, true) // Transformation
f("a.b.", "< 1.44", true, false) // Invalid current version
f("1.45", "z 1.44", false, false) // Invalid version constraint
}

View File

@@ -1,7 +1,6 @@
package images
import (
"cmp"
"strings"
"time"
@@ -42,7 +41,8 @@ func (c *RegistryClient) RegistryAuth(image Image) (string, string, error) {
}
func (c *RegistryClient) CertainRegistryAuth(registry *portainer.Registry) (string, string, error) {
if err := registryutils.EnsureRegTokenValid(c.dataStore, registry); err != nil {
err := registryutils.EnsureRegTokenValid(c.dataStore, registry)
if err != nil {
return "", "", err
}
@@ -72,7 +72,8 @@ func (c *RegistryClient) EncodedRegistryAuth(image Image) (string, error) {
}
func (c *RegistryClient) EncodedCertainRegistryAuth(registry *portainer.Registry) (string, error) {
if err := registryutils.EnsureRegTokenValid(c.dataStore, registry); err != nil {
err := registryutils.EnsureRegTokenValid(c.dataStore, registry)
if err != nil {
return "", err
}
@@ -91,32 +92,38 @@ func findBestMatchRegistry(repository string, registries []portainer.Registry) (
}
var match1, match2, match3 *portainer.Registry
for i := 0; i < len(registries); i++ {
registry := registries[i]
if registry.Type == portainer.DockerHubRegistry {
// try to match repository examples:
// <USERNAME>/nginx:latest
// docker.io/<USERNAME>/nginx:latest
if strings.HasPrefix(repository, registry.Username+"/") || strings.HasPrefix(repository, registry.URL+"/"+registry.Username+"/") {
match1 = &registry
}
// try to match repository examples:
// portainer/portainer-ee:latest
// <NON-USERNAME>/portainer-ee:latest
if match3 == nil {
match3 = &registry
}
}
for _, registry := range registries {
if strings.Contains(repository, registry.URL) {
match2 = &registry
}
if registry.Type != portainer.DockerHubRegistry {
continue
}
// try to match repository examples:
// <USERNAME>/nginx:latest
// docker.io/<USERNAME>/nginx:latest
if strings.HasPrefix(repository, registry.Username+"/") || strings.HasPrefix(repository, registry.URL+"/"+registry.Username+"/") {
match1 = &registry
}
// try to match repository examples:
// portainer/portainer-ee:latest
// <NON-USERNAME>/portainer-ee:latest
if match3 == nil {
match3 = &registry
}
}
match := cmp.Or(match1, match2, match3)
match := match1
if match == nil {
match = match2
}
if match == nil {
match = match3
}
if match == nil {
return nil, errors.New("no registries matched")
}

View File

@@ -48,11 +48,11 @@ func (c *DigestClient) ContainersImageStatus(ctx context.Context, containers []t
statuses := make([]Status, len(containers))
for i, ct := range containers {
var nodeName string
if swarmNodeId := ct.Labels[consts.SwarmNodeIDLabel]; swarmNodeId != "" {
if swarmNodeId := ct.Labels[consts.SwarmNodeIdLabel]; swarmNodeId != "" {
if swarmNodeName, ok := swarmID2NameCache.Get(swarmNodeId); ok {
nodeName, _ = swarmNodeName.(string)
} else {
node, _, err := cli.NodeInspectWithRaw(ctx, ct.Labels[consts.SwarmNodeIDLabel])
node, _, err := cli.NodeInspectWithRaw(ctx, ct.Labels[consts.SwarmNodeIdLabel])
if err != nil {
return Error
}
@@ -160,7 +160,7 @@ func (c *DigestClient) ServiceImageStatus(ctx context.Context, serviceID string,
containers, err := cli.ContainerList(ctx, container.ListOptions{
All: true,
Filters: filters.NewArgs(filters.Arg("label", consts.SwarmServiceIDLabel+"="+serviceID)),
Filters: filters.NewArgs(filters.Arg("label", consts.SwarmServiceIdLabel+"="+serviceID)),
})
if err != nil {
log.Warn().Err(err).Str("serviceID", serviceID).Msg("cannot list container for the service")

View File

@@ -5,15 +5,14 @@ import (
"strings"
"time"
portainer "github.com/portainer/portainer/api"
dockerclient "github.com/portainer/portainer/api/docker/client"
"github.com/portainer/portainer/api/docker/consts"
"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"
portainer "github.com/portainer/portainer/api"
dockerclient "github.com/portainer/portainer/api/docker/client"
"github.com/portainer/portainer/api/docker/consts"
"github.com/rs/zerolog/log"
)
@@ -41,7 +40,8 @@ func (snapshotter *Snapshotter) CreateSnapshot(endpoint *portainer.Endpoint) (*p
}
func snapshot(cli *client.Client, endpoint *portainer.Endpoint) (*portainer.DockerSnapshot, error) {
if _, err := cli.Ping(context.Background()); err != nil {
_, err := cli.Ping(context.Background())
if err != nil {
return nil, err
}
@@ -49,42 +49,49 @@ func snapshot(cli *client.Client, endpoint *portainer.Endpoint) (*portainer.Dock
StackCount: 0,
}
if err := snapshotInfo(snapshot, cli); err != nil {
err = snapshotInfo(snapshot, cli)
if err != nil {
log.Warn().Str("environment", endpoint.Name).Err(err).Msg("unable to snapshot engine information")
}
if snapshot.Swarm {
if err := snapshotSwarmServices(snapshot, cli); err != nil {
err = snapshotSwarmServices(snapshot, cli)
if err != nil {
log.Warn().Str("environment", endpoint.Name).Err(err).Msg("unable to snapshot Swarm services")
}
if err := snapshotNodes(snapshot, cli); err != nil {
err = snapshotNodes(snapshot, cli)
if err != nil {
log.Warn().Str("environment", endpoint.Name).Err(err).Msg("unable to snapshot Swarm nodes")
}
}
if err := snapshotContainers(snapshot, cli); err != nil {
err = snapshotContainers(snapshot, cli)
if err != nil {
log.Warn().Str("environment", endpoint.Name).Err(err).Msg("unable to snapshot containers")
}
if err := snapshotImages(snapshot, cli); err != nil {
err = snapshotImages(snapshot, cli)
if err != nil {
log.Warn().Str("environment", endpoint.Name).Err(err).Msg("unable to snapshot images")
}
if err := snapshotVolumes(snapshot, cli); err != nil {
err = snapshotVolumes(snapshot, cli)
if err != nil {
log.Warn().Str("environment", endpoint.Name).Err(err).Msg("unable to snapshot volumes")
}
if err := snapshotNetworks(snapshot, cli); err != nil {
err = snapshotNetworks(snapshot, cli)
if err != nil {
log.Warn().Str("environment", endpoint.Name).Err(err).Msg("unable to snapshot networks")
}
if err := snapshotVersion(snapshot, cli); err != nil {
err = snapshotVersion(snapshot, cli)
if err != nil {
log.Warn().Str("environment", endpoint.Name).Err(err).Msg("unable to snapshot engine version")
}
snapshot.Time = time.Now().Unix()
return snapshot, nil
}
@@ -99,7 +106,6 @@ func snapshotInfo(snapshot *portainer.DockerSnapshot, cli *client.Client) error
snapshot.TotalCPU = info.NCPU
snapshot.TotalMemory = info.MemTotal
snapshot.SnapshotRaw.Info = info
return nil
}
@@ -108,19 +114,15 @@ func snapshotNodes(snapshot *portainer.DockerSnapshot, cli *client.Client) error
if err != nil {
return err
}
var nanoCpus int64
var totalMem int64
for _, node := range nodes {
nanoCpus += node.Description.Resources.NanoCPUs
totalMem += node.Description.Resources.MemoryBytes
}
snapshot.TotalCPU = int(nanoCpus / 1e9)
snapshot.TotalMemory = totalMem
snapshot.NodeCount = len(nodes)
return nil
}
@@ -142,7 +144,6 @@ func snapshotSwarmServices(snapshot *portainer.DockerSnapshot, cli *client.Clien
snapshot.ServiceCount = len(services)
snapshot.StackCount += len(stacks)
return nil
}
@@ -155,10 +156,9 @@ func snapshotContainers(snapshot *portainer.DockerSnapshot, cli *client.Client)
stacks := make(map[string]struct{})
gpuUseSet := make(map[string]struct{})
gpuUseAll := false
for _, container := range containers {
if container.State == "running" {
// Snapshot GPUs
// snapshot GPUs
response, err := cli.ContainerInspect(context.Background(), container.ID)
if err != nil {
// Inspect a container will fail when the container runs on a different
@@ -177,6 +177,7 @@ func snapshotContainers(snapshot *portainer.DockerSnapshot, cli *client.Client)
} else {
var gpuOptions *_container.DeviceRequest = nil
for _, deviceRequest := range response.HostConfig.Resources.DeviceRequests {
deviceRequest := deviceRequest
if deviceRequest.Driver == "nvidia" || deviceRequest.Capabilities[0][0] == "gpu" {
gpuOptions = &deviceRequest
}
@@ -186,7 +187,6 @@ func snapshotContainers(snapshot *portainer.DockerSnapshot, cli *client.Client)
if gpuOptions.Count == -1 {
gpuUseAll = true
}
for _, id := range gpuOptions.DeviceIDs {
gpuUseSet[id] = struct{}{}
}
@@ -217,11 +217,9 @@ func snapshotContainers(snapshot *portainer.DockerSnapshot, cli *client.Client)
snapshot.HealthyContainerCount = stats.Healthy
snapshot.UnhealthyContainerCount = stats.Unhealthy
snapshot.StackCount += len(stacks)
for _, container := range containers {
snapshot.SnapshotRaw.Containers = append(snapshot.SnapshotRaw.Containers, portainer.DockerContainerSnapshot{Container: container})
}
return nil
}
@@ -233,7 +231,6 @@ func snapshotImages(snapshot *portainer.DockerSnapshot, cli *client.Client) erro
snapshot.ImageCount = len(images)
snapshot.SnapshotRaw.Images = images
return nil
}
@@ -245,7 +242,6 @@ func snapshotVolumes(snapshot *portainer.DockerSnapshot, cli *client.Client) err
snapshot.VolumeCount = len(volumes.Volumes)
snapshot.SnapshotRaw.Volumes = volumes
return nil
}
@@ -254,9 +250,7 @@ func snapshotNetworks(snapshot *portainer.DockerSnapshot, cli *client.Client) er
if err != nil {
return err
}
snapshot.SnapshotRaw.Networks = networks
return nil
}
@@ -265,19 +259,6 @@ func snapshotVersion(snapshot *portainer.DockerSnapshot, cli *client.Client) err
if err != nil {
return err
}
snapshot.SnapshotRaw.Version = version
snapshot.IsPodman = isPodman(version)
return nil
}
// isPodman checks if the version is for Podman by checking if any of the components contain "podman".
// If it's podman, a component name should be "Podman Engine"
func isPodman(version types.Version) bool {
for _, component := range version.Components {
if strings.Contains(strings.ToLower(component.Name), "podman") {
return true
}
}
return false
}

View File

@@ -38,7 +38,7 @@ func (manager *ComposeStackManager) ComposeSyntaxMaxVersion() string {
}
// Up builds, (re)creates and starts containers in the background. Wraps `docker-compose up -d` command
func (manager *ComposeStackManager) Up(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint, options portainer.ComposeUpOptions) error {
func (manager *ComposeStackManager) Up(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint, forceRecreate bool) error {
url, proxy, err := manager.fetchEndpointProxy(endpoint)
if err != nil {
return errors.Wrap(err, "failed to fetch environment proxy")
@@ -61,39 +61,7 @@ func (manager *ComposeStackManager) Up(ctx context.Context, stack *portainer.Sta
Host: url,
ProjectName: stack.Name,
},
ForceRecreate: options.ForceRecreate,
AbortOnContainerExit: options.AbortOnContainerExit,
})
return errors.Wrap(err, "failed to deploy a stack")
}
// Run runs a one-off command on a service. Wraps `docker-compose run` command
func (manager *ComposeStackManager) Run(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint, serviceName string, options portainer.ComposeRunOptions) error {
url, proxy, err := manager.fetchEndpointProxy(endpoint)
if err != nil {
return errors.Wrap(err, "failed to fetch environment proxy")
}
if proxy != nil {
defer proxy.Close()
}
envFilePath, err := createEnvFile(stack)
if err != nil {
return errors.Wrap(err, "failed to create env file")
}
filePaths := stackutils.GetStackFilePaths(stack, true)
err = manager.deployer.Run(ctx, filePaths, serviceName, libstack.RunOptions{
Options: libstack.Options{
WorkingDir: stack.ProjectPath,
EnvFilePath: envFilePath,
Host: url,
ProjectName: stack.Name,
},
Remove: options.Remove,
Args: options.Args,
Detached: options.Detached,
ForceRecreate: forceRecreate,
})
return errors.Wrap(err, "failed to deploy a stack")
}

View File

@@ -60,7 +60,7 @@ func Test_UpAndDown(t *testing.T) {
ctx := context.TODO()
err = w.Up(ctx, stack, endpoint, portainer.ComposeUpOptions{})
err = w.Up(ctx, stack, endpoint, false)
if err != nil {
t.Fatalf("Error calling docker-compose up: %s", err)
}

View File

@@ -18,3 +18,7 @@ func (deployer *kubernetesMockDeployer) Deploy(userID portainer.UserID, endpoint
func (deployer *kubernetesMockDeployer) Remove(userID portainer.UserID, endpoint *portainer.Endpoint, manifestFiles []string, namespace string) (string, error) {
return "", nil
}
func (deployer *kubernetesMockDeployer) ConvertCompose(data []byte) ([]byte, error) {
return nil, nil
}

View File

@@ -44,7 +44,7 @@ func NewKubernetesDeployer(kubernetesTokenCacheManager *kubernetes.TokenCacheMan
}
func (deployer *KubernetesDeployer) getToken(userID portainer.UserID, endpoint *portainer.Endpoint, setLocalAdminToken bool) (string, error) {
kubeCLI, err := deployer.kubernetesClientFactory.GetPrivilegedKubeClient(endpoint)
kubeCLI, err := deployer.kubernetesClientFactory.GetKubeClient(endpoint)
if err != nil {
return "", err
}
@@ -137,6 +137,29 @@ func (deployer *KubernetesDeployer) command(operation string, userID portainer.U
return string(output), nil
}
// ConvertCompose leverages the kompose binary to deploy a compose compliant manifest.
func (deployer *KubernetesDeployer) ConvertCompose(data []byte) ([]byte, error) {
command := path.Join(deployer.binaryPath, "kompose")
if runtime.GOOS == "windows" {
command = path.Join(deployer.binaryPath, "kompose.exe")
}
args := make([]string, 0)
args = append(args, "convert", "-f", "-", "--stdout")
var stderr bytes.Buffer
cmd := exec.Command(command, args...)
cmd.Stderr = &stderr
cmd.Stdin = bytes.NewReader(data)
output, err := cmd.Output()
if err != nil {
return nil, errors.New(stderr.String())
}
return output, nil
}
func (deployer *KubernetesDeployer) getAgentURL(endpoint *portainer.Endpoint) (string, *factory.ProxyServer, error) {
proxy, err := deployer.proxyManager.CreateAgentProxyServer(endpoint)
if err != nil {

View File

@@ -227,10 +227,10 @@ func (manager *SwarmStackManager) updateDockerCLIConfiguration(configPath string
}
if config["HttpHeaders"] == nil {
config["HttpHeaders"] = make(map[string]any)
config["HttpHeaders"] = make(map[string]interface{})
}
headersObject := config["HttpHeaders"].(map[string]any)
headersObject := config["HttpHeaders"].(map[string]interface{})
headersObject["X-PortainerAgent-ManagerOperation"] = "1"
headersObject["X-PortainerAgent-Signature"] = signature
headersObject["X-PortainerAgent-PublicKey"] = manager.signatureService.EncodedPublicKey()
@@ -238,12 +238,12 @@ func (manager *SwarmStackManager) updateDockerCLIConfiguration(configPath string
return manager.fileService.WriteJSONToFile(configFilePath, config)
}
func (manager *SwarmStackManager) retrieveConfigurationFromDisk(path string) (map[string]any, error) {
var config map[string]any
func (manager *SwarmStackManager) retrieveConfigurationFromDisk(path string) (map[string]interface{}, error) {
var config map[string]interface{}
raw, err := manager.fileService.GetFileContent(path, "")
if err != nil {
return make(map[string]any), nil
return make(map[string]interface{}), nil
}
err = json.Unmarshal(raw, &config)

View File

@@ -36,6 +36,8 @@ const (
ManifestFileDefaultName = "k8s-deployment.yml"
// EdgeStackStorePath represents the subfolder where edge stack files are stored in the file store folder.
EdgeStackStorePath = "edge_stacks"
// FDOProfileStorePath represents the subfolder where FDO profiles files are stored in the file store folder.
FDOProfileStorePath = "fdo_profiles"
// PrivateKeyFile represents the name on disk of the file containing the private key.
PrivateKeyFile = "portainer.key"
// PublicKeyFile represents the name on disk of the file containing the public key.
@@ -599,7 +601,7 @@ func (service *Service) Rename(oldPath, newPath string) error {
}
// WriteJSONToFile writes JSON to the specified file.
func (service *Service) WriteJSONToFile(path string, content any) error {
func (service *Service) WriteJSONToFile(path string, content interface{}) error {
jsonContent, err := json.Marshal(content)
if err != nil {
return err
@@ -1001,6 +1003,23 @@ func MoveDirectory(originalPath, newPath string, overwriteTargetPath bool) error
return os.Rename(originalPath, newPath)
}
// StoreFDOProfileFileFromBytes creates a subfolder in the FDOProfileStorePath and stores a new file from bytes.
// It returns the path to the folder where the file is stored.
func (service *Service) StoreFDOProfileFileFromBytes(fdoProfileIdentifier string, data []byte) (string, error) {
err := service.createDirectoryInStore(FDOProfileStorePath)
if err != nil {
return "", err
}
filePath := JoinPaths(FDOProfileStorePath, fdoProfileIdentifier)
err = service.createFileInStore(filePath, bytes.NewReader(data))
if err != nil {
return "", err
}
return service.wrapFileStore(filePath), nil
}
func CreateFile(path string, r io.Reader) error {
out, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {

View File

@@ -6,7 +6,7 @@ import (
"path/filepath"
"strings"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api"
)
type MultiFilterArgs []struct {

View File

@@ -1,10 +1,9 @@
package filesystem
import (
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/stretchr/testify/assert"
"testing"
)
func TestMultiFilterDirForPerDevConfigs(t *testing.T) {

View File

@@ -6,8 +6,6 @@ import (
"path/filepath"
"strings"
gittypes "github.com/portainer/portainer/api/git/types"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing"
@@ -16,6 +14,7 @@ import (
githttp "github.com/go-git/go-git/v5/plumbing/transport/http"
"github.com/go-git/go-git/v5/storage/memory"
"github.com/pkg/errors"
gittypes "github.com/portainer/portainer/api/git/types"
)
type gitClient struct {
@@ -144,7 +143,6 @@ func (c *gitClient) listFiles(ctx context.Context, opt fetchOption) ([]string, e
ReferenceName: plumbing.ReferenceName(opt.referenceName),
Auth: getAuth(opt.username, opt.password),
InsecureSkipTLS: opt.tlsSkipVerify,
Tags: git.NoTags,
}
repo, err := git.Clone(memory.NewStorage(), nil, cloneOption)
@@ -168,10 +166,7 @@ func (c *gitClient) listFiles(ctx context.Context, opt fetchOption) ([]string, e
}
var allPaths []string
w := object.NewTreeWalker(tree, true, nil)
defer w.Close()
for {
name, entry, err := w.Next()
if err != nil {

View File

@@ -91,29 +91,6 @@ func Test_latestCommitID(t *testing.T) {
assert.Equal(t, "68dcaa7bd452494043c64252ab90db0f98ecf8d2", id)
}
func Test_ListRefs(t *testing.T) {
service := Service{git: NewGitClient(true)}
repositoryURL := setup(t)
fs, err := service.ListRefs(repositoryURL, "", "", false, false)
assert.NoError(t, err)
assert.Equal(t, []string{"refs/heads/main"}, fs)
}
func Test_ListFiles(t *testing.T) {
service := Service{git: NewGitClient(true)}
repositoryURL := setup(t)
referenceName := "refs/heads/main"
fs, err := service.ListFiles(repositoryURL, referenceName, "", "", false, false, []string{".yml"}, false)
assert.NoError(t, err)
assert.Equal(t, []string{"docker-compose.yml"}, fs)
}
func getCommitHistoryLength(t *testing.T, err error, dir string) int {
repo, err := git.PlainOpen(dir)
if err != nil {

View File

@@ -9,7 +9,6 @@ import (
lru "github.com/hashicorp/golang-lru"
"github.com/rs/zerolog/log"
"golang.org/x/sync/singleflight"
)
const (
@@ -140,18 +139,12 @@ func (service *Service) CloneRepository(destination, repositoryURL, referenceNam
return service.cloneRepository(destination, options)
}
func (service *Service) repoManager(options baseOption) repoManager {
repoManager := service.git
func (service *Service) cloneRepository(destination string, options cloneOption) error {
if isAzureUrl(options.repositoryUrl) {
repoManager = service.azure
return service.azure.download(context.TODO(), destination, options)
}
return repoManager
}
func (service *Service) cloneRepository(destination string, options cloneOption) error {
return service.repoManager(options.baseOption).download(context.TODO(), destination, options)
return service.git.download(context.TODO(), destination, options)
}
// LatestCommitID returns SHA1 of the latest commit of the specified reference
@@ -166,7 +159,11 @@ func (service *Service) LatestCommitID(repositoryURL, referenceName, username, p
referenceName: referenceName,
}
return service.repoManager(options.baseOption).latestCommitID(context.TODO(), options)
if isAzureUrl(options.repositoryUrl) {
return service.azure.latestCommitID(context.TODO(), options)
}
return service.git.latestCommitID(context.TODO(), options)
}
// ListRefs will list target repository's references without cloning the repository
@@ -177,16 +174,21 @@ func (service *Service) ListRefs(repositoryURL, username, password string, hardR
service.repoRefCache.Remove(refCacheKey)
// Remove file caches pointed to the same repository
for _, fileCacheKey := range service.repoFileCache.Keys() {
if key, ok := fileCacheKey.(string); ok && strings.HasPrefix(key, repositoryURL) {
service.repoFileCache.Remove(key)
key, ok := fileCacheKey.(string)
if ok {
if strings.HasPrefix(key, repositoryURL) {
service.repoFileCache.Remove(key)
}
}
}
}
if service.repoRefCache != nil {
// Lookup the refs cache first
if cache, ok := service.repoRefCache.Get(refCacheKey); ok {
if refs, ok := cache.([]string); ok {
cache, ok := service.repoRefCache.Get(refCacheKey)
if ok {
refs, success := cache.([]string)
if success {
return refs, nil
}
}
@@ -199,35 +201,33 @@ func (service *Service) ListRefs(repositoryURL, username, password string, hardR
tlsSkipVerify: tlsSkipVerify,
}
refs, err := service.repoManager(options).listRefs(context.TODO(), options)
if err != nil {
return nil, err
var (
refs []string
err error
)
if isAzureUrl(options.repositoryUrl) {
refs, err = service.azure.listRefs(context.TODO(), options)
if err != nil {
return nil, err
}
} else {
refs, err = service.git.listRefs(context.TODO(), options)
if err != nil {
return nil, err
}
}
if service.cacheEnabled && service.repoRefCache != nil {
service.repoRefCache.Add(refCacheKey, refs)
}
return refs, nil
}
var singleflightGroup = &singleflight.Group{}
// ListFiles will list all the files of the target repository with specific extensions.
// If extension is not provided, it will list all the files under the target repository
func (service *Service) ListFiles(repositoryURL, referenceName, username, password string, dirOnly, hardRefresh bool, includedExts []string, tlsSkipVerify bool) ([]string, error) {
repoKey := generateCacheKey(repositoryURL, referenceName, username, password, strconv.FormatBool(tlsSkipVerify), strconv.FormatBool(dirOnly))
fs, err, _ := singleflightGroup.Do(repoKey, func() (any, error) {
return service.listFiles(repositoryURL, referenceName, username, password, dirOnly, hardRefresh, tlsSkipVerify)
})
return filterFiles(fs.([]string), includedExts), err
}
func (service *Service) listFiles(repositoryURL, referenceName, username, password string, dirOnly, hardRefresh bool, tlsSkipVerify bool) ([]string, error) {
repoKey := generateCacheKey(repositoryURL, referenceName, username, password, strconv.FormatBool(tlsSkipVerify), strconv.FormatBool(dirOnly))
if service.cacheEnabled && hardRefresh {
// Should remove the cache explicitly, so that the following normal list can show the correct result
service.repoFileCache.Remove(repoKey)
@@ -235,9 +235,14 @@ func (service *Service) listFiles(repositoryURL, referenceName, username, passwo
if service.repoFileCache != nil {
// lookup the files cache first
if cache, ok := service.repoFileCache.Get(repoKey); ok {
if files, ok := cache.([]string); ok {
return files, nil
cache, ok := service.repoFileCache.Get(repoKey)
if ok {
files, success := cache.([]string)
if success {
// For the case while searching files in a repository without include extensions for the first time,
// but with include extensions for the second time
includedFiles := filterFiles(files, includedExts)
return includedFiles, nil
}
}
}
@@ -253,16 +258,28 @@ func (service *Service) listFiles(repositoryURL, referenceName, username, passwo
dirOnly: dirOnly,
}
files, err := service.repoManager(options.baseOption).listFiles(context.TODO(), options)
if err != nil {
return nil, err
var (
files []string
err error
)
if isAzureUrl(options.repositoryUrl) {
files, err = service.azure.listFiles(context.TODO(), options)
if err != nil {
return nil, err
}
} else {
files, err = service.git.listFiles(context.TODO(), options)
if err != nil {
return nil, err
}
}
includedFiles := filterFiles(files, includedExts)
if service.cacheEnabled && service.repoFileCache != nil {
service.repoFileCache.Add(repoKey, files)
service.repoFileCache.Add(repoKey, includedFiles)
return includedFiles, nil
}
return files, nil
return includedFiles, nil
}
func (service *Service) purgeCache() {
@@ -289,7 +306,6 @@ func matchExtensions(target string, exts []string) bool {
return true
}
}
return false
}
@@ -300,11 +316,10 @@ func filterFiles(paths []string, includedExts []string) []string {
var includedFiles []string
for _, filename := range paths {
// Filter out the filenames with non-included extension
// filter out the filenames with non-included extension
if matchExtensions(filename, includedExts) {
includedFiles = append(includedFiles, filename)
}
}
return includedFiles
}

View File

@@ -8,7 +8,7 @@ import (
)
func ValidateRepoConfig(repoConfig *gittypes.RepoConfig) error {
if len(repoConfig.URL) == 0 || !govalidator.IsURL(repoConfig.URL) {
if govalidator.IsNull(repoConfig.URL) || !govalidator.IsURL(repoConfig.URL) {
return httperrors.NewInvalidPayloadError("Invalid repository URL. Must correspond to a valid URL format")
}
@@ -17,7 +17,7 @@ func ValidateRepoConfig(repoConfig *gittypes.RepoConfig) error {
}
func ValidateRepoAuthentication(auth *gittypes.GitAuthentication) error {
if auth != nil && len(auth.Password) == 0 && auth.GitCredentialID == 0 {
if auth != nil && govalidator.IsNull(auth.Password) && auth.GitCredentialID == 0 {
return httperrors.NewInvalidPayloadError("Invalid repository credentials. Password or GitCredentialID must be specified when authentication is enabled")
}

View File

@@ -0,0 +1,247 @@
package fdo
import (
"bytes"
"errors"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/portainer/portainer/third_party/digest"
)
type FDOOwnerClient struct {
OwnerURL string
Username string
Password string
Timeout time.Duration
}
type ServiceInfo struct {
Module string
Var string
Filename string
Bytes []byte
GUID string
Device string
Priority int
OS string
Version string
Arch string
CRID int
Hash string
}
func (c FDOOwnerClient) doDigestAuthReq(method, endpoint, contentType string, body io.Reader) (*http.Response, error) {
transport := digest.NewTransport(c.Username, c.Password)
client, err := transport.Client()
if err != nil {
return nil, err
}
client.Timeout = c.Timeout
e, err := url.Parse(endpoint)
if err != nil {
return nil, err
}
u, err := url.Parse(c.OwnerURL)
if err != nil {
return nil, err
}
req, err := http.NewRequest(method, u.ResolveReference(e).String(), body)
if err != nil {
return nil, err
}
if contentType != "" {
req.Header.Set("Content-Type", contentType)
}
return client.Do(req)
}
func (c FDOOwnerClient) PostVoucher(ov []byte) (string, error) {
resp, err := c.doDigestAuthReq(
http.MethodPost,
"api/v1/owner/vouchers",
"application/cbor",
bytes.NewReader(ov),
)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
if resp.StatusCode != http.StatusOK {
return "", errors.New(http.StatusText(resp.StatusCode))
}
return string(body), nil
}
func (c FDOOwnerClient) PutDeviceSVI(info ServiceInfo) error {
values := url.Values{}
values.Set("module", info.Module)
values.Set("var", info.Var)
values.Set("filename", info.Filename)
values.Set("guid", info.GUID)
values.Set("device", info.Device)
values.Set("priority", strconv.Itoa(info.Priority))
values.Set("os", info.OS)
values.Set("version", info.Version)
values.Set("arch", info.Arch)
values.Set("crid", strconv.Itoa(info.CRID))
values.Set("hash", info.Hash)
resp, err := c.doDigestAuthReq(
http.MethodPut,
"api/v1/device/svi?"+values.Encode(),
"application/octet-stream",
strings.NewReader(string(info.Bytes)),
)
if err != nil {
return err
}
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return errors.New(http.StatusText(resp.StatusCode))
}
return nil
}
func (c FDOOwnerClient) PutDeviceSVIRaw(info url.Values, body []byte) error {
resp, err := c.doDigestAuthReq(
http.MethodPut,
"api/v1/device/svi?"+info.Encode(),
"application/octet-stream",
strings.NewReader(string(body)),
)
if err != nil {
return err
}
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return errors.New(http.StatusText(resp.StatusCode))
}
return nil
}
func (c FDOOwnerClient) GetVouchers() ([]string, error) {
resp, err := c.doDigestAuthReq(
http.MethodGet,
"api/v1/owner/vouchers",
"",
nil,
)
if err != nil {
return nil, err
}
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, errors.New(http.StatusText(resp.StatusCode))
}
contents, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
guids := strings.FieldsFunc(
strings.TrimSpace(string(contents)),
func(c rune) bool {
return c == ','
},
)
return guids, nil
}
func (c FDOOwnerClient) DeleteVoucher(guid string) error {
resp, err := c.doDigestAuthReq(
http.MethodDelete,
"api/v1/owner/vouchers?id="+guid,
"",
nil,
)
if err != nil {
return err
}
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return errors.New(http.StatusText(resp.StatusCode))
}
return nil
}
func (c FDOOwnerClient) GetDeviceSVI(guid string) (string, error) {
resp, err := c.doDigestAuthReq(
http.MethodGet,
"api/v1/device/svi?guid="+guid,
"",
nil,
)
if err != nil {
return "", err
}
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
if resp.StatusCode != http.StatusOK {
return "", errors.New(http.StatusText(resp.StatusCode))
}
return string(body), nil
}
func (c FDOOwnerClient) DeleteDeviceSVI(id string) error {
resp, err := c.doDigestAuthReq(
http.MethodDelete,
"api/v1/device/svi?id="+id,
"",
nil,
)
if err != nil {
return err
}
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return errors.New(http.StatusText(resp.StatusCode))
}
return nil
}

View File

@@ -12,7 +12,7 @@ import (
func (service *Service) enableDeviceFeatures(configuration portainer.OpenAMTConfiguration, deviceGUID string, features portainer.OpenAMTDeviceEnabledFeatures) error {
url := fmt.Sprintf("https://%s/mps/api/v1/amt/features/%s", configuration.MPSServer, deviceGUID)
payload := map[string]any{
payload := map[string]interface{}{
"enableSOL": features.SOL,
"enableIDER": features.IDER,
"enableKVM": features.KVM,

View File

@@ -4,41 +4,35 @@ import (
"crypto/rand"
"fmt"
"net/http"
"os"
"github.com/portainer/portainer/api/http/security"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
gorillacsrf "github.com/gorilla/csrf"
"github.com/portainer/portainer/api/http/security"
"github.com/urfave/negroni"
)
func WithProtect(handler http.Handler) (http.Handler, error) {
// IsDockerDesktopExtension is used to check if we should skip csrf checks in the request bouncer (ShouldSkipCSRFCheck)
// DOCKER_EXTENSION is set to '1' in build/docker-extension/docker-compose.yml
isDockerDesktopExtension := false
if val, ok := os.LookupEnv("DOCKER_EXTENSION"); ok && val == "1" {
isDockerDesktopExtension = true
}
handler = withSendCSRFToken(handler)
token := make([]byte, 32)
if _, err := rand.Read(token); err != nil {
_, err := rand.Read(token)
if err != nil {
return nil, fmt.Errorf("failed to generate CSRF token: %w", err)
}
handler = gorillacsrf.Protect(
token,
[]byte(token),
gorillacsrf.Path("/"),
gorillacsrf.Secure(false),
)(handler)
return withSkipCSRF(handler, isDockerDesktopExtension), nil
return withSkipCSRF(handler), nil
}
func withSendCSRFToken(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
sw := negroni.NewResponseWriter(w)
sw.Before(func(sw negroni.ResponseWriter) {
@@ -50,15 +44,16 @@ func withSendCSRFToken(handler http.Handler) http.Handler {
})
handler.ServeHTTP(sw, r)
})
}
func withSkipCSRF(handler http.Handler, isDockerDesktopExtension bool) http.Handler {
func withSkipCSRF(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
skip, err := security.ShouldSkipCSRFCheck(r, isDockerDesktopExtension)
skip, err := security.ShouldSkipCSRFCheck(r)
if err != nil {
httperror.WriteError(w, http.StatusForbidden, err.Error(), err)
return
}

View File

@@ -12,6 +12,7 @@ import (
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/libhttp/response"
"github.com/asaskevich/govalidator"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
)
@@ -29,11 +30,11 @@ type authenticateResponse struct {
}
func (payload *authenticatePayload) Validate(r *http.Request) error {
if len(payload.Username) == 0 {
if govalidator.IsNull(payload.Username) {
return errors.New("Invalid username")
}
if len(payload.Password) == 0 {
if govalidator.IsNull(payload.Password) {
return errors.New("Invalid password")
}
@@ -55,7 +56,8 @@ func (payload *authenticatePayload) Validate(r *http.Request) error {
// @router /auth [post]
func (handler *Handler) authenticate(rw http.ResponseWriter, r *http.Request) *httperror.HandlerError {
var payload authenticatePayload
if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil {
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return httperror.BadRequest("Invalid request payload", err)
}
@@ -102,7 +104,8 @@ func isUserInitialAdmin(user *portainer.User) bool {
}
func (handler *Handler) authenticateInternal(w http.ResponseWriter, user *portainer.User, password string) *httperror.HandlerError {
if err := handler.CryptoService.CompareHashAndData(user.Password, password); err != nil {
err := handler.CryptoService.CompareHashAndData(user.Password, password)
if err != nil {
return httperror.NewError(http.StatusUnprocessableEntity, "Invalid credentials", httperrors.ErrUnauthorized)
}
@@ -112,7 +115,8 @@ 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 {
if err := handler.LDAPService.AuthenticateUser(username, password, ldapSettings); err != nil {
err := handler.LDAPService.AuthenticateUser(username, password, ldapSettings)
if err != nil {
if errors.Is(err, httperrors.ErrUnauthorized) {
return httperror.NewError(http.StatusUnprocessableEntity, "Invalid credentials", httperrors.ErrUnauthorized)
}
@@ -127,12 +131,14 @@ func (handler *Handler) authenticateLDAP(w http.ResponseWriter, user *portainer.
PortainerAuthorizations: authorization.DefaultPortainerAuthorizations(),
}
if err := handler.DataStore.User().Create(user); err != nil {
err = handler.DataStore.User().Create(user)
if err != nil {
return httperror.InternalServerError("Unable to persist user inside the database", err)
}
}
if err := handler.syncUserTeamsWithLDAPGroups(user, ldapSettings); err != nil {
err = handler.syncUserTeamsWithLDAPGroups(user, ldapSettings)
if err != nil {
log.Warn().Err(err).Msg("unable to automatically sync user teams with ldap")
}
@@ -154,6 +160,7 @@ func (handler *Handler) persistAndWriteToken(w http.ResponseWriter, tokenData *p
security.AddAuthCookie(w, token, expirationTime)
return response.JSON(w, &authenticateResponse{JWT: token})
}
func (handler *Handler) syncUserTeamsWithLDAPGroups(user *portainer.User, settings *portainer.LDAPSettings) error {
@@ -178,18 +185,22 @@ func (handler *Handler) syncUserTeamsWithLDAPGroups(user *portainer.User, settin
}
for _, team := range teams {
if !teamExists(team.Name, userGroups) || teamMembershipExists(team.ID, userMemberships) {
continue
}
if teamExists(team.Name, userGroups) {
membership := &portainer.TeamMembership{
UserID: user.ID,
TeamID: team.ID,
Role: portainer.TeamMember,
}
if teamMembershipExists(team.ID, userMemberships) {
continue
}
if err := handler.DataStore.TeamMembership().Create(membership); err != nil {
return err
membership := &portainer.TeamMembership{
UserID: user.ID,
TeamID: team.ID,
Role: portainer.TeamMember,
}
err := handler.DataStore.TeamMembership().Create(membership)
if err != nil {
return err
}
}
}

View File

@@ -9,6 +9,7 @@ import (
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/asaskevich/govalidator"
"github.com/rs/zerolog/log"
)
@@ -18,7 +19,7 @@ type oauthPayload struct {
}
func (payload *oauthPayload) Validate(r *http.Request) error {
if len(payload.Code) == 0 {
if govalidator.IsNull(payload.Code) {
return errors.New("Invalid OAuth authorization code")
}

View File

@@ -41,6 +41,5 @@ func NewHandler(bouncer security.BouncerService, rateLimiter *security.RateLimit
rateLimiter.LimitAccess(bouncer.PublicAccess(httperror.LoggerHandler(h.authenticate)))).Methods(http.MethodPost)
h.Handle("/auth/logout",
bouncer.PublicAccess(httperror.LoggerHandler(h.logout))).Methods(http.MethodPost)
return h
}

View File

@@ -4,7 +4,7 @@ import (
"net/http"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/logoutcontext"
"github.com/portainer/portainer/api/internal/logoutcontext"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/response"
)
@@ -28,7 +28,5 @@ func (handler *Handler) logout(w http.ResponseWriter, r *http.Request) *httperro
security.RemoveAuthCookie(w)
handler.bouncer.RevokeJWT(tokenData.Token)
return response.Empty(w)
}

View File

@@ -108,13 +108,13 @@ type customTemplateFromFileContentPayload struct {
}
func (payload *customTemplateFromFileContentPayload) Validate(r *http.Request) error {
if len(payload.Title) == 0 {
if govalidator.IsNull(payload.Title) {
return errors.New("Invalid custom template title")
}
if len(payload.Description) == 0 {
if govalidator.IsNull(payload.Description) {
return errors.New("Invalid custom template description")
}
if len(payload.FileContent) == 0 {
if govalidator.IsNull(payload.FileContent) {
return errors.New("Invalid file content")
}
if payload.Type != portainer.KubernetesStack && payload.Platform != portainer.CustomTemplatePlatformLinux && payload.Platform != portainer.CustomTemplatePlatformWindows {
@@ -132,7 +132,7 @@ func (payload *customTemplateFromFileContentPayload) Validate(r *http.Request) e
}
func isValidNote(note string) bool {
if len(note) == 0 {
if govalidator.IsNull(note) {
return true
}
match, _ := regexp.MatchString("<img", note)
@@ -226,19 +226,19 @@ type customTemplateFromGitRepositoryPayload struct {
}
func (payload *customTemplateFromGitRepositoryPayload) Validate(r *http.Request) error {
if len(payload.Title) == 0 {
if govalidator.IsNull(payload.Title) {
return errors.New("Invalid custom template title")
}
if len(payload.Description) == 0 {
if govalidator.IsNull(payload.Description) {
return errors.New("Invalid custom template description")
}
if len(payload.RepositoryURL) == 0 || !govalidator.IsURL(payload.RepositoryURL) {
if govalidator.IsNull(payload.RepositoryURL) || !govalidator.IsURL(payload.RepositoryURL) {
return errors.New("Invalid repository URL. Must correspond to a valid URL format")
}
if payload.RepositoryAuthentication && (len(payload.RepositoryUsername) == 0 || len(payload.RepositoryPassword) == 0) {
if payload.RepositoryAuthentication && (govalidator.IsNull(payload.RepositoryUsername) || govalidator.IsNull(payload.RepositoryPassword)) {
return errors.New("Invalid repository credentials. Username and password must be specified when authentication is enabled")
}
if len(payload.ComposeFilePathInRepository) == 0 {
if govalidator.IsNull(payload.ComposeFilePathInRepository) {
payload.ComposeFilePathInRepository = filesystem.ComposeFileDefaultName
}
@@ -482,7 +482,7 @@ func (handler *Handler) createCustomTemplateFromFileUpload(r *http.Request) (*po
}
templateFolder := strconv.Itoa(customTemplateID)
projectPath, err := handler.FileService.StoreCustomTemplateFileFromBytes(templateFolder, customTemplate.EntryPoint, payload.FileContent)
projectPath, err := handler.FileService.StoreCustomTemplateFileFromBytes(templateFolder, customTemplate.EntryPoint, []byte(payload.FileContent))
if err != nil {
return nil, err
}

View File

@@ -70,7 +70,8 @@ func (handler *Handler) customTemplateGitFetch(w http.ResponseWriter, r *http.Re
if err != nil {
log.Warn().Err(err).Msg("failed to download git repository")
if rbErr := rollbackCustomTemplate(backupPath, customTemplate.ProjectPath); rbErr != nil {
if err != nil {
rbErr := rollbackCustomTemplate(backupPath, customTemplate.ProjectPath)
return httperror.InternalServerError("Failed to rollback the custom template folder", rbErr)
}

View File

@@ -2,7 +2,6 @@ package customtemplates
import (
"bytes"
"errors"
"io"
"io/fs"
"net/http"
@@ -20,7 +19,6 @@ import (
"github.com/portainer/portainer/api/internal/authorization"
"github.com/portainer/portainer/api/internal/testhelpers"
"github.com/portainer/portainer/api/jwt"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/segmentio/encoding/json"
"github.com/stretchr/testify/assert"
@@ -51,19 +49,6 @@ func (f *TestFileService) GetFileContent(projectPath, configFilePath string) ([]
return os.ReadFile(filepath.Join(projectPath, configFilePath))
}
type InvalidTestGitService struct {
portainer.GitService
targetFilePath string
}
func (g *InvalidTestGitService) CloneRepository(dest, repoUrl, refName, username, password string, tlsSkipVerify bool) error {
return errors.New("simulate network error")
}
func (g *InvalidTestGitService) LatestCommitID(repositoryURL, referenceName, username, password string, tlsSkipVerify bool) (string, error) {
return "", nil
}
func createTestFile(targetPath string) error {
f, err := os.Create(targetPath)
if err != nil {
@@ -90,7 +75,7 @@ func singleAPIRequest(h *Handler, jwt string, is *assert.Assertions, expect stri
FileContent string
}
req := httptest.NewRequest(http.MethodPut, "/custom_templates/1/git_fetch", bytes.NewBufferString("{}"))
req := httptest.NewRequest(http.MethodPut, "/custom_templates/1/git_fetch", bytes.NewBuffer([]byte("{}")))
testhelpers.AddTestSecurityCookie(req, jwt)
rr := httptest.NewRecorder()
@@ -157,33 +142,28 @@ func Test_customTemplateGitFetch(t *testing.T) {
t.Run("can return the expected file content by multiple calls from one user", func(t *testing.T) {
var wg sync.WaitGroup
wg.Add(5)
for range 5 {
for i := 0; i < 5; i++ {
go func() {
singleAPIRequest(h, jwt1, is, "abcdefg")
wg.Done()
}()
}
wg.Wait()
})
t.Run("can return the expected file content by multiple calls from different users", func(t *testing.T) {
var wg sync.WaitGroup
wg.Add(10)
for i := range 10 {
for i := 0; i < 10; i++ {
go func(j int) {
if j%2 == 0 {
singleAPIRequest(h, jwt1, is, "abcdefg")
} else {
singleAPIRequest(h, jwt2, is, "abcdefg")
}
wg.Done()
}(i)
}
wg.Wait()
})
@@ -194,28 +174,4 @@ func Test_customTemplateGitFetch(t *testing.T) {
singleAPIRequest(h, jwt2, is, "gfedcba")
})
t.Run("restore git repository if it is failed to download the new git repository", func(t *testing.T) {
invalidGitService := &InvalidTestGitService{
targetFilePath: filepath.Join(template1.ProjectPath, template1.GitConfig.ConfigFilePath),
}
h := NewHandler(requestBouncer, store, fileService, invalidGitService)
req := httptest.NewRequest(http.MethodPut, "/custom_templates/1/git_fetch", bytes.NewBufferString("{}"))
testhelpers.AddTestSecurityCookie(req, jwt1)
rr := httptest.NewRecorder()
h.ServeHTTP(rr, req)
is.Equal(http.StatusInternalServerError, rr.Code)
var errResp httperror.HandlerError
err = json.NewDecoder(rr.Body).Decode(&errResp)
assert.NoError(t, err, "failed to parse error body")
assert.FileExists(t, gitService.targetFilePath, "previous git repository is not restored")
fileContent, err := os.ReadFile(gitService.targetFilePath)
assert.NoError(t, err, "failed to read target file")
assert.Equal(t, "gfedcba", string(fileContent))
})
}

View File

@@ -4,15 +4,14 @@ import (
"net/http"
"strconv"
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/authorization"
"github.com/portainer/portainer/api/slicesx"
"github.com/portainer/portainer/api/internal/slices"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/libhttp/response"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
)
@@ -71,7 +70,7 @@ func (handler *Handler) customTemplateList(w http.ResponseWriter, r *http.Reques
customTemplates = filterByType(customTemplates, templateTypes)
if edge != nil {
customTemplates = slicesx.Filter(customTemplates, func(customTemplate portainer.CustomTemplate) bool {
customTemplates = slices.Filter(customTemplates, func(customTemplate portainer.CustomTemplate) bool {
return customTemplate.EdgeTemplate == *edge
})
}

Some files were not shown because too many files have changed in this diff Show More