Compare commits

..

2 Commits

Author SHA1 Message Date
Oscar Zhou
65e4dfa4c0 test(adminmonitor): update the testDatastore options 2022-03-15 11:22:56 +13:00
Oscar Zhou
08d0354371 feat(adminmonitor): disable the app instance if the admin is not initialized in 5 mins 2022-03-14 20:14:04 +13:00
1205 changed files with 11129 additions and 26928 deletions

View File

@@ -1,5 +1,3 @@
*
!dist
!build
!metadata.json
!docker-extension/build

View File

@@ -31,12 +31,7 @@ rules:
[
'error',
{
pathGroups:
[
{ pattern: '@@/**', group: 'internal', position: 'after' },
{ pattern: '@/**', group: 'internal' },
{ pattern: '{Kubernetes,Portainer,Agent,Azure,Docker}/**', group: 'internal' },
],
pathGroups: [{ pattern: '@/**', group: 'internal' }, { pattern: '{Kubernetes,Portainer,Agent,Azure,Docker}/**', group: 'internal' }],
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
pathGroupsExcludedImportTypes: ['internal'],
},
@@ -46,7 +41,6 @@ settings:
'import/resolver':
alias:
map:
- ['@@', './app/react/components']
- ['@', './app']
extensions: ['.js', '.ts', '.tsx']
@@ -74,14 +68,7 @@ overrides:
version: 'detect'
rules:
import/order:
[
'error',
{
pathGroups: [{ pattern: '@@/**', group: 'internal', position: 'after' }, { pattern: '@/**', group: 'internal' }],
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
'newlines-between': 'always',
},
]
['error', { pathGroups: [{ pattern: '@/**', group: 'internal' }], groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'], 'newlines-between': 'always' }]
func-style: [error, 'declaration']
import/prefer-default-export: off
no-use-before-define: ['error', { functions: false }]

View File

@@ -34,5 +34,3 @@ jobs:
prettier_dir: app/
gofmt: true
gofmt_dir: api/
- name: Typecheck
uses: icrawl/action-tsc@v1

View File

@@ -1,230 +0,0 @@
name: Nightly Code Security Scan
on:
schedule:
- cron: '0 8 * * *'
workflow_dispatch:
jobs:
client-dependencies:
name: Client dependency check
runs-on: ubuntu-latest
if: >- # only run for develop branch
github.ref == 'refs/heads/develop'
outputs:
js: ${{ steps.set-matrix.outputs.js_result }}
steps:
- uses: actions/checkout@master
- name: Run Snyk to check for vulnerabilities
uses: snyk/actions/node@master
continue-on-error: true # To make sure that artifact upload gets called
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
json: true
- name: Upload js security scan result as artifact
uses: actions/upload-artifact@v3
with:
name: js-security-scan-develop-result
path: snyk.json
- name: Export scan result to html file
run: |
$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=snyk -path="/data/snyk.json" -output-type=table -export -export-filename="/data/js-result")
- name: Upload js result html file
uses: actions/upload-artifact@v3
with:
name: html-js-result-${{github.run_id}}
path: js-result.html
- name: Analyse the js result
id: set-matrix
run: |
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=snyk -path="/data/snyk.json" -output-type=matrix)
echo "::set-output name=js_result::${result}"
server-dependencies:
name: Server dependency check
runs-on: ubuntu-latest
if: >- # only run for develop branch
github.ref == 'refs/heads/develop'
outputs:
go: ${{ steps.set-matrix.outputs.go_result }}
steps:
- uses: actions/checkout@master
- name: Download go modules
run: cd ./api && go get -t -v -d ./...
- name: Run Snyk to check for vulnerabilities
uses: snyk/actions/golang@master
continue-on-error: true # To make sure that artifact upload gets called
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: --file=./api/go.mod
json: true
- name: Upload go security scan result as artifact
uses: actions/upload-artifact@v3
with:
name: go-security-scan-develop-result
path: snyk.json
- name: Export scan result to html file
run: |
$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=snyk -path="/data/snyk.json" -output-type=table -export -export-filename="/data/go-result")
- name: Upload go result html file
uses: actions/upload-artifact@v3
with:
name: html-go-result-${{github.run_id}}
path: go-result.html
- name: Analyse the go result
id: set-matrix
run: |
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=snyk -path="/data/snyk.json" -output-type=matrix)
echo "::set-output name=go_result::${result}"
image-vulnerability:
name: Build docker image and Image vulnerability check
runs-on: ubuntu-latest
if: >-
github.ref == 'refs/heads/develop'
outputs:
image: ${{ steps.set-matrix.outputs.image_result }}
steps:
- name: Checkout code
uses: actions/checkout@master
- name: Use golang 1.18
uses: actions/setup-go@v3
with:
go-version: '1.18'
- name: Use Node.js 12.x
uses: actions/setup-node@v1
with:
node-version: 12.x
- name: Install packages and build
run: yarn install && yarn build
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Build and push
uses: docker/build-push-action@v2
with:
context: .
file: build/linux/Dockerfile
tags: trivy-portainer:${{ github.sha }}
outputs: type=docker,dest=/tmp/trivy-portainer-image.tar
- name: Load docker image
run: |
docker load --input /tmp/trivy-portainer-image.tar
- name: Run Trivy vulnerability scanner
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 trivy-portainer:${{ github.sha }}
- name: Upload image security scan result as artifact
uses: actions/upload-artifact@v3
with:
name: image-security-scan-develop-result
path: image-trivy.json
- name: Export scan result to html file
run: |
$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=trivy -path="/data/image-trivy.json" -output-type=table -export -export-filename="/data/image-result")
- name: Upload go result html file
uses: actions/upload-artifact@v3
with:
name: html-image-result-${{github.run_id}}
path: image-result.html
- name: Analyse the trivy result
id: set-matrix
run: |
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=trivy -path="/data/image-trivy.json" -output-type=matrix)
echo "::set-output name=image_result::${result}"
result-analysis:
name: Analyse scan result
needs: [client-dependencies, server-dependencies, image-vulnerability]
runs-on: ubuntu-latest
if: >-
github.ref == 'refs/heads/develop'
strategy:
matrix:
js: ${{fromJson(needs.client-dependencies.outputs.js)}}
go: ${{fromJson(needs.server-dependencies.outputs.go)}}
image: ${{fromJson(needs.image-vulnerability.outputs.image)}}
steps:
- name: Display the results of js, go and image
run: |
echo ${{ matrix.js.status }}
echo ${{ matrix.go.status }}
echo ${{ matrix.image.status }}
echo ${{ matrix.js.summary }}
echo ${{ matrix.go.summary }}
echo ${{ matrix.image.summary }}
- name: Send Slack message
if: >-
matrix.js.status == 'failure' ||
matrix.go.status == 'failure' ||
matrix.image.status == 'failure'
uses: slackapi/slack-github-action@v1.18.0
with:
payload: |
{
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "Code Scanning Result (*${{ github.repository }}*)\n*<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|GitHub Actions Workflow URL>*"
}
}
],
"attachments": [
{
"color": "#FF0000",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*JS dependency check*: *${{ matrix.js.status }}*\n${{ matrix.js.summary }}"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Go dependency check*: *${{ matrix.go.status }}*\n${{ matrix.go.summary }}"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Image vulnerability check*: *${{ matrix.image.status }}*\n${{ matrix.image.summary }}\n"
}
}
]
}
]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SECURITY_SLACK_WEBHOOK_URL }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK

View File

@@ -1,233 +0,0 @@
name: PR Code Security Scan
on:
pull_request_review:
types:
- submitted
- edited
paths:
- 'package.json'
- 'api/go.mod'
- 'gruntfile.js'
- 'build/linux/Dockerfile'
- 'build/linux/alpine.Dockerfile'
- 'build/windows/Dockerfile'
jobs:
client-dependencies:
name: Client dependency check
runs-on: ubuntu-latest
if: >-
github.event.pull_request &&
github.event.review.body == '/scan'
outputs:
jsdiff: ${{ steps.set-diff-matrix.outputs.js_diff_result }}
steps:
- uses: actions/checkout@master
- name: Run Snyk to check for vulnerabilities
uses: snyk/actions/node@master
continue-on-error: true # To make sure that artifact upload gets called
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
json: true
- name: Upload js security scan result as artifact
uses: actions/upload-artifact@v3
with:
name: js-security-scan-feat-result
path: snyk.json
- name: Download artifacts from develop branch
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
mv ./snyk.json ./js-snyk-feature.json
(gh run download -n js-security-scan-develop-result -R ${{ github.repository }} 2>&1 >/dev/null) || :
if [[ -e ./snyk.json ]]; then
mv ./snyk.json ./js-snyk-develop.json
else
echo "null" > ./js-snyk-develop.json
fi
- name: Export scan result to html file
run: |
$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=snyk -path="/data/js-snyk-feature.json" -compare-to="/data/js-snyk-develop.json" -output-type=table -export -export-filename="/data/js-result")
- name: Upload js result html file
uses: actions/upload-artifact@v3
with:
name: html-js-result-compare-to-develop-${{github.run_id}}
path: js-result.html
- name: Analyse the js diff result
id: set-diff-matrix
run: |
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=snyk -path="/data/js-snyk-feature.json" -compare-to="./data/js-snyk-develop.json" -output-type=matrix)
echo "::set-output name=js_diff_result::${result}"
server-dependencies:
name: Server dependency check
runs-on: ubuntu-latest
if: >-
github.event.pull_request &&
github.event.review.body == '/scan'
outputs:
godiff: ${{ steps.set-diff-matrix.outputs.go_diff_result }}
steps:
- uses: actions/checkout@master
- name: Download go modules
run: cd ./api && go get -t -v -d ./...
- name: Run Snyk to check for vulnerabilities
uses: snyk/actions/golang@master
continue-on-error: true # To make sure that artifact upload gets called
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: --file=./api/go.mod
json: true
- name: Upload go security scan result as artifact
uses: actions/upload-artifact@v3
with:
name: go-security-scan-feature-result
path: snyk.json
- name: Download artifacts from develop branch
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
mv ./snyk.json ./go-snyk-feature.json
(gh run download -n go-security-scan-develop-result -R ${{ github.repository }} 2>&1 >/dev/null) || :
if [[ -e ./snyk.json ]]; then
mv ./snyk.json ./go-snyk-develop.json
else
echo "null" > ./go-snyk-develop.json
fi
- name: Export scan result to html file
run: |
$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=snyk -path="/data/go-snyk-feature.json" -compare-to="/data/go-snyk-develop.json" -output-type=table -export -export-filename="/data/go-result")
- name: Upload go result html file
uses: actions/upload-artifact@v3
with:
name: html-go-result-compare-to-develop-${{github.run_id}}
path: go-result.html
- name: Analyse the go diff result
id: set-diff-matrix
run: |
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=snyk -path="/data/go-snyk-feature.json" -compare-to="/data/go-snyk-develop.json" -output-type=matrix)
echo "::set-output name=go_diff_result::${result}"
image-vulnerability:
name: Build docker image and Image vulnerability check
runs-on: ubuntu-latest
if: >-
github.event.pull_request &&
github.event.review.body == '/scan'
outputs:
imagediff: ${{ steps.set-diff-matrix.outputs.image_diff_result }}
steps:
- name: Checkout code
uses: actions/checkout@master
- name: Use golang 1.18
uses: actions/setup-go@v3
with:
go-version: '1.18'
- name: Use Node.js 12.x
uses: actions/setup-node@v1
with:
node-version: 12.x
- name: Install packages and build
run: yarn install && yarn build
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Build and push
uses: docker/build-push-action@v2
with:
context: .
file: build/linux/Dockerfile
tags: trivy-portainer:${{ github.sha }}
outputs: type=docker,dest=/tmp/trivy-portainer-image.tar
- name: Load docker image
run: |
docker load --input /tmp/trivy-portainer-image.tar
- name: Run Trivy vulnerability scanner
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 trivy-portainer:${{ github.sha }}
- name: Upload image security scan result as artifact
uses: actions/upload-artifact@v3
with:
name: image-security-scan-feature-result
path: image-trivy.json
- name: Download artifacts from develop branch
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
mv ./image-trivy.json ./image-trivy-feature.json
(gh run download -n image-security-scan-develop-result -R ${{ github.repository }} 2>&1 >/dev/null) || :
if [[ -e ./image-trivy.json ]]; then
mv ./image-trivy.json ./image-trivy-develop.json
else
echo "null" > ./image-trivy-develop.json
fi
- name: Export scan result to html file
run: |
$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=trivy -path="/data/image-trivy-feature.json" -compare-to="/data/image-trivy-develop.json" -output-type=table -export -export-filename="/data/image-result")
- name: Upload image result html file
uses: actions/upload-artifact@v3
with:
name: html-image-result-compare-to-develop-${{github.run_id}}
path: image-result.html
- name: Analyse the image diff result
id: set-diff-matrix
run: |
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=trivy -path="/data/image-trivy-feature.json" -compare-to="./data/image-trivy-develop.json" -output-type=matrix)
echo "::set-output name=image_diff_result::${result}"
result-analysis:
name: Analyse scan result compared to develop
needs: [client-dependencies, server-dependencies, image-vulnerability]
runs-on: ubuntu-latest
if: >-
github.event.pull_request &&
github.event.review.body == '/scan'
strategy:
matrix:
jsdiff: ${{fromJson(needs.client-dependencies.outputs.jsdiff)}}
godiff: ${{fromJson(needs.server-dependencies.outputs.godiff)}}
imagediff: ${{fromJson(needs.image-vulnerability.outputs.imagediff)}}
steps:
- name: Check job status of diff result
if: >-
matrix.jsdiff.status == 'failure' ||
matrix.godiff.status == 'failure' ||
matrix.imagediff.status == 'failure'
run: |
echo ${{ matrix.jsdiff.status }}
echo ${{ matrix.godiff.status }}
echo ${{ matrix.imagediff.status }}
echo ${{ matrix.jsdiff.summary }}
echo ${{ matrix.godiff.summary }}
echo ${{ matrix.imagediff.summary }}
exit 1

15
.github/workflows/test-client.yaml vendored Normal file
View File

@@ -0,0 +1,15 @@
name: Test Frontend
on: push
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '14'
cache: 'yarn'
- run: yarn install --frozen-lockfile
- name: Run tests
run: yarn test:client

View File

@@ -1,29 +0,0 @@
name: Test
on: push
jobs:
test-client:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '14'
cache: 'yarn'
- run: yarn --frozen-lockfile
- name: Run tests
run: yarn test:client
# test-server:
# runs-on: ubuntu-latest
# env:
# GOPRIVATE: "github.com/portainer"
# steps:
# - uses: actions/checkout@v3
# - uses: actions/setup-go@v3
# with:
# go-version: '1.18'
# - name: Run tests
# run: |
# cd api
# go test ./...

View File

@@ -1,2 +1 @@
dist
api/datastore/test_data
dist

View File

@@ -16,9 +16,6 @@ module.exports = {
exportLocalsConvention: 'camelCaseOnly',
},
},
postcssLoaderOptions: {
implementation: require('postcss'),
},
},
},
],

View File

@@ -3,7 +3,6 @@ import '../app/assets/css';
import { pushStateLocationPlugin, UIRouter } from '@uirouter/react';
import { initialize as initMSW, mswDecorator } from 'msw-storybook-addon';
import { handlers } from '@/setup-tests/server-handlers';
import { QueryClient, QueryClientProvider } from 'react-query';
// Initialize MSW
initMSW({
@@ -32,17 +31,11 @@ export const parameters = {
},
};
const testQueryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
export const decorators = [
(Story) => (
<QueryClientProvider client={testQueryClient}>
<UIRouter plugins={[pushStateLocationPlugin]}>
<Story />
</UIRouter>
</QueryClientProvider>
<UIRouter plugins={[pushStateLocationPlugin]}>
<Story />
</UIRouter>
),
mswDecorator,
];

View File

@@ -22,7 +22,7 @@ Please note that the public demo cluster is **reset every 15min**.
Portainer CE is updated regularly. We aim to do an update release every couple of months.
**The latest version of Portainer is 2.13.x**.
**The latest version of Portainer is 2.9.x**. Portainer is on version 2, the second number denotes the month of release.
## Getting started

View File

@@ -3,48 +3,32 @@ package adminmonitor
import (
"context"
"log"
"net/http"
"strings"
"sync"
"time"
httperror "github.com/portainer/libhttp/error"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
)
var logFatalf = log.Fatalf
const RedirectReasonAdminInitTimeout string = "AdminInitTimeout"
type Monitor struct {
timeout time.Duration
datastore dataservices.DataStore
shutdownCtx context.Context
cancellationFunc context.CancelFunc
mu sync.Mutex
adminInitDisabled bool
timeout time.Duration
datastore dataservices.DataStore
shutdownCtx context.Context
cancellationFunc context.CancelFunc
}
// New creates a monitor that when started will wait for the timeout duration and then sends the timeout signal to disable the application
// New creates a monitor that when started will wait for the timeout duration and then shutdown the application unless it has been initialized.
func New(timeout time.Duration, datastore dataservices.DataStore, shutdownCtx context.Context) *Monitor {
return &Monitor{
timeout: timeout,
datastore: datastore,
shutdownCtx: shutdownCtx,
adminInitDisabled: false,
timeout: timeout,
datastore: datastore,
shutdownCtx: shutdownCtx,
}
}
// Starts starts the monitor. Active monitor could be stopped or shuttted down by cancelling the shutdown context.
func (m *Monitor) Start() {
m.mu.Lock()
defer m.mu.Unlock()
if m.cancellationFunc != nil {
return
}
cancellationCtx, cancellationFunc := context.WithCancel(context.Background())
m.cancellationFunc = cancellationFunc
@@ -57,11 +41,12 @@ func (m *Monitor) Start() {
logFatalf("%s", err)
}
if !initialized {
log.Println("[INFO] [internal,init] The Portainer instance timed out for security purposes. To re-enable your Portainer instance, you will need to restart Portainer")
m.mu.Lock()
defer m.mu.Unlock()
m.adminInitDisabled = true
return
err = m.DisableInstanceInit()
if err != nil {
log.Printf("[DEBUG] [internal,init] [message: fail to disable initialization monitor: %s]\n", err)
}
logFatalf("[FATAL] [internal,init] No administrator account was created in %f mins. Shutting down the Portainer instance for security reasons", m.timeout.Minutes())
}
case <-cancellationCtx.Done():
log.Println("[DEBUG] [internal,init] [message: canceling initialization monitor]")
@@ -73,9 +58,6 @@ func (m *Monitor) Start() {
// Stop stops monitor. Safe to call even if monitor wasn't started.
func (m *Monitor) Stop() {
m.mu.Lock()
defer m.mu.Unlock()
if m.cancellationFunc == nil {
return
}
@@ -92,24 +74,9 @@ func (m *Monitor) WasInitialized() (bool, error) {
return len(users) > 0, nil
}
func (m *Monitor) WasInstanceDisabled() bool {
m.mu.Lock()
defer m.mu.Unlock()
return m.adminInitDisabled
}
// WithRedirect checks whether administrator initialisation timeout. If so, it will return the error with redirect reason.
// Otherwise, it will pass through the request to next
func (m *Monitor) WithRedirect(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if m.WasInstanceDisabled() {
if strings.HasPrefix(r.RequestURI, "/api") && r.RequestURI != "/api/status" && r.RequestURI != "/api/settings/public" {
w.Header().Set("redirect-reason", RedirectReasonAdminInitTimeout)
httperror.WriteError(w, http.StatusSeeOther, "Administrator initialization timeout", nil)
return
}
}
next.ServeHTTP(w, r)
})
func (m *Monitor) DisableInstanceInit() error {
state := portainer.State{
DisableAdminInit: true,
}
return m.datastore.State().UpdateState(&state)
}

View File

@@ -21,18 +21,6 @@ func Test_stopCouldBeCalledMultipleTimes(t *testing.T) {
monitor.Stop()
}
func Test_startOrStopCouldBeCalledMultipleTimesConcurrently(t *testing.T) {
monitor := New(1*time.Minute, nil, context.Background())
go monitor.Start()
monitor.Start()
go monitor.Stop()
monitor.Stop()
time.Sleep(2 * time.Second)
}
func Test_canStopStartedMonitor(t *testing.T) {
monitor := New(1*time.Minute, nil, context.Background())
monitor.Start()
@@ -42,13 +30,21 @@ func Test_canStopStartedMonitor(t *testing.T) {
assert.Nil(t, monitor.cancellationFunc, "cancellation function should absent in stopped monitor")
}
func Test_start_shouldDisableInstanceAfterTimeout_ifNotInitialized(t *testing.T) {
func Test_start_shouldFatalAfterTimeout_ifNotInitialized(t *testing.T) {
timeout := 10 * time.Millisecond
datastore := i.NewDatastore(i.WithUsers([]portainer.User{}))
datastore := i.NewDatastore(i.WithUsers([]portainer.User{}), i.WithState(&portainer.State{}))
var fataled bool
origLogFatalf := logFatalf
logFatalf = func(s string, v ...interface{}) { fataled = true }
defer func() {
logFatalf = origLogFatalf
}()
monitor := New(timeout, datastore, context.Background())
monitor.Start()
<-time.After(2 * timeout)
<-time.After(20 * timeout)
assert.True(t, monitor.WasInstanceDisabled(), "monitor should have been timeout and instance is disabled")
assert.True(t, fataled, "monitor should been timeout and fatal")
}

View File

@@ -50,15 +50,4 @@ Instead, it acts as a reverse-proxy to the Docker HTTP API. This means that you
To do so, you can use the `/endpoints/{id}/docker` Portainer API environment(endpoint) (which is not documented below due to Swagger limitations). This environment(endpoint) has a restricted access policy so you still need to be authenticated to be able to query this environment(endpoint). Any query on this environment(endpoint) will be proxied to the Docker API of the associated environment(endpoint) (requests and responses objects are the same as documented in the Docker API).
# Private Registry
Using private registry, you will need to pass a based64 encoded JSON string {"registryId":\<registryID value\>} inside the Request Header. The parameter name is "X-Registry-Auth".
\<registryID value\> - The registry ID where the repository was created.
Example:
```
eyJyZWdpc3RyeUlkIjoxfQ==
```
**NOTE**: You can find more information on how to query the Docker API in the [Docker official documentation](https://docs.docker.com/engine/api/v1.30/) as well as in [this Portainer example](https://documentation.portainer.io/api/api-examples/).

View File

@@ -20,7 +20,7 @@ func Test_SatisfiesAPIKeyServiceInterface(t *testing.T) {
func Test_GenerateApiKey(t *testing.T) {
is := assert.New(t)
_, store, teardown := datastore.MustNewTestStore(true, true)
_, store, teardown := datastore.MustNewTestStore(true)
defer teardown()
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
@@ -74,7 +74,7 @@ func Test_GenerateApiKey(t *testing.T) {
func Test_GetAPIKey(t *testing.T) {
is := assert.New(t)
_, store, teardown := datastore.MustNewTestStore(true, true)
_, store, teardown := datastore.MustNewTestStore(true)
defer teardown()
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
@@ -94,7 +94,7 @@ func Test_GetAPIKey(t *testing.T) {
func Test_GetAPIKeys(t *testing.T) {
is := assert.New(t)
_, store, teardown := datastore.MustNewTestStore(true, true)
_, store, teardown := datastore.MustNewTestStore(true)
defer teardown()
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
@@ -115,7 +115,7 @@ func Test_GetAPIKeys(t *testing.T) {
func Test_GetDigestUserAndKey(t *testing.T) {
is := assert.New(t)
_, store, teardown := datastore.MustNewTestStore(true, true)
_, store, teardown := datastore.MustNewTestStore(true)
defer teardown()
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
@@ -151,7 +151,7 @@ func Test_GetDigestUserAndKey(t *testing.T) {
func Test_UpdateAPIKey(t *testing.T) {
is := assert.New(t)
_, store, teardown := datastore.MustNewTestStore(true, true)
_, store, teardown := datastore.MustNewTestStore(true)
defer teardown()
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
@@ -199,7 +199,7 @@ func Test_UpdateAPIKey(t *testing.T) {
func Test_DeleteAPIKey(t *testing.T) {
is := assert.New(t)
_, store, teardown := datastore.MustNewTestStore(true, true)
_, store, teardown := datastore.MustNewTestStore(true)
defer teardown()
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
@@ -240,7 +240,7 @@ func Test_DeleteAPIKey(t *testing.T) {
func Test_InvalidateUserKeyCache(t *testing.T) {
is := assert.New(t)
_, store, teardown := datastore.MustNewTestStore(true, true)
_, store, teardown := datastore.MustNewTestStore(true)
defer teardown()
service := NewAPIKeyService(store.APIKeyRepository(), store.User())

View File

@@ -5,14 +5,16 @@ import (
"fmt"
"log"
"net/http"
"strconv"
"sync"
"time"
"github.com/portainer/portainer/api/http/proxy"
"github.com/dchest/uniuri"
chserver "github.com/jpillora/chisel/server"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/http/proxy"
)
const (
@@ -27,7 +29,7 @@ const (
type Service struct {
serverFingerprint string
serverPort string
tunnelDetailsMap map[portainer.EndpointID]*portainer.TunnelDetails
tunnelDetailsMap map[string]*portainer.TunnelDetails
dataStore dataservices.DataStore
snapshotService portainer.SnapshotService
chiselServer *chserver.Server
@@ -39,7 +41,7 @@ type Service struct {
// NewService returns a pointer to a new instance of Service
func NewService(dataStore dataservices.DataStore, shutdownCtx context.Context) *Service {
return &Service{
tunnelDetailsMap: make(map[portainer.EndpointID]*portainer.TunnelDetails),
tunnelDetailsMap: make(map[string]*portainer.TunnelDetails),
dataStore: dataStore,
shutdownCtx: shutdownCtx,
}
@@ -181,41 +183,48 @@ func (service *Service) startTunnelVerificationLoop() {
}
func (service *Service) checkTunnels() {
tunnels := make(map[portainer.EndpointID]portainer.TunnelDetails)
service.mu.Lock()
for key, tunnel := range service.tunnelDetailsMap {
tunnels[key] = *tunnel
}
service.mu.Unlock()
for endpointID, tunnel := range tunnels {
for key, tunnel := range service.tunnelDetailsMap {
if tunnel.LastActivity.IsZero() || tunnel.Status == portainer.EdgeAgentIdle {
continue
}
elapsed := time.Since(tunnel.LastActivity)
log.Printf("[DEBUG] [chisel,monitoring] [endpoint_id: %d] [status: %s] [status_time_seconds: %f] [message: environment tunnel monitoring]", endpointID, tunnel.Status, elapsed.Seconds())
log.Printf("[DEBUG] [chisel,monitoring] [endpoint_id: %s] [status: %s] [status_time_seconds: %f] [message: environment tunnel monitoring]", key, tunnel.Status, elapsed.Seconds())
if tunnel.Status == portainer.EdgeAgentManagementRequired && elapsed.Seconds() < requiredTimeout.Seconds() {
continue
} else if tunnel.Status == portainer.EdgeAgentManagementRequired && elapsed.Seconds() > requiredTimeout.Seconds() {
log.Printf("[DEBUG] [chisel,monitoring] [endpoint_id: %d] [status: %s] [status_time_seconds: %f] [timeout_seconds: %f] [message: REQUIRED state timeout exceeded]", endpointID, tunnel.Status, elapsed.Seconds(), requiredTimeout.Seconds())
log.Printf("[DEBUG] [chisel,monitoring] [endpoint_id: %s] [status: %s] [status_time_seconds: %f] [timeout_seconds: %f] [message: REQUIRED state timeout exceeded]", key, tunnel.Status, elapsed.Seconds(), requiredTimeout.Seconds())
}
if tunnel.Status == portainer.EdgeAgentActive && elapsed.Seconds() < activeTimeout.Seconds() {
continue
} else if tunnel.Status == portainer.EdgeAgentActive && elapsed.Seconds() > activeTimeout.Seconds() {
log.Printf("[DEBUG] [chisel,monitoring] [endpoint_id: %d] [status: %s] [status_time_seconds: %f] [timeout_seconds: %f] [message: ACTIVE state timeout exceeded]", endpointID, tunnel.Status, elapsed.Seconds(), activeTimeout.Seconds())
log.Printf("[DEBUG] [chisel,monitoring] [endpoint_id: %s] [status: %s] [status_time_seconds: %f] [timeout_seconds: %f] [message: ACTIVE state timeout exceeded]", key, tunnel.Status, elapsed.Seconds(), activeTimeout.Seconds())
err := service.snapshotEnvironment(endpointID, tunnel.Port)
endpointID, err := strconv.Atoi(key)
if err != nil {
log.Printf("[ERROR] [snapshot] Unable to snapshot Edge environment (id: %d): %s", endpointID, err)
log.Printf("[ERROR] [chisel,snapshot,conversion] Invalid environment identifier (id: %s): %s", key, err)
}
err = service.snapshotEnvironment(portainer.EndpointID(endpointID), tunnel.Port)
if err != nil {
log.Printf("[ERROR] [snapshot] Unable to snapshot Edge environment (id: %s): %s", key, err)
}
}
service.SetTunnelStatusToIdle(portainer.EndpointID(endpointID))
endpointID, err := strconv.Atoi(key)
if err != nil {
log.Printf("[ERROR] [chisel,conversion] Invalid environment identifier (id: %s): %s", key, err)
continue
}
service.setTunnelStatusToIdle(portainer.EndpointID(endpointID))
}
service.mu.Unlock()
}
func (service *Service) snapshotEnvironment(endpointID portainer.EndpointID, tunnelPort int) error {

View File

@@ -4,11 +4,13 @@ import (
"encoding/base64"
"fmt"
"math/rand"
"strconv"
"strings"
"time"
"github.com/dchest/uniuri"
"github.com/portainer/libcrypto"
"github.com/dchest/uniuri"
portainer "github.com/portainer/portainer/api"
)
@@ -17,7 +19,6 @@ const (
maxAvailablePort = 65535
)
// NOTE: it needs to be called with the lock acquired
// getUnusedPort is used to generate an unused random port in the dynamic port range.
// Dynamic ports (also called private ports) are 49152 to 65535.
func (service *Service) getUnusedPort() int {
@@ -38,8 +39,9 @@ func randomInt(min, max int) int {
// NOTE: it needs to be called with the lock acquired
func (service *Service) getTunnelDetails(endpointID portainer.EndpointID) *portainer.TunnelDetails {
key := strconv.Itoa(int(endpointID))
if tunnel, ok := service.tunnelDetailsMap[endpointID]; ok {
if tunnel, ok := service.tunnelDetailsMap[key]; ok {
return tunnel
}
@@ -47,7 +49,7 @@ func (service *Service) getTunnelDetails(endpointID portainer.EndpointID) *porta
Status: portainer.EdgeAgentIdle,
}
service.tunnelDetailsMap[endpointID] = tunnel
service.tunnelDetailsMap[key] = tunnel
return tunnel
}
@@ -101,12 +103,8 @@ func (service *Service) SetTunnelStatusToActive(endpointID portainer.EndpointID)
service.mu.Unlock()
}
// SetTunnelStatusToIdle update the status of the tunnel associated to the specified environment(endpoint).
// It sets the status to IDLE.
// It removes any existing credentials associated to the tunnel.
func (service *Service) SetTunnelStatusToIdle(endpointID portainer.EndpointID) {
service.mu.Lock()
// NOTE: it needs to be called with the lock acquired
func (service *Service) setTunnelStatusToIdle(endpointID portainer.EndpointID) {
tunnel := service.getTunnelDetails(endpointID)
tunnel.Status = portainer.EdgeAgentIdle
tunnel.Port = 0
@@ -119,7 +117,14 @@ func (service *Service) SetTunnelStatusToIdle(endpointID portainer.EndpointID) {
}
service.ProxyManager.DeleteEndpointProxy(endpointID)
}
// SetTunnelStatusToIdle update the status of the tunnel associated to the specified environment(endpoint).
// It sets the status to IDLE.
// It removes any existing credentials associated to the tunnel.
func (service *Service) SetTunnelStatusToIdle(endpointID portainer.EndpointID) {
service.mu.Lock()
service.setTunnelStatusToIdle(endpointID)
service.mu.Unlock()
}

View File

@@ -35,7 +35,6 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
TunnelPort: kingpin.Flag("tunnel-port", "Port to serve the tunnel server").Default(defaultTunnelServerPort).String(),
Assets: kingpin.Flag("assets", "Path to the assets").Default(defaultAssetsDirectory).Short('a').String(),
Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default(defaultDataDirectory).Short('d').String(),
DemoEnvironment: kingpin.Flag("demo", "Demo environment").Bool(),
EndpointURL: kingpin.Flag("host", "Environment URL").Short('H').String(),
FeatureFlags: BoolPairs(kingpin.Flag("feat", "List of feature flags").Hidden()),
EnableEdgeComputeFeatures: kingpin.Flag("edge-compute", "Enable Edge Compute features").Bool(),
@@ -52,7 +51,7 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
SSLKey: kingpin.Flag("sslkey", "Path to the SSL key used to secure the Portainer instance").String(),
Rollback: kingpin.Flag("rollback", "Rollback the database store to the previous version").Bool(),
SnapshotInterval: kingpin.Flag("snapshot-interval", "Duration between each environment snapshot job").String(),
AdminPassword: kingpin.Flag("admin-password", "Set admin password with provided hash").String(),
AdminPassword: kingpin.Flag("admin-password", "Hashed admin password").String(),
AdminPasswordFile: kingpin.Flag("admin-password-file", "Path to the file containing the password for the admin user").String(),
Labels: pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')),
Logo: kingpin.Flag("logo", "URL for the logo displayed in the UI").String(),
@@ -62,6 +61,7 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
MaxBatchSize: kingpin.Flag("max-batch-size", "Maximum size of a batch").Int(),
MaxBatchDelay: kingpin.Flag("max-batch-delay", "Maximum delay before a batch starts").Duration(),
SecretKeyName: kingpin.Flag("secret-key-name", "Secret key name for encryption and will be used as /run/secrets/<secret-key-name>.").Default(defaultSecretKeyName).String(),
EnableAdminInit: kingpin.Flag("enable-admin-init", "Enable Portainer admin initialization ability if the previous initialization expires").Bool(),
}
kingpin.Parse()

View File

@@ -18,6 +18,8 @@ const (
defaultHTTPDisabled = "false"
defaultHTTPEnabled = "false"
defaultSSL = "false"
defaultSSLCertPath = "/certs/portainer.crt"
defaultSSLKeyPath = "/certs/portainer.key"
defaultBaseURL = "/"
defaultSecretKeyName = "portainer"
)

View File

@@ -15,6 +15,8 @@ const (
defaultHTTPDisabled = "false"
defaultHTTPEnabled = "false"
defaultSSL = "false"
defaultSSLCertPath = "C:\\certs\\portainer.crt"
defaultSSLKeyPath = "C:\\certs\\portainer.key"
defaultSnapshotInterval = "5m"
defaultBaseURL = "/"
defaultSecretKeyName = "portainer"

View File

@@ -1,19 +1,41 @@
package main
import (
"fmt"
"log"
"strings"
"github.com/sirupsen/logrus"
)
type portainerFormatter struct {
logrus.TextFormatter
}
func (f *portainerFormatter) Format(entry *logrus.Entry) ([]byte, error) {
var levelColor int
switch entry.Level {
case logrus.DebugLevel, logrus.TraceLevel:
levelColor = 31 // gray
case logrus.WarnLevel:
levelColor = 33 // yellow
case logrus.ErrorLevel, logrus.FatalLevel, logrus.PanicLevel:
levelColor = 31 // red
default:
levelColor = 36 // blue
}
return []byte(fmt.Sprintf("\x1b[%dm%s\x1b[0m %s %s\n", levelColor, strings.ToUpper(entry.Level.String()), entry.Time.Format(f.TimestampFormat), entry.Message)), nil
}
func configureLogger() {
logger := logrus.New() // logger is to implicitly substitute stdlib's log
log.SetOutput(logger.Writer())
formatter := &logrus.TextFormatter{DisableTimestamp: false, DisableLevelTruncation: true}
formatter := &logrus.TextFormatter{DisableTimestamp: true, DisableLevelTruncation: true}
formatterLogrus := &portainerFormatter{logrus.TextFormatter{DisableTimestamp: false, DisableLevelTruncation: true, TimestampFormat: "2006/01/02 15:04:05", FullTimestamp: true}}
logger.SetFormatter(formatter)
logrus.SetFormatter(formatter)
logrus.SetFormatter(formatterLogrus)
logger.SetLevel(logrus.DebugLevel)
logrus.SetLevel(logrus.DebugLevel)

View File

@@ -23,7 +23,6 @@ import (
"github.com/portainer/portainer/api/database/boltdb"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/datastore"
"github.com/portainer/portainer/api/demo"
"github.com/portainer/portainer/api/docker"
"github.com/portainer/portainer/api/exec"
"github.com/portainer/portainer/api/filesystem"
@@ -126,6 +125,11 @@ func initDataStore(flags *portainer.CLIFlags, secretKey []byte, fileService port
}
}
err = updateStateFromFlags(store, flags)
if err != nil {
log.Fatalf("Failed updating state from flags: %v", err)
}
err = updateSettingsFromFlags(store, flags)
if err != nil {
log.Fatalf("Failed updating settings from flags: %v", err)
@@ -209,7 +213,7 @@ func initGitService() portainer.GitService {
return git.NewService()
}
func initSSLService(addr, certPath, keyPath string, fileService portainer.FileService, dataStore dataservices.DataStore, shutdownTrigger context.CancelFunc) (*ssl.Service, error) {
func initSSLService(addr, dataPath, certPath, keyPath string, fileService portainer.FileService, dataStore dataservices.DataStore, shutdownTrigger context.CancelFunc) (*ssl.Service, error) {
slices := strings.Split(addr, ":")
host := slices[0]
if host == "" {
@@ -253,6 +257,24 @@ func initStatus(instanceID string) *portainer.Status {
}
}
func updateStateFromFlags(dataStore dataservices.DataStore, flags *portainer.CLIFlags) error {
state, err := dataStore.State().State()
if err != nil {
return err
}
if !state.DisableAdminInit {
return nil
}
if !*flags.EnableAdminInit {
logrus.Fatalln("[FATAL] [internal,init] The Portainer instance initialization is disabled. Restarting the Portainer instance with the optional parameter enable-init")
}
state.DisableAdminInit = !*flags.EnableAdminInit
return dataStore.State().UpdateState(state)
}
func updateSettingsFromFlags(dataStore dataservices.DataStore, flags *portainer.CLIFlags) error {
settings, err := dataStore.Settings().Settings()
if err != nil {
@@ -279,12 +301,6 @@ func updateSettingsFromFlags(dataStore dataservices.DataStore, flags *portainer.
settings.BlackListedLabels = *flags.Labels
}
if agentKey, ok := os.LookupEnv("AGENT_SECRET"); ok {
settings.AgentSecret = agentKey
} else {
settings.AgentSecret = ""
}
err = dataStore.Settings().UpdateSettings(settings)
if err != nil {
return err
@@ -573,10 +589,9 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
openAMTService := openamt.NewService()
cryptoService := initCryptoService()
digitalSignatureService := initDigitalSignatureService()
sslService, err := initSSLService(*flags.AddrHTTPS, *flags.SSLCert, *flags.SSLKey, fileService, dataStore, shutdownTrigger)
sslService, err := initSSLService(*flags.AddrHTTPS, *flags.Data, *flags.SSLCert, *flags.SSLKey, fileService, dataStore, shutdownTrigger)
if err != nil {
logrus.Fatal(err)
}
@@ -607,9 +622,9 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
kubernetesTokenCacheManager := kubeproxy.NewTokenCacheManager()
kubeClusterAccessService := kubernetes.NewKubeClusterAccessService(*flags.BaseURL, *flags.AddrHTTPS, sslSettings.CertPath)
kubeConfigService := kubernetes.NewKubeConfigCAService(*flags.AddrHTTPS, sslSettings.CertPath)
proxyManager := proxy.NewManager(dataStore, digitalSignatureService, reverseTunnelService, dockerClientFactory, kubernetesClientFactory, kubernetesTokenCacheManager, gitService)
proxyManager := proxy.NewManager(dataStore, digitalSignatureService, reverseTunnelService, dockerClientFactory, kubernetesClientFactory, kubernetesTokenCacheManager)
reverseTunnelService.ProxyManager = proxyManager
@@ -636,14 +651,6 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
applicationStatus := initStatus(instanceID)
demoService := demo.NewService()
if *flags.DemoEnvironment {
err := demoService.Init(dataStore, cryptoService)
if err != nil {
log.Fatalf("failed initializing demo environment: %v", err)
}
}
err = initEndpoint(flags, dataStore, snapshotService)
if err != nil {
logrus.Fatalf("Failed initializing environment: %v", err)
@@ -722,7 +729,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
OpenAMTService: openAMTService,
ProxyManager: proxyManager,
KubernetesTokenCacheManager: kubernetesTokenCacheManager,
KubeClusterAccessService: kubeClusterAccessService,
KubeConfigService: kubeConfigService,
SignatureService: digitalSignatureService,
SnapshotService: snapshotService,
SSLService: sslService,
@@ -732,7 +739,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
ShutdownCtx: shutdownCtx,
ShutdownTrigger: shutdownTrigger,
StackDeployer: stackDeployer,
DemoService: demoService,
BaseURL: *flags.BaseURL,
}
}

View File

@@ -21,7 +21,7 @@ func (m mockKingpinSetting) SetValue(value kingpin.Value) {
func Test_enableFeaturesFromFlags(t *testing.T) {
is := assert.New(t)
_, store, teardown := datastore.MustNewTestStore(true, true)
_, store, teardown := datastore.MustNewTestStore(true)
defer teardown()
tests := []struct {
@@ -76,7 +76,7 @@ func Test_optionalFeature(t *testing.T) {
is := assert.New(t)
_, store, teardown := datastore.MustNewTestStore(true, true)
_, store, teardown := datastore.MustNewTestStore(true)
defer teardown()
// Enable the test feature

View File

@@ -9,11 +9,11 @@ type Service struct{}
// Hash hashes a string using the bcrypt algorithm
func (*Service) Hash(data string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(data), bcrypt.DefaultCost)
hash, err := bcrypt.GenerateFromPassword([]byte(data), bcrypt.DefaultCost)
if err != nil {
return "", err
return "", nil
}
return string(bytes), err
return string(hash), nil
}
// CompareHashAndData compares a hash to clear data and returns an error if the comparison fails.

View File

@@ -1,53 +0,0 @@
package crypto
import (
"testing"
)
func TestService_Hash(t *testing.T) {
var s = &Service{}
type args struct {
hash string
data string
}
tests := []struct {
name string
args args
expect bool
}{
{
name: "Empty",
args: args{
hash: "",
data: "",
},
expect: false,
},
{
name: "Matching",
args: args{
hash: "$2a$10$6BFGd94oYx8k0bFNO6f33uPUpcpAJyg8UVX.akLe9EthF/ZBTXqcy",
data: "Passw0rd!",
},
expect: true,
},
{
name: "Not matching",
args: args{
hash: "$2a$10$ltKrUZ7492xyutHOb0/XweevU4jyw7QO66rP32jTVOMb3EX3JxA/a",
data: "Passw0rd!",
},
expect: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := s.CompareHashAndData(tt.args.hash, tt.args.data)
if (err != nil) == tt.expect {
t.Errorf("Service.CompareHashAndData() = %v", err)
}
})
}
}

View File

@@ -10,9 +10,9 @@ import (
"path"
"time"
"github.com/boltdb/bolt"
dserrors "github.com/portainer/portainer/api/dataservices/errors"
"github.com/sirupsen/logrus"
bolt "go.etcd.io/bbolt"
)
const (
@@ -161,7 +161,7 @@ func (connection *DbConnection) ExportRaw(filename string) error {
return fmt.Errorf("stat on %s failed: %s", databasePath, err)
}
b, err := connection.ExportJson(databasePath, true)
b, err := connection.exportJson(databasePath)
if err != nil {
return err
}
@@ -181,7 +181,10 @@ func (connection *DbConnection) ConvertToKey(v int) []byte {
func (connection *DbConnection) SetServiceName(bucketName string) error {
return connection.Batch(func(tx *bolt.Tx) error {
_, err := tx.CreateBucketIfNotExists([]byte(bucketName))
return err
if err != nil {
return err
}
return nil
})
}

View File

@@ -4,34 +4,13 @@ import (
"encoding/json"
"time"
"github.com/boltdb/bolt"
"github.com/sirupsen/logrus"
bolt "go.etcd.io/bbolt"
)
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 {
bucketName := string(name)
bucket = tx.Bucket([]byte(bucketName))
seqId := bucket.Sequence()
buckets[bucketName] = int(seqId)
return nil
})
return err
})
return buckets, err
}
// ExportJSON creates a JSON representation from a DbConnection. You can include
// the database's metadata or ignore it. Ensure the database is closed before
// using this function
// inspired by github.com/konoui/boltdb-exporter (which has no license)
// but very much simplified, based on how we use boltdb
func (c *DbConnection) ExportJson(databasePath string, metadata bool) ([]byte, error) {
func (c *DbConnection) exportJson(databasePath string) ([]byte, error) {
logrus.WithField("databasePath", databasePath).Infof("exportJson")
connection, err := bolt.Open(databasePath, 0600, &bolt.Options{Timeout: 1 * time.Second, ReadOnly: true})
@@ -41,13 +20,6 @@ func (c *DbConnection) ExportJson(databasePath string, metadata bool) ([]byte, e
defer connection.Close()
backup := make(map[string]interface{})
if metadata {
meta, err := backupMetadata(connection)
if err != nil {
logrus.WithError(err).Errorf("Failed exporting metadata: %v", err)
}
backup["__metadata"] = meta
}
err = connection.View(func(tx *bolt.Tx) error {
err = tx.ForEach(func(name []byte, bucket *bolt.Bucket) error {
@@ -73,20 +45,15 @@ func (c *DbConnection) ExportJson(databasePath string, metadata bool) ([]byte, e
}
if bucketName == "version" {
backup[bucketName] = version
return nil
}
if len(list) > 0 {
if bucketName == "ssl" ||
bucketName == "settings" ||
bucketName == "tunnel_server" {
backup[bucketName] = nil
if len(list) > 0 {
backup[bucketName] = list[0]
}
backup[bucketName] = list[0]
return nil
}
backup[bucketName] = list
return nil
}
return nil

View File

@@ -10,7 +10,7 @@ import (
)
const (
jsonobject = `{"LogoURL":"","BlackListedLabels":[],"AuthenticationMethod":1,"InternalAuthSettings": {"RequiredPasswordLength": 12}"LDAPSettings":{"AnonymousMode":true,"ReaderDN":"","URL":"","TLSConfig":{"TLS":false,"TLSSkipVerify":false},"StartTLS":false,"SearchSettings":[{"BaseDN":"","Filter":"","UserNameAttribute":""}],"GroupSearchSettings":[{"GroupBaseDN":"","GroupFilter":"","GroupAttribute":""}],"AutoCreateUsers":true},"OAuthSettings":{"ClientID":"","AccessTokenURI":"","AuthorizationURI":"","ResourceURI":"","RedirectURI":"","UserIdentifier":"","Scopes":"","OAuthAutoCreateUsers":false,"DefaultTeamID":0,"SSO":true,"LogoutURI":"","KubeSecretKey":"j0zLVtY/lAWBk62ByyF0uP80SOXaitsABP0TTJX8MhI="},"OpenAMTConfiguration":{"Enabled":false,"MPSServer":"","MPSUser":"","MPSPassword":"","MPSToken":"","CertFileContent":"","CertFileName":"","CertFilePassword":"","DomainName":""},"FeatureFlagSettings":{},"SnapshotInterval":"5m","TemplatesURL":"https://raw.githubusercontent.com/portainer/templates/master/templates-2.0.json","EdgeAgentCheckinInterval":5,"EnableEdgeComputeFeatures":false,"UserSessionTimeout":"8h","KubeconfigExpiry":"0","EnableTelemetry":true,"HelmRepositoryURL":"https://charts.bitnami.com/bitnami","KubectlShellImage":"portainer/kubectl-shell","DisplayDonationHeader":false,"DisplayExternalContributors":false,"EnableHostManagementFeatures":false,"AllowVolumeBrowserForRegularUsers":false,"AllowBindMountsForRegularUsers":false,"AllowPrivilegedModeForRegularUsers":false,"AllowHostNamespaceForRegularUsers":false,"AllowStackManagementForRegularUsers":false,"AllowDeviceMappingForRegularUsers":false,"AllowContainerCapabilitiesForRegularUsers":false}`
jsonobject = `{"LogoURL":"","BlackListedLabels":[],"AuthenticationMethod":1,"LDAPSettings":{"AnonymousMode":true,"ReaderDN":"","URL":"","TLSConfig":{"TLS":false,"TLSSkipVerify":false},"StartTLS":false,"SearchSettings":[{"BaseDN":"","Filter":"","UserNameAttribute":""}],"GroupSearchSettings":[{"GroupBaseDN":"","GroupFilter":"","GroupAttribute":""}],"AutoCreateUsers":true},"OAuthSettings":{"ClientID":"","AccessTokenURI":"","AuthorizationURI":"","ResourceURI":"","RedirectURI":"","UserIdentifier":"","Scopes":"","OAuthAutoCreateUsers":false,"DefaultTeamID":0,"SSO":true,"LogoutURI":"","KubeSecretKey":"j0zLVtY/lAWBk62ByyF0uP80SOXaitsABP0TTJX8MhI="},"OpenAMTConfiguration":{"Enabled":false,"MPSServer":"","MPSUser":"","MPSPassword":"","MPSToken":"","CertFileContent":"","CertFileName":"","CertFilePassword":"","DomainName":""},"FeatureFlagSettings":{},"SnapshotInterval":"5m","TemplatesURL":"https://raw.githubusercontent.com/portainer/templates/master/templates-2.0.json","EdgeAgentCheckinInterval":5,"EnableEdgeComputeFeatures":false,"UserSessionTimeout":"8h","KubeconfigExpiry":"0","EnableTelemetry":true,"HelmRepositoryURL":"https://charts.bitnami.com/bitnami","KubectlShellImage":"portainer/kubectl-shell","DisplayDonationHeader":false,"DisplayExternalContributors":false,"EnableHostManagementFeatures":false,"AllowVolumeBrowserForRegularUsers":false,"AllowBindMountsForRegularUsers":false,"AllowPrivilegedModeForRegularUsers":false,"AllowHostNamespaceForRegularUsers":false,"AllowStackManagementForRegularUsers":false,"AllowDeviceMappingForRegularUsers":false,"AllowContainerCapabilitiesForRegularUsers":false}`
passphrase = "my secret key"
)

View File

@@ -67,14 +67,14 @@ func (service *Service) EdgeJob(ID portainer.EdgeJobID) (*portainer.EdgeJob, err
return &edgeJob, nil
}
// Create creates a new EdgeJob
func (service *Service) Create(ID portainer.EdgeJobID, edgeJob *portainer.EdgeJob) error {
edgeJob.ID = ID
return service.connection.CreateObjectWithId(
// CreateEdgeJob creates a new Edge job
func (service *Service) Create(edgeJob *portainer.EdgeJob) error {
return service.connection.CreateObject(
BucketName,
int(edgeJob.ID),
edgeJob,
func(id uint64) (int, interface{}) {
edgeJob.ID = portainer.EdgeJobID(id)
return int(edgeJob.ID), edgeJob
},
)
}

View File

@@ -47,6 +47,7 @@ type (
User() UserService
Version() VersionService
Webhook() WebhookService
State() StateService
}
// CustomTemplateService represents a service to manage custom templates
@@ -74,7 +75,7 @@ type (
EdgeJobService interface {
EdgeJobs() ([]portainer.EdgeJob, error)
EdgeJob(ID portainer.EdgeJobID) (*portainer.EdgeJob, error)
Create(ID portainer.EdgeJobID, edgeJob *portainer.EdgeJob) error
Create(edgeJob *portainer.EdgeJob) error
UpdateEdgeJob(ID portainer.EdgeJobID, edgeJob *portainer.EdgeJob) error
DeleteEdgeJob(ID portainer.EdgeJobID) error
GetNextIdentifier() int
@@ -298,6 +299,13 @@ type (
DeleteWebhook(ID portainer.WebhookID) error
BucketName() string
}
// State represents a service for managing application state
StateService interface {
State() (*portainer.State, error)
UpdateState(state *portainer.State) error
BucketName() string
}
)
func IsErrObjectNotFound(e error) bool {

View File

@@ -29,7 +29,7 @@ func TestService_StackByWebhookID(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode. Normally takes ~1s to run.")
}
_, store, teardown := datastore.MustNewTestStore(true, true)
_, store, teardown := datastore.MustNewTestStore(true)
defer teardown()
b := stackBuilder{t: t, store: store}
@@ -87,7 +87,7 @@ func Test_RefreshableStacks(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode. Normally takes ~1s to run.")
}
_, store, teardown := datastore.MustNewTestStore(true, true)
_, store, teardown := datastore.MustNewTestStore(true)
defer teardown()
staticStack := portainer.Stack{ID: 1}

View File

@@ -0,0 +1,49 @@
package state
import (
portainer "github.com/portainer/portainer/api"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "state"
key = "STATE"
)
// Service represents a service for managing environment(endpoint) data.
type Service struct {
connection portainer.Connection
}
func (service *Service) BucketName() string {
return BucketName
}
// 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{
connection: connection,
}, nil
}
// State retrieve the state object.
func (service *Service) State() (*portainer.State, error) {
var state portainer.State
err := service.connection.GetObject(BucketName, []byte(key), &state)
if err != nil {
return nil, err
}
return &state, nil
}
// UpdateState persists a State object.
func (service *Service) UpdateState(state *portainer.State) error {
return service.connection.UpdateObject(BucketName, []byte(key), state)
}

View File

@@ -10,7 +10,7 @@ import (
func Test_teamByName(t *testing.T) {
t.Run("When store is empty should return ErrObjectNotFound", func(t *testing.T) {
_, store, teardown := datastore.MustNewTestStore(true, true)
_, store, teardown := datastore.MustNewTestStore(true)
defer teardown()
_, err := store.Team().TeamByName("name")
@@ -19,7 +19,7 @@ func Test_teamByName(t *testing.T) {
})
t.Run("When there is no object with the same name should return ErrObjectNotFound", func(t *testing.T) {
_, store, teardown := datastore.MustNewTestStore(true, true)
_, store, teardown := datastore.MustNewTestStore(true)
defer teardown()
teamBuilder := teamBuilder{
@@ -35,7 +35,7 @@ func Test_teamByName(t *testing.T) {
})
t.Run("When there is an object with the same name should return the object", func(t *testing.T) {
_, store, teardown := datastore.MustNewTestStore(true, true)
_, store, teardown := datastore.MustNewTestStore(true)
defer teardown()
teamBuilder := teamBuilder{

View File

@@ -69,11 +69,6 @@ func getBackupRestoreOptions(backupDir string) *BackupOptions {
}
}
// Backup current database with default options
func (store *Store) Backup() (string, error) {
return store.backupWithOptions(nil)
}
func (store *Store) setupOptions(options *BackupOptions) *BackupOptions {
if options == nil {
options = &BackupOptions{}

View File

@@ -10,7 +10,7 @@ import (
)
func TestCreateBackupFolders(t *testing.T) {
_, store, teardown := MustNewTestStore(false, true)
_, store, teardown := MustNewTestStore(false)
defer teardown()
connection := store.GetConnection()
@@ -27,7 +27,7 @@ func TestCreateBackupFolders(t *testing.T) {
}
func TestStoreCreation(t *testing.T) {
_, store, teardown := MustNewTestStore(true, true)
_, store, teardown := MustNewTestStore(true)
defer teardown()
if store == nil {
@@ -40,7 +40,7 @@ func TestStoreCreation(t *testing.T) {
}
func TestBackup(t *testing.T) {
_, store, teardown := MustNewTestStore(true, true)
_, store, teardown := MustNewTestStore(true)
connection := store.GetConnection()
defer teardown()
@@ -67,7 +67,7 @@ func TestBackup(t *testing.T) {
}
func TestRemoveWithOptions(t *testing.T) {
_, store, teardown := MustNewTestStore(true, true)
_, store, teardown := MustNewTestStore(true)
defer teardown()
t.Run("successfully removes file if existent", func(t *testing.T) {
@@ -86,7 +86,7 @@ func TestRemoveWithOptions(t *testing.T) {
err = store.removeWithOptions(options)
if err != nil {
t.Errorf("RemoveWithOptions should successfully remove file; err=%v", err)
t.Errorf("RemoveWithOptions should successfully remove file; err=%w", err)
}
if isFileExist(f.Name()) {

View File

@@ -27,7 +27,7 @@ const (
// TestStoreFull an eventually comprehensive set of tests for the Store.
// The idea is what we write to the store, we should read back.
func TestStoreFull(t *testing.T) {
_, store, teardown := MustNewTestStore(true, true)
_, store, teardown := MustNewTestStore(true)
defer teardown()
testCases := map[string]func(t *testing.T){

View File

@@ -22,18 +22,28 @@ func (store *Store) Init() error {
return err
}
return store.checkOrCreateDefaultData()
err = store.checkOrCreateDefaultData()
if err != nil {
return err
}
err = store.checkOrCreateDefaultState()
if err != nil {
return err
}
return nil
}
func (store *Store) checkOrCreateInstanceID() error {
_, err := store.VersionService.InstanceID()
instanceID, err := store.VersionService.InstanceID()
if store.IsErrObjectNotFound(err) {
uid, err := uuid.NewV4()
if err != nil {
return err
}
instanceID := uid.String()
instanceID = uid.String()
return store.VersionService.StoreInstanceID(instanceID)
}
return err
@@ -47,9 +57,6 @@ func (store *Store) checkOrCreateDefaultSettings() error {
EnableTelemetry: true,
AuthenticationMethod: portainer.AuthenticationInternal,
BlackListedLabels: make([]portainer.Pair, 0),
InternalAuthSettings: portainer.InternalAuthSettings{
RequiredPasswordLength: 12,
},
LDAPSettings: portainer.LDAPSettings{
AnonymousMode: true,
AutoCreateUsers: true,
@@ -122,3 +129,16 @@ func (store *Store) checkOrCreateDefaultData() error {
}
return nil
}
func (store *Store) checkOrCreateDefaultState() error {
_, err := store.State().State()
if store.IsErrObjectNotFound(err) {
defaultState := &portainer.State{
DisableAdminInit: false,
}
return store.State().UpdateState(defaultState)
}
return err
}

View File

@@ -28,20 +28,10 @@ func (slog *ScopedLog) Debug(message string) {
slog.print(DEBUG, fmt.Sprintf("[message: %s]", message))
}
func (slog *ScopedLog) Debugf(message string, vars ...interface{}) {
message = fmt.Sprintf(message, vars...)
slog.print(DEBUG, fmt.Sprintf("[message: %s]", message))
}
func (slog *ScopedLog) Info(message string) {
slog.print(INFO, fmt.Sprintf("[message: %s]", message))
}
func (slog *ScopedLog) Infof(message string, vars ...interface{}) {
message = fmt.Sprintf(message, vars...)
slog.print(INFO, fmt.Sprintf("[message: %s]", message))
}
func (slog *ScopedLog) Error(message string, err error) {
slog.print(ERROR, fmt.Sprintf("[message: %s] [error: %s]", message, err))
}

View File

@@ -9,7 +9,6 @@ import (
plog "github.com/portainer/portainer/api/datastore/log"
"github.com/portainer/portainer/api/datastore/migrator"
"github.com/portainer/portainer/api/internal/authorization"
"github.com/sirupsen/logrus"
werrors "github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
@@ -25,12 +24,6 @@ func (store *Store) MigrateData() error {
return err
}
// Backup Database
backupPath, err := store.Backup()
if err != nil {
return werrors.Wrap(err, "while backing up db before migration")
}
migratorParams := &migrator.MigratorParameters{
DatabaseVersion: version,
EndpointGroupService: store.EndpointGroupService,
@@ -53,27 +46,7 @@ func (store *Store) MigrateData() error {
AuthorizationService: authorization.NewService(store),
}
// restore on error
err = store.connectionMigrateData(migratorParams)
if err != nil {
logrus.Errorf("While DB migration %v. Restoring DB", err)
// Restore options
options := BackupOptions{
BackupPath: backupPath,
}
err := store.restoreWithOptions(&options)
if err != nil {
logrus.Fatalf(
"Failed restoring the backup. portainer database file needs to restored manually by "+
"replacing %s database file with recent backup %s. Error %v",
store.databasePath(),
options.BackupPath,
err,
)
}
}
return err
return store.connectionMigrateData(migratorParams)
}
// FailSafeMigrate backup and restore DB if migration fail

View File

@@ -1,19 +1,13 @@
package datastore
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"os"
"path/filepath"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/database/boltdb"
)
// testVersion is a helper which tests current store version against wanted version
@@ -28,32 +22,8 @@ func testVersion(store *Store, versionWant int, t *testing.T) {
}
func TestMigrateData(t *testing.T) {
snapshotTests := []struct {
testName string
srcPath string
wantPath string
}{
{
testName: "migrate version 24 to latest",
srcPath: "test_data/input_24.json",
wantPath: "test_data/output_24_to_latest.json",
},
}
for _, test := range snapshotTests {
t.Run(test.testName, func(t *testing.T) {
err := migrateDBTestHelper(t, test.srcPath, test.wantPath)
if err != nil {
t.Errorf(
"Failed migrating mock database %v: %v",
test.srcPath,
err,
)
}
})
}
t.Run("MigrateData for New Store & Re-Open Check", func(t *testing.T) {
newStore, store, teardown := MustNewTestStore(false, true)
newStore, store, teardown := MustNewTestStore(false)
defer teardown()
if !newStore {
@@ -80,7 +50,7 @@ func TestMigrateData(t *testing.T) {
{version: 21, expectedVersion: portainer.DBVersion},
}
for _, tc := range tests {
_, store, teardown := MustNewTestStore(true, true)
_, store, teardown := MustNewTestStore(true)
defer teardown()
// Setup data
@@ -105,7 +75,7 @@ func TestMigrateData(t *testing.T) {
}
t.Run("Error in MigrateData should restore backup before MigrateData", func(t *testing.T) {
_, store, teardown := MustNewTestStore(false, true)
_, store, teardown := MustNewTestStore(false)
defer teardown()
version := 17
@@ -117,7 +87,7 @@ func TestMigrateData(t *testing.T) {
})
t.Run("MigrateData should create backup file upon update", func(t *testing.T) {
_, store, teardown := MustNewTestStore(false, true)
_, store, teardown := MustNewTestStore(false)
defer teardown()
store.VersionService.StoreDBVersion(0)
@@ -131,7 +101,7 @@ func TestMigrateData(t *testing.T) {
})
t.Run("MigrateData should fail to create backup if database file is set to updating", func(t *testing.T) {
_, store, teardown := MustNewTestStore(false, true)
_, store, teardown := MustNewTestStore(false)
defer teardown()
store.VersionService.StoreIsUpdating(true)
@@ -146,7 +116,7 @@ func TestMigrateData(t *testing.T) {
})
t.Run("MigrateData should not create backup on startup if portainer version matches db", func(t *testing.T) {
_, store, teardown := MustNewTestStore(false, true)
_, store, teardown := MustNewTestStore(false)
defer teardown()
store.MigrateData()
@@ -161,7 +131,7 @@ func TestMigrateData(t *testing.T) {
}
func Test_getBackupRestoreOptions(t *testing.T) {
_, store, teardown := MustNewTestStore(false, true)
_, store, teardown := MustNewTestStore(false)
defer teardown()
options := getBackupRestoreOptions(store.commonBackupDir())
@@ -180,7 +150,7 @@ func Test_getBackupRestoreOptions(t *testing.T) {
func TestRollback(t *testing.T) {
t.Run("Rollback should restore upgrade after backup", func(t *testing.T) {
version := 21
_, store, teardown := MustNewTestStore(false, true)
_, store, teardown := MustNewTestStore(false)
defer teardown()
store.VersionService.StoreDBVersion(version)
@@ -215,250 +185,3 @@ func isFileExist(path string) bool {
}
return len(matches) > 0
}
// migrateDBTestHelper loads a json representation of a bolt database from srcPath,
// parses it into a database, runs a migration on that database, and then
// compares it with an expected output database.
func migrateDBTestHelper(t *testing.T, srcPath, wantPath string) error {
srcJSON, err := os.ReadFile(srcPath)
if err != nil {
t.Fatalf("failed loading source JSON file %v: %v", srcPath, err)
}
// Parse source json to db.
_, store, teardown := MustNewTestStore(true, false)
defer teardown()
err = importJSON(t, bytes.NewReader(srcJSON), store)
if err != nil {
return err
}
// Run the actual migrations on our input database.
err = store.MigrateData()
if err != nil {
return err
}
// Assert that our database connection is using bolt so we can call
// exportJson rather than ExportRaw. The exportJson function allows us to
// strip out the metadata which we don't want for our tests.
// TODO: update connection interface in CE to allow us to use ExportRaw and pass meta false
err = store.connection.Close()
if err != nil {
t.Fatalf("err closing bolt connection: %v", err)
}
con, ok := store.connection.(*boltdb.DbConnection)
if !ok {
t.Fatalf("backing database is not using boltdb, but the migrations test requires it")
}
// Convert database back to json.
databasePath := con.GetDatabaseFilePath()
if _, err := os.Stat(databasePath); err != nil {
return fmt.Errorf("stat on %s failed: %s", databasePath, err)
}
gotJSON, err := con.ExportJson(databasePath, false)
if err != nil {
t.Logf(
"failed re-exporting database %s to JSON: %v",
databasePath,
err,
)
}
wantJSON, err := os.ReadFile(wantPath)
if err != nil {
t.Fatalf("failed loading want JSON file %v: %v", wantPath, err)
}
// Compare the result we got with the one we wanted.
if diff := cmp.Diff(wantJSON, gotJSON); diff != "" {
gotPath := filepath.Join(os.TempDir(), "portainer-migrator-test-fail.json")
os.WriteFile(
gotPath,
gotJSON,
0600,
)
t.Errorf(
"migrate data from %s to %s failed\nwrote migrated input to %s\nmismatch (-want +got):\n%s",
srcPath,
wantPath,
gotPath,
diff,
)
}
return nil
}
// 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]interface{})
// Parse json into map of objects.
d := json.NewDecoder(r)
d.UseNumber()
err := d.Decode(&objects)
if err != nil {
return err
}
// Get database connection from store.
con := store.connection
for k, v := range objects {
switch k {
case "version":
versions, ok := v.(map[string]interface{})
if !ok {
t.Logf("failed casting %s to map[string]interface{}", k)
}
dbVersion, ok := versions["DB_VERSION"]
if !ok {
t.Logf("failed getting DB_VERSION from %s", k)
}
numDBVersion, ok := dbVersion.(json.Number)
if !ok {
t.Logf("failed parsing DB_VERSION as json number from %s", k)
}
intDBVersion, err := numDBVersion.Int64()
if err != nil {
t.Logf("failed casting %v to int: %v", numDBVersion, intDBVersion)
}
err = con.CreateObjectWithStringId(
k,
[]byte("DB_VERSION"),
int(intDBVersion),
)
if err != nil {
t.Logf("failed writing DB_VERSION in %s: %v", k, err)
}
instanceID, ok := versions["INSTANCE_ID"]
if !ok {
t.Logf("failed getting INSTANCE_ID from %s", k)
}
err = con.CreateObjectWithStringId(
k,
[]byte("INSTANCE_ID"),
instanceID,
)
if err != nil {
t.Logf("failed writing INSTANCE_ID in %s: %v", k, err)
}
case "dockerhub":
obj, ok := v.([]interface{})
if !ok {
t.Logf("failed to cast %s to []interface{}", k)
}
err := con.CreateObjectWithStringId(
k,
[]byte("DOCKERHUB"),
obj[0],
)
if err != nil {
t.Logf("failed writing DOCKERHUB in %s: %v", k, err)
}
case "ssl":
obj, ok := v.(map[string]interface{})
if !ok {
t.Logf("failed to case %s to map[string]interface{}", k)
}
err := con.CreateObjectWithStringId(
k,
[]byte("SSL"),
obj,
)
if err != nil {
t.Logf("failed writing SSL in %s: %v", k, err)
}
case "settings":
obj, ok := v.(map[string]interface{})
if !ok {
t.Logf("failed to case %s to map[string]interface{}", k)
}
err := con.CreateObjectWithStringId(
k,
[]byte("SETTINGS"),
obj,
)
if err != nil {
t.Logf("failed writing SETTINGS in %s: %v", k, err)
}
case "tunnel_server":
obj, ok := v.(map[string]interface{})
if !ok {
t.Logf("failed to case %s to map[string]interface{}", k)
}
err := con.CreateObjectWithStringId(
k,
[]byte("INFO"),
obj,
)
if err != nil {
t.Logf("failed writing INFO in %s: %v", k, err)
}
case "templates":
continue
default:
objlist, ok := v.([]interface{})
if !ok {
t.Logf("failed to cast %s to []interface{}", k)
}
for _, obj := range objlist {
value, ok := obj.(map[string]interface{})
if !ok {
t.Logf("failed to cast %v to map[string]interface{}", obj)
} else {
var ok bool
var id interface{}
switch k {
case "endpoint_relations":
// TODO: need to make into an int, then do that weird
// stringification
id, ok = value["EndpointID"]
default:
id, ok = value["Id"]
}
if !ok {
// endpoint_relations: EndpointID
t.Logf("missing Id field: %s", k)
id = "error"
}
n, ok := id.(json.Number)
if !ok {
t.Logf("failed to cast %v to json.Number in %s", id, k)
} else {
key, err := n.Int64()
if err != nil {
t.Logf("failed to cast %v to int in %s", n, k)
} else {
err := con.CreateObjectWithId(
k,
int(key),
value,
)
if err != nil {
t.Logf("failed writing %v in %s: %v", key, k, err)
}
}
}
}
}
}
}
return nil
}

View File

@@ -33,7 +33,7 @@ func setup(store *Store) error {
}
func TestMigrateSettings(t *testing.T) {
_, store, teardown := MustNewTestStore(false, true)
_, store, teardown := MustNewTestStore(false)
defer teardown()
err := setup(store)

View File

@@ -10,7 +10,7 @@ import (
)
func TestMigrateStackEntryPoint(t *testing.T) {
_, store, teardown := MustNewTestStore(false, true)
_, store, teardown := MustNewTestStore(false)
defer teardown()
stackService := store.Stack()

View File

@@ -1,38 +1,16 @@
package migrator
import (
"errors"
"reflect"
"runtime"
"fmt"
werrors "github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
)
type migration struct {
dbversion int
migrate func() error
}
func migrationError(err error, context string) error {
return werrors.Wrap(err, "failed in "+context)
}
func newMigration(dbversion int, migrate func() error) migration {
return migration{
dbversion: dbversion,
migrate: migrate,
}
}
func dbTooOldError() error {
return errors.New("migrating from less than Portainer 1.21.0 is not supported, please contact Portainer support.")
}
func GetFunctionName(i interface{}) string {
return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
}
// Migrate checks the database version and migrate the existing data to the most recent data model.
func (m *Migrator) Migrate() error {
// set DB to updating status
@@ -41,93 +19,175 @@ func (m *Migrator) Migrate() error {
return migrationError(err, "StoreIsUpdating")
}
migrations := []migration{
// Portainer < 1.21.0
newMigration(17, dbTooOldError),
// Portainer 1.21.0
newMigration(18, m.updateUsersToDBVersion18),
newMigration(18, m.updateEndpointsToDBVersion18),
newMigration(18, m.updateEndpointGroupsToDBVersion18),
newMigration(18, m.updateRegistriesToDBVersion18),
// 1.22.0
newMigration(19, m.updateSettingsToDBVersion19),
// 1.22.1
newMigration(20, m.updateUsersToDBVersion20),
newMigration(20, m.updateSettingsToDBVersion20),
newMigration(20, m.updateSchedulesToDBVersion20),
// Portainer 1.23.0
// DBVersion 21 is missing as it was shipped as via hotfix 1.22.2
newMigration(22, m.updateResourceControlsToDBVersion22),
newMigration(22, m.updateUsersAndRolesToDBVersion22),
// Portainer 1.24.0
newMigration(23, m.updateTagsToDBVersion23),
newMigration(23, m.updateEndpointsAndEndpointGroupsToDBVersion23),
// Portainer 1.24.1
newMigration(24, m.updateSettingsToDB24),
// Portainer 2.0.0
newMigration(25, m.updateSettingsToDB25),
newMigration(25, m.updateStacksToDB24), // yes this looks odd. Don't be tempted to move it
// Portainer 2.1.0
newMigration(26, m.updateEndpointSettingsToDB25),
// Portainer 2.2.0
newMigration(27, m.updateStackResourceControlToDB27),
// Portainer 2.6.0
newMigration(30, m.migrateDBVersionToDB30),
// Portainer 2.9.0
newMigration(32, m.migrateDBVersionToDB32),
// Portainer 2.9.1, 2.9.2
newMigration(33, m.migrateDBVersionToDB33),
// Portainer 2.10
newMigration(34, m.migrateDBVersionToDB34),
// Portainer 2.9.3 (yep out of order, but 2.10 is EE only)
newMigration(35, m.migrateDBVersionToDB35),
newMigration(36, m.migrateDBVersionToDB36),
// Portainer 2.13
newMigration(40, m.migrateDBVersionToDB40),
// Portainer 2.14
newMigration(50, m.migrateDBVersionToDB50),
if m.currentDBVersion < 17 {
return migrationError(err, "migrating from less than Portainer 1.21.0 is not supported, please contact Portainer support.")
}
var lastDbVersion int
for _, migration := range migrations {
if m.currentDBVersion < migration.dbversion {
// Print the next line only when the version changes
if migration.dbversion > lastDbVersion {
migrateLog.Infof("Migrating DB to version %d", migration.dbversion)
}
err := migration.migrate()
if err != nil {
return migrationError(err, GetFunctionName(migration.migrate))
}
// Portainer 1.21.0
if m.currentDBVersion < 18 {
err := m.updateUsersToDBVersion18()
if err != nil {
return migrationError(err, "updateUsersToDBVersion18")
}
err = m.updateEndpointsToDBVersion18()
if err != nil {
return migrationError(err, "updateEndpointsToDBVersion18")
}
err = m.updateEndpointGroupsToDBVersion18()
if err != nil {
return migrationError(err, "updateEndpointGroupsToDBVersion18")
}
err = m.updateRegistriesToDBVersion18()
if err != nil {
return migrationError(err, "updateRegistriesToDBVersion18")
}
}
// Portainer 1.22.0
if m.currentDBVersion < 19 {
err := m.updateSettingsToDBVersion19()
if err != nil {
return migrationError(err, "updateSettingsToDBVersion19")
}
}
// Portainer 1.22.1
if m.currentDBVersion < 20 {
err := m.updateUsersToDBVersion20()
if err != nil {
return migrationError(err, "updateUsersToDBVersion20")
}
err = m.updateSettingsToDBVersion20()
if err != nil {
return migrationError(err, "updateSettingsToDBVersion20")
}
err = m.updateSchedulesToDBVersion20()
if err != nil {
return migrationError(err, "updateSchedulesToDBVersion20")
}
}
// Portainer 1.23.0
// DBVersion 21 is missing as it was shipped as via hotfix 1.22.2
if m.currentDBVersion < 22 {
err := m.updateResourceControlsToDBVersion22()
if err != nil {
return migrationError(err, "updateResourceControlsToDBVersion22")
}
err = m.updateUsersAndRolesToDBVersion22()
if err != nil {
return migrationError(err, "updateUsersAndRolesToDBVersion22")
}
}
// Portainer 1.24.0
if m.currentDBVersion < 23 {
migrateLog.Info("Migrating to DB 23")
err := m.updateTagsToDBVersion23()
if err != nil {
return migrationError(err, "updateTagsToDBVersion23")
}
err = m.updateEndpointsAndEndpointGroupsToDBVersion23()
if err != nil {
return migrationError(err, "updateEndpointsAndEndpointGroupsToDBVersion23")
}
}
// Portainer 1.24.1
if m.currentDBVersion < 24 {
migrateLog.Info("Migrating to DB 24")
err := m.updateSettingsToDB24()
if err != nil {
return migrationError(err, "updateSettingsToDB24")
}
}
// Portainer 2.0.0
if m.currentDBVersion < 25 {
migrateLog.Info("Migrating to DB 25")
err := m.updateSettingsToDB25()
if err != nil {
return migrationError(err, "updateSettingsToDB25")
}
err = m.updateStacksToDB24()
if err != nil {
return migrationError(err, "updateStacksToDB24")
}
}
// Portainer 2.1.0
if m.currentDBVersion < 26 {
migrateLog.Info("Migrating to DB 26")
err := m.updateEndpointSettingsToDB25()
if err != nil {
return migrationError(err, "updateEndpointSettingsToDB25")
}
}
// Portainer 2.2.0
if m.currentDBVersion < 27 {
migrateLog.Info("Migrating to DB 27")
err := m.updateStackResourceControlToDB27()
if err != nil {
return migrationError(err, "updateStackResourceControlToDB27")
}
}
// Portainer 2.6.0
if m.currentDBVersion < 30 {
migrateLog.Info("Migrating to DB 30")
err := m.migrateDBVersionToDB30()
if err != nil {
return migrationError(err, "migrateDBVersionToDB30")
}
}
// Portainer 2.9.0
if m.currentDBVersion < 32 {
err := m.migrateDBVersionToDB32()
if err != nil {
return migrationError(err, "migrateDBVersionToDB32")
}
}
// Portainer 2.9.1, 2.9.2
if m.currentDBVersion < 33 {
migrateLog.Info("Migrating to DB 33")
err := m.migrateDBVersionToDB33()
if err != nil {
return migrationError(err, "migrateDBVersionToDB33")
}
}
// Portainer 2.10
if m.currentDBVersion < 34 {
migrateLog.Info("Migrating to DB 34")
if err := m.migrateDBVersionToDB34(); err != nil {
return migrationError(err, "migrateDBVersionToDB34")
}
}
// Portainer 2.9.3 (yep out of order, but 2.10 is EE only)
if m.currentDBVersion < 35 {
migrateLog.Info("Migrating to DB 35")
if err := m.migrateDBVersionToDB35(); err != nil {
return migrationError(err, "migrateDBVersionToDB35")
}
lastDbVersion = migration.dbversion
}
migrateLog.Infof("Setting DB version to %d", portainer.DBVersion)
err = m.versionService.StoreDBVersion(portainer.DBVersion)
if err != nil {
return migrationError(err, "StoreDBVersion")
}
migrateLog.Infof("Updated DB version to %d", portainer.DBVersion)
migrateLog.Info(fmt.Sprintf("Updated DB version to %d", portainer.DBVersion))
// reset DB updating status
return m.versionService.StoreIsUpdating(false)

View File

@@ -5,7 +5,6 @@ import (
)
func (m *Migrator) updateUsersToDBVersion18() error {
migrateLog.Info("- updating users")
legacyUsers, err := m.userService.Users()
if err != nil {
return err
@@ -40,7 +39,6 @@ func (m *Migrator) updateUsersToDBVersion18() error {
}
func (m *Migrator) updateEndpointsToDBVersion18() error {
migrateLog.Info("- updating endpoints")
legacyEndpoints, err := m.endpointService.Endpoints()
if err != nil {
return err
@@ -71,7 +69,6 @@ func (m *Migrator) updateEndpointsToDBVersion18() error {
}
func (m *Migrator) updateEndpointGroupsToDBVersion18() error {
migrateLog.Info("- updating endpoint groups")
legacyEndpointGroups, err := m.endpointGroupService.EndpointGroups()
if err != nil {
return err
@@ -102,7 +99,6 @@ func (m *Migrator) updateEndpointGroupsToDBVersion18() error {
}
func (m *Migrator) updateRegistriesToDBVersion18() error {
migrateLog.Info("- updating registries")
legacyRegistries, err := m.registryService.Registries()
if err != nil {
return err

View File

@@ -3,7 +3,6 @@ package migrator
import portainer "github.com/portainer/portainer/api"
func (m *Migrator) updateSettingsToDBVersion19() error {
migrateLog.Info("- updating settings")
legacySettings, err := m.settingsService.Settings()
if err != nil {
return err

View File

@@ -7,7 +7,6 @@ import (
const scheduleScriptExecutionJobType = 1
func (m *Migrator) updateUsersToDBVersion20() error {
migrateLog.Info("- updating user authentication")
return m.authorizationService.UpdateUsersAuthorizations()
}
@@ -23,7 +22,6 @@ func (m *Migrator) updateSettingsToDBVersion20() error {
}
func (m *Migrator) updateSchedulesToDBVersion20() error {
migrateLog.Info("- updating schedules")
legacySchedules, err := m.scheduleService.Schedules()
if err != nil {
return err

View File

@@ -6,7 +6,6 @@ import (
)
func (m *Migrator) updateResourceControlsToDBVersion22() error {
migrateLog.Info("- updating resource controls")
legacyResourceControls, err := m.resourceControlService.ResourceControls()
if err != nil {
return err
@@ -25,7 +24,6 @@ func (m *Migrator) updateResourceControlsToDBVersion22() error {
}
func (m *Migrator) updateUsersAndRolesToDBVersion22() error {
migrateLog.Info("- updating users and roles")
legacyUsers, err := m.userService.Users()
if err != nil {
return err

View File

@@ -3,7 +3,7 @@ package migrator
import portainer "github.com/portainer/portainer/api"
func (m *Migrator) updateTagsToDBVersion23() error {
migrateLog.Info("- Updating tags")
migrateLog.Info("Updating tags")
tags, err := m.tagService.Tags()
if err != nil {
return err
@@ -21,7 +21,7 @@ func (m *Migrator) updateTagsToDBVersion23() error {
}
func (m *Migrator) updateEndpointsAndEndpointGroupsToDBVersion23() error {
migrateLog.Info("- updating endpoints and endpoint groups")
migrateLog.Info("Updating endpoints and endpoint groups")
tags, err := m.tagService.Tags()
if err != nil {
return err

View File

@@ -3,7 +3,7 @@ package migrator
import portainer "github.com/portainer/portainer/api"
func (m *Migrator) updateSettingsToDB24() error {
migrateLog.Info("- updating Settings")
migrateLog.Info("Updating Settings")
legacySettings, err := m.settingsService.Settings()
if err != nil {
@@ -18,7 +18,7 @@ func (m *Migrator) updateSettingsToDB24() error {
}
func (m *Migrator) updateStacksToDB24() error {
migrateLog.Info("- updating stacks")
migrateLog.Info("Updating stacks")
stacks, err := m.stackService.Stacks()
if err != nil {
return err

View File

@@ -5,7 +5,7 @@ import (
)
func (m *Migrator) updateSettingsToDB25() error {
migrateLog.Info("- updating settings")
migrateLog.Info("Updating settings")
legacySettings, err := m.settingsService.Settings()
if err != nil {

View File

@@ -5,7 +5,7 @@ import (
)
func (m *Migrator) updateEndpointSettingsToDB25() error {
migrateLog.Info("- updating endpoint settings")
migrateLog.Info("Updating endpoint settings")
settings, err := m.settingsService.Settings()
if err != nil {
return err

View File

@@ -7,7 +7,7 @@ import (
)
func (m *Migrator) updateStackResourceControlToDB27() error {
migrateLog.Info("- updating stack resource controls")
migrateLog.Info("Updating stack resource controls")
resourceControls, err := m.resourceControlService.ResourceControls()
if err != nil {
return err

View File

@@ -1,7 +1,7 @@
package migrator
func (m *Migrator) migrateDBVersionToDB30() error {
migrateLog.Info("- updating legacy settings")
migrateLog.Info("Updating legacy settings")
if err := m.MigrateSettingsToDB30(); err != nil {
return err
}

View File

@@ -2,10 +2,8 @@ package migrator
import (
"fmt"
"log"
"github.com/docker/docker/api/types/volume"
"github.com/portainer/portainer/api/dataservices/errors"
"log"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/internal/endpointutils"
@@ -13,24 +11,29 @@ import (
)
func (m *Migrator) migrateDBVersionToDB32() error {
migrateLog.Info("Updating registries")
err := m.updateRegistriesToDB32()
if err != nil {
return err
}
migrateLog.Info("Updating dockerhub")
err = m.updateDockerhubToDB32()
if err != nil {
return err
}
migrateLog.Info("Updating resource controls")
if err := m.updateVolumeResourceControlToDB32(); err != nil {
return err
}
migrateLog.Info("Updating kubeconfig expiry")
if err := m.kubeconfigExpiryToDB32(); err != nil {
return err
}
migrateLog.Info("Setting default helm repository url")
if err := m.helmRepositoryURLToDB32(); err != nil {
return err
}
@@ -39,7 +42,6 @@ func (m *Migrator) migrateDBVersionToDB32() error {
}
func (m *Migrator) updateRegistriesToDB32() error {
migrateLog.Info("- updating registries")
registries, err := m.registryService.Registries()
if err != nil {
return err
@@ -82,7 +84,6 @@ func (m *Migrator) updateRegistriesToDB32() error {
}
func (m *Migrator) updateDockerhubToDB32() error {
migrateLog.Info("- updating dockerhub")
dockerhub, err := m.dockerhubService.DockerHub()
if err == errors.ErrObjectNotFound {
return nil
@@ -171,7 +172,6 @@ func (m *Migrator) updateDockerhubToDB32() error {
}
func (m *Migrator) updateVolumeResourceControlToDB32() error {
migrateLog.Info("- updating resource controls")
endpoints, err := m.endpointService.Endpoints()
if err != nil {
return fmt.Errorf("failed fetching environments: %w", err)
@@ -211,14 +211,14 @@ func (m *Migrator) updateVolumeResourceControlToDB32() error {
continue
}
volumesData := snapshot.SnapshotRaw.Volumes
if volumesData.Volumes == nil {
log.Println("[DEBUG] [volume migration] [message: no volume data found]")
continue
if volumesData, done := snapshot.SnapshotRaw.Volumes.(map[string]interface{}); done {
if volumesData["Volumes"] == nil {
log.Println("[DEBUG] [volume migration] [message: no volume data found]")
continue
}
findResourcesToUpdateForDB32(endpointDockerID, volumesData, toUpdate, volumeResourceControls)
}
findResourcesToUpdateForDB32(endpointDockerID, volumesData, toUpdate, volumeResourceControls)
}
for _, resourceControl := range volumeResourceControls {
@@ -241,11 +241,18 @@ func (m *Migrator) updateVolumeResourceControlToDB32() error {
return nil
}
func findResourcesToUpdateForDB32(dockerID string, volumesData volume.VolumeListOKBody, toUpdate map[portainer.ResourceControlID]string, volumeResourceControls map[string]*portainer.ResourceControl) {
volumes := volumesData.Volumes
for _, volume := range volumes {
volumeName := volume.Name
createTime := volume.CreatedAt
func findResourcesToUpdateForDB32(dockerID string, volumesData map[string]interface{}, toUpdate map[portainer.ResourceControlID]string, volumeResourceControls map[string]*portainer.ResourceControl) {
volumes := volumesData["Volumes"].([]interface{})
for _, volumeMeta := range volumes {
volume := volumeMeta.(map[string]interface{})
volumeName, nameExist := volume["Name"].(string)
if !nameExist {
continue
}
createTime, createTimeExist := volume["CreatedAt"].(string)
if !createTimeExist {
continue
}
oldResourceID := fmt.Sprintf("%s%s", volumeName, createTime)
resourceControl, ok := volumeResourceControls[oldResourceID]
@@ -257,7 +264,6 @@ func findResourcesToUpdateForDB32(dockerID string, volumesData volume.VolumeList
}
func (m *Migrator) kubeconfigExpiryToDB32() error {
migrateLog.Info("- updating kubeconfig expiry")
settings, err := m.settingsService.Settings()
if err != nil {
return err
@@ -267,7 +273,6 @@ func (m *Migrator) kubeconfigExpiryToDB32() error {
}
func (m *Migrator) helmRepositoryURLToDB32() error {
migrateLog.Info("- setting default helm repository URL")
settings, err := m.settingsService.Settings()
if err != nil {
return err

View File

@@ -1,11 +1,8 @@
package migrator
import (
portainer "github.com/portainer/portainer/api"
)
import portainer "github.com/portainer/portainer/api"
func (m *Migrator) migrateDBVersionToDB33() error {
migrateLog.Info("- updating settings")
if err := m.migrateSettingsToDB33(); err != nil {
return err
}
@@ -19,7 +16,7 @@ func (m *Migrator) migrateSettingsToDB33() error {
return err
}
migrateLog.Info("- setting default kubectl shell image")
migrateLog.Info("Setting default kubectl shell image")
settings.KubectlShellImage = portainer.DefaultKubectlShellImage
return m.settingsService.UpdateSettings(settings)
}

View File

@@ -1,11 +1,9 @@
package migrator
import (
"github.com/portainer/portainer/api/dataservices"
)
import "github.com/portainer/portainer/api/dataservices"
func (m *Migrator) migrateDBVersionToDB34() error {
migrateLog.Info("- updating stacks")
migrateLog.Info("Migrating stacks")
err := MigrateStackEntryPoint(m.stackService)
if err != nil {
return err

View File

@@ -3,7 +3,7 @@ package migrator
func (m *Migrator) migrateDBVersionToDB35() error {
// These should have been migrated already, but due to an earlier bug and a bunch of duplicates,
// calling it again will now fix the issue as the function has been repaired.
migrateLog.Info("- updating dockerhub registries")
migrateLog.Info("Updating dockerhub registries")
err := m.updateDockerhubToDB32()
if err != nil {
return err

View File

@@ -1,36 +0,0 @@
package migrator
import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/internal/authorization"
)
func (m *Migrator) migrateDBVersionToDB36() error {
migrateLog.Info("Updating user authorizations")
if err := m.migrateUsersToDB36(); err != nil {
return err
}
return nil
}
func (m *Migrator) migrateUsersToDB36() error {
users, err := m.userService.Users()
if err != nil {
return err
}
for _, user := range users {
currentAuthorizations := authorization.DefaultPortainerAuthorizations()
currentAuthorizations[portainer.OperationPortainerUserListToken] = true
currentAuthorizations[portainer.OperationPortainerUserCreateToken] = true
currentAuthorizations[portainer.OperationPortainerUserRevokeToken] = true
user.PortainerAuthorizations = currentAuthorizations
err = m.userService.UpdateUser(user.ID, &user)
if err != nil {
return err
}
}
return nil
}

View File

@@ -1,31 +0,0 @@
package migrator
import "github.com/portainer/portainer/api/internal/endpointutils"
func (m *Migrator) migrateDBVersionToDB40() error {
if err := m.trustCurrentEdgeEndpointsDB40(); err != nil {
return err
}
return nil
}
func (m *Migrator) trustCurrentEdgeEndpointsDB40() error {
migrateLog.Info("- trusting current edge endpoints")
endpoints, err := m.endpointService.Endpoints()
if err != nil {
return err
}
for _, endpoint := range endpoints {
if endpointutils.IsEdgeEndpoint(&endpoint) {
endpoint.UserTrusted = true
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return err
}
}
}
return nil
}

View File

@@ -1,20 +0,0 @@
package migrator
import (
"github.com/pkg/errors"
)
func (m *Migrator) migrateDBVersionToDB50() error {
return m.migratePasswordLengthSettings()
}
func (m *Migrator) migratePasswordLengthSettings() error {
migrateLog.Info("Updating required password length")
s, err := m.settingsService.Settings()
if err != nil {
return errors.Wrap(err, "unable to retrieve settings")
}
s.InternalAuthSettings.RequiredPasswordLength = 12
return m.settingsService.UpdateSettings(s)
}

View File

@@ -26,6 +26,7 @@ import (
"github.com/portainer/portainer/api/dataservices/settings"
"github.com/portainer/portainer/api/dataservices/ssl"
"github.com/portainer/portainer/api/dataservices/stack"
"github.com/portainer/portainer/api/dataservices/state"
"github.com/portainer/portainer/api/dataservices/tag"
"github.com/portainer/portainer/api/dataservices/team"
"github.com/portainer/portainer/api/dataservices/teammembership"
@@ -68,6 +69,7 @@ type Store struct {
UserService *user.Service
VersionService *version.Service
WebhookService *webhook.Service
StateService *state.Service
}
func (store *Store) initServices() error {
@@ -227,6 +229,12 @@ func (store *Store) initServices() error {
}
store.ScheduleService = scheduleService
stateService, err := state.NewService(store.connection)
if err != nil {
return err
}
store.StateService = stateService
return nil
}
@@ -345,6 +353,11 @@ func (store *Store) Webhook() dataservices.WebhookService {
return store.WebhookService
}
// State gives access to the State data management layer
func (store *Store) State() dataservices.StateService {
return store.StateService
}
type storeExport struct {
CustomTemplate []portainer.CustomTemplate `json:"customtemplates,omitempty"`
EdgeGroup []portainer.EdgeGroup `json:"edgegroups,omitempty"`

File diff suppressed because it is too large Load Diff

View File

@@ -1,904 +0,0 @@
{
"dockerhub": [
{
"Authentication": false,
"Username": ""
}
],
"endpoint_groups": [
{
"AuthorizedTeams": null,
"AuthorizedUsers": null,
"Description": "Unassigned endpoints",
"Id": 1,
"Labels": [],
"Name": "Unassigned",
"TagIds": [],
"Tags": null,
"TeamAccessPolicies": {},
"UserAccessPolicies": {}
}
],
"endpoint_relations": [
{
"EdgeStacks": {},
"EndpointID": 1
}
],
"endpoints": [
{
"AuthorizedTeams": null,
"AuthorizedUsers": null,
"AzureCredentials": {
"ApplicationID": "",
"AuthenticationKey": "",
"TenantID": ""
},
"ComposeSyntaxMaxVersion": "",
"EdgeCheckinInterval": 0,
"EdgeKey": "",
"GroupId": 1,
"Id": 1,
"IsEdgeDevice": false,
"Kubernetes": {
"Configuration": {
"IngressClasses": null,
"RestrictDefaultNamespace": false,
"StorageClasses": null,
"UseLoadBalancer": false,
"UseServerMetrics": false
},
"Snapshots": null
},
"LastCheckInDate": 0,
"Name": "local",
"PublicURL": "",
"QueryDate": 0,
"SecuritySettings": {
"allowBindMountsForRegularUsers": true,
"allowContainerCapabilitiesForRegularUsers": true,
"allowDeviceMappingForRegularUsers": true,
"allowHostNamespaceForRegularUsers": true,
"allowPrivilegedModeForRegularUsers": true,
"allowStackManagementForRegularUsers": true,
"allowSysctlSettingForRegularUsers": false,
"allowVolumeBrowserForRegularUsers": false,
"enableHostManagementFeatures": false
},
"Snapshots": [
{
"DockerSnapshotRaw": {
"Containers": null,
"Images": null,
"Info": {
"Architecture": "",
"BridgeNfIp6tables": false,
"BridgeNfIptables": false,
"CPUSet": false,
"CPUShares": false,
"CgroupDriver": "",
"ContainerdCommit": {
"Expected": "",
"ID": ""
},
"Containers": 0,
"ContainersPaused": 0,
"ContainersRunning": 0,
"ContainersStopped": 0,
"CpuCfsPeriod": false,
"CpuCfsQuota": false,
"Debug": false,
"DefaultRuntime": "",
"DockerRootDir": "",
"Driver": "",
"DriverStatus": null,
"ExperimentalBuild": false,
"GenericResources": null,
"HttpProxy": "",
"HttpsProxy": "",
"ID": "",
"IPv4Forwarding": false,
"Images": 0,
"IndexServerAddress": "",
"InitBinary": "",
"InitCommit": {
"Expected": "",
"ID": ""
},
"Isolation": "",
"KernelMemory": false,
"KernelMemoryTCP": false,
"KernelVersion": "",
"Labels": null,
"LiveRestoreEnabled": false,
"LoggingDriver": "",
"MemTotal": 0,
"MemoryLimit": false,
"NCPU": 0,
"NEventsListener": 0,
"NFd": 0,
"NGoroutines": 0,
"Name": "",
"NoProxy": "",
"OSType": "",
"OSVersion": "",
"OomKillDisable": false,
"OperatingSystem": "",
"PidsLimit": false,
"Plugins": {
"Authorization": null,
"Log": null,
"Network": null,
"Volume": null
},
"RegistryConfig": null,
"RuncCommit": {
"Expected": "",
"ID": ""
},
"Runtimes": null,
"SecurityOptions": null,
"ServerVersion": "",
"SwapLimit": false,
"Swarm": {
"ControlAvailable": false,
"Error": "",
"LocalNodeState": "",
"NodeAddr": "",
"NodeID": "",
"RemoteManagers": null
},
"SystemTime": "",
"Warnings": null
},
"Networks": null,
"Version": {
"ApiVersion": "",
"Arch": "",
"GitCommit": "",
"GoVersion": "",
"Os": "",
"Platform": {
"Name": ""
},
"Version": ""
},
"Volumes": {
"Volumes": null,
"Warnings": null
}
},
"DockerVersion": "20.10.13",
"HealthyContainerCount": 0,
"ImageCount": 9,
"NodeCount": 0,
"RunningContainerCount": 5,
"ServiceCount": 0,
"StackCount": 2,
"StoppedContainerCount": 0,
"Swarm": false,
"Time": 1648610112,
"TotalCPU": 8,
"TotalMemory": 25098706944,
"UnhealthyContainerCount": 0,
"VolumeCount": 10
}
],
"Status": 1,
"TLSConfig": {
"TLS": false,
"TLSSkipVerify": false
},
"TagIds": [],
"Tags": null,
"TeamAccessPolicies": {},
"Type": 1,
"URL": "unix:///var/run/docker.sock",
"UserAccessPolicies": {},
"UserTrusted": false
}
],
"registries": [
{
"Authentication": true,
"AuthorizedTeams": null,
"AuthorizedUsers": null,
"BaseURL": "",
"Ecr": {
"Region": ""
},
"Gitlab": {
"InstanceURL": "",
"ProjectId": 0,
"ProjectPath": ""
},
"Id": 1,
"ManagementConfiguration": null,
"Name": "canister.io",
"Password": "MjWbx8A6YK7cw7",
"Quay": {
"OrganisationName": "",
"UseOrganisation": false
},
"RegistryAccesses": {
"1": {
"Namespaces": [],
"TeamAccessPolicies": {},
"UserAccessPolicies": {}
}
},
"TeamAccessPolicies": {},
"Type": 3,
"URL": "cloud.canister.io:5000",
"UserAccessPolicies": {},
"Username": "prabhatkhera"
}
],
"resource_control": [
{
"AdministratorsOnly": false,
"Id": 2,
"Public": true,
"ResourceId": "762gbwaj8r4gcsdy8ld1u4why",
"SubResourceIds": [],
"System": false,
"TeamAccesses": [],
"Type": 5,
"UserAccesses": []
},
{
"AdministratorsOnly": false,
"Id": 3,
"Public": true,
"ResourceId": "1_alpine",
"SubResourceIds": [],
"System": false,
"TeamAccesses": [],
"Type": 6,
"UserAccesses": []
},
{
"AdministratorsOnly": false,
"Id": 4,
"Public": true,
"ResourceId": "1_redis",
"SubResourceIds": [],
"System": false,
"TeamAccesses": [],
"Type": 6,
"UserAccesses": []
},
{
"AdministratorsOnly": false,
"Id": 5,
"Public": false,
"ResourceId": "1_nginx",
"SubResourceIds": [],
"System": false,
"TeamAccesses": [
{
"AccessLevel": 1,
"TeamId": 1
}
],
"Type": 6,
"UserAccesses": []
}
],
"roles": [
{
"Authorizations": {
"DockerAgentBrowseDelete": true,
"DockerAgentBrowseGet": true,
"DockerAgentBrowseList": true,
"DockerAgentBrowsePut": true,
"DockerAgentBrowseRename": true,
"DockerAgentHostInfo": true,
"DockerAgentList": true,
"DockerAgentPing": true,
"DockerAgentUndefined": true,
"DockerBuildCancel": true,
"DockerBuildPrune": true,
"DockerConfigCreate": true,
"DockerConfigDelete": true,
"DockerConfigInspect": true,
"DockerConfigList": true,
"DockerConfigUpdate": true,
"DockerContainerArchive": true,
"DockerContainerArchiveInfo": true,
"DockerContainerAttach": true,
"DockerContainerAttachWebsocket": true,
"DockerContainerChanges": true,
"DockerContainerCreate": true,
"DockerContainerDelete": true,
"DockerContainerExec": true,
"DockerContainerExport": true,
"DockerContainerInspect": true,
"DockerContainerKill": true,
"DockerContainerList": true,
"DockerContainerLogs": true,
"DockerContainerPause": true,
"DockerContainerPrune": true,
"DockerContainerPutContainerArchive": true,
"DockerContainerRename": true,
"DockerContainerResize": true,
"DockerContainerRestart": true,
"DockerContainerStart": true,
"DockerContainerStats": true,
"DockerContainerStop": true,
"DockerContainerTop": true,
"DockerContainerUnpause": true,
"DockerContainerUpdate": true,
"DockerContainerWait": true,
"DockerDistributionInspect": true,
"DockerEvents": true,
"DockerExecInspect": true,
"DockerExecResize": true,
"DockerExecStart": true,
"DockerImageBuild": true,
"DockerImageCommit": true,
"DockerImageCreate": true,
"DockerImageDelete": true,
"DockerImageGet": true,
"DockerImageGetAll": true,
"DockerImageHistory": true,
"DockerImageInspect": true,
"DockerImageList": true,
"DockerImageLoad": true,
"DockerImagePrune": true,
"DockerImagePush": true,
"DockerImageSearch": true,
"DockerImageTag": true,
"DockerInfo": true,
"DockerNetworkConnect": true,
"DockerNetworkCreate": true,
"DockerNetworkDelete": true,
"DockerNetworkDisconnect": true,
"DockerNetworkInspect": true,
"DockerNetworkList": true,
"DockerNetworkPrune": true,
"DockerNodeDelete": true,
"DockerNodeInspect": true,
"DockerNodeList": true,
"DockerNodeUpdate": true,
"DockerPing": true,
"DockerPluginCreate": true,
"DockerPluginDelete": true,
"DockerPluginDisable": true,
"DockerPluginEnable": true,
"DockerPluginInspect": true,
"DockerPluginList": true,
"DockerPluginPrivileges": true,
"DockerPluginPull": true,
"DockerPluginPush": true,
"DockerPluginSet": true,
"DockerPluginUpgrade": true,
"DockerSecretCreate": true,
"DockerSecretDelete": true,
"DockerSecretInspect": true,
"DockerSecretList": true,
"DockerSecretUpdate": true,
"DockerServiceCreate": true,
"DockerServiceDelete": true,
"DockerServiceInspect": true,
"DockerServiceList": true,
"DockerServiceLogs": true,
"DockerServiceUpdate": true,
"DockerSessionStart": true,
"DockerSwarmInit": true,
"DockerSwarmInspect": true,
"DockerSwarmJoin": true,
"DockerSwarmLeave": true,
"DockerSwarmUnlock": true,
"DockerSwarmUnlockKey": true,
"DockerSwarmUpdate": true,
"DockerSystem": true,
"DockerTaskInspect": true,
"DockerTaskList": true,
"DockerTaskLogs": true,
"DockerUndefined": true,
"DockerVersion": true,
"DockerVolumeCreate": true,
"DockerVolumeDelete": true,
"DockerVolumeInspect": true,
"DockerVolumeList": true,
"DockerVolumePrune": true,
"EndpointResourcesAccess": true,
"IntegrationStoridgeAdmin": true,
"PortainerResourceControlCreate": true,
"PortainerResourceControlUpdate": true,
"PortainerStackCreate": true,
"PortainerStackDelete": true,
"PortainerStackFile": true,
"PortainerStackInspect": true,
"PortainerStackList": true,
"PortainerStackMigrate": true,
"PortainerStackUpdate": true,
"PortainerWebhookCreate": true,
"PortainerWebhookDelete": true,
"PortainerWebhookList": true,
"PortainerWebsocketExec": true
},
"Description": "Full control of all resources in an endpoint",
"Id": 1,
"Name": "Endpoint administrator",
"Priority": 1
},
{
"Authorizations": {
"DockerAgentHostInfo": true,
"DockerAgentList": true,
"DockerAgentPing": true,
"DockerConfigInspect": true,
"DockerConfigList": true,
"DockerContainerArchiveInfo": true,
"DockerContainerChanges": true,
"DockerContainerInspect": true,
"DockerContainerList": true,
"DockerContainerLogs": true,
"DockerContainerStats": true,
"DockerContainerTop": true,
"DockerDistributionInspect": true,
"DockerEvents": true,
"DockerImageGet": true,
"DockerImageGetAll": true,
"DockerImageHistory": true,
"DockerImageInspect": true,
"DockerImageList": true,
"DockerImageSearch": true,
"DockerInfo": true,
"DockerNetworkInspect": true,
"DockerNetworkList": true,
"DockerNodeInspect": true,
"DockerNodeList": true,
"DockerPing": true,
"DockerPluginList": true,
"DockerSecretInspect": true,
"DockerSecretList": true,
"DockerServiceInspect": true,
"DockerServiceList": true,
"DockerServiceLogs": true,
"DockerSwarmInspect": true,
"DockerSystem": true,
"DockerTaskInspect": true,
"DockerTaskList": true,
"DockerTaskLogs": true,
"DockerVersion": true,
"DockerVolumeInspect": true,
"DockerVolumeList": true,
"EndpointResourcesAccess": true,
"PortainerStackFile": true,
"PortainerStackInspect": true,
"PortainerStackList": true,
"PortainerWebhookList": true
},
"Description": "Read-only access of all resources in an endpoint",
"Id": 2,
"Name": "Helpdesk",
"Priority": 2
},
{
"Authorizations": {
"DockerAgentHostInfo": true,
"DockerAgentList": true,
"DockerAgentPing": true,
"DockerAgentUndefined": true,
"DockerBuildCancel": true,
"DockerBuildPrune": true,
"DockerConfigCreate": true,
"DockerConfigDelete": true,
"DockerConfigInspect": true,
"DockerConfigList": true,
"DockerConfigUpdate": true,
"DockerContainerArchive": true,
"DockerContainerArchiveInfo": true,
"DockerContainerAttach": true,
"DockerContainerAttachWebsocket": true,
"DockerContainerChanges": true,
"DockerContainerCreate": true,
"DockerContainerDelete": true,
"DockerContainerExec": true,
"DockerContainerExport": true,
"DockerContainerInspect": true,
"DockerContainerKill": true,
"DockerContainerList": true,
"DockerContainerLogs": true,
"DockerContainerPause": true,
"DockerContainerPutContainerArchive": true,
"DockerContainerRename": true,
"DockerContainerResize": true,
"DockerContainerRestart": true,
"DockerContainerStart": true,
"DockerContainerStats": true,
"DockerContainerStop": true,
"DockerContainerTop": true,
"DockerContainerUnpause": true,
"DockerContainerUpdate": true,
"DockerContainerWait": true,
"DockerDistributionInspect": true,
"DockerEvents": true,
"DockerExecInspect": true,
"DockerExecResize": true,
"DockerExecStart": true,
"DockerImageBuild": true,
"DockerImageCommit": true,
"DockerImageCreate": true,
"DockerImageDelete": true,
"DockerImageGet": true,
"DockerImageGetAll": true,
"DockerImageHistory": true,
"DockerImageInspect": true,
"DockerImageList": true,
"DockerImageLoad": true,
"DockerImagePush": true,
"DockerImageSearch": true,
"DockerImageTag": true,
"DockerInfo": true,
"DockerNetworkConnect": true,
"DockerNetworkCreate": true,
"DockerNetworkDelete": true,
"DockerNetworkDisconnect": true,
"DockerNetworkInspect": true,
"DockerNetworkList": true,
"DockerNodeDelete": true,
"DockerNodeInspect": true,
"DockerNodeList": true,
"DockerNodeUpdate": true,
"DockerPing": true,
"DockerPluginCreate": true,
"DockerPluginDelete": true,
"DockerPluginDisable": true,
"DockerPluginEnable": true,
"DockerPluginInspect": true,
"DockerPluginList": true,
"DockerPluginPrivileges": true,
"DockerPluginPull": true,
"DockerPluginPush": true,
"DockerPluginSet": true,
"DockerPluginUpgrade": true,
"DockerSecretCreate": true,
"DockerSecretDelete": true,
"DockerSecretInspect": true,
"DockerSecretList": true,
"DockerSecretUpdate": true,
"DockerServiceCreate": true,
"DockerServiceDelete": true,
"DockerServiceInspect": true,
"DockerServiceList": true,
"DockerServiceLogs": true,
"DockerServiceUpdate": true,
"DockerSessionStart": true,
"DockerSwarmInit": true,
"DockerSwarmInspect": true,
"DockerSwarmJoin": true,
"DockerSwarmLeave": true,
"DockerSwarmUnlock": true,
"DockerSwarmUnlockKey": true,
"DockerSwarmUpdate": true,
"DockerSystem": true,
"DockerTaskInspect": true,
"DockerTaskList": true,
"DockerTaskLogs": true,
"DockerUndefined": true,
"DockerVersion": true,
"DockerVolumeCreate": true,
"DockerVolumeDelete": true,
"DockerVolumeInspect": true,
"DockerVolumeList": true,
"PortainerResourceControlUpdate": true,
"PortainerStackCreate": true,
"PortainerStackDelete": true,
"PortainerStackFile": true,
"PortainerStackInspect": true,
"PortainerStackList": true,
"PortainerStackMigrate": true,
"PortainerStackUpdate": true,
"PortainerWebhookCreate": true,
"PortainerWebhookList": true,
"PortainerWebsocketExec": true
},
"Description": "Full control of assigned resources in an endpoint",
"Id": 3,
"Name": "Standard user",
"Priority": 3
},
{
"Authorizations": {
"DockerAgentHostInfo": true,
"DockerAgentList": true,
"DockerAgentPing": true,
"DockerConfigInspect": true,
"DockerConfigList": true,
"DockerContainerArchiveInfo": true,
"DockerContainerChanges": true,
"DockerContainerInspect": true,
"DockerContainerList": true,
"DockerContainerLogs": true,
"DockerContainerStats": true,
"DockerContainerTop": true,
"DockerDistributionInspect": true,
"DockerEvents": true,
"DockerImageGet": true,
"DockerImageGetAll": true,
"DockerImageHistory": true,
"DockerImageInspect": true,
"DockerImageList": true,
"DockerImageSearch": true,
"DockerInfo": true,
"DockerNetworkInspect": true,
"DockerNetworkList": true,
"DockerNodeInspect": true,
"DockerNodeList": true,
"DockerPing": true,
"DockerPluginList": true,
"DockerSecretInspect": true,
"DockerSecretList": true,
"DockerServiceInspect": true,
"DockerServiceList": true,
"DockerServiceLogs": true,
"DockerSwarmInspect": true,
"DockerSystem": true,
"DockerTaskInspect": true,
"DockerTaskList": true,
"DockerTaskLogs": true,
"DockerVersion": true,
"DockerVolumeInspect": true,
"DockerVolumeList": true,
"PortainerStackFile": true,
"PortainerStackInspect": true,
"PortainerStackList": true,
"PortainerWebhookList": true
},
"Description": "Read-only access of assigned resources in an endpoint",
"Id": 4,
"Name": "Read-only user",
"Priority": 4
}
],
"schedules": [
{
"Created": 1648608136,
"CronExpression": "@every 5m",
"EdgeSchedule": null,
"EndpointSyncJob": null,
"Id": 1,
"JobType": 2,
"Name": "system_snapshot",
"Recurring": true,
"ScriptExecutionJob": null,
"SnapshotJob": {}
}
],
"settings": {
"AgentSecret": "",
"AllowBindMountsForRegularUsers": true,
"AllowContainerCapabilitiesForRegularUsers": true,
"AllowDeviceMappingForRegularUsers": true,
"AllowHostNamespaceForRegularUsers": true,
"AllowPrivilegedModeForRegularUsers": true,
"AllowStackManagementForRegularUsers": true,
"AllowVolumeBrowserForRegularUsers": false,
"AuthenticationMethod": 1,
"BlackListedLabels": [],
"DisplayDonationHeader": false,
"DisplayExternalContributors": false,
"EdgeAgentCheckinInterval": 5,
"EdgePortainerUrl": "",
"EnableEdgeComputeFeatures": false,
"EnableHostManagementFeatures": false,
"EnableTelemetry": true,
"EnforceEdgeID": false,
"FeatureFlagSettings": null,
"HelmRepositoryURL": "https://charts.bitnami.com/bitnami",
"InternalAuthSettings": {
"RequiredPasswordLength": 12
},
"KubeconfigExpiry": "0",
"KubectlShellImage": "portainer/kubectl-shell",
"LDAPSettings": {
"AnonymousMode": true,
"AutoCreateUsers": true,
"GroupSearchSettings": [
{
"GroupAttribute": "",
"GroupBaseDN": "",
"GroupFilter": ""
}
],
"ReaderDN": "",
"SearchSettings": [
{
"BaseDN": "",
"Filter": "",
"UserNameAttribute": ""
}
],
"StartTLS": false,
"TLSConfig": {
"TLS": false,
"TLSSkipVerify": false
},
"URL": ""
},
"LogoURL": "",
"OAuthSettings": {
"AccessTokenURI": "",
"AuthorizationURI": "",
"ClientID": "",
"DefaultTeamID": 0,
"KubeSecretKey": null,
"LogoutURI": "",
"OAuthAutoCreateUsers": false,
"RedirectURI": "",
"ResourceURI": "",
"SSO": false,
"Scopes": "",
"UserIdentifier": ""
},
"SnapshotInterval": "5m",
"TemplatesURL": "https://raw.githubusercontent.com/portainer/templates/master/templates-2.0.json",
"TrustOnFirstConnect": false,
"UserSessionTimeout": "8h",
"fdoConfiguration": {
"enabled": false,
"ownerPassword": "",
"ownerURL": "",
"ownerUsername": ""
},
"openAMTConfiguration": {
"certFileContent": "",
"certFileName": "",
"certFilePassword": "",
"domainName": "",
"enabled": false,
"mpsPassword": "",
"mpsServer": "",
"mpsToken": "",
"mpsUser": ""
}
},
"ssl": {
"certPath": "",
"httpEnabled": true,
"keyPath": "",
"selfSigned": false
},
"stacks": [
{
"AdditionalFiles": null,
"AutoUpdate": null,
"CreatedBy": "",
"CreationDate": 0,
"EndpointId": 1,
"EntryPoint": "docker/alpine37-compose.yml",
"Env": [],
"FromAppTemplate": false,
"GitConfig": null,
"Id": 2,
"IsComposeFormat": false,
"Name": "alpine",
"Namespace": "",
"ProjectPath": "/home/prabhat/portainer/data/ce1.25/compose/2",
"ResourceControl": null,
"Status": 1,
"SwarmId": "s3fd604zdba7z13tbq2x6lyue",
"Type": 1,
"UpdateDate": 0,
"UpdatedBy": ""
},
{
"AdditionalFiles": null,
"AutoUpdate": null,
"CreatedBy": "",
"CreationDate": 0,
"EndpointId": 1,
"EntryPoint": "docker-compose.yml",
"Env": [],
"FromAppTemplate": false,
"GitConfig": null,
"Id": 5,
"IsComposeFormat": false,
"Name": "redis",
"Namespace": "",
"ProjectPath": "/home/prabhat/portainer/data/ce1.25/compose/5",
"ResourceControl": null,
"Status": 1,
"SwarmId": "",
"Type": 2,
"UpdateDate": 0,
"UpdatedBy": ""
},
{
"AdditionalFiles": null,
"AutoUpdate": null,
"CreatedBy": "",
"CreationDate": 0,
"EndpointId": 1,
"EntryPoint": "docker-compose.yml",
"Env": [],
"FromAppTemplate": false,
"GitConfig": null,
"Id": 6,
"IsComposeFormat": false,
"Name": "nginx",
"Namespace": "",
"ProjectPath": "/home/prabhat/portainer/data/ce1.25/compose/6",
"ResourceControl": null,
"Status": 1,
"SwarmId": "",
"Type": 2,
"UpdateDate": 0,
"UpdatedBy": ""
}
],
"teams": [
{
"Id": 1,
"Name": "hello"
}
],
"tunnel_server": {
"PrivateKeySeed": "IvX6ZPRuWtLS5zyg"
},
"users": [
{
"EndpointAuthorizations": null,
"Id": 1,
"Password": "$2a$10$siRDprr/5uUFAU8iom3Sr./WXQkN2dhSNjAC471pkJaALkghS762a",
"PortainerAuthorizations": {
"PortainerDockerHubInspect": true,
"PortainerEndpointGroupList": true,
"PortainerEndpointInspect": true,
"PortainerEndpointList": true,
"PortainerMOTD": true,
"PortainerRegistryInspect": true,
"PortainerRegistryList": true,
"PortainerTeamList": true,
"PortainerTemplateInspect": true,
"PortainerTemplateList": true,
"PortainerUserCreateToken": true,
"PortainerUserInspect": true,
"PortainerUserList": true,
"PortainerUserListToken": true,
"PortainerUserMemberships": true,
"PortainerUserRevokeToken": true
},
"Role": 1,
"TokenIssueAt": 0,
"UserTheme": "",
"Username": "admin"
},
{
"EndpointAuthorizations": null,
"Id": 2,
"Password": "$2a$10$WpCAW8mSt6FRRp1GkynbFOGSZnHR6E5j9cETZ8HiMlw06hVlDW/Li",
"PortainerAuthorizations": {
"PortainerDockerHubInspect": true,
"PortainerEndpointGroupList": true,
"PortainerEndpointInspect": true,
"PortainerEndpointList": true,
"PortainerMOTD": true,
"PortainerRegistryInspect": true,
"PortainerRegistryList": true,
"PortainerTeamList": true,
"PortainerTemplateInspect": true,
"PortainerTemplateList": true,
"PortainerUserCreateToken": true,
"PortainerUserInspect": true,
"PortainerUserList": true,
"PortainerUserListToken": true,
"PortainerUserMemberships": true,
"PortainerUserRevokeToken": true
},
"Role": 1,
"TokenIssueAt": 0,
"UserTheme": "",
"Username": "prabhat"
}
],
"version": {
"DB_UPDATING": "false",
"DB_VERSION": "60",
"INSTANCE_ID": "null"
}
}

View File

@@ -18,8 +18,8 @@ func (store *Store) GetConnection() portainer.Connection {
return store.connection
}
func MustNewTestStore(init, secure bool) (bool, *Store, func()) {
newStore, store, teardown, err := NewTestStore(init, secure)
func MustNewTestStore(init bool) (bool, *Store, func()) {
newStore, store, teardown, err := NewTestStore(init)
if err != nil {
if !errors.Is(err, errTempDir) {
teardown()
@@ -30,7 +30,7 @@ func MustNewTestStore(init, secure bool) (bool, *Store, func()) {
return newStore, store, teardown
}
func NewTestStore(init, secure bool) (bool, *Store, func(), error) {
func NewTestStore(init bool) (bool, *Store, func(), error) {
// Creates unique temp directory in a concurrency friendly manner.
storePath, err := ioutil.TempDir("", "test-store")
if err != nil {
@@ -42,12 +42,7 @@ func NewTestStore(init, secure bool) (bool, *Store, func(), error) {
return false, nil, nil, err
}
secretKey := []byte("apassphrasewhichneedstobe32bytes")
if !secure {
secretKey = nil
}
connection, err := database.NewDatabase("boltdb", storePath, secretKey)
connection, err := database.NewDatabase("boltdb", storePath, []byte("apassphrasewhichneedstobe32bytes"))
if err != nil {
panic(err)
}

View File

@@ -1,15 +0,0 @@
package validate
import (
"github.com/go-playground/validator/v10"
portainer "github.com/portainer/portainer/api"
)
var validate *validator.Validate
func ValidateLDAPSettings(ldp *portainer.LDAPSettings) error {
validate = validator.New()
registerValidationMethods(validate)
return validate.Struct(ldp)
}

View File

@@ -1,61 +0,0 @@
package validate
import (
"testing"
portainer "github.com/portainer/portainer/api"
)
func TestValidateLDAPSettings(t *testing.T) {
tests := []struct {
name string
ldap portainer.LDAPSettings
wantErr bool
}{
{
name: "Empty LDAP Settings",
ldap: portainer.LDAPSettings{},
wantErr: true,
},
{
name: "With URL",
ldap: portainer.LDAPSettings{
AnonymousMode: true,
URL: "192.168.0.1:323",
},
wantErr: false,
},
{
name: "Validate URL and URLs",
ldap: portainer.LDAPSettings{
AnonymousMode: true,
URL: "192.168.0.1:323",
},
wantErr: false,
},
{
name: "validate client ldap",
ldap: portainer.LDAPSettings{
AnonymousMode: false,
ReaderDN: "CN=LDAP API Service Account",
Password: "Qu**dfUUU**",
URL: "aukdc15.pgc.co:389",
TLSConfig: portainer.TLSConfiguration{
TLS: false,
TLSSkipVerify: false,
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateLDAPSettings(&tt.ldap)
if (err == nil) == tt.wantErr {
t.Errorf("No error expected but got %s", err)
}
})
}
}

View File

@@ -1,17 +0,0 @@
package validate
import (
"github.com/go-playground/validator/v10"
)
func registerValidationMethods(v *validator.Validate) {
v.RegisterValidation("validate_bool", ValidateBool)
}
/**
* Validation methods below are being used for custom validation
*/
func ValidateBool(fl validator.FieldLevel) bool {
_, ok := fl.Field().Interface().(bool)
return ok
}

View File

@@ -1,118 +0,0 @@
package demo
import (
"log"
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
)
type EnvironmentDetails struct {
Enabled bool `json:"enabled"`
Users []portainer.UserID `json:"users"`
Environments []portainer.EndpointID `json:"environments"`
}
type Service struct {
details EnvironmentDetails
}
func NewService() *Service {
return &Service{}
}
func (service *Service) Details() EnvironmentDetails {
return service.details
}
func (service *Service) Init(store dataservices.DataStore, cryptoService portainer.CryptoService) error {
log.Print("[INFO] [main] Starting demo environment")
isClean, err := isCleanStore(store)
if err != nil {
return errors.WithMessage(err, "failed checking if store is clean")
}
if !isClean {
return errors.New(" Demo environment can only be initialized on a clean database")
}
id, err := initDemoUser(store, cryptoService)
if err != nil {
return errors.WithMessage(err, "failed creating demo user")
}
endpointIds, err := initDemoEndpoints(store)
if err != nil {
return errors.WithMessage(err, "failed creating demo endpoint")
}
err = initDemoSettings(store)
if err != nil {
return errors.WithMessage(err, "failed updating demo settings")
}
service.details = EnvironmentDetails{
Enabled: true,
Users: []portainer.UserID{id},
// endpoints 2,3 are created after deployment of portainer
Environments: endpointIds,
}
return nil
}
func isCleanStore(store dataservices.DataStore) (bool, error) {
endpoints, err := store.Endpoint().Endpoints()
if err != nil {
return false, err
}
if len(endpoints) > 0 {
return false, nil
}
users, err := store.User().Users()
if err != nil {
return false, err
}
if len(users) > 0 {
return false, nil
}
return true, nil
}
func (service *Service) IsDemo() bool {
return service.details.Enabled
}
func (service *Service) IsDemoEnvironment(environmentID portainer.EndpointID) bool {
if !service.IsDemo() {
return false
}
for _, demoEndpointID := range service.details.Environments {
if environmentID == demoEndpointID {
return true
}
}
return false
}
func (service *Service) IsDemoUser(userID portainer.UserID) bool {
if !service.IsDemo() {
return false
}
for _, demoUserID := range service.details.Users {
if userID == demoUserID {
return true
}
}
return false
}

View File

@@ -1,79 +0,0 @@
package demo
import (
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
)
func initDemoUser(
store dataservices.DataStore,
cryptoService portainer.CryptoService,
) (portainer.UserID, error) {
password, err := cryptoService.Hash("tryportainer")
if err != nil {
return 0, errors.WithMessage(err, "failed creating password hash")
}
admin := &portainer.User{
Username: "admin",
Password: password,
Role: portainer.AdministratorRole,
}
err = store.User().Create(admin)
return admin.ID, errors.WithMessage(err, "failed creating user")
}
func initDemoEndpoints(store dataservices.DataStore) ([]portainer.EndpointID, error) {
localEndpointId, err := initDemoLocalEndpoint(store)
if err != nil {
return nil, err
}
// second and third endpoints are going to be created with docker-compose as a part of the demo environment set up.
// ref: https://github.com/portainer/portainer-demo/blob/master/docker-compose.yml
return []portainer.EndpointID{localEndpointId, localEndpointId + 1, localEndpointId + 2}, nil
}
func initDemoLocalEndpoint(store dataservices.DataStore) (portainer.EndpointID, error) {
id := portainer.EndpointID(store.Endpoint().GetNextIdentifier())
localEndpoint := &portainer.Endpoint{
ID: id,
Name: "local",
URL: "unix:///var/run/docker.sock",
PublicURL: "demo.portainer.io",
Type: portainer.DockerEnvironment,
GroupID: portainer.EndpointGroupID(1),
TLSConfig: portainer.TLSConfiguration{
TLS: false,
},
AuthorizedUsers: []portainer.UserID{},
AuthorizedTeams: []portainer.TeamID{},
UserAccessPolicies: portainer.UserAccessPolicies{},
TeamAccessPolicies: portainer.TeamAccessPolicies{},
TagIDs: []portainer.TagID{},
Status: portainer.EndpointStatusUp,
Snapshots: []portainer.DockerSnapshot{},
Kubernetes: portainer.KubernetesDefault(),
}
err := store.Endpoint().Create(localEndpoint)
return id, errors.WithMessage(err, "failed creating local endpoint")
}
func initDemoSettings(
store dataservices.DataStore,
) error {
settings, err := store.Settings().Settings()
if err != nil {
return errors.WithMessage(err, "failed fetching settings")
}
settings.EnableTelemetry = false
settings.LogoURL = ""
err = store.Settings().UpdateSettings(settings)
return errors.WithMessage(err, "failed updating settings")
}

View File

@@ -11,7 +11,6 @@ import (
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/internal/testhelpers"
)
const composeFile = `version: "3.9"
@@ -42,8 +41,6 @@ func setup(t *testing.T) (*portainer.Stack, *portainer.Endpoint) {
func Test_UpAndDown(t *testing.T) {
testhelpers.IntegrationTest(t)
stack, endpoint := setup(t)
w, err := NewComposeStackManager("", "", nil)

View File

@@ -3,7 +3,6 @@ package exec
import (
"bytes"
"fmt"
"os"
"os/exec"
"path"
"runtime"
@@ -124,8 +123,6 @@ func (deployer *KubernetesDeployer) command(operation string, userID portainer.U
var stderr bytes.Buffer
cmd := exec.Command(command, args...)
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, "POD_NAMESPACE=default")
cmd.Stderr = &stderr
output, err := cmd.Output()

View File

@@ -56,12 +56,10 @@ const (
TempPath = "tmp"
// SSLCertPath represents the default ssl certificates path
SSLCertPath = "certs"
// SSLCertFilename represents the ssl certificate file name
SSLCertFilename = "cert.pem"
// SSLKeyFilename represents the ssl key file name
SSLKeyFilename = "key.pem"
// SSLCACertFilename represents the CA ssl certificate file name for mTLS
SSLCACertFilename = "ca-cert.pem"
// DefaultSSLCertFilename represents the default ssl certificate file name
DefaultSSLCertFilename = "cert.pem"
// DefaultSSLKeyFilename represents the default ssl key file name
DefaultSSLKeyFilename = "key.pem"
)
// ErrUndefinedTLSFileType represents an error returned on undefined TLS file type
@@ -163,7 +161,7 @@ func (service *Service) Copy(fromFilePath string, toFilePath string, deleteIfExi
}
if !exists {
return errors.New(fmt.Sprintf("File (%s) doesn't exist", fromFilePath))
return errors.New("File doesn't exist")
}
finput, err := os.Open(fromFilePath)
@@ -582,8 +580,8 @@ func (service *Service) wrapFileStore(filepath string) string {
}
func defaultCertPathUnderFileStore() (string, string) {
certPath := JoinPaths(SSLCertPath, SSLCertFilename)
keyPath := JoinPaths(SSLCertPath, SSLKeyFilename)
certPath := JoinPaths(SSLCertPath, DefaultSSLCertFilename)
keyPath := JoinPaths(SSLCertPath, DefaultSSLKeyFilename)
return certPath, keyPath
}
@@ -629,18 +627,6 @@ func (service *Service) CopySSLCertPair(certPath, keyPath string) (string, strin
return defCertPath, defKeyPath, nil
}
// CopySSLCACert copies the specified caCert pem file
func (service *Service) CopySSLCACert(caCertPath string) (string, error) {
toFilePath := service.wrapFileStore(JoinPaths(SSLCertPath, SSLCACertFilename))
err := service.Copy(caCertPath, toFilePath, true)
if err != nil {
return "", err
}
return toFilePath, nil
}
// FileExists checks for the existence of the specified file.
func FileExists(filePath string) (bool, error) {
if _, err := os.Stat(filePath); err != nil {

View File

@@ -7,7 +7,6 @@ import (
"github.com/pkg/errors"
)
// WriteToFile creates a file in the filesystem storage
func WriteToFile(dst string, content []byte) error {
if err := os.MkdirAll(filepath.Dir(dst), 0744); err != nil {
return errors.Wrapf(err, "failed to create filestructure for the path %q", dst)

View File

@@ -108,12 +108,12 @@ func (a *azureDownloader) latestCommitID(ctx context.Context, options fetchOptio
return "", errors.WithMessage(err, "failed to parse url")
}
rootItemUrl, err := a.buildRootItemUrl(config, options.referenceName)
refsUrl, err := a.buildRefsUrl(config, options.referenceName)
if err != nil {
return "", errors.WithMessage(err, "failed to build azure root item url")
return "", errors.WithMessage(err, "failed to build azure refs url")
}
req, err := http.NewRequestWithContext(ctx, "GET", rootItemUrl, nil)
req, err := http.NewRequestWithContext(ctx, "GET", refsUrl, nil)
if options.username != "" || options.password != "" {
req.SetBasicAuth(options.username, options.password)
} else if config.username != "" || config.password != "" {
@@ -131,24 +131,26 @@ func (a *azureDownloader) latestCommitID(ctx context.Context, options fetchOptio
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("failed to get repository root item with a status \"%v\"", resp.Status)
return "", fmt.Errorf("failed to get repository refs with a status \"%v\"", resp.Status)
}
var items struct {
var refs struct {
Value []struct {
CommitId string `json:"commitId"`
Name string `json:"name"`
ObjectId string `json:"objectId"`
}
}
if err := json.NewDecoder(resp.Body).Decode(&refs); err != nil {
return "", errors.Wrap(err, "could not parse Azure Refs response")
}
for _, ref := range refs.Value {
if strings.EqualFold(ref.Name, options.referenceName) {
return ref.ObjectId, nil
}
}
if err := json.NewDecoder(resp.Body).Decode(&items); err != nil {
return "", errors.Wrap(err, "could not parse Azure items response")
}
if len(items.Value) == 0 || items.Value[0].CommitId == "" {
return "", errors.Errorf("failed to get latest commitID in the repository")
}
return items.Value[0].CommitId, nil
return "", errors.Errorf("could not find ref %q in the repository", options.referenceName)
}
func parseUrl(rawUrl string) (*azureOptions, error) {
@@ -234,10 +236,8 @@ func (a *azureDownloader) buildDownloadUrl(config *azureOptions, referenceName s
// scopePath=/&download=true&versionDescriptor.version=main&$format=zip&recursionLevel=full&api-version=6.0
q.Set("scopePath", "/")
q.Set("download", "true")
if referenceName != "" {
q.Set("versionDescriptor.versionType", getVersionType(referenceName))
q.Set("versionDescriptor.version", formatReferenceName(referenceName))
}
q.Set("versionDescriptor.versionType", getVersionType(referenceName))
q.Set("versionDescriptor.version", formatReferenceName(referenceName))
q.Set("$format", "zip")
q.Set("recursionLevel", "full")
q.Set("api-version", "6.0")
@@ -246,8 +246,8 @@ func (a *azureDownloader) buildDownloadUrl(config *azureOptions, referenceName s
return u.String(), nil
}
func (a *azureDownloader) buildRootItemUrl(config *azureOptions, referenceName string) (string, error) {
rawUrl := fmt.Sprintf("%s/%s/%s/_apis/git/repositories/%s/items",
func (a *azureDownloader) buildRefsUrl(config *azureOptions, referenceName string) (string, error) {
rawUrl := fmt.Sprintf("%s/%s/%s/_apis/git/repositories/%s/refs",
a.baseUrl,
url.PathEscape(config.organisation),
url.PathEscape(config.project),
@@ -255,15 +255,12 @@ func (a *azureDownloader) buildRootItemUrl(config *azureOptions, referenceName s
u, err := url.Parse(rawUrl)
if err != nil {
return "", errors.Wrapf(err, "failed to parse root item url path %s", rawUrl)
return "", errors.Wrapf(err, "failed to parse refs url path %s", rawUrl)
}
// filterContains=main&api-version=6.0
q := u.Query()
q.Set("scopePath", "/")
if referenceName != "" {
q.Set("versionDescriptor.versionType", getVersionType(referenceName))
q.Set("versionDescriptor.version", formatReferenceName(referenceName))
}
q.Set("filterContains", formatReferenceName(referenceName))
q.Set("api-version", "6.0")
u.RawQuery = q.Encode()

View File

@@ -28,15 +28,15 @@ func Test_buildDownloadUrl(t *testing.T) {
}
}
func Test_buildRootItemUrl(t *testing.T) {
func Test_buildRefsUrl(t *testing.T) {
a := NewAzureDownloader(nil)
u, err := a.buildRootItemUrl(&azureOptions{
u, err := a.buildRefsUrl(&azureOptions{
organisation: "organisation",
project: "project",
repository: "repository",
}, "refs/heads/main")
expectedUrl, _ := url.Parse("https://dev.azure.com/organisation/project/_apis/git/repositories/repository/items?scopePath=/&api-version=6.0&versionDescriptor.version=main&versionDescriptor.versionType=branch")
expectedUrl, _ := url.Parse("https://dev.azure.com/organisation/project/_apis/git/repositories/repository/refs?filterContains=main&api-version=6.0")
actualUrl, _ := url.Parse(u)
assert.NoError(t, err)
assert.Equal(t, expectedUrl.Host, actualUrl.Host)
@@ -270,17 +270,63 @@ func Test_azureDownloader_downloadZipFromAzureDevOps(t *testing.T) {
func Test_azureDownloader_latestCommitID(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
response := `{
"count": 1,
"value": [
{
"objectId": "1a5630f017127db7de24d8771da0f536ff98fc9b",
"gitObjectType": "tree",
"commitId": "27104ad7549d9e66685e115a497533f18024be9c",
"path": "/",
"isFolder": true,
"url": "https://dev.azure.com/simonmeng0474/4b546a97-c481-4506-bdd5-976e9592f91a/_apis/git/repositories/a22247ad-053f-43bc-88a7-62ff4846bb97/items?path=%2F&versionType=Branch&versionOptions=None"
}
]
"value": [
{
"name": "refs/heads/feature/calcApp",
"objectId": "ffe9cba521f00d7f60e322845072238635edb451",
"creator": {
"displayName": "Normal Paulk",
"url": "https://vssps.dev.azure.com/fabrikam/_apis/Identities/ac5aaba6-a66a-4e1d-b508-b060ec624fa9",
"_links": {
"avatar": {
"href": "https://dev.azure.com/fabrikam/_apis/GraphProfile/MemberAvatars/aad.YmFjMGYyZDctNDA3ZC03OGRhLTlhMjUtNmJhZjUwMWFjY2U5"
}
},
"id": "ac5aaba6-a66a-4e1d-b508-b060ec624fa9",
"uniqueName": "dev@mailserver.com",
"imageUrl": "https://dev.azure.com/fabrikam/_api/_common/identityImage?id=ac5aaba6-a66a-4e1d-b508-b060ec624fa9",
"descriptor": "aad.YmFjMGYyZDctNDA3ZC03OGRhLTlhMjUtNmJhZjUwMWFjY2U5"
},
"url": "https://dev.azure.com/fabrikam/7484f783-66a3-4f27-b7cd-6b08b0b077ed/_apis/git/repositories/d3d1760b-311c-4175-a726-20dfc6a7f885/refs?filter=heads%2Ffeature%2FcalcApp"
},
{
"name": "refs/heads/feature/replacer",
"objectId": "917131a709996c5cfe188c3b57e9a6ad90e8b85c",
"creator": {
"displayName": "Normal Paulk",
"url": "https://vssps.dev.azure.com/fabrikam/_apis/Identities/ac5aaba6-a66a-4e1d-b508-b060ec624fa9",
"_links": {
"avatar": {
"href": "https://dev.azure.com/fabrikam/_apis/GraphProfile/MemberAvatars/aad.YmFjMGYyZDctNDA3ZC03OGRhLTlhMjUtNmJhZjUwMWFjY2U5"
}
},
"id": "ac5aaba6-a66a-4e1d-b508-b060ec624fa9",
"uniqueName": "dev@mailserver.com",
"imageUrl": "https://dev.azure.com/fabrikam/_api/_common/identityImage?id=ac5aaba6-a66a-4e1d-b508-b060ec624fa9",
"descriptor": "aad.YmFjMGYyZDctNDA3ZC03OGRhLTlhMjUtNmJhZjUwMWFjY2U5"
},
"url": "https://dev.azure.com/fabrikam/7484f783-66a3-4f27-b7cd-6b08b0b077ed/_apis/git/repositories/d3d1760b-311c-4175-a726-20dfc6a7f885/refs?filter=heads%2Ffeature%2Freplacer"
},
{
"name": "refs/heads/master",
"objectId": "ffe9cba521f00d7f60e322845072238635edb451",
"creator": {
"displayName": "Normal Paulk",
"url": "https://vssps.dev.azure.com/fabrikam/_apis/Identities/ac5aaba6-a66a-4e1d-b508-b060ec624fa9",
"_links": {
"avatar": {
"href": "https://dev.azure.com/fabrikam/_apis/GraphProfile/MemberAvatars/aad.YmFjMGYyZDctNDA3ZC03OGRhLTlhMjUtNmJhZjUwMWFjY2U5"
}
},
"id": "ac5aaba6-a66a-4e1d-b508-b060ec624fa9",
"uniqueName": "dev@mailserver.com",
"imageUrl": "https://dev.azure.com/fabrikam/_api/_common/identityImage?id=ac5aaba6-a66a-4e1d-b508-b060ec624fa9",
"descriptor": "aad.YmFjMGYyZDctNDA3ZC03OGRhLTlhMjUtNmJhZjUwMWFjY2U5"
},
"url": "https://dev.azure.com/fabrikam/7484f783-66a3-4f27-b7cd-6b08b0b077ed/_apis/git/repositories/d3d1760b-311c-4175-a726-20dfc6a7f885/refs?filter=heads%2Fmaster"
}
],
"count": 3
}`
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(response))
@@ -301,11 +347,19 @@ func Test_azureDownloader_latestCommitID(t *testing.T) {
{
name: "should be able to parse response",
args: fetchOptions{
referenceName: "",
referenceName: "refs/heads/master",
repositoryUrl: "https://dev.azure.com/Organisation/Project/_git/Repository"},
want: "27104ad7549d9e66685e115a497533f18024be9c",
want: "ffe9cba521f00d7f60e322845072238635edb451",
wantErr: false,
},
{
name: "should be able to parse response",
args: fetchOptions{
referenceName: "refs/heads/unknown",
repositoryUrl: "https://dev.azure.com/Organisation/Project/_git/Repository"},
want: "",
wantErr: true,
},
}
for _, tt := range tests {

View File

@@ -82,17 +82,8 @@ func (c gitClient) latestCommitID(ctx context.Context, opt fetchOptions) (string
return "", errors.Wrap(err, "failed to list repository refs")
}
referenceName := opt.referenceName
if referenceName == "" {
for _, ref := range refs {
if strings.EqualFold(ref.Name().String(), "HEAD") {
referenceName = ref.Target().String()
}
}
}
for _, ref := range refs {
if strings.EqualFold(ref.Name().String(), referenceName) {
if strings.EqualFold(ref.Name().String(), opt.referenceName) {
return ref.Hash().String(), nil
}
}

View File

@@ -3,36 +3,34 @@ module github.com/portainer/portainer/api
go 1.17
require (
github.com/Microsoft/go-winio v0.5.1
github.com/Microsoft/go-winio v0.4.17
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535
github.com/aws/aws-sdk-go-v2 v1.11.1
github.com/aws/aws-sdk-go-v2/credentials v1.6.2
github.com/aws/aws-sdk-go-v2/service/ecr v1.10.1
github.com/boltdb/bolt v1.3.1
github.com/coreos/go-semver v0.3.0
github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/docker/cli v20.10.9+incompatible
github.com/docker/docker v20.10.9+incompatible
github.com/fvbommel/sortorder v1.0.2
github.com/fxamacker/cbor/v2 v2.3.0
github.com/g07cha/defender v0.0.0-20180505193036-5665c627c814
github.com/go-git/go-git/v5 v5.3.0
github.com/go-ldap/ldap/v3 v3.1.8
github.com/go-playground/validator/v10 v10.10.1
github.com/gofrs/uuid v4.0.0+incompatible
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/google/go-cmp v0.5.6
github.com/gorilla/handlers v1.5.1
github.com/gorilla/mux v1.7.3
github.com/gorilla/securecookie v1.1.1
github.com/gorilla/websocket v1.5.0
github.com/gorilla/websocket v1.4.2
github.com/hashicorp/golang-lru v0.5.4
github.com/joho/godotenv v1.3.0
github.com/jpillora/chisel v0.0.0-20190724232113-f3a8df20e389
github.com/json-iterator/go v1.1.12
github.com/json-iterator/go v1.1.11
github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6
github.com/pkg/errors v0.9.1
github.com/portainer/docker-compose-wrapper v0.0.0-20220531190153-c597b853e410
github.com/portainer/docker-compose-wrapper v0.0.0-20220225003350-cec58db3549e
github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a
github.com/portainer/libhelm v0.0.0-20210929000907-825e93d62108
github.com/portainer/libhttp v0.0.0-20211208103139-07a5f798eb3f
@@ -40,16 +38,14 @@ require (
github.com/robfig/cron/v3 v3.0.1
github.com/sirupsen/logrus v1.8.1
github.com/stretchr/testify v1.7.0
github.com/viney-shih/go-lock v1.1.1
go.etcd.io/bbolt v1.3.6
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
gopkg.in/alecthomas/kingpin.v2 v2.2.6
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
k8s.io/api v0.22.5
k8s.io/apimachinery v0.22.5
k8s.io/client-go v0.22.5
k8s.io/api v0.22.2
k8s.io/apimachinery v0.22.2
k8s.io/client-go v0.22.2
software.sslmate.com/src/go-pkcs12 v0.0.0-20210415151418-c5206de65a78
)
@@ -61,42 +57,41 @@ require (
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.1 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.0.1 // indirect
github.com/aws/smithy-go v1.9.0 // indirect
github.com/containerd/containerd v1.6.1 // indirect
github.com/containerd/containerd v1.5.7 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/distribution v2.8.0+incompatible // indirect
github.com/docker/distribution v2.7.1+incompatible // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/emirpasic/gods v1.12.0 // indirect
github.com/evanphx/json-patch v4.11.0+incompatible // indirect
github.com/felixge/httpsnoop v1.0.1 // indirect
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/fsnotify/fsnotify v1.4.9 // indirect
github.com/go-asn1-ber/asn1-ber v1.3.1 // indirect
github.com/go-git/gcfg v1.5.0 // indirect
github.com/go-git/go-billy/v5 v5.1.0 // indirect
github.com/go-logr/logr v1.2.2 // indirect
github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-logr/logr v0.4.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/go-cmp v0.5.6 // indirect
github.com/google/gofuzz v1.1.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/googleapis/gnostic v0.5.5 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/jpillora/ansi v1.0.2 // indirect
github.com/jpillora/requestlog v1.0.0 // indirect
github.com/jpillora/sizestr v1.0.0 // indirect
github.com/jpillora/ansi v0.0.0-20170202005112-f496b27cd669 // indirect
github.com/jpillora/requestlog v0.0.0-20181015073026-df8817be5f82 // indirect
github.com/jpillora/sizestr v0.0.0-20160130011556-e2ea2fa42fb9 // indirect
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.1.2 // indirect
github.com/moby/spdystream v0.2.0 // indirect
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.2 // indirect
github.com/opencontainers/image-spec v1.0.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/sergi/go-diff v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
@@ -106,21 +101,21 @@ require (
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f // indirect
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023 // indirect
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 // indirect
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d // indirect
golang.org/x/text v0.3.6 // indirect
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa // indirect
google.golang.org/grpc v1.43.0 // indirect
google.golang.org/protobuf v1.27.1 // indirect
google.golang.org/appengine v1.6.5 // indirect
google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a // indirect
google.golang.org/grpc v1.33.2 // indirect
google.golang.org/protobuf v1.26.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/klog/v2 v2.30.0 // indirect
k8s.io/kube-openapi v0.0.0-20211109043538-20434351676c // indirect
k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b // indirect
k8s.io/klog/v2 v2.9.0 // indirect
k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e // indirect
k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.1.2 // indirect
sigs.k8s.io/yaml v1.2.0 // indirect
)

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,4 @@ var (
ErrUnauthorized = errors.New("Unauthorized")
// ErrResourceAccessDenied Access denied to resource error
ErrResourceAccessDenied = errors.New("Access denied to resource")
// ErrNotAvailableInDemo feature is not allowed in demo
ErrNotAvailableInDemo = errors.New("This feature is not available in the demo version of Portainer")
)

View File

@@ -100,8 +100,7 @@ func (handler *Handler) authenticateInternal(w http.ResponseWriter, user *portai
return &httperror.HandlerError{http.StatusUnprocessableEntity, "Invalid credentials", httperrors.ErrUnauthorized}
}
forceChangePassword := !handler.passwordStrengthChecker.Check(password)
return handler.writeToken(w, user, forceChangePassword)
return handler.writeToken(w, user)
}
func (handler *Handler) authenticateLDAP(w http.ResponseWriter, user *portainer.User, username, password string, ldapSettings *portainer.LDAPSettings) *httperror.HandlerError {
@@ -132,11 +131,11 @@ func (handler *Handler) authenticateLDAP(w http.ResponseWriter, user *portainer.
log.Printf("Warning: unable to automatically add user into teams: %s\n", err.Error())
}
return handler.writeToken(w, user, false)
return handler.writeToken(w, user)
}
func (handler *Handler) writeToken(w http.ResponseWriter, user *portainer.User, forceChangePassword bool) *httperror.HandlerError {
tokenData := composeTokenData(user, forceChangePassword)
func (handler *Handler) writeToken(w http.ResponseWriter, user *portainer.User) *httperror.HandlerError {
tokenData := composeTokenData(user)
return handler.persistAndWriteToken(w, tokenData)
}
@@ -207,11 +206,10 @@ func teamMembershipExists(teamID portainer.TeamID, memberships []portainer.TeamM
return false
}
func composeTokenData(user *portainer.User, forceChangePassword bool) *portainer.TokenData {
func composeTokenData(user *portainer.User) *portainer.TokenData {
return &portainer.TokenData{
ID: user.ID,
Username: user.Username,
Role: user.Role,
ForceChangePassword: forceChangePassword,
ID: user.ID,
Username: user.Username,
Role: user.Role,
}
}

View File

@@ -110,5 +110,5 @@ func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) *h
}
return handler.writeToken(w, user, false)
return handler.writeToken(w, user)
}

View File

@@ -22,14 +22,12 @@ type Handler struct {
OAuthService portainer.OAuthService
ProxyManager *proxy.Manager
KubernetesTokenCacheManager *kubernetes.TokenCacheManager
passwordStrengthChecker security.PasswordStrengthChecker
}
// NewHandler creates a handler to manage authentication operations.
func NewHandler(bouncer *security.RequestBouncer, rateLimiter *security.RateLimiter, passwordStrengthChecker security.PasswordStrengthChecker) *Handler {
func NewHandler(bouncer *security.RequestBouncer, rateLimiter *security.RateLimiter) *Handler {
h := &Handler{
Router: mux.NewRouter(),
passwordStrengthChecker: passwordStrengthChecker,
Router: mux.NewRouter(),
}
h.Handle("/auth/oauth/validate",

View File

@@ -18,7 +18,6 @@ import (
"github.com/docker/docker/pkg/ioutils"
"github.com/portainer/portainer/api/adminmonitor"
"github.com/portainer/portainer/api/crypto"
"github.com/portainer/portainer/api/demo"
"github.com/portainer/portainer/api/http/offlinegate"
i "github.com/portainer/portainer/api/internal/testhelpers"
"github.com/stretchr/testify/assert"
@@ -50,7 +49,7 @@ func Test_backupHandlerWithoutPassword_shouldCreateATarballArchive(t *testing.T)
gate := offlinegate.NewOfflineGate()
adminMonitor := adminmonitor.New(time.Hour, nil, context.Background())
handlerErr := NewHandler(nil, i.NewDatastore(), gate, "./test_assets/handler_test", func() {}, adminMonitor, &demo.Service{}).backup(w, r)
handlerErr := NewHandler(nil, i.NewDatastore(), gate, "./test_assets/handler_test", func() {}, adminMonitor).backup(w, r)
assert.Nil(t, handlerErr, "Handler should not fail")
response := w.Result()
@@ -87,7 +86,7 @@ func Test_backupHandlerWithPassword_shouldCreateEncryptedATarballArchive(t *test
gate := offlinegate.NewOfflineGate()
adminMonitor := adminmonitor.New(time.Hour, nil, nil)
handlerErr := NewHandler(nil, i.NewDatastore(), gate, "./test_assets/handler_test", func() {}, adminMonitor, &demo.Service{}).backup(w, r)
handlerErr := NewHandler(nil, i.NewDatastore(), gate, "./test_assets/handler_test", func() {}, adminMonitor).backup(w, r)
assert.Nil(t, handlerErr, "Handler should not fail")
response := w.Result()

View File

@@ -9,8 +9,6 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/adminmonitor"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/demo"
"github.com/portainer/portainer/api/http/middlewares"
"github.com/portainer/portainer/api/http/offlinegate"
"github.com/portainer/portainer/api/http/security"
)
@@ -27,17 +25,7 @@ type Handler struct {
}
// NewHandler creates an new instance of backup handler
func NewHandler(
bouncer *security.RequestBouncer,
dataStore dataservices.DataStore,
gate *offlinegate.OfflineGate,
filestorePath string,
shutdownTrigger context.CancelFunc,
adminMonitor *adminmonitor.Monitor,
demoService *demo.Service,
) *Handler {
func NewHandler(bouncer *security.RequestBouncer, dataStore dataservices.DataStore, gate *offlinegate.OfflineGate, filestorePath string, shutdownTrigger context.CancelFunc, adminMonitor *adminmonitor.Monitor) *Handler {
h := &Handler{
Router: mux.NewRouter(),
bouncer: bouncer,
@@ -48,11 +36,8 @@ func NewHandler(
adminMonitor: adminMonitor,
}
demoRestrictedRouter := h.NewRoute().Subrouter()
demoRestrictedRouter.Use(middlewares.RestrictDemoEnv(demoService.IsDemo))
demoRestrictedRouter.Handle("/backup", bouncer.RestrictedAccess(adminAccess(httperror.LoggerHandler(h.backup)))).Methods(http.MethodPost)
demoRestrictedRouter.Handle("/restore", bouncer.PublicAccess(httperror.LoggerHandler(h.restore))).Methods(http.MethodPost)
h.Handle("/backup", bouncer.RestrictedAccess(adminAccess(httperror.LoggerHandler(h.backup)))).Methods(http.MethodPost)
h.Handle("/restore", bouncer.PublicAccess(httperror.LoggerHandler(h.restore))).Methods(http.MethodPost)
return h
}
@@ -65,7 +50,7 @@ func adminAccess(next http.Handler) http.Handler {
}
if !securityContext.IsAdmin {
httperror.WriteError(w, http.StatusUnauthorized, "User is not authorized to perform the action", nil)
httperror.WriteError(w, http.StatusUnauthorized, "User is not authorized to perfom the action", nil)
}
next.ServeHTTP(w, r)

View File

@@ -14,7 +14,6 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/adminmonitor"
"github.com/portainer/portainer/api/demo"
"github.com/portainer/portainer/api/http/offlinegate"
i "github.com/portainer/portainer/api/internal/testhelpers"
"github.com/stretchr/testify/assert"
@@ -52,7 +51,7 @@ func Test_restoreArchive_usingCombinationOfPasswords(t *testing.T) {
datastore := i.NewDatastore(i.WithUsers([]portainer.User{}), i.WithEdgeJobs([]portainer.EdgeJob{}))
adminMonitor := adminmonitor.New(time.Hour, datastore, context.Background())
h := NewHandler(nil, datastore, offlinegate.NewOfflineGate(), "./test_assets/handler_test", func() {}, adminMonitor, &demo.Service{})
h := NewHandler(nil, datastore, offlinegate.NewOfflineGate(), "./test_assets/handler_test", func() {}, adminMonitor)
//backup
archive := backup(t, h, test.backupPassword)
@@ -75,7 +74,7 @@ func Test_restoreArchive_shouldFailIfSystemWasAlreadyInitialized(t *testing.T) {
datastore := i.NewDatastore(i.WithUsers([]portainer.User{admin}), i.WithEdgeJobs([]portainer.EdgeJob{}))
adminMonitor := adminmonitor.New(time.Hour, datastore, context.Background())
h := NewHandler(nil, datastore, offlinegate.NewOfflineGate(), "./test_assets/handler_test", func() {}, adminMonitor, &demo.Service{})
h := NewHandler(nil, datastore, offlinegate.NewOfflineGate(), "./test_assets/handler_test", func() {}, adminMonitor)
//backup
archive := backup(t, h, "password")

View File

@@ -1,7 +1,6 @@
package customtemplates
import (
"encoding/json"
"errors"
"log"
"net/http"
@@ -116,8 +115,6 @@ type customTemplateFromFileContentPayload struct {
Type portainer.StackType `example:"1" enums:"1,2,3" validate:"required"`
// Content of stack file
FileContent string `validate:"required"`
// Definitions of variables in the stack file
Variables []portainer.CustomTemplateVariableDefinition
}
func (payload *customTemplateFromFileContentPayload) Validate(r *http.Request) error {
@@ -139,12 +136,6 @@ func (payload *customTemplateFromFileContentPayload) Validate(r *http.Request) e
if !isValidNote(payload.Note) {
return errors.New("Invalid note. <img> tag is not supported")
}
err := validateVariablesDefinitions(payload.Variables)
if err != nil {
return err
}
return nil
}
@@ -173,7 +164,6 @@ func (handler *Handler) createCustomTemplateFromFileContent(r *http.Request) (*p
Platform: (payload.Platform),
Type: (payload.Type),
Logo: payload.Logo,
Variables: payload.Variables,
}
templateFolder := strconv.Itoa(customTemplateID)
@@ -214,8 +204,6 @@ type customTemplateFromGitRepositoryPayload struct {
RepositoryPassword string `example:"myGitPassword"`
// Path to the Stack file inside the Git repository
ComposeFilePathInRepository string `example:"docker-compose.yml" default:"docker-compose.yml"`
// Definitions of variables in the stack file
Variables []portainer.CustomTemplateVariableDefinition
}
func (payload *customTemplateFromGitRepositoryPayload) Validate(r *http.Request) error {
@@ -248,12 +236,6 @@ func (payload *customTemplateFromGitRepositoryPayload) Validate(r *http.Request)
if !isValidNote(payload.Note) {
return errors.New("Invalid note. <img> tag is not supported")
}
err := validateVariablesDefinitions(payload.Variables)
if err != nil {
return err
}
return nil
}
@@ -274,7 +256,6 @@ func (handler *Handler) createCustomTemplateFromGitRepository(r *http.Request) (
Platform: payload.Platform,
Type: payload.Type,
Logo: payload.Logo,
Variables: payload.Variables,
}
projectPath := handler.FileService.GetCustomTemplateProjectPath(strconv.Itoa(customTemplateID))
@@ -335,8 +316,6 @@ type customTemplateFromFileUploadPayload struct {
Platform portainer.CustomTemplatePlatform
Type portainer.StackType
FileContent []byte
// Definitions of variables in the stack file
Variables []portainer.CustomTemplateVariableDefinition
}
func (payload *customTemplateFromFileUploadPayload) Validate(r *http.Request) error {
@@ -382,17 +361,6 @@ func (payload *customTemplateFromFileUploadPayload) Validate(r *http.Request) er
}
payload.FileContent = composeFileContent
varsString, _ := request.RetrieveMultiPartFormValue(r, "Variables", true)
err = json.Unmarshal([]byte(varsString), &payload.Variables)
if err != nil {
return errors.New("Invalid variables. Ensure that the variables are valid JSON")
}
err = validateVariablesDefinitions(payload.Variables)
if err != nil {
return err
}
return nil
}
@@ -413,7 +381,6 @@ func (handler *Handler) createCustomTemplateFromFileUpload(r *http.Request) (*po
Type: payload.Type,
Logo: payload.Logo,
EntryPoint: filesystem.ComposeFileDefaultName,
Variables: payload.Variables,
}
templateFolder := strconv.Itoa(customTemplateID)

View File

@@ -31,8 +31,6 @@ type customTemplateUpdatePayload struct {
Type portainer.StackType `example:"1" enums:"1,2,3" validate:"required"`
// Content of stack file
FileContent string `validate:"required"`
// Definitions of variables in the stack file
Variables []portainer.CustomTemplateVariableDefinition
}
func (payload *customTemplateUpdatePayload) Validate(r *http.Request) error {
@@ -54,12 +52,6 @@ func (payload *customTemplateUpdatePayload) Validate(r *http.Request) error {
if !isValidNote(payload.Note) {
return errors.New("Invalid note. <img> tag is not supported")
}
err := validateVariablesDefinitions(payload.Variables)
if err != nil {
return err
}
return nil
}
@@ -132,7 +124,6 @@ func (handler *Handler) customTemplateUpdate(w http.ResponseWriter, r *http.Requ
customTemplate.Note = payload.Note
customTemplate.Platform = payload.Platform
customTemplate.Type = payload.Type
customTemplate.Variables = payload.Variables
err = handler.DataStore.CustomTemplate().UpdateCustomTemplate(customTemplate.ID, customTemplate)
if err != nil {

View File

@@ -1,19 +0,0 @@
package customtemplates
import (
"errors"
portainer "github.com/portainer/portainer/api"
)
func validateVariablesDefinitions(variables []portainer.CustomTemplateVariableDefinition) error {
for _, variable := range variables {
if variable.Name == "" {
return errors.New("variable name is required")
}
if variable.Label == "" {
return errors.New("variable label is required")
}
}
return nil
}

View File

@@ -119,7 +119,7 @@ func (payload *edgeJobCreateFromFilePayload) Validate(r *http.Request) error {
payload.CronExpression = cronExpression
var endpoints []portainer.EndpointID
err = request.RetrieveMultiPartFormJSONValue(r, "Endpoints", &endpoints, false)
err = request.RetrieveMultiPartFormJSONValue(r, "Environments", &endpoints, false)
if err != nil {
return errors.New("Invalid environments")
}
@@ -219,7 +219,7 @@ func (handler *Handler) addAndPersistEdgeJob(edgeJob *portainer.EdgeJob, file []
handler.ReverseTunnelService.AddEdgeJob(endpointID, edgeJob)
}
return handler.DataStore.EdgeJob().Create(edgeJob.ID, edgeJob)
return handler.DataStore.EdgeJob().Create(edgeJob)
}
func convertEndpointsToMetaObject(endpoints []portainer.EndpointID) map[portainer.EndpointID]portainer.EdgeJobEndpointMeta {

View File

@@ -39,7 +39,7 @@ func (handler *Handler) edgeJobFile(w http.ResponseWriter, r *http.Request) *htt
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an Edge job with the specified identifier inside the database", err}
}
edgeJobFileContent, err := handler.FileService.GetFileContent(edgeJob.ScriptPath, "")
edgeJobFileContent, err := handler.FileService.GetFileContent("", edgeJob.ScriptPath)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Edge job script file from disk", err}
}

View File

@@ -92,7 +92,7 @@ func (handler *Handler) updateEdgeSchedule(edgeJob *portainer.EdgeJob, payload *
continue
}
if meta, exists := edgeJob.Endpoints[endpointID]; exists {
if meta, ok := edgeJob.Endpoints[endpointID]; ok {
endpointsMap[endpointID] = meta
} else {
endpointsMap[endpointID] = portainer.EdgeJobEndpointMeta{}
@@ -103,19 +103,13 @@ func (handler *Handler) updateEdgeSchedule(edgeJob *portainer.EdgeJob, payload *
}
updateVersion := false
if payload.CronExpression != nil && *payload.CronExpression != edgeJob.CronExpression {
if payload.CronExpression != nil {
edgeJob.CronExpression = *payload.CronExpression
updateVersion = true
}
fileContent, err := handler.FileService.GetFileContent(edgeJob.ScriptPath, "")
if err != nil {
return err
}
if payload.FileContent != nil && *payload.FileContent != string(fileContent) {
fileContent = []byte(*payload.FileContent)
_, err := handler.FileService.StoreEdgeJobFileFromBytes(strconv.Itoa(int(edgeJob.ID)), fileContent)
if payload.FileContent != nil {
_, err := handler.FileService.StoreEdgeJobFileFromBytes(strconv.Itoa(int(edgeJob.ID)), []byte(*payload.FileContent))
if err != nil {
return err
}
@@ -123,7 +117,7 @@ func (handler *Handler) updateEdgeSchedule(edgeJob *portainer.EdgeJob, payload *
updateVersion = true
}
if payload.Recurring != nil && *payload.Recurring != edgeJob.Recurring {
if payload.Recurring != nil {
edgeJob.Recurring = *payload.Recurring
updateVersion = true
}

View File

@@ -1,64 +0,0 @@
package edgestacks
import (
"net/http"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/middlewares"
)
func (handler *Handler) handlerDBErr(err error, msg string) *httperror.HandlerError {
httpErr := &httperror.HandlerError{http.StatusInternalServerError, msg, err}
if handler.DataStore.IsErrObjectNotFound(err) {
httpErr.StatusCode = http.StatusNotFound
}
return httpErr
}
// @id EdgeStackStatusDelete
// @summary Delete an EdgeStack status
// @description Authorized only if the request is done by an Edge Environment(Endpoint)
// @tags edge_stacks
// @produce json
// @param id path string true "EdgeStack Id"
// @success 200 {object} portainer.EdgeStack
// @failure 500
// @failure 400
// @failure 404
// @failure 403
// @router /edge_stacks/{id}/status/{endpoint_id} [delete]
func (handler *Handler) edgeStackStatusDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
stackID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid stack identifier route variable", err}
}
endpoint, err := middlewares.FetchEndpoint(r)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a valid endpoint from the handler context", err}
}
err = handler.requestBouncer.AuthorizedEdgeEndpointOperation(r, endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err}
}
stack, err := handler.DataStore.EdgeStack().EdgeStack(portainer.EdgeStackID(stackID))
if err != nil {
return handler.handlerDBErr(err, "Unable to find a stack with the specified identifier inside the database")
}
delete(stack.Status, endpoint.ID)
err = handler.DataStore.EdgeStack().UpdateEdgeStack(stack.ID, stack)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack changes inside the database", err}
}
return response.JSON(w, stack)
}

View File

@@ -1,924 +0,0 @@
package edgestacks
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"os"
"reflect"
"strconv"
"testing"
"time"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/apikey"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/datastore"
"github.com/portainer/portainer/api/filesystem"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/jwt"
)
type gitService struct {
cloneErr error
id string
}
func (g *gitService) CloneRepository(destination, repositoryURL, referenceName, username, password string) error {
return g.cloneErr
}
func (g *gitService) LatestCommitID(repositoryURL, referenceName, username, password string) (string, error) {
return g.id, nil
}
// Helpers
func setupHandler(t *testing.T) (*Handler, string, func()) {
t.Helper()
_, store, storeTeardown := datastore.MustNewTestStore(true, true)
jwtService, err := jwt.NewService("1h", store)
if err != nil {
storeTeardown()
t.Fatal(err)
}
user := &portainer.User{ID: 2, Username: "admin", Role: portainer.AdministratorRole}
err = store.User().Create(user)
if err != nil {
storeTeardown()
t.Fatal(err)
}
apiKeyService := apikey.NewAPIKeyService(store.APIKeyRepository(), store.User())
rawAPIKey, _, err := apiKeyService.GenerateApiKey(*user, "test")
if err != nil {
storeTeardown()
t.Fatal(err)
}
handler := NewHandler(
security.NewRequestBouncer(store, jwtService, apiKeyService),
store,
)
tmpDir, err := os.MkdirTemp(os.TempDir(), "portainer-test")
if err != nil {
storeTeardown()
t.Fatal(err)
}
fs, err := filesystem.NewService(tmpDir, "")
if err != nil {
storeTeardown()
t.Fatal(err)
}
handler.FileService = fs
settings, err := handler.DataStore.Settings().Settings()
if err != nil {
t.Fatal(err)
}
settings.EnableEdgeComputeFeatures = true
err = handler.DataStore.Settings().UpdateSettings(settings)
if err != nil {
t.Fatal(err)
}
handler.GitService = &gitService{errors.New("Clone error"), "git-service-id"}
return handler, rawAPIKey, storeTeardown
}
func createEndpoint(t *testing.T, store dataservices.DataStore) portainer.Endpoint {
t.Helper()
endpointID := portainer.EndpointID(5)
endpoint := portainer.Endpoint{
ID: endpointID,
Name: "test-endpoint-" + strconv.Itoa(int(endpointID)),
Type: portainer.EdgeAgentOnDockerEnvironment,
URL: "https://portainer.io:9443",
EdgeID: "edge-id",
LastCheckInDate: time.Now().Unix(),
}
err := store.Endpoint().Create(&endpoint)
if err != nil {
t.Fatal(err)
}
return endpoint
}
func createEdgeStack(t *testing.T, store dataservices.DataStore, endpointID portainer.EndpointID) portainer.EdgeStack {
t.Helper()
edgeGroup := portainer.EdgeGroup{
ID: 1,
Name: "EdgeGroup 1",
Dynamic: false,
TagIDs: nil,
Endpoints: []portainer.EndpointID{endpointID},
PartialMatch: false,
}
err := store.EdgeGroup().Create(&edgeGroup)
if err != nil {
t.Fatal(err)
}
edgeStackID := portainer.EdgeStackID(14)
edgeStack := portainer.EdgeStack{
ID: edgeStackID,
Name: "test-edge-stack-" + strconv.Itoa(int(edgeStackID)),
Status: map[portainer.EndpointID]portainer.EdgeStackStatus{
endpointID: {Type: portainer.StatusOk, Error: "", EndpointID: endpointID},
},
CreationDate: time.Now().Unix(),
EdgeGroups: []portainer.EdgeGroupID{edgeGroup.ID},
ProjectPath: "/project/path",
EntryPoint: "entrypoint",
Version: 237,
ManifestPath: "/manifest/path",
DeploymentType: portainer.EdgeStackDeploymentKubernetes,
}
endpointRelation := portainer.EndpointRelation{
EndpointID: endpointID,
EdgeStacks: map[portainer.EdgeStackID]bool{
edgeStack.ID: true,
},
}
err = store.EdgeStack().Create(edgeStack.ID, &edgeStack)
if err != nil {
t.Fatal(err)
}
err = store.EndpointRelation().Create(&endpointRelation)
if err != nil {
t.Fatal(err)
}
return edgeStack
}
// Inspect
func TestInspectInvalidEdgeID(t *testing.T) {
handler, rawAPIKey, teardown := setupHandler(t)
defer teardown()
cases := []struct {
Name string
EdgeStackID string
ExpectedStatusCode int
}{
{"Invalid EdgeStackID", "x", 400},
{"Non-existing EdgeStackID", "5", 404},
}
for _, tc := range cases {
t.Run(tc.Name, func(t *testing.T) {
req, err := http.NewRequest(http.MethodGet, "/edge_stacks/"+tc.EdgeStackID, nil)
if err != nil {
t.Fatal("request error:", err)
}
req.Header.Add("x-api-key", rawAPIKey)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != tc.ExpectedStatusCode {
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", tc.ExpectedStatusCode, rec.Code))
}
})
}
}
// Create
func TestCreateAndInspect(t *testing.T) {
handler, rawAPIKey, teardown := setupHandler(t)
defer teardown()
// Create Endpoint, EdgeGroup and EndpointRelation
endpoint := createEndpoint(t, handler.DataStore)
edgeGroup := portainer.EdgeGroup{
ID: 1,
Name: "EdgeGroup 1",
Dynamic: false,
TagIDs: nil,
Endpoints: []portainer.EndpointID{endpoint.ID},
PartialMatch: false,
}
err := handler.DataStore.EdgeGroup().Create(&edgeGroup)
if err != nil {
t.Fatal(err)
}
endpointRelation := portainer.EndpointRelation{
EndpointID: endpoint.ID,
EdgeStacks: map[portainer.EdgeStackID]bool{},
}
err = handler.DataStore.EndpointRelation().Create(&endpointRelation)
if err != nil {
t.Fatal(err)
}
payload := swarmStackFromFileContentPayload{
Name: "Test Stack",
StackFileContent: "stack content",
EdgeGroups: []portainer.EdgeGroupID{1},
DeploymentType: portainer.EdgeStackDeploymentCompose,
}
jsonPayload, err := json.Marshal(payload)
if err != nil {
t.Fatal("JSON marshal error:", err)
}
r := bytes.NewBuffer(jsonPayload)
// Create EdgeStack
req, err := http.NewRequest(http.MethodPost, "/edge_stacks?method=string", r)
if err != nil {
t.Fatal("request error:", err)
}
req.Header.Add("x-api-key", rawAPIKey)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusOK, rec.Code))
}
data := portainer.EdgeStack{}
err = json.NewDecoder(rec.Body).Decode(&data)
if err != nil {
t.Fatal("error decoding response:", err)
}
// Inspect
req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("/edge_stacks/%d", data.ID), nil)
if err != nil {
t.Fatal("request error:", err)
}
req.Header.Add("x-api-key", rawAPIKey)
rec = httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusOK, rec.Code))
}
data = portainer.EdgeStack{}
err = json.NewDecoder(rec.Body).Decode(&data)
if err != nil {
t.Fatal("error decoding response:", err)
}
if payload.Name != data.Name {
t.Fatalf(fmt.Sprintf("expected EdgeStack Name %s, found %s", payload.Name, data.Name))
}
}
func TestCreateWithInvalidPayload(t *testing.T) {
handler, rawAPIKey, teardown := setupHandler(t)
defer teardown()
endpoint := createEndpoint(t, handler.DataStore)
edgeStack := createEdgeStack(t, handler.DataStore, endpoint.ID)
cases := []struct {
Name string
Payload interface{}
QueryString string
ExpectedStatusCode int
}{
{
Name: "Invalid query string parameter",
Payload: swarmStackFromFileContentPayload{},
QueryString: "invalid=query-string",
ExpectedStatusCode: 400,
},
{
Name: "Invalid creation method",
Payload: swarmStackFromFileContentPayload{},
QueryString: "method=invalid-creation-method",
ExpectedStatusCode: 500,
},
{
Name: "Empty swarmStackFromFileContentPayload with string method",
Payload: swarmStackFromFileContentPayload{},
QueryString: "method=string",
ExpectedStatusCode: 500,
},
{
Name: "Empty swarmStackFromFileContentPayload with repository method",
Payload: swarmStackFromFileContentPayload{},
QueryString: "method=repository",
ExpectedStatusCode: 500,
},
{
Name: "Empty swarmStackFromFileContentPayload with file method",
Payload: swarmStackFromFileContentPayload{},
QueryString: "method=file",
ExpectedStatusCode: 500,
},
{
Name: "Duplicated EdgeStack Name",
Payload: swarmStackFromFileContentPayload{
Name: edgeStack.Name,
StackFileContent: "content",
EdgeGroups: edgeStack.EdgeGroups,
DeploymentType: edgeStack.DeploymentType,
},
QueryString: "method=string",
ExpectedStatusCode: 500,
},
{
Name: "Empty EdgeStack Groups",
Payload: swarmStackFromFileContentPayload{
Name: edgeStack.Name,
StackFileContent: "content",
EdgeGroups: []portainer.EdgeGroupID{},
DeploymentType: edgeStack.DeploymentType,
},
QueryString: "method=string",
ExpectedStatusCode: 500,
},
{
Name: "EdgeStackDeploymentKubernetes with Docker endpoint",
Payload: swarmStackFromFileContentPayload{
Name: "Stack name",
StackFileContent: "content",
EdgeGroups: []portainer.EdgeGroupID{1},
DeploymentType: portainer.EdgeStackDeploymentKubernetes,
},
QueryString: "method=string",
ExpectedStatusCode: 500,
},
{
Name: "Empty Stack File Content",
Payload: swarmStackFromFileContentPayload{
Name: "Stack name",
StackFileContent: "",
EdgeGroups: []portainer.EdgeGroupID{1},
DeploymentType: portainer.EdgeStackDeploymentCompose,
},
QueryString: "method=string",
ExpectedStatusCode: 500,
},
{
Name: "Clone Git respository error",
Payload: swarmStackFromGitRepositoryPayload{
Name: "Stack name",
RepositoryURL: "github.com/portainer/portainer",
RepositoryReferenceName: "ref name",
RepositoryAuthentication: false,
RepositoryUsername: "",
RepositoryPassword: "",
FilePathInRepository: "/file/path",
EdgeGroups: []portainer.EdgeGroupID{1},
DeploymentType: portainer.EdgeStackDeploymentCompose,
},
QueryString: "method=repository",
ExpectedStatusCode: 500,
},
}
for _, tc := range cases {
t.Run(tc.Name, func(t *testing.T) {
jsonPayload, err := json.Marshal(tc.Payload)
if err != nil {
t.Fatal("JSON marshal error:", err)
}
r := bytes.NewBuffer(jsonPayload)
// Create EdgeStack
req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("/edge_stacks?%s", tc.QueryString), r)
if err != nil {
t.Fatal("request error:", err)
}
req.Header.Add("x-api-key", rawAPIKey)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != tc.ExpectedStatusCode {
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", tc.ExpectedStatusCode, rec.Code))
}
})
}
}
// Delete
func TestDeleteAndInspect(t *testing.T) {
handler, rawAPIKey, teardown := setupHandler(t)
defer teardown()
// Create
endpoint := createEndpoint(t, handler.DataStore)
edgeStack := createEdgeStack(t, handler.DataStore, endpoint.ID)
// Inspect
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/edge_stacks/%d", edgeStack.ID), nil)
if err != nil {
t.Fatal("request error:", err)
}
req.Header.Add("x-api-key", rawAPIKey)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusOK, rec.Code))
}
data := portainer.EdgeStack{}
err = json.NewDecoder(rec.Body).Decode(&data)
if err != nil {
t.Fatal("error decoding response:", err)
}
if data.ID != edgeStack.ID {
t.Fatalf(fmt.Sprintf("expected EdgeStackID %d, found %d", int(edgeStack.ID), data.ID))
}
// Delete
req, err = http.NewRequest(http.MethodDelete, fmt.Sprintf("/edge_stacks/%d", edgeStack.ID), nil)
if err != nil {
t.Fatal("request error:", err)
}
req.Header.Add("x-api-key", rawAPIKey)
rec = httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusNoContent {
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusNoContent, rec.Code))
}
// Inspect
req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("/edge_stacks/%d", edgeStack.ID), nil)
if err != nil {
t.Fatal("request error:", err)
}
req.Header.Add("x-api-key", rawAPIKey)
rec = httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusNotFound {
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusNotFound, rec.Code))
}
}
func TestDeleteInvalidEdgeStack(t *testing.T) {
handler, rawAPIKey, teardown := setupHandler(t)
defer teardown()
cases := []struct {
Name string
URL string
ExpectedStatusCode int
}{
{Name: "Non-existing EdgeStackID", URL: "/edge_stacks/-1", ExpectedStatusCode: http.StatusNotFound},
{Name: "Invalid EdgeStackID", URL: "/edge_stacks/aaaaaaa", ExpectedStatusCode: http.StatusBadRequest},
}
for _, tc := range cases {
t.Run(tc.Name, func(t *testing.T) {
req, err := http.NewRequest(http.MethodDelete, tc.URL, nil)
if err != nil {
t.Fatal("request error:", err)
}
req.Header.Add("x-api-key", rawAPIKey)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != tc.ExpectedStatusCode {
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", tc.ExpectedStatusCode, rec.Code))
}
})
}
}
// Update
func TestUpdateAndInspect(t *testing.T) {
handler, rawAPIKey, teardown := setupHandler(t)
defer teardown()
endpoint := createEndpoint(t, handler.DataStore)
edgeStack := createEdgeStack(t, handler.DataStore, endpoint.ID)
// Update edge stack: create new Endpoint, EndpointRelation and EdgeGroup
endpointID := portainer.EndpointID(6)
newEndpoint := portainer.Endpoint{
ID: endpointID,
Name: "test-endpoint-" + strconv.Itoa(int(endpointID)),
Type: portainer.EdgeAgentOnDockerEnvironment,
URL: "https://portainer.io:9443",
EdgeID: "edge-id",
LastCheckInDate: time.Now().Unix(),
}
err := handler.DataStore.Endpoint().Create(&newEndpoint)
if err != nil {
t.Fatal(err)
}
endpointRelation := portainer.EndpointRelation{
EndpointID: endpointID,
EdgeStacks: map[portainer.EdgeStackID]bool{
edgeStack.ID: true,
},
}
err = handler.DataStore.EndpointRelation().Create(&endpointRelation)
if err != nil {
t.Fatal(err)
}
newEdgeGroup := portainer.EdgeGroup{
ID: 2,
Name: "EdgeGroup 2",
Dynamic: false,
TagIDs: nil,
Endpoints: []portainer.EndpointID{newEndpoint.ID},
PartialMatch: false,
}
err = handler.DataStore.EdgeGroup().Create(&newEdgeGroup)
if err != nil {
t.Fatal(err)
}
newVersion := 238
payload := updateEdgeStackPayload{
StackFileContent: "update-test",
Version: &newVersion,
EdgeGroups: append(edgeStack.EdgeGroups, newEdgeGroup.ID),
DeploymentType: portainer.EdgeStackDeploymentCompose,
}
jsonPayload, err := json.Marshal(payload)
if err != nil {
t.Fatal("request error:", err)
}
r := bytes.NewBuffer(jsonPayload)
req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("/edge_stacks/%d", edgeStack.ID), r)
if err != nil {
t.Fatal("request error:", err)
}
req.Header.Add("x-api-key", rawAPIKey)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusOK, rec.Code))
}
// Get updated edge stack
req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("/edge_stacks/%d", edgeStack.ID), nil)
if err != nil {
t.Fatal("request error:", err)
}
req.Header.Add("x-api-key", rawAPIKey)
rec = httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusOK, rec.Code))
}
data := portainer.EdgeStack{}
err = json.NewDecoder(rec.Body).Decode(&data)
if err != nil {
t.Fatal("error decoding response:", err)
}
if data.Version != *payload.Version {
t.Fatalf(fmt.Sprintf("expected EdgeStackID %d, found %d", edgeStack.Version, data.Version))
}
if data.DeploymentType != payload.DeploymentType {
t.Fatalf(fmt.Sprintf("expected DeploymentType %d, found %d", edgeStack.DeploymentType, data.DeploymentType))
}
if !reflect.DeepEqual(data.EdgeGroups, payload.EdgeGroups) {
t.Fatalf("expected EdgeGroups to be equal")
}
}
func TestUpdateWithInvalidEdgeGroups(t *testing.T) {
handler, rawAPIKey, teardown := setupHandler(t)
defer teardown()
endpoint := createEndpoint(t, handler.DataStore)
edgeStack := createEdgeStack(t, handler.DataStore, endpoint.ID)
//newEndpoint := createEndpoint(t, handler.DataStore)
newEdgeGroup := portainer.EdgeGroup{
ID: 2,
Name: "EdgeGroup 2",
Dynamic: false,
TagIDs: nil,
Endpoints: []portainer.EndpointID{8889},
PartialMatch: false,
}
handler.DataStore.EdgeGroup().Create(&newEdgeGroup)
newVersion := 238
cases := []struct {
Name string
Payload updateEdgeStackPayload
ExpectedStatusCode int
}{
{
"Update with non-existing EdgeGroupID",
updateEdgeStackPayload{
StackFileContent: "error-test",
Version: &newVersion,
EdgeGroups: []portainer.EdgeGroupID{9999},
DeploymentType: edgeStack.DeploymentType,
},
http.StatusInternalServerError,
},
{
"Update with invalid EdgeGroup (non-existing Endpoint)",
updateEdgeStackPayload{
StackFileContent: "error-test",
Version: &newVersion,
EdgeGroups: []portainer.EdgeGroupID{2},
DeploymentType: edgeStack.DeploymentType,
},
http.StatusInternalServerError,
},
{
"Update DeploymentType from Docker to Kubernetes",
updateEdgeStackPayload{
StackFileContent: "error-test",
Version: &newVersion,
EdgeGroups: []portainer.EdgeGroupID{1},
DeploymentType: portainer.EdgeStackDeploymentKubernetes,
},
http.StatusBadRequest,
},
}
for _, tc := range cases {
t.Run(tc.Name, func(t *testing.T) {
jsonPayload, err := json.Marshal(tc.Payload)
if err != nil {
t.Fatal("JSON marshal error:", err)
}
r := bytes.NewBuffer(jsonPayload)
req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("/edge_stacks/%d", edgeStack.ID), r)
if err != nil {
t.Fatal("request error:", err)
}
req.Header.Add("x-api-key", rawAPIKey)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != tc.ExpectedStatusCode {
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", tc.ExpectedStatusCode, rec.Code))
}
})
}
}
func TestUpdateWithInvalidPayload(t *testing.T) {
handler, rawAPIKey, teardown := setupHandler(t)
defer teardown()
endpoint := createEndpoint(t, handler.DataStore)
edgeStack := createEdgeStack(t, handler.DataStore, endpoint.ID)
newVersion := 238
cases := []struct {
Name string
Payload updateEdgeStackPayload
ExpectedStatusCode int
}{
{
"Update with empty StackFileContent",
updateEdgeStackPayload{
StackFileContent: "",
Version: &newVersion,
EdgeGroups: edgeStack.EdgeGroups,
DeploymentType: edgeStack.DeploymentType,
},
http.StatusBadRequest,
},
{
"Update with empty EdgeGroups",
updateEdgeStackPayload{
StackFileContent: "error-test",
Version: &newVersion,
EdgeGroups: []portainer.EdgeGroupID{},
DeploymentType: edgeStack.DeploymentType,
},
http.StatusBadRequest,
},
}
for _, tc := range cases {
t.Run(tc.Name, func(t *testing.T) {
jsonPayload, err := json.Marshal(tc.Payload)
if err != nil {
t.Fatal("request error:", err)
}
r := bytes.NewBuffer(jsonPayload)
req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("/edge_stacks/%d", edgeStack.ID), r)
if err != nil {
t.Fatal("request error:", err)
}
req.Header.Add("x-api-key", rawAPIKey)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != tc.ExpectedStatusCode {
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", tc.ExpectedStatusCode, rec.Code))
}
})
}
}
// Update Status
func TestUpdateStatusAndInspect(t *testing.T) {
handler, rawAPIKey, teardown := setupHandler(t)
defer teardown()
endpoint := createEndpoint(t, handler.DataStore)
edgeStack := createEdgeStack(t, handler.DataStore, endpoint.ID)
// Update edge stack status
newStatus := portainer.StatusError
payload := updateStatusPayload{
Error: "test-error",
Status: &newStatus,
EndpointID: &endpoint.ID,
}
jsonPayload, err := json.Marshal(payload)
if err != nil {
t.Fatal("request error:", err)
}
r := bytes.NewBuffer(jsonPayload)
req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("/edge_stacks/%d/status", edgeStack.ID), r)
if err != nil {
t.Fatal("request error:", err)
}
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, endpoint.EdgeID)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusOK, rec.Code))
}
// Get updated edge stack
req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("/edge_stacks/%d", edgeStack.ID), nil)
if err != nil {
t.Fatal("request error:", err)
}
req.Header.Add("x-api-key", rawAPIKey)
rec = httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusOK, rec.Code))
}
data := portainer.EdgeStack{}
err = json.NewDecoder(rec.Body).Decode(&data)
if err != nil {
t.Fatal("error decoding response:", err)
}
if data.Status[endpoint.ID].Type != *payload.Status {
t.Fatalf(fmt.Sprintf("expected EdgeStackStatusType %d, found %d", payload.Status, data.Status[endpoint.ID].Type))
}
if data.Status[endpoint.ID].Error != payload.Error {
t.Fatalf(fmt.Sprintf("expected EdgeStackStatusError %s, found %s", payload.Error, data.Status[endpoint.ID].Error))
}
if data.Status[endpoint.ID].EndpointID != *payload.EndpointID {
t.Fatalf(fmt.Sprintf("expected EndpointID %d, found %d", payload.EndpointID, data.Status[endpoint.ID].EndpointID))
}
}
func TestUpdateStatusWithInvalidPayload(t *testing.T) {
handler, _, teardown := setupHandler(t)
defer teardown()
endpoint := createEndpoint(t, handler.DataStore)
edgeStack := createEdgeStack(t, handler.DataStore, endpoint.ID)
// Update edge stack status
statusError := portainer.StatusError
statusOk := portainer.StatusOk
cases := []struct {
Name string
Payload updateStatusPayload
ExpectedErrorMessage string
ExpectedStatusCode int
}{
{
"Update with nil Status",
updateStatusPayload{
Error: "test-error",
Status: nil,
EndpointID: &endpoint.ID,
},
"Invalid status",
400,
},
{
"Update with error status and empty error message",
updateStatusPayload{
Error: "",
Status: &statusError,
EndpointID: &endpoint.ID,
},
"Error message is mandatory when status is error",
400,
},
{
"Update with nil EndpointID",
updateStatusPayload{
Error: "",
Status: &statusOk,
EndpointID: nil,
},
"Invalid EnvironmentID",
400,
},
}
for _, tc := range cases {
t.Run(tc.Name, func(t *testing.T) {
jsonPayload, err := json.Marshal(tc.Payload)
if err != nil {
t.Fatal("request error:", err)
}
r := bytes.NewBuffer(jsonPayload)
req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("/edge_stacks/%d/status", edgeStack.ID), r)
if err != nil {
t.Fatal("request error:", err)
}
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, endpoint.EdgeID)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != tc.ExpectedStatusCode {
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", tc.ExpectedStatusCode, rec.Code))
}
})
}
}
// Delete Status
func TestDeleteStatus(t *testing.T) {
handler, _, teardown := setupHandler(t)
defer teardown()
endpoint := createEndpoint(t, handler.DataStore)
edgeStack := createEdgeStack(t, handler.DataStore, endpoint.ID)
req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("/edge_stacks/%d/status/%d", edgeStack.ID, endpoint.ID), nil)
if err != nil {
t.Fatal("request error:", err)
}
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, endpoint.EdgeID)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusOK, rec.Code))
}
}

View File

@@ -2,7 +2,6 @@ package edgestacks
import (
"errors"
"github.com/portainer/portainer/api/internal/endpointutils"
"net/http"
"strconv"
@@ -81,8 +80,8 @@ func (handler *Handler) edgeStackUpdate(w http.ResponseWriter, r *http.Request)
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge stack related environments from database", err}
}
oldRelatedSet := endpointutils.EndpointSet(relatedEndpointIds)
newRelatedSet := endpointutils.EndpointSet(newRelated)
oldRelatedSet := EndpointSet(relatedEndpointIds)
newRelatedSet := EndpointSet(newRelated)
endpointsToRemove := map[portainer.EndpointID]bool{}
for endpointID := range oldRelatedSet {
@@ -190,3 +189,13 @@ func (handler *Handler) edgeStackUpdate(w http.ResponseWriter, r *http.Request)
return response.JSON(w, stack)
}
func EndpointSet(endpointIDs []portainer.EndpointID) map[portainer.EndpointID]bool {
set := map[portainer.EndpointID]bool{}
for _, endpointID := range endpointIDs {
set[endpointID] = true
}
return set
}

View File

@@ -10,7 +10,6 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/filesystem"
"github.com/portainer/portainer/api/http/middlewares"
"github.com/portainer/portainer/api/http/security"
)
@@ -25,11 +24,10 @@ type Handler struct {
}
// NewHandler creates a handler to manage environment(endpoint) group operations.
func NewHandler(bouncer *security.RequestBouncer, dataStore dataservices.DataStore) *Handler {
func NewHandler(bouncer *security.RequestBouncer) *Handler {
h := &Handler{
Router: mux.NewRouter(),
requestBouncer: bouncer,
DataStore: dataStore,
}
h.Handle("/edge_stacks",
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeStackCreate)))).Methods(http.MethodPost)
@@ -45,12 +43,6 @@ func NewHandler(bouncer *security.RequestBouncer, dataStore dataservices.DataSto
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeStackFile)))).Methods(http.MethodGet)
h.Handle("/edge_stacks/{id}/status",
bouncer.PublicAccess(httperror.LoggerHandler(h.edgeStackStatusUpdate))).Methods(http.MethodPut)
edgeStackStatusRouter := h.NewRoute().Subrouter()
edgeStackStatusRouter.Use(middlewares.WithEndpoint(h.DataStore.Endpoint(), "endpoint_id"))
edgeStackStatusRouter.PathPrefix("/edge_stacks/{id}/status/{endpoint_id}").Handler(bouncer.PublicAccess(httperror.LoggerHandler(h.edgeStackStatusDelete))).Methods(http.MethodDelete)
return h
}

View File

@@ -8,7 +8,6 @@ import (
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/middlewares"
)
type logsPayload struct {
@@ -32,9 +31,16 @@ func (payload *logsPayload) Validate(r *http.Request) error {
// @failure 400
// @router /endpoints/{id}/edge/jobs/{jobID}/logs [post]
func (handler *Handler) endpointEdgeJobsLogs(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpoint, err := middlewares.FetchEndpoint(r)
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return httperror.BadRequest("Unable to find an environment on request context", err)
return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment identifier route variable", err}
}
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
if handler.DataStore.IsErrObjectNotFound(err) {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err}
}
err = handler.requestBouncer.AuthorizedEdgeEndpointOperation(r, endpoint)
@@ -60,7 +66,7 @@ func (handler *Handler) endpointEdgeJobsLogs(w http.ResponseWriter, r *http.Requ
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an edge job with the specified identifier inside the database", err}
}
err = handler.FileService.StoreEdgeJobTaskLogFileFromBytes(strconv.Itoa(edgeJobID), strconv.Itoa(int(endpoint.ID)), []byte(payload.FileContent))
err = handler.FileService.StoreEdgeJobTaskLogFileFromBytes(strconv.Itoa(edgeJobID), strconv.Itoa(endpointID), []byte(payload.FileContent))
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to save task log to the filesystem", err}
}

View File

@@ -8,7 +8,6 @@ import (
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/middlewares"
"github.com/portainer/portainer/api/internal/endpointutils"
)
@@ -30,9 +29,16 @@ type configResponse struct {
// @failure 404
// @router /endpoints/{id}/edge/stacks/{stackId} [get]
func (handler *Handler) endpointEdgeStackInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpoint, err := middlewares.FetchEndpoint(r)
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return httperror.BadRequest("Unable to find an environment on request context", err)
return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment identifier route variable", err}
}
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
if handler.DataStore.IsErrObjectNotFound(err) {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err}
}
err = handler.requestBouncer.AuthorizedEdgeEndpointOperation(r, endpoint)

View File

@@ -1,197 +0,0 @@
package endpointedge
import (
"encoding/base64"
"errors"
"fmt"
"net/http"
"strconv"
"time"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/middlewares"
)
type stackStatusResponse struct {
// EdgeStack Identifier
ID portainer.EdgeStackID `example:"1"`
// Version of this stack
Version int `example:"3"`
}
type edgeJobResponse struct {
// EdgeJob Identifier
ID portainer.EdgeJobID `json:"Id" example:"2"`
// Whether to collect logs
CollectLogs bool `json:"CollectLogs" example:"true"`
// A cron expression to schedule this job
CronExpression string `json:"CronExpression" example:"* * * * *"`
// Script to run
Script string `json:"Script" example:"echo hello"`
// Version of this EdgeJob
Version int `json:"Version" example:"2"`
}
type endpointEdgeStatusInspectResponse struct {
// Status represents the environment(endpoint) status
Status string `json:"status" example:"REQUIRED"`
// The tunnel port
Port int `json:"port" example:"8732"`
// List of requests for jobs to run on the environment(endpoint)
Schedules []edgeJobResponse `json:"schedules"`
// The current value of CheckinInterval
CheckinInterval int `json:"checkin" example:"5"`
//
Credentials string `json:"credentials"`
// List of stacks to be deployed on the environments(endpoints)
Stacks []stackStatusResponse `json:"stacks"`
}
// @id EndpointEdgeStatusInspect
// @summary Get environment(endpoint) status
// @description environment(endpoint) for edge agent to check status of environment(endpoint)
// @description **Access policy**: restricted only to Edge environments(endpoints)
// @tags endpoints
// @security ApiKeyAuth
// @security jwt
// @param id path int true "Environment(Endpoint) identifier"
// @success 200 {object} endpointEdgeStatusInspectResponse "Success"
// @failure 400 "Invalid request"
// @failure 403 "Permission denied to access environment(endpoint)"
// @failure 404 "Environment(Endpoint) not found"
// @failure 500 "Server error"
// @router /endpoints/{id}/edge/status [get]
func (handler *Handler) endpointEdgeStatusInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpoint, err := middlewares.FetchEndpoint(r)
if err != nil {
return httperror.BadRequest("Unable to find an environment on request context", err)
}
err = handler.requestBouncer.AuthorizedEdgeEndpointOperation(r, endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err}
}
if endpoint.EdgeID == "" {
edgeIdentifier := r.Header.Get(portainer.PortainerAgentEdgeIDHeader)
endpoint.EdgeID = edgeIdentifier
agentPlatform, agentPlatformErr := parseAgentPlatform(r)
if agentPlatformErr != nil {
return httperror.BadRequest("agent platform header is not valid", err)
}
endpoint.Type = agentPlatform
}
endpoint.LastCheckInDate = time.Now().Unix()
err = handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to Unable to persist environment changes inside the database", err}
}
checkinInterval := endpoint.EdgeCheckinInterval
if endpoint.EdgeCheckinInterval == 0 {
settings, err := handler.DataStore.Settings().Settings()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
}
checkinInterval = settings.EdgeAgentCheckinInterval
}
tunnel := handler.ReverseTunnelService.GetTunnelDetails(endpoint.ID)
statusResponse := endpointEdgeStatusInspectResponse{
Status: tunnel.Status,
Port: tunnel.Port,
CheckinInterval: checkinInterval,
Credentials: tunnel.Credentials,
}
schedules, handlerErr := handler.buildSchedules(endpoint.ID, tunnel)
if handlerErr != nil {
return handlerErr
}
statusResponse.Schedules = schedules
if tunnel.Status == portainer.EdgeAgentManagementRequired {
handler.ReverseTunnelService.SetTunnelStatusToActive(endpoint.ID)
}
edgeStacksStatus, handlerErr := handler.buildEdgeStacks(endpoint.ID)
if handlerErr != nil {
return handlerErr
}
statusResponse.Stacks = edgeStacksStatus
return response.JSON(w, statusResponse)
}
func parseAgentPlatform(r *http.Request) (portainer.EndpointType, error) {
agentPlatformHeader := r.Header.Get(portainer.HTTPResponseAgentPlatform)
if agentPlatformHeader == "" {
return 0, errors.New("agent platform header is missing")
}
agentPlatformNumber, err := strconv.Atoi(agentPlatformHeader)
if err != nil {
return 0, err
}
agentPlatform := portainer.AgentPlatform(agentPlatformNumber)
switch agentPlatform {
case portainer.AgentPlatformDocker:
return portainer.EdgeAgentOnDockerEnvironment, nil
case portainer.AgentPlatformKubernetes:
return portainer.EdgeAgentOnKubernetesEnvironment, nil
default:
return 0, fmt.Errorf("agent platform %v is not valid", agentPlatform)
}
}
func (handler *Handler) buildSchedules(endpointID portainer.EndpointID, tunnel portainer.TunnelDetails) ([]edgeJobResponse, *httperror.HandlerError) {
schedules := []edgeJobResponse{}
for _, job := range tunnel.Jobs {
schedule := edgeJobResponse{
ID: job.ID,
CronExpression: job.CronExpression,
CollectLogs: job.Endpoints[endpointID].CollectLogs,
Version: job.Version,
}
file, err := handler.FileService.GetFileContent(job.ScriptPath, "")
if err != nil {
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Edge job script file", err}
}
schedule.Script = base64.RawStdEncoding.EncodeToString(file)
schedules = append(schedules, schedule)
}
return schedules, nil
}
func (handler *Handler) buildEdgeStacks(endpointID portainer.EndpointID) ([]stackStatusResponse, *httperror.HandlerError) {
relation, err := handler.DataStore.EndpointRelation().EndpointRelation(endpointID)
if err != nil {
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve relation object from the database", err}
}
edgeStacksStatus := []stackStatusResponse{}
for stackID := range relation.EdgeStacks {
stack, err := handler.DataStore.EdgeStack().EdgeStack(stackID)
if err != nil {
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge stack from the database", err}
}
stackStatus := stackStatusResponse{
ID: stack.ID,
Version: stack.Version,
}
edgeStacksStatus = append(edgeStacksStatus, stackStatus)
}
return edgeStacksStatus, nil
}

View File

@@ -1,445 +0,0 @@
package endpointedge
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"os"
"testing"
"time"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/apikey"
"github.com/portainer/portainer/api/chisel"
"github.com/portainer/portainer/api/datastore"
"github.com/portainer/portainer/api/filesystem"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/jwt"
"github.com/stretchr/testify/assert"
)
type endpointTestCase struct {
endpoint portainer.Endpoint
endpointRelation portainer.EndpointRelation
expectedStatusCode int
}
var endpointTestCases = []endpointTestCase{
{
portainer.Endpoint{},
portainer.EndpointRelation{},
http.StatusNotFound,
},
{
portainer.Endpoint{
ID: -1,
Name: "endpoint-id--1",
Type: portainer.EdgeAgentOnDockerEnvironment,
URL: "https://portainer.io:9443",
EdgeID: "edge-id",
},
portainer.EndpointRelation{
EndpointID: -1,
},
http.StatusNotFound,
},
{
portainer.Endpoint{
ID: 2,
Name: "endpoint-id-2",
Type: portainer.EdgeAgentOnDockerEnvironment,
URL: "https://portainer.io:9443",
EdgeID: "",
},
portainer.EndpointRelation{
EndpointID: 2,
},
http.StatusBadRequest,
},
{
portainer.Endpoint{
ID: 4,
Name: "endpoint-id-4",
Type: portainer.EdgeAgentOnDockerEnvironment,
URL: "https://portainer.io:9443",
EdgeID: "edge-id",
},
portainer.EndpointRelation{
EndpointID: 4,
},
http.StatusOK,
},
}
func setupHandler() (*Handler, func(), error) {
tmpDir, err := os.MkdirTemp(os.TempDir(), "portainer-test")
if err != nil {
return nil, nil, fmt.Errorf("could not create a tmp dir: %w", err)
}
fs, err := filesystem.NewService(tmpDir, "")
if err != nil {
return nil, nil, fmt.Errorf("could not start a new filesystem service: %w", err)
}
_, store, storeTeardown := datastore.MustNewTestStore(true, true)
ctx := context.Background()
shutdownCtx, cancelFn := context.WithCancel(ctx)
teardown := func() {
cancelFn()
storeTeardown()
}
jwtService, err := jwt.NewService("1h", store)
if err != nil {
teardown()
return nil, nil, fmt.Errorf("could not start a new jwt service: %w", err)
}
apiKeyService := apikey.NewAPIKeyService(nil, nil)
settings, err := store.Settings().Settings()
if err != nil {
teardown()
return nil, nil, fmt.Errorf("could not create new settings: %w", err)
}
settings.TrustOnFirstConnect = true
err = store.Settings().UpdateSettings(settings)
if err != nil {
teardown()
return nil, nil, fmt.Errorf("could not update settings: %w", err)
}
handler := NewHandler(
security.NewRequestBouncer(store, jwtService, apiKeyService),
store,
fs,
chisel.NewService(store, shutdownCtx),
)
handler.ReverseTunnelService = chisel.NewService(store, shutdownCtx)
return handler, teardown, nil
}
func createEndpoint(handler *Handler, endpoint portainer.Endpoint, endpointRelation portainer.EndpointRelation) (err error) {
// Avoid setting ID below 0 to generate invalid test cases
if endpoint.ID <= 0 {
return nil
}
err = handler.DataStore.Endpoint().Create(&endpoint)
if err != nil {
return err
}
return handler.DataStore.EndpointRelation().Create(&endpointRelation)
}
func TestMissingEdgeIdentifier(t *testing.T) {
handler, teardown, err := setupHandler()
defer teardown()
if err != nil {
t.Fatal(err)
}
endpointID := portainer.EndpointID(45)
err = createEndpoint(handler, portainer.Endpoint{
ID: endpointID,
Name: "endpoint-id-45",
Type: portainer.EdgeAgentOnDockerEnvironment,
URL: "https://portainer.io:9443",
EdgeID: "edge-id",
}, portainer.EndpointRelation{EndpointID: endpointID})
if err != nil {
t.Fatal(err)
}
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/%d/edge/status", endpointID), nil)
if err != nil {
t.Fatal("request error:", err)
}
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusForbidden {
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d without Edge identifier", http.StatusForbidden, rec.Code))
}
}
func TestWithEndpoints(t *testing.T) {
handler, teardown, err := setupHandler()
defer teardown()
if err != nil {
t.Fatal(err)
}
for _, test := range endpointTestCases {
err = createEndpoint(handler, test.endpoint, test.endpointRelation)
if err != nil {
t.Fatal(err)
}
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/%d/edge/status", test.endpoint.ID), nil)
if err != nil {
t.Fatal("request error:", err)
}
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, "edge-id")
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != test.expectedStatusCode {
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d for endpoint ID: %d", test.expectedStatusCode, rec.Code, test.endpoint.ID))
}
}
}
func TestLastCheckInDateIncreases(t *testing.T) {
handler, teardown, err := setupHandler()
defer teardown()
if err != nil {
t.Fatal(err)
}
endpointID := portainer.EndpointID(56)
endpoint := portainer.Endpoint{
ID: endpointID,
Name: "test-endpoint-56",
Type: portainer.EdgeAgentOnDockerEnvironment,
URL: "https://portainer.io:9443",
EdgeID: "edge-id",
LastCheckInDate: time.Now().Unix(),
}
endpointRelation := portainer.EndpointRelation{
EndpointID: endpoint.ID,
}
err = createEndpoint(handler, endpoint, endpointRelation)
if err != nil {
t.Fatal(err)
}
time.Sleep(1 * time.Second)
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/%d/edge/status", endpoint.ID), nil)
if err != nil {
t.Fatal("request error:", err)
}
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, "edge-id")
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusOK, rec.Code))
}
updatedEndpoint, err := handler.DataStore.Endpoint().Endpoint(endpoint.ID)
if err != nil {
t.Fatal(err)
}
assert.Greater(t, updatedEndpoint.LastCheckInDate, endpoint.LastCheckInDate)
}
func TestEmptyEdgeIdWithAgentPlatformHeader(t *testing.T) {
handler, teardown, err := setupHandler()
defer teardown()
if err != nil {
t.Fatal(err)
}
endpointID := portainer.EndpointID(44)
edgeId := "edge-id"
endpoint := portainer.Endpoint{
ID: endpointID,
Name: "test-endpoint-44",
Type: portainer.EdgeAgentOnDockerEnvironment,
URL: "https://portainer.io:9443",
EdgeID: "",
}
endpointRelation := portainer.EndpointRelation{
EndpointID: endpoint.ID,
}
err = createEndpoint(handler, endpoint, endpointRelation)
if err != nil {
t.Fatal(err)
}
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/%d/edge/status", endpoint.ID), nil)
if err != nil {
t.Fatal("request error:", err)
}
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, edgeId)
req.Header.Set(portainer.HTTPResponseAgentPlatform, "1")
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d with empty edge ID", http.StatusOK, rec.Code))
}
updatedEndpoint, err := handler.DataStore.Endpoint().Endpoint(endpoint.ID)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, updatedEndpoint.EdgeID, edgeId)
}
func TestEdgeStackStatus(t *testing.T) {
handler, teardown, err := setupHandler()
defer teardown()
if err != nil {
t.Fatal(err)
}
endpointID := portainer.EndpointID(7)
endpoint := portainer.Endpoint{
ID: endpointID,
Name: "test-endpoint-7",
Type: portainer.EdgeAgentOnDockerEnvironment,
URL: "https://portainer.io:9443",
EdgeID: "edge-id",
LastCheckInDate: time.Now().Unix(),
}
edgeStackID := portainer.EdgeStackID(17)
edgeStack := portainer.EdgeStack{
ID: edgeStackID,
Name: "test-edge-stack-17",
Status: map[portainer.EndpointID]portainer.EdgeStackStatus{
endpointID: {Type: portainer.StatusOk, Error: "", EndpointID: endpoint.ID},
},
CreationDate: time.Now().Unix(),
EdgeGroups: []portainer.EdgeGroupID{1, 2},
ProjectPath: "/project/path",
EntryPoint: "entrypoint",
Version: 237,
ManifestPath: "/manifest/path",
DeploymentType: 1,
}
endpointRelation := portainer.EndpointRelation{
EndpointID: endpoint.ID,
EdgeStacks: map[portainer.EdgeStackID]bool{
edgeStack.ID: true,
},
}
handler.DataStore.EdgeStack().Create(edgeStack.ID, &edgeStack)
err = createEndpoint(handler, endpoint, endpointRelation)
if err != nil {
t.Fatal(err)
}
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/%d/edge/status", endpoint.ID), nil)
if err != nil {
t.Fatal("request error:", err)
}
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, "edge-id")
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusOK, rec.Code))
}
var data endpointEdgeStatusInspectResponse
err = json.NewDecoder(rec.Body).Decode(&data)
if err != nil {
t.Fatal("error decoding response:", err)
}
assert.Len(t, data.Stacks, 1)
assert.Equal(t, edgeStack.ID, data.Stacks[0].ID)
assert.Equal(t, edgeStack.Version, data.Stacks[0].Version)
}
func TestEdgeJobsResponse(t *testing.T) {
handler, teardown, err := setupHandler()
defer teardown()
if err != nil {
t.Fatal(err)
}
endpointID := portainer.EndpointID(77)
endpoint := portainer.Endpoint{
ID: endpointID,
Name: "test-endpoint-77",
Type: portainer.EdgeAgentOnDockerEnvironment,
URL: "https://portainer.io:9443",
EdgeID: "edge-id",
LastCheckInDate: time.Now().Unix(),
}
endpointRelation := portainer.EndpointRelation{
EndpointID: endpoint.ID,
}
err = createEndpoint(handler, endpoint, endpointRelation)
if err != nil {
t.Fatal(err)
}
path, err := handler.FileService.StoreEdgeJobFileFromBytes("test-script", []byte("pwd"))
if err != nil {
t.Fatal(err)
}
edgeJobID := portainer.EdgeJobID(35)
edgeJob := portainer.EdgeJob{
ID: edgeJobID,
Created: time.Now().Unix(),
CronExpression: "* * * * *",
Name: "test-edge-job",
ScriptPath: path,
Recurring: true,
Version: 57,
}
handler.ReverseTunnelService.AddEdgeJob(endpoint.ID, &edgeJob)
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/%d/edge/status", endpoint.ID), nil)
if err != nil {
t.Fatal("request error:", err)
}
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, "edge-id")
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusOK, rec.Code))
}
var data endpointEdgeStatusInspectResponse
err = json.NewDecoder(rec.Body).Decode(&data)
if err != nil {
t.Fatal("error decoding response:", err)
}
assert.Len(t, data.Schedules, 1)
assert.Equal(t, edgeJob.ID, data.Schedules[0].ID)
assert.Equal(t, edgeJob.CronExpression, data.Schedules[0].CronExpression)
assert.Equal(t, edgeJob.Version, data.Schedules[0].Version)
}

View File

@@ -3,8 +3,6 @@ package endpointedge
import (
"net/http"
"github.com/portainer/portainer/api/http/middlewares"
httperror "github.com/portainer/libhttp/error"
"github.com/gorilla/mux"
@@ -23,23 +21,15 @@ type Handler struct {
}
// NewHandler creates a handler to manage environment(endpoint) operations.
func NewHandler(bouncer *security.RequestBouncer, dataStore dataservices.DataStore, fileService portainer.FileService, reverseTunnelService portainer.ReverseTunnelService) *Handler {
func NewHandler(bouncer *security.RequestBouncer) *Handler {
h := &Handler{
Router: mux.NewRouter(),
requestBouncer: bouncer,
DataStore: dataStore,
FileService: fileService,
ReverseTunnelService: reverseTunnelService,
Router: mux.NewRouter(),
requestBouncer: bouncer,
}
endpointRouter := h.PathPrefix("/{id}").Subrouter()
endpointRouter.Use(middlewares.WithEndpoint(dataStore.Endpoint(), "id"))
endpointRouter.PathPrefix("/edge/status").Handler(
bouncer.PublicAccess(httperror.LoggerHandler(h.endpointEdgeStatusInspect))).Methods(http.MethodGet)
endpointRouter.PathPrefix("/edge/stacks/{stackId}").Handler(
h.Handle("/{id}/edge/stacks/{stackId}",
bouncer.PublicAccess(httperror.LoggerHandler(h.endpointEdgeStackInspect))).Methods(http.MethodGet)
endpointRouter.PathPrefix("/edge/jobs/{jobID}/logs").Handler(
h.Handle("/{id}/edge/jobs/{jobID}/logs",
bouncer.PublicAccess(httperror.LoggerHandler(h.endpointEdgeJobsLogs))).Methods(http.MethodPost)
return h
}

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