Compare commits

..

1 Commits

Author SHA1 Message Date
LP B
64b91d5258 feat(app): introduce help and support view
feat(app): help and support secondary view layout proposal

feat(app): redirect to help page on first login only

refactor(app): make UI layout more in line with mockups

feat(app/help): redirect to the appropriate view on help exit
2021-07-27 19:49:37 +02:00
469 changed files with 6215 additions and 16681 deletions

54
.github/stale.yml vendored Normal file
View File

@@ -0,0 +1,54 @@
# Config for Stalebot, limited to only `issues`
only: issues
# Issues config
issues:
daysUntilStale: 60
daysUntilClose: 7
# Limit the number of actions per hour, from 1-30. Default is 30
limitPerRun: 30
# Issues with these labels will never be considered stale
exemptLabels:
- kind/enhancement
- kind/question
- kind/style
- kind/workaround
- kind/refactor
- bug/need-confirmation
- bug/confirmed
- status/discuss
# Only issues with all of these labels are checked if stale. Defaults to `[]` (disabled)
onlyLabels: []
# Set to true to ignore issues in a project (defaults to false)
exemptProjects: true
# Set to true to ignore issues in a milestone (defaults to false)
exemptMilestones: true
# Set to true to ignore issues with an assignee (defaults to false)
exemptAssignees: true
# Label to use when marking an issue as stale
staleLabel: status/stale
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been marked as stale as it has not had recent activity,
it will be closed if no further activity occurs in the next 7 days.
If you believe that it has been incorrectly labelled as stale,
leave a comment and the label will be removed.
# Comment to post when removing the stale label.
# unmarkComment: >
# Your comment here.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: >
Since no further activity has appeared on this issue it will be closed.
If you believe that it has been incorrectly closed, leave a comment
mentioning `ametdoohan`, `balasu` or `keverv` and one of our staff will then review the issue.
Note - If it is an old bug report, make sure that it is reproduceable in the
latest version of Portainer as it may have already been fixed.

View File

@@ -1,15 +0,0 @@
on:
push:
branches:
- develop
- 'release/**'
jobs:
triage:
runs-on: ubuntu-latest
steps:
- uses: mschilde/auto-label-merge-conflicts@master
with:
CONFLICT_LABEL_NAME: 'has conflicts'
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
MAX_RETRIES: 5
WAIT_MS: 5000

View File

@@ -1,27 +0,0 @@
name: Close Stale Issues
on:
schedule:
- cron: '0 12 * * *'
jobs:
stale:
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- uses: actions/stale@v4.0.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
# Issue Config
days-before-issue-stale: 60
days-before-issue-close: 7
stale-issue-label: 'status/stale'
exempt-all-issue-milestones: true # Do not stale issues in a milestone
exempt-issue-labels: kind/enhancement, kind/style, kind/workaround, kind/refactor, bug/need-confirmation, bug/confirmed, status/discuss
stale-issue-message: 'This issue has been marked as stale as it has not had recent activity, it will be closed if no further activity occurs in the next 7 days. If you believe that it has been incorrectly labelled as stale, leave a comment and the label will be removed.'
close-issue-message: 'Since no further activity has appeared on this issue it will be closed. If you believe that it has been incorrectly closed, leave a comment mentioning `portainer/support` and one of our staff will then review the issue. Note - If it is an old bug report, make sure that it is reproduceable in the latest version of Portainer as it may have already been fixed.'
# Pull Request Config
days-before-pr-stale: -1 # Do not stale pull request
days-before-pr-close: -1 # Do not close pull request

View File

@@ -163,19 +163,5 @@
"// @failure 500 \"Server error\"",
"// @router /{id} [get]"
]
},
"analytics": {
"prefix": "nlt",
"body": ["analytics-on", "analytics-category=\"$1\"", "analytics-event=\"$2\""],
"description": "analytics"
},
"analytics-if": {
"prefix": "nltf",
"body": ["analytics-if=\"$1\""],
"description": "analytics"
},
"analytics-metadata": {
"prefix": "nltm",
"body": "analytics-properties=\"{ metadata: { $1 } }\""
}
}

View File

@@ -1,14 +1,16 @@
<p align="center">
<img title="portainer" src='https://github.com/portainer/portainer/blob/develop/app/assets/images/portainer-github-banner.png?raw=true' />
<img title="portainer" src='https://github.com/portainer/portainer/blob/develop/app/assets/images/logo_alt.png?raw=true' />
</p>
**Portainer CE** is a lightweight universal management GUI that can be used to **easily** manage Docker, Swarm, Kubernetes and ACI environments. It is designed to be as **simple** to deploy as it is to use.
[![Docker Pulls](https://img.shields.io/docker/pulls/portainer/portainer.svg)](https://hub.docker.com/r/portainer/portainer/)
[![Microbadger](https://images.microbadger.com/badges/image/portainer/portainer.svg)](http://microbadger.com/images/portainer/portainer 'Image size')
[![Build Status](https://portainer.visualstudio.com/Portainer%20CI/_apis/build/status/Portainer%20CI?branchName=develop)](https://portainer.visualstudio.com/Portainer%20CI/_build/latest?definitionId=3&branchName=develop)
[![Code Climate](https://codeclimate.com/github/portainer/portainer/badges/gpa.svg)](https://codeclimate.com/github/portainer/portainer)
[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=YHXZJQNJQ36H6)
Portainer consists of a single container that can run on any cluster. It can be deployed as a Linux container or a Windows native container.
**Portainer** allows you to manage all your orchestrator resources (containers, images, volumes, networks and more) through a super-simple graphical interface.
A fully supported version of Portainer is available for business use. Visit http://www.portainer.io to learn more
**_Portainer_** is a lightweight management UI which allows you to **easily** manage your different Docker environments (Docker hosts or Swarm clusters).
**_Portainer_** is meant to be as **simple** to deploy as it is to use. It consists of a single container that can run on any Docker engine (can be deployed as Linux container or a Windows native container, supports other platforms too).
**_Portainer_** allows you to manage all your Docker resources (containers, images, volumes, networks and more!) It is compatible with the _standalone Docker_ engine and with _Docker Swarm mode_.
## Demo
@@ -16,38 +18,30 @@ You can try out the public demo instance: http://demo.portainer.io/ (login with
Please note that the public demo cluster is **reset every 15min**.
## Latest Version
Alternatively, you can deploy a copy of the demo stack inside a [play-with-docker (PWD)](https://labs.play-with-docker.com) playground:
Portainer CE is updated regularly. We aim to do an update release every couple of months.
- Browse [PWD/?stack=portainer-demo/play-with-docker/docker-stack.yml](http://play-with-docker.com/?stack=https://raw.githubusercontent.com/portainer/portainer-demo/master/play-with-docker/docker-stack.yml)
- Sign in with your [Docker ID](https://docs.docker.com/docker-id)
- Follow [these](https://github.com/portainer/portainer-demo/blob/master/play-with-docker/docker-stack.yml#L5-L8) steps.
**The latest version of Portainer is 2.6.x** And you can find the release notes [here.](https://www.portainer.io/blog/new-portainer-ce-2.6.0-release)
Portainer is on version 2, the second number denotes the month of release.
Unlike the public demo, the playground sessions are deleted after 4 hours. Apart from that, all the settings are the same, including default credentials.
## Getting started
- [Deploy Portainer](https://documentation.portainer.io/quickstart/)
- [Documentation](https://documentation.portainer.io)
- [Contribute to the project](https://documentation.portainer.io/contributing/instructions/)
## Features & Functions
View [this](https://www.portainer.io/products) table to see all of the Portainer CE functionality and compare to Portainer Business.
- [Portainer CE for Docker / Docker Swarm](https://www.portainer.io/solutions/docker)
- [Portainer CE for Kubernetes](https://www.portainer.io/solutions/kubernetes-ui)
- [Portainer CE for Azure ACI](https://www.portainer.io/solutions/serverless-containers)
- [Building Portainer](https://documentation.portainer.io/contributing/instructions/)
## Getting help
Portainer CE is an open source project and is supported by the community. You can buy a supported version of Portainer at portainer.io
For FORMAL Support, please purchase a support subscription from here: https://www.portainer.io/products/portainer-business
Learn more about Portainers community support channels [here.](https://www.portainer.io/help_about)
For community support: You can find more information about Portainer's community support framework policy here: https://www.portainer.io/products/community-edition/customer-success
- Issues: https://github.com/portainer/portainer/issues
- FAQ: https://documentation.portainer.io
- Slack (chat): https://portainer.io/slack/
You can join the Portainer Community by visiting community.portainer.io. This will give you advance notice of events, content and other related Portainer content.
## Reporting bugs and contributing
- Want to report a bug or request a feature? Please open [an issue](https://github.com/portainer/portainer/issues/new).
@@ -57,10 +51,6 @@ You can join the Portainer Community by visiting community.portainer.io. This wi
- Here at Portainer, we believe in [responsible disclosure](https://en.wikipedia.org/wiki/Responsible_disclosure) of security issues. If you have found a security issue, please report it to <security@portainer.io>.
## WORK FOR US
If you are a developer, and our code in this repo makes sense to you, we would love to hear from you. We are always on the hunt for awesome devs, either freelance or employed. Drop us a line to info@portainer.io with your details and we will be in touch.
## Privacy
**To make sure we focus our development effort in the right places we need to know which features get used most often. To give us this information we use [Matomo Analytics](https://matomo.org/), which is hosted in Germany and is fully GDPR compliant.**

View File

@@ -10,7 +10,6 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/archive"
"github.com/portainer/portainer/api/crypto"
"github.com/portainer/portainer/api/filesystem"
"github.com/portainer/portainer/api/http/offlinegate"
)
@@ -33,7 +32,7 @@ func CreateBackupArchive(password string, gate *offlinegate.OfflineGate, datasto
}
for _, filename := range filesToBackup {
err := filesystem.CopyPath(filepath.Join(filestorePath, filename), backupDirPath)
err := copyPath(filepath.Join(filestorePath, filename), backupDirPath)
if err != nil {
return "", errors.Wrap(err, "Failed to create backup file")
}

View File

@@ -1,4 +1,4 @@
package filesystem
package backup
import (
"errors"
@@ -8,8 +8,7 @@ import (
"strings"
)
// CopyPath copies file or directory defined by the path to the toDir path
func CopyPath(path string, toDir string) error {
func copyPath(path string, toDir string) error {
info, err := os.Stat(path)
if err != nil && errors.Is(err, os.ErrNotExist) {
// skip copy if file does not exist
@@ -21,30 +20,17 @@ func CopyPath(path string, toDir string) error {
return copyFile(path, destination)
}
return CopyDir(path, toDir, true)
return copyDir(path, toDir)
}
// CopyDir copies contents of fromDir to toDir.
// When keepParent is true, contents will be copied with their immediate parent dir,
// i.e. given /from/dirA and /to/dirB with keepParent == true, result will be /to/dirB/dirA/<children>
func CopyDir(fromDir, toDir string, keepParent bool) error {
func copyDir(fromDir, toDir string) error {
cleanedSourcePath := filepath.Clean(fromDir)
parentDirectory := filepath.Dir(cleanedSourcePath)
err := filepath.Walk(cleanedSourcePath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
var destination string
if keepParent {
destination = filepath.Join(toDir, strings.TrimPrefix(path, parentDirectory))
} else {
destination = filepath.Join(toDir, strings.TrimPrefix(path, cleanedSourcePath))
}
if destination == "" {
return nil
}
destination := filepath.Join(toDir, strings.TrimPrefix(path, parentDirectory))
if info.IsDir() {
return nil // skip directory creations
}

105
api/backup/copy_test.go Normal file
View File

@@ -0,0 +1,105 @@
package backup
import (
"io/ioutil"
"os"
"path"
"path/filepath"
"testing"
"github.com/docker/docker/pkg/ioutils"
"github.com/stretchr/testify/assert"
)
func listFiles(dir string) []string {
items := make([]string, 0)
filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if path == dir {
return nil
}
items = append(items, path)
return nil
})
return items
}
func contains(t *testing.T, list []string, path string) {
assert.Contains(t, list, path)
copyContent, _ := ioutil.ReadFile(path)
assert.Equal(t, "content\n", string(copyContent))
}
func Test_copyFile_returnsError_whenSourceDoesNotExist(t *testing.T) {
tmpdir, _ := ioutils.TempDir("", "backup")
defer os.RemoveAll(tmpdir)
err := copyFile("does-not-exist", tmpdir)
assert.NotNil(t, err)
}
func Test_copyFile_shouldMakeAbackup(t *testing.T) {
tmpdir, _ := ioutils.TempDir("", "backup")
defer os.RemoveAll(tmpdir)
content := []byte("content")
ioutil.WriteFile(path.Join(tmpdir, "origin"), content, 0600)
err := copyFile(path.Join(tmpdir, "origin"), path.Join(tmpdir, "copy"))
assert.Nil(t, err)
copyContent, _ := ioutil.ReadFile(path.Join(tmpdir, "copy"))
assert.Equal(t, content, copyContent)
}
func Test_copyDir_shouldCopyAllFilesAndDirectories(t *testing.T) {
destination, _ := ioutils.TempDir("", "destination")
defer os.RemoveAll(destination)
err := copyDir("./test_assets/copy_test", destination)
assert.Nil(t, err)
createdFiles := listFiles(destination)
contains(t, createdFiles, filepath.Join(destination, "copy_test", "outer"))
contains(t, createdFiles, filepath.Join(destination, "copy_test", "dir", ".dotfile"))
contains(t, createdFiles, filepath.Join(destination, "copy_test", "dir", "inner"))
}
func Test_backupPath_shouldSkipWhenNotExist(t *testing.T) {
tmpdir, _ := ioutils.TempDir("", "backup")
defer os.RemoveAll(tmpdir)
err := copyPath("does-not-exists", tmpdir)
assert.Nil(t, err)
assert.Empty(t, listFiles(tmpdir))
}
func Test_backupPath_shouldCopyFile(t *testing.T) {
tmpdir, _ := ioutils.TempDir("", "backup")
defer os.RemoveAll(tmpdir)
content := []byte("content")
ioutil.WriteFile(path.Join(tmpdir, "file"), content, 0600)
os.MkdirAll(path.Join(tmpdir, "backup"), 0700)
err := copyPath(path.Join(tmpdir, "file"), path.Join(tmpdir, "backup"))
assert.Nil(t, err)
copyContent, err := ioutil.ReadFile(path.Join(tmpdir, "backup", "file"))
assert.Nil(t, err)
assert.Equal(t, content, copyContent)
}
func Test_backupPath_shouldCopyDir(t *testing.T) {
destination, _ := ioutils.TempDir("", "destination")
defer os.RemoveAll(destination)
err := copyPath("./test_assets/copy_test", destination)
assert.Nil(t, err)
createdFiles := listFiles(destination)
contains(t, createdFiles, filepath.Join(destination, "copy_test", "outer"))
contains(t, createdFiles, filepath.Join(destination, "copy_test", "dir", ".dotfile"))
contains(t, createdFiles, filepath.Join(destination, "copy_test", "dir", "inner"))
}

View File

@@ -11,7 +11,6 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/archive"
"github.com/portainer/portainer/api/crypto"
"github.com/portainer/portainer/api/filesystem"
"github.com/portainer/portainer/api/http/offlinegate"
)
@@ -60,7 +59,7 @@ func extractArchive(r io.Reader, destinationDirPath string) error {
func restoreFiles(srcDir string, destinationDir string) error {
for _, filename := range filesToRestore {
err := filesystem.CopyPath(filepath.Join(srcDir, filename), destinationDir)
err := copyPath(filepath.Join(srcDir, filename), destinationDir)
if err != nil {
return err
}

View File

@@ -25,7 +25,6 @@ import (
"github.com/portainer/portainer/api/bolt/role"
"github.com/portainer/portainer/api/bolt/schedule"
"github.com/portainer/portainer/api/bolt/settings"
"github.com/portainer/portainer/api/bolt/ssl"
"github.com/portainer/portainer/api/bolt/stack"
"github.com/portainer/portainer/api/bolt/tag"
"github.com/portainer/portainer/api/bolt/team"
@@ -62,7 +61,6 @@ type Store struct {
RoleService *role.Service
ScheduleService *schedule.Service
SettingsService *settings.Service
SSLSettingsService *ssl.Service
StackService *stack.Service
TagService *tag.Service
TeamMembershipService *teammembership.Service
@@ -116,7 +114,6 @@ func (store *Store) Open() error {
}
// Close closes the BoltDB database.
// Safe to being called multiple times.
func (store *Store) Close() error {
if store.connection.DB != nil {
return store.connection.Close()

View File

@@ -45,7 +45,6 @@ func (store *Store) Init() error {
EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds,
TemplatesURL: portainer.DefaultTemplatesURL,
UserSessionTimeout: portainer.DefaultUserSessionTimeout,
KubeconfigExpiry: portainer.DefaultKubeconfigExpiry,
}
err = store.SettingsService.UpdateSettings(defaultSettings)
@@ -56,22 +55,6 @@ func (store *Store) Init() error {
return err
}
_, err = store.SSLSettings().Settings()
if err != nil {
if err != errors.ErrObjectNotFound {
return err
}
defaultSSLSettings := &portainer.SSLSettings{
HTTPEnabled: true,
}
err = store.SSLSettings().UpdateSettings(defaultSSLSettings)
if err != nil {
return err
}
}
groups, err := store.EndpointGroupService.EndpointGroups()
if err != nil {
return err

View File

@@ -5,7 +5,7 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/internal/endpointutils"
endpointutils "github.com/portainer/portainer/api/internal/endpoint"
snapshotutils "github.com/portainer/portainer/api/internal/snapshot"
)
@@ -24,10 +24,6 @@ func (m *Migrator) migrateDBVersionToDB32() error {
return err
}
if err := m.kubeconfigExpiryToDB32(); err != nil {
return err
}
return nil
}
@@ -215,12 +211,3 @@ func findResourcesToUpdateForDB32(dockerID string, volumesData map[string]interf
}
}
}
func (m *Migrator) kubeconfigExpiryToDB32() error {
settings, err := m.settingsService.Settings()
if err != nil {
return err
}
settings.KubeconfigExpiry = portainer.DefaultKubeconfigExpiry
return m.settingsService.UpdateSettings(settings)
}

View File

@@ -1,32 +0,0 @@
package migrator
import (
portainer "github.com/portainer/portainer/api"
)
func (m *Migrator) migrateDBVersionTo33() error {
err := migrateStackEntryPoint(m.stackService)
if err != nil {
return err
}
return nil
}
func migrateStackEntryPoint(stackService portainer.StackService) error {
stacks, err := stackService.Stacks()
if err != nil {
return err
}
for i := range stacks {
stack := &stacks[i]
if stack.GitConfig == nil {
continue
}
stack.GitConfig.ConfigFilePath = stack.EntryPoint
if err := stackService.UpdateStack(stack.ID, stack); err != nil {
return err
}
}
return nil
}

View File

@@ -1,51 +0,0 @@
package migrator
import (
"path"
"testing"
"time"
"github.com/boltdb/bolt"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
"github.com/portainer/portainer/api/bolt/stack"
gittypes "github.com/portainer/portainer/api/git/types"
"github.com/stretchr/testify/assert"
)
func TestMigrateStackEntryPoint(t *testing.T) {
dbConn, err := bolt.Open(path.Join(t.TempDir(), "portainer-ee-mig-33.db"), 0600, &bolt.Options{Timeout: 1 * time.Second})
assert.NoError(t, err, "failed to init testing DB connection")
defer dbConn.Close()
stackService, err := stack.NewService(&internal.DbConnection{DB: dbConn})
assert.NoError(t, err, "failed to init testing Stack service")
stacks := []*portainer.Stack{
{
ID: 1,
EntryPoint: "dir/sub/compose.yml",
},
{
ID: 2,
EntryPoint: "dir/sub/compose.yml",
GitConfig: &gittypes.RepoConfig{},
},
}
for _, s := range stacks {
err := stackService.CreateStack(s)
assert.NoError(t, err, "failed to create stack")
}
err = migrateStackEntryPoint(stackService)
assert.NoError(t, err, "failed to migrate entry point to Git ConfigFilePath")
s, err := stackService.Stack(1)
assert.NoError(t, err)
assert.Nil(t, s.GitConfig, "first stack should not have git config")
s, err = stackService.Stack(2)
assert.NoError(t, err)
assert.Equal(t, "dir/sub/compose.yml", s.GitConfig.ConfigFilePath, "second stack should have config file path migrated")
}

View File

@@ -381,11 +381,5 @@ func (m *Migrator) Migrate() error {
}
}
if m.currentDBVersion < 33 {
if err := m.migrateDBVersionTo33(); err != nil {
return err
}
}
return m.versionService.StoreDBVersion(portainer.DBVersion)
}

View File

@@ -16,7 +16,6 @@ import (
"github.com/portainer/portainer/api/bolt/role"
"github.com/portainer/portainer/api/bolt/schedule"
"github.com/portainer/portainer/api/bolt/settings"
"github.com/portainer/portainer/api/bolt/ssl"
"github.com/portainer/portainer/api/bolt/stack"
"github.com/portainer/portainer/api/bolt/tag"
"github.com/portainer/portainer/api/bolt/team"
@@ -106,12 +105,6 @@ func (store *Store) initServices() error {
}
store.SettingsService = settingsService
sslSettingsService, err := ssl.NewService(store.connection)
if err != nil {
return err
}
store.SSLSettingsService = sslSettingsService
stackService, err := stack.NewService(store.connection)
if err != nil {
return err
@@ -224,11 +217,6 @@ func (store *Store) Settings() portainer.SettingsService {
return store.SettingsService
}
// SSLSettings gives access to the SSL Settings data management layer
func (store *Store) SSLSettings() portainer.SSLSettingsService {
return store.SSLSettingsService
}
// Stack gives access to the Stack data management layer
func (store *Store) Stack() portainer.StackService {
return store.StackService

View File

@@ -1,46 +0,0 @@
package ssl
import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "ssl"
key = "SSL"
)
// Service represents a service for managing ssl data.
type Service struct {
connection *internal.DbConnection
}
// NewService creates a new instance of a service.
func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(connection, BucketName)
if err != nil {
return nil, err
}
return &Service{
connection: connection,
}, nil
}
// Settings retrieve the ssl settings object.
func (service *Service) Settings() (*portainer.SSLSettings, error) {
var settings portainer.SSLSettings
err := internal.GetObject(service.connection, BucketName, []byte(key), &settings)
if err != nil {
return nil, err
}
return &settings, nil
}
// UpdateSettings persists a SSLSettings object.
func (service *Service) UpdateSettings(settings *portainer.SSLSettings) error {
return internal.UpdateObject(service.connection, BucketName, []byte(key), settings)
}

View File

@@ -1,14 +1,11 @@
package stack
import (
"strings"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/bolt/internal"
"github.com/boltdb/bolt"
pkgerrors "github.com/pkg/errors"
)
const (
@@ -136,76 +133,3 @@ func (service *Service) DeleteStack(ID portainer.StackID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.connection, BucketName, identifier)
}
// StackByWebhookID returns a pointer to a stack object by webhook ID.
// It returns nil, errors.ErrObjectNotFound if there's no stack associated with the webhook ID.
func (service *Service) StackByWebhookID(id string) (*portainer.Stack, error) {
if id == "" {
return nil, pkgerrors.New("webhook ID can't be empty string")
}
var stack portainer.Stack
found := false
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var t struct {
AutoUpdate *struct {
WebhookID string `json:"Webhook"`
} `json:"AutoUpdate"`
}
err := internal.UnmarshalObject(v, &t)
if err != nil {
return err
}
if t.AutoUpdate != nil && strings.EqualFold(t.AutoUpdate.WebhookID, id) {
found = true
err := internal.UnmarshalObject(v, &stack)
if err != nil {
return err
}
break
}
}
return nil
})
if err != nil {
return nil, err
}
if !found {
return nil, errors.ErrObjectNotFound
}
return &stack, nil
}
// RefreshableStacks returns stacks that are configured for a periodic update
func (service *Service) RefreshableStacks() ([]portainer.Stack, error) {
stacks := make([]portainer.Stack, 0)
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
var stack portainer.Stack
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
err := internal.UnmarshalObject(v, &stack)
if err != nil {
return err
}
if stack.AutoUpdate != nil && stack.AutoUpdate.Interval != "" {
stacks = append(stacks, stack)
}
}
return nil
})
return stacks, err
}

View File

@@ -1,111 +0,0 @@
package tests
import (
"testing"
"time"
"github.com/portainer/portainer/api/bolt"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/bolt/bolttest"
"github.com/gofrs/uuid"
"github.com/stretchr/testify/assert"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/filesystem"
)
func newGuidString(t *testing.T) string {
uuid, err := uuid.NewV4()
assert.NoError(t, err)
return uuid.String()
}
type stackBuilder struct {
t *testing.T
count int
store *bolt.Store
}
func TestService_StackByWebhookID(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode. Normally takes ~1s to run.")
}
store, teardown := bolttest.MustNewTestStore(true)
defer teardown()
b := stackBuilder{t: t, store: store}
b.createNewStack(newGuidString(t))
for i := 0; i < 10; i++ {
b.createNewStack("")
}
webhookID := newGuidString(t)
stack := b.createNewStack(webhookID)
// can find a stack by webhook ID
got, err := store.StackService.StackByWebhookID(webhookID)
assert.NoError(t, err)
assert.Equal(t, stack, *got)
// returns nil and object not found error if there's no stack associated with the webhook
got, err = store.StackService.StackByWebhookID(newGuidString(t))
assert.Nil(t, got)
assert.ErrorIs(t, err, bolterrors.ErrObjectNotFound)
}
func (b *stackBuilder) createNewStack(webhookID string) portainer.Stack {
b.count++
stack := portainer.Stack{
ID: portainer.StackID(b.count),
Name: "Name",
Type: portainer.DockerComposeStack,
EndpointID: 2,
EntryPoint: filesystem.ComposeFileDefaultName,
Env: []portainer.Pair{{"Name1", "Value1"}},
Status: portainer.StackStatusActive,
CreationDate: time.Now().Unix(),
ProjectPath: "/tmp/project",
CreatedBy: "test",
}
if webhookID == "" {
if b.count%2 == 0 {
stack.AutoUpdate = &portainer.StackAutoUpdate{
Interval: "",
Webhook: "",
}
} // else keep AutoUpdate nil
} else {
stack.AutoUpdate = &portainer.StackAutoUpdate{Webhook: webhookID}
}
err := b.store.StackService.CreateStack(&stack)
assert.NoError(b.t, err)
return stack
}
func Test_RefreshableStacks(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode. Normally takes ~1s to run.")
}
store, teardown := bolttest.MustNewTestStore(true)
defer teardown()
staticStack := portainer.Stack{ID: 1}
stackWithWebhook := portainer.Stack{ID: 2, AutoUpdate: &portainer.StackAutoUpdate{Webhook: "webhook"}}
refreshableStack := portainer.Stack{ID: 3, AutoUpdate: &portainer.StackAutoUpdate{Interval: "1m"}}
for _, stack := range []*portainer.Stack{&staticStack, &stackWithWebhook, &refreshableStack} {
err := store.Stack().CreateStack(stack)
assert.NoError(t, err)
}
stacks, err := store.Stack().RefreshableStacks()
assert.NoError(t, err)
assert.ElementsMatch(t, []portainer.Stack{refreshableStack}, stacks)
}

View File

@@ -139,6 +139,7 @@ func (service *Service) CreateUser(user *portainer.User) error {
id, _ := bucket.NextSequence()
user.ID = portainer.UserID(id)
user.Username = strings.ToLower(user.Username)
user.HasAuthenticatedOnce = false
data, err := internal.MarshalObject(user)
if err != nil {

View File

@@ -56,32 +56,6 @@ func (service *Service) GetTunnelDetails(endpointID portainer.EndpointID) *porta
}
}
// GetActiveTunnel retrieves an active tunnel which allows communicating with edge agent
func (service *Service) GetActiveTunnel(endpoint *portainer.Endpoint) (*portainer.TunnelDetails, error) {
tunnel := service.GetTunnelDetails(endpoint.ID)
if tunnel.Status == portainer.EdgeAgentIdle || tunnel.Status == portainer.EdgeAgentManagementRequired {
err := service.SetTunnelStatusToRequired(endpoint.ID)
if err != nil {
return nil, fmt.Errorf("failed opening tunnel to endpoint: %w", err)
}
if endpoint.EdgeCheckinInterval == 0 {
settings, err := service.dataStore.Settings().Settings()
if err != nil {
return nil, fmt.Errorf("failed fetching settings from db: %w", err)
}
endpoint.EdgeCheckinInterval = settings.EdgeAgentCheckinInterval
}
waitForAgentToConnect := time.Duration(endpoint.EdgeCheckinInterval) * time.Second
time.Sleep(waitForAgentToConnect * 2)
}
tunnel = service.GetTunnelDetails(endpoint.ID)
return tunnel, nil
}
// SetTunnelStatusToActive update the status of the tunnel associated to the specified endpoint.
// It sets the status to ACTIVE.
func (service *Service) SetTunnelStatusToActive(endpointID portainer.EndpointID) {

View File

@@ -5,7 +5,7 @@ import (
"log"
"time"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api"
"os"
"path/filepath"
@@ -30,7 +30,6 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
flags := &portainer.CLIFlags{
Addr: kingpin.Flag("bind", "Address and port to serve Portainer").Default(defaultBindAddress).Short('p').String(),
AddrHTTPS: kingpin.Flag("bind-https", "Address and port to serve Portainer via https").Default(defaultHTTPSBindAddress).String(),
TunnelAddr: kingpin.Flag("tunnel-addr", "Address to serve the tunnel server").Default(defaultTunnelServerAddress).String(),
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(),
@@ -43,10 +42,9 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
TLSCacert: kingpin.Flag("tlscacert", "Path to the CA").Default(defaultTLSCACertPath).String(),
TLSCert: kingpin.Flag("tlscert", "Path to the TLS certificate file").Default(defaultTLSCertPath).String(),
TLSKey: kingpin.Flag("tlskey", "Path to the TLS key").Default(defaultTLSKeyPath).String(),
HTTPDisabled: kingpin.Flag("http-disabled", "Serve portainer only on https").Default(defaultHTTPDisabled).Bool(),
SSL: kingpin.Flag("ssl", "Secure Portainer instance using SSL (deprecated)").Default(defaultSSL).Bool(),
SSLCert: kingpin.Flag("sslcert", "Path to the SSL certificate used to secure the Portainer instance").String(),
SSLKey: kingpin.Flag("sslkey", "Path to the SSL key used to secure the Portainer instance").String(),
SSL: kingpin.Flag("ssl", "Secure Portainer instance using SSL").Default(defaultSSL).Bool(),
SSLCert: kingpin.Flag("sslcert", "Path to the SSL certificate used to secure the Portainer instance").Default(defaultSSLCertPath).String(),
SSLKey: kingpin.Flag("sslkey", "Path to the SSL key used to secure the Portainer instance").Default(defaultSSLKeyPath).String(),
SnapshotInterval: kingpin.Flag("snapshot-interval", "Duration between each endpoint snapshot job").Default(defaultSnapshotInterval).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(),
@@ -94,10 +92,6 @@ func displayDeprecationWarnings(flags *portainer.CLIFlags) {
if *flags.NoAnalytics {
log.Println("Warning: The --no-analytics flag has been kept to allow migration of instances running a previous version of Portainer with this flag enabled, to version 2.0 where enabling this flag will have no effect.")
}
if *flags.SSL {
log.Println("Warning: SSL is enabled by default and there is no need for the --ssl flag. It has been kept to allow migration of instances running a previous version of Portainer with this flag enabled")
}
}
func validateEndpointURL(endpointURL string) error {

View File

@@ -4,7 +4,6 @@ package cli
const (
defaultBindAddress = ":9000"
defaultHTTPSBindAddress = ":9443"
defaultTunnelServerAddress = "0.0.0.0"
defaultTunnelServerPort = "8000"
defaultDataDirectory = "/data"
@@ -14,7 +13,6 @@ const (
defaultTLSCACertPath = "/certs/ca.pem"
defaultTLSCertPath = "/certs/cert.pem"
defaultTLSKeyPath = "/certs/key.pem"
defaultHTTPDisabled = "false"
defaultSSL = "false"
defaultSSLCertPath = "/certs/portainer.crt"
defaultSSLKeyPath = "/certs/portainer.key"

View File

@@ -2,7 +2,6 @@ package cli
const (
defaultBindAddress = ":9000"
defaultHTTPSBindAddress = ":9443"
defaultTunnelServerAddress = "0.0.0.0"
defaultTunnelServerPort = "8000"
defaultDataDirectory = "C:\\data"
@@ -12,7 +11,6 @@ const (
defaultTLSCACertPath = "C:\\certs\\ca.pem"
defaultTLSCertPath = "C:\\certs\\cert.pem"
defaultTLSKeyPath = "C:\\certs\\key.pem"
defaultHTTPDisabled = "false"
defaultSSL = "false"
defaultSSLCertPath = "C:\\certs\\portainer.crt"
defaultSSLKeyPath = "C:\\certs\\portainer.key"

View File

@@ -1,19 +0,0 @@
package main
import (
"log"
"github.com/sirupsen/logrus"
)
func configureLogger() {
logger := logrus.New() // logger is to implicitly substitute stdlib's log
log.SetOutput(logger.Writer())
formatter := &logrus.TextFormatter{DisableTimestamp: true, DisableLevelTruncation: true}
logger.SetFormatter(formatter)
logrus.SetFormatter(formatter)
logger.SetLevel(logrus.DebugLevel)
logrus.SetLevel(logrus.DebugLevel)
}

View File

@@ -6,6 +6,7 @@ import (
"os"
"strings"
wrapper "github.com/portainer/docker-compose-wrapper"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt"
"github.com/portainer/portainer/api/chisel"
@@ -23,14 +24,12 @@ import (
"github.com/portainer/portainer/api/internal/authorization"
"github.com/portainer/portainer/api/internal/edge"
"github.com/portainer/portainer/api/internal/snapshot"
"github.com/portainer/portainer/api/internal/ssl"
"github.com/portainer/portainer/api/jwt"
"github.com/portainer/portainer/api/kubernetes"
kubecli "github.com/portainer/portainer/api/kubernetes/cli"
"github.com/portainer/portainer/api/ldap"
"github.com/portainer/portainer/api/libcompose"
"github.com/portainer/portainer/api/oauth"
"github.com/portainer/portainer/api/scheduler"
"github.com/portainer/portainer/api/stacks"
)
func initCLI() *portainer.CLIFlags {
@@ -55,7 +54,7 @@ func initFileService(dataStorePath string) portainer.FileService {
return fileService
}
func initDataStore(dataStorePath string, fileService portainer.FileService, shutdownCtx context.Context) portainer.DataStore {
func initDataStore(dataStorePath string, fileService portainer.FileService) portainer.DataStore {
store, err := bolt.NewStore(dataStorePath, fileService)
if err != nil {
log.Fatalf("failed creating data store: %v", err)
@@ -75,31 +74,29 @@ func initDataStore(dataStorePath string, fileService portainer.FileService, shut
if err != nil {
log.Fatalf("failed migration: %v", err)
}
go shutdownDatastore(shutdownCtx, store)
return store
}
func shutdownDatastore(shutdownCtx context.Context, datastore portainer.DataStore) {
<-shutdownCtx.Done()
datastore.Close()
}
func initComposeStackManager(assetsPath string, configPath string, reverseTunnelService portainer.ReverseTunnelService, proxyManager *proxy.Manager) portainer.ComposeStackManager {
composeWrapper, err := exec.NewComposeStackManager(assetsPath, configPath, proxyManager)
func initComposeStackManager(assetsPath string, dataStorePath string, reverseTunnelService portainer.ReverseTunnelService, proxyManager *proxy.Manager) portainer.ComposeStackManager {
composeWrapper, err := exec.NewComposeStackManager(assetsPath, dataStorePath, proxyManager)
if err != nil {
log.Fatalf("failed creating compose manager: %s", err)
if err == wrapper.ErrBinaryNotFound {
log.Printf("[INFO] [message: docker-compose binary not found, falling back to libcompose]")
return libcompose.NewComposeStackManager(dataStorePath, reverseTunnelService)
}
log.Fatalf("failed initalizing compose stack manager; err=%s", err)
}
return composeWrapper
}
func initSwarmStackManager(assetsPath string, configPath string, signatureService portainer.DigitalSignatureService, fileService portainer.FileService, reverseTunnelService portainer.ReverseTunnelService) (portainer.SwarmStackManager, error) {
return exec.NewSwarmStackManager(assetsPath, configPath, signatureService, fileService, reverseTunnelService)
func initSwarmStackManager(assetsPath string, dataStorePath string, signatureService portainer.DigitalSignatureService, fileService portainer.FileService, reverseTunnelService portainer.ReverseTunnelService) (portainer.SwarmStackManager, error) {
return exec.NewSwarmStackManager(assetsPath, dataStorePath, signatureService, fileService, reverseTunnelService)
}
func initKubernetesDeployer(kubernetesTokenCacheManager *kubeproxy.TokenCacheManager, kubernetesClientFactory *kubecli.ClientFactory, dataStore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, proxyManager *proxy.Manager, assetsPath string) portainer.KubernetesDeployer {
return exec.NewKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, signatureService, proxyManager, assetsPath)
func initKubernetesDeployer(dataStore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, assetsPath string) portainer.KubernetesDeployer {
return exec.NewKubernetesDeployer(dataStore, reverseTunnelService, signatureService, assetsPath)
}
func initJWTService(dataStore portainer.DataStore) (portainer.JWTService, error) {
@@ -112,7 +109,7 @@ func initJWTService(dataStore portainer.DataStore) (portainer.JWTService, error)
settings.UserSessionTimeout = portainer.DefaultUserSessionTimeout
dataStore.Settings().UpdateSettings(settings)
}
jwtService, err := jwt.NewService(settings.UserSessionTimeout, dataStore)
jwtService, err := jwt.NewService(settings.UserSessionTimeout)
if err != nil {
return nil, err
}
@@ -139,23 +136,6 @@ func initGitService() portainer.GitService {
return git.NewService()
}
func initSSLService(addr, dataPath, certPath, keyPath string, fileService portainer.FileService, dataStore portainer.DataStore, shutdownTrigger context.CancelFunc) (*ssl.Service, error) {
slices := strings.Split(addr, ":")
host := slices[0]
if host == "" {
host = "0.0.0.0"
}
sslService := ssl.NewService(fileService, dataStore, shutdownTrigger)
err := sslService.Init(host, certPath, keyPath)
if err != nil {
return nil, err
}
return sslService, nil
}
func initDockerClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService) *docker.ClientFactory {
return docker.NewClientFactory(signatureService, reverseTunnelService)
}
@@ -176,10 +156,9 @@ func initSnapshotService(snapshotInterval string, dataStore portainer.DataStore,
return snapshotService, nil
}
func initStatus(instanceID string) *portainer.Status {
func initStatus(flags *portainer.CLIFlags) *portainer.Status {
return &portainer.Status{
Version: portainer.APIVersion,
InstanceID: instanceID,
Version: portainer.APIVersion,
}
}
@@ -203,26 +182,7 @@ func updateSettingsFromFlags(dataStore portainer.DataStore, flags *portainer.CLI
settings.BlackListedLabels = *flags.Labels
}
err = dataStore.Settings().UpdateSettings(settings)
if err != nil {
return err
}
httpEnabled := !*flags.HTTPDisabled
sslSettings, err := dataStore.SSLSettings().Settings()
if err != nil {
return err
}
sslSettings.HTTPEnabled = httpEnabled
err = dataStore.SSLSettings().UpdateSettings(sslSettings)
if err != nil {
return err
}
return nil
return dataStore.Settings().UpdateSettings(settings)
}
func loadAndParseKeyPair(fileService portainer.FileService, signatureService portainer.DigitalSignatureService) error {
@@ -394,7 +354,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
fileService := initFileService(*flags.Data)
dataStore := initDataStore(*flags.Data, fileService, shutdownCtx)
dataStore := initDataStore(*flags.Data, fileService)
if err := dataStore.CheckCurrentEdition(); err != nil {
log.Fatal(err)
@@ -415,11 +375,6 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
digitalSignatureService := initDigitalSignatureService()
sslService, err := initSSLService(*flags.AddrHTTPS, *flags.Data, *flags.SSLCert, *flags.SSLKey, fileService, dataStore, shutdownTrigger)
if err != nil {
log.Fatal(err)
}
err = initKeyPair(fileService, digitalSignatureService)
if err != nil {
log.Fatalf("failed initializing key pai: %v", err)
@@ -444,19 +399,16 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
authorizationService := authorization.NewService(dataStore)
authorizationService.K8sClientFactory = kubernetesClientFactory
swarmStackManager, err := initSwarmStackManager(*flags.Assets, *flags.Data, digitalSignatureService, fileService, reverseTunnelService)
if err != nil {
log.Fatalf("failed initializing swarm stack manager: %v", err)
}
kubernetesTokenCacheManager := kubeproxy.NewTokenCacheManager()
proxyManager := proxy.NewManager(dataStore, digitalSignatureService, reverseTunnelService, dockerClientFactory, kubernetesClientFactory, kubernetesTokenCacheManager)
dockerConfigPath := fileService.GetDockerConfigPath()
composeStackManager := initComposeStackManager(*flags.Assets, *flags.Data, reverseTunnelService, proxyManager)
composeStackManager := initComposeStackManager(*flags.Assets, dockerConfigPath, reverseTunnelService, proxyManager)
swarmStackManager, err := initSwarmStackManager(*flags.Assets, dockerConfigPath, digitalSignatureService, fileService, reverseTunnelService)
if err != nil {
log.Fatalf("failed initializing swarm stack manager: %s", err)
}
kubernetesDeployer := initKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, digitalSignatureService, proxyManager, *flags.Assets)
kubernetesDeployer := initKubernetesDeployer(dataStore, reverseTunnelService, digitalSignatureService, *flags.Assets)
if dataStore.IsNew() {
err = updateSettingsFromFlags(dataStore, flags)
@@ -470,7 +422,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
log.Fatalf("failed loading edge jobs from database: %v", err)
}
applicationStatus := initStatus(instanceID)
applicationStatus := initStatus(flags)
err = initEndpoint(flags, dataStore, snapshotService)
if err != nil {
@@ -515,25 +467,14 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
err = reverseTunnelService.StartTunnelServer(*flags.TunnelAddr, *flags.TunnelPort, snapshotService)
if err != nil {
log.Fatalf("failed starting tunnel server: %s", err)
log.Fatalf("failed starting license service: %s", err)
}
sslSettings, err := dataStore.SSLSettings().Settings()
if err != nil {
log.Fatalf("failed to fetch ssl settings from DB")
}
scheduler := scheduler.NewScheduler(shutdownCtx)
stackDeployer := stacks.NewStackDeployer(swarmStackManager, composeStackManager, kubernetesDeployer)
stacks.StartStackSchedules(scheduler, stackDeployer, dataStore, gitService)
return &http.Server{
AuthorizationService: authorizationService,
ReverseTunnelService: reverseTunnelService,
Status: applicationStatus,
BindAddress: *flags.Addr,
BindAddressHTTPS: *flags.AddrHTTPS,
HTTPEnabled: sslSettings.HTTPEnabled,
AssetsPath: *flags.Assets,
DataStore: dataStore,
SwarmStackManager: swarmStackManager,
@@ -549,25 +490,23 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
KubernetesTokenCacheManager: kubernetesTokenCacheManager,
SignatureService: digitalSignatureService,
SnapshotService: snapshotService,
SSLService: sslService,
SSL: *flags.SSL,
SSLCert: *flags.SSLCert,
SSLKey: *flags.SSLKey,
DockerClientFactory: dockerClientFactory,
KubernetesClientFactory: kubernetesClientFactory,
Scheduler: scheduler,
ShutdownCtx: shutdownCtx,
ShutdownTrigger: shutdownTrigger,
StackDeployer: stackDeployer,
}
}
func main() {
flags := initCLI()
configureLogger()
for {
server := buildServer(flags)
log.Printf("[INFO] [cmd,main] Starting Portainer version %s\n", portainer.APIVersion)
log.Printf("Starting Portainer %s on %s\n", portainer.APIVersion, *flags.Addr)
err := server.Start()
log.Printf("[INFO] [cmd,main] Http server exited: %s\n", err)
log.Printf("Http server exited: %s\n", err)
}
}

View File

@@ -42,7 +42,7 @@ func (factory *ClientFactory) CreateClient(endpoint *portainer.Endpoint, nodeNam
} else if endpoint.Type == portainer.AgentOnDockerEnvironment {
return createAgentClient(endpoint, factory.signatureService, nodeName)
} else if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment {
return createEdgeClient(endpoint, factory.signatureService, factory.reverseTunnelService, nodeName)
return createEdgeClient(endpoint, factory.reverseTunnelService, nodeName)
}
if strings.HasPrefix(endpoint.URL, "unix://") || strings.HasPrefix(endpoint.URL, "npipe://") {
@@ -71,22 +71,13 @@ func createTCPClient(endpoint *portainer.Endpoint) (*client.Client, error) {
)
}
func createEdgeClient(endpoint *portainer.Endpoint, signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService, nodeName string) (*client.Client, error) {
func createEdgeClient(endpoint *portainer.Endpoint, reverseTunnelService portainer.ReverseTunnelService, nodeName string) (*client.Client, error) {
httpCli, err := httpClient(endpoint)
if err != nil {
return nil, err
}
signature, err := signatureService.CreateSignature(portainer.PortainerAgentSignatureMessage)
if err != nil {
return nil, err
}
headers := map[string]string{
portainer.PortainerAgentPublicKeyHeader: signatureService.EncodedPublicKey(),
portainer.PortainerAgentSignatureHeader: signature,
}
headers := map[string]string{}
if nodeName != "" {
headers[portainer.PortainerAgentTargetHeader] = nodeName
}

View File

@@ -1,17 +1,13 @@
package exec
import (
"context"
"fmt"
"os"
"path"
"regexp"
"strings"
"github.com/pkg/errors"
libstack "github.com/portainer/docker-compose-wrapper"
"github.com/portainer/docker-compose-wrapper/compose"
wrapper "github.com/portainer/docker-compose-wrapper"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/proxy"
@@ -20,33 +16,41 @@ import (
// ComposeStackManager is a wrapper for docker-compose binary
type ComposeStackManager struct {
deployer libstack.Deployer
wrapper *wrapper.ComposeWrapper
configPath string
proxyManager *proxy.Manager
}
// NewComposeStackManager returns a docker-compose wrapper if corresponding binary present, otherwise nil
func NewComposeStackManager(binaryPath string, configPath string, proxyManager *proxy.Manager) (*ComposeStackManager, error) {
deployer, err := compose.NewComposeDeployer(binaryPath, configPath)
wrap, err := wrapper.NewComposeWrapper(binaryPath)
if err != nil {
return nil, err
}
return &ComposeStackManager{
deployer: deployer,
wrapper: wrap,
proxyManager: proxyManager,
configPath: configPath,
}, nil
}
// NormalizeStackName returns a new stack name with unsupported characters replaced
func (w *ComposeStackManager) NormalizeStackName(name string) string {
r := regexp.MustCompile("[^a-z0-9]+")
return r.ReplaceAllString(strings.ToLower(name), "")
}
// ComposeSyntaxMaxVersion returns the maximum supported version of the docker compose syntax
func (manager *ComposeStackManager) ComposeSyntaxMaxVersion() string {
func (w *ComposeStackManager) ComposeSyntaxMaxVersion() string {
return portainer.ComposeSyntaxMaxVersion
}
// Up builds, (re)creates and starts containers in the background. Wraps `docker-compose up -d` command
func (manager *ComposeStackManager) Up(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint) error {
url, proxy, err := manager.fetchEndpointProxy(endpoint)
func (w *ComposeStackManager) Up(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
url, proxy, err := w.fetchEndpointProxy(endpoint)
if err != nil {
return errors.Wrap(err, "failed to fetch endpoint proxy")
return err
}
if proxy != nil {
@@ -55,16 +59,18 @@ func (manager *ComposeStackManager) Up(ctx context.Context, stack *portainer.Sta
envFilePath, err := createEnvFile(stack)
if err != nil {
return errors.Wrap(err, "failed to create env file")
return err
}
filePaths := append([]string{stack.EntryPoint}, stack.AdditionalFiles...)
return manager.deployer.Deploy(ctx, stack.ProjectPath, url, stack.Name, filePaths, envFilePath)
filePath := stackFilePath(stack)
_, err = w.wrapper.Up([]string{filePath}, url, stack.Name, envFilePath, w.configPath)
return err
}
// Down stops and removes containers, networks, images, and volumes. Wraps `docker-compose down --remove-orphans` command
func (manager *ComposeStackManager) Down(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint) error {
url, proxy, err := manager.fetchEndpointProxy(endpoint)
func (w *ComposeStackManager) Down(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
url, proxy, err := w.fetchEndpointProxy(endpoint)
if err != nil {
return err
}
@@ -72,28 +78,27 @@ func (manager *ComposeStackManager) Down(ctx context.Context, stack *portainer.S
defer proxy.Close()
}
filePaths := append([]string{stack.EntryPoint}, stack.AdditionalFiles...)
filePath := stackFilePath(stack)
return manager.deployer.Remove(ctx, stack.ProjectPath, url, stack.Name, filePaths)
_, err = w.wrapper.Down([]string{filePath}, url, stack.Name)
return err
}
// NormalizeStackName returns a new stack name with unsupported characters replaced
func (manager *ComposeStackManager) NormalizeStackName(name string) string {
r := regexp.MustCompile("[^a-z0-9]+")
return r.ReplaceAllString(strings.ToLower(name), "")
func stackFilePath(stack *portainer.Stack) string {
return path.Join(stack.ProjectPath, stack.EntryPoint)
}
func (manager *ComposeStackManager) fetchEndpointProxy(endpoint *portainer.Endpoint) (string, *factory.ProxyServer, error) {
func (w *ComposeStackManager) fetchEndpointProxy(endpoint *portainer.Endpoint) (string, *factory.ProxyServer, error) {
if strings.HasPrefix(endpoint.URL, "unix://") || strings.HasPrefix(endpoint.URL, "npipe://") {
return "", nil, nil
}
proxy, err := manager.proxyManager.CreateAgentProxyServer(endpoint)
proxy, err := w.proxyManager.CreateComposeProxyServer(endpoint)
if err != nil {
return "", nil, err
}
return fmt.Sprintf("tcp://127.0.0.1:%d", proxy.Port), proxy, nil
return fmt.Sprintf("http://127.0.0.1:%d", proxy.Port), proxy, nil
}
func createEnvFile(stack *portainer.Stack) (string, error) {
@@ -113,5 +118,5 @@ func createEnvFile(stack *portainer.Stack) (string, error) {
}
envfile.Close()
return "stack.env", nil
return envFilePath, nil
}

View File

@@ -1,7 +1,8 @@
// +build integration
package exec
import (
"context"
"fmt"
"log"
"os"
@@ -48,9 +49,7 @@ func Test_UpAndDown(t *testing.T) {
t.Fatalf("Failed creating manager: %s", err)
}
ctx := context.TODO()
err = w.Up(ctx, stack, endpoint)
err = w.Up(stack, endpoint)
if err != nil {
t.Fatalf("Error calling docker-compose up: %s", err)
}
@@ -59,7 +58,7 @@ func Test_UpAndDown(t *testing.T) {
t.Fatal("container should exist")
}
err = w.Down(ctx, stack, endpoint)
err = w.Down(stack, endpoint)
if err != nil {
t.Fatalf("Error calling docker-compose down: %s", err)
}

View File

@@ -10,6 +10,47 @@ import (
"github.com/stretchr/testify/assert"
)
func Test_stackFilePath(t *testing.T) {
tests := []struct {
name string
stack *portainer.Stack
expected string
}{
// {
// name: "should return empty result if stack is missing",
// stack: nil,
// expected: "",
// },
// {
// name: "should return empty result if stack don't have entrypoint",
// stack: &portainer.Stack{},
// expected: "",
// },
{
name: "should allow file name and dir",
stack: &portainer.Stack{
ProjectPath: "dir",
EntryPoint: "file",
},
expected: path.Join("dir", "file"),
},
{
name: "should allow file name only",
stack: &portainer.Stack{
EntryPoint: "file",
},
expected: "file",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := stackFilePath(tt.stack)
assert.Equal(t, tt.expected, result)
})
}
}
func Test_createEnvFile(t *testing.T) {
dir := t.TempDir()
@@ -19,6 +60,11 @@ func Test_createEnvFile(t *testing.T) {
expected string
expectedFile bool
}{
// {
// name: "should not add env file option if stack is missing",
// stack: nil,
// expected: "",
// },
{
name: "should not add env file option if stack doesn't have env variables",
stack: &portainer.Stack{
@@ -52,7 +98,7 @@ func Test_createEnvFile(t *testing.T) {
result, _ := createEnvFile(tt.stack)
if tt.expected != "" {
assert.Equal(t, "stack.env", result)
assert.Equal(t, path.Join(tt.stack.ProjectPath, "stack.env"), result)
f, _ := os.Open(path.Join(dir, "stack.env"))
content, _ := ioutil.ReadAll(f)

View File

@@ -2,135 +2,188 @@ package exec
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os/exec"
"path"
"runtime"
"strings"
"github.com/pkg/errors"
"github.com/portainer/portainer/api/http/proxy"
"github.com/portainer/portainer/api/http/proxy/factory"
"github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
"github.com/portainer/portainer/api/kubernetes/cli"
"time"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/crypto"
)
// KubernetesDeployer represents a service to deploy resources inside a Kubernetes environment.
type KubernetesDeployer struct {
binaryPath string
dataStore portainer.DataStore
reverseTunnelService portainer.ReverseTunnelService
signatureService portainer.DigitalSignatureService
kubernetesClientFactory *cli.ClientFactory
kubernetesTokenCacheManager *kubernetes.TokenCacheManager
proxyManager *proxy.Manager
binaryPath string
dataStore portainer.DataStore
reverseTunnelService portainer.ReverseTunnelService
signatureService portainer.DigitalSignatureService
}
// NewKubernetesDeployer initializes a new KubernetesDeployer service.
func NewKubernetesDeployer(kubernetesTokenCacheManager *kubernetes.TokenCacheManager, kubernetesClientFactory *cli.ClientFactory, datastore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, proxyManager *proxy.Manager, binaryPath string) *KubernetesDeployer {
func NewKubernetesDeployer(datastore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, binaryPath string) *KubernetesDeployer {
return &KubernetesDeployer{
binaryPath: binaryPath,
dataStore: datastore,
reverseTunnelService: reverseTunnelService,
signatureService: signatureService,
kubernetesClientFactory: kubernetesClientFactory,
kubernetesTokenCacheManager: kubernetesTokenCacheManager,
proxyManager: proxyManager,
binaryPath: binaryPath,
dataStore: datastore,
reverseTunnelService: reverseTunnelService,
signatureService: signatureService,
}
}
func (deployer *KubernetesDeployer) getToken(userID portainer.UserID, endpoint *portainer.Endpoint, setLocalAdminToken bool) (string, error) {
kubeCLI, err := deployer.kubernetesClientFactory.GetKubeClient(endpoint)
if err != nil {
return "", err
}
tokenCache := deployer.kubernetesTokenCacheManager.GetOrCreateTokenCache(int(endpoint.ID))
tokenManager, err := kubernetes.NewTokenManager(kubeCLI, deployer.dataStore, tokenCache, setLocalAdminToken)
if err != nil {
return "", err
}
user, err := deployer.dataStore.User().User(userID)
if err != nil {
return "", errors.Wrap(err, "failed to fetch the user")
}
if user.Role == portainer.AdministratorRole {
return tokenManager.GetAdminServiceAccountToken(), nil
}
token, err := tokenManager.GetUserServiceAccountToken(int(user.ID), endpoint.ID)
if err != nil {
return "", err
}
if token == "" {
return "", fmt.Errorf("can not get a valid user service account token")
}
return token, nil
}
// Deploy upserts Kubernetes resources defined in manifest(s)
func (deployer *KubernetesDeployer) Deploy(userID portainer.UserID, endpoint *portainer.Endpoint, manifestFiles []string, namespace string) (string, error) {
return deployer.command("apply", userID, endpoint, manifestFiles, namespace)
}
// Remove deletes Kubernetes resources defined in manifest(s)
func (deployer *KubernetesDeployer) Remove(userID portainer.UserID, endpoint *portainer.Endpoint, manifestFiles []string, namespace string) (string, error) {
return deployer.command("delete", userID, endpoint, manifestFiles, namespace)
}
func (deployer *KubernetesDeployer) command(operation string, userID portainer.UserID, endpoint *portainer.Endpoint, manifestFiles []string, namespace string) (string, error) {
token, err := deployer.getToken(userID, endpoint, endpoint.Type == portainer.KubernetesLocalEnvironment)
if err != nil {
return "", errors.Wrap(err, "failed generating a user token")
}
command := path.Join(deployer.binaryPath, "kubectl")
if runtime.GOOS == "windows" {
command = path.Join(deployer.binaryPath, "kubectl.exe")
}
args := []string{
"--token", token,
"--namespace", namespace,
}
if endpoint.Type == portainer.AgentOnKubernetesEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment {
url, proxy, err := deployer.getAgentURL(endpoint)
// Deploy will deploy a Kubernetes manifest inside a specific namespace in a Kubernetes endpoint.
// Otherwise it will use kubectl to deploy the manifest.
func (deployer *KubernetesDeployer) Deploy(endpoint *portainer.Endpoint, stackConfig string, namespace string) (string, error) {
if endpoint.Type == portainer.KubernetesLocalEnvironment {
token, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/token")
if err != nil {
return "", errors.WithMessage(err, "failed generating endpoint URL")
return "", err
}
defer proxy.Close()
args = append(args, "--server", url)
command := path.Join(deployer.binaryPath, "kubectl")
if runtime.GOOS == "windows" {
command = path.Join(deployer.binaryPath, "kubectl.exe")
}
args := make([]string, 0)
args = append(args, "--server", endpoint.URL)
args = append(args, "--insecure-skip-tls-verify")
args = append(args, "--token", string(token))
args = append(args, "--namespace", namespace)
args = append(args, "apply", "-f", "-")
var stderr bytes.Buffer
cmd := exec.Command(command, args...)
cmd.Stderr = &stderr
cmd.Stdin = strings.NewReader(stackConfig)
output, err := cmd.Output()
if err != nil {
return "", errors.New(stderr.String())
}
return string(output), nil
}
args = append(args, operation)
for _, path := range manifestFiles {
args = append(args, "-f", strings.TrimSpace(path))
// agent
endpointURL := endpoint.URL
if endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment {
tunnel := deployer.reverseTunnelService.GetTunnelDetails(endpoint.ID)
if tunnel.Status == portainer.EdgeAgentIdle {
err := deployer.reverseTunnelService.SetTunnelStatusToRequired(endpoint.ID)
if err != nil {
return "", err
}
settings, err := deployer.dataStore.Settings().Settings()
if err != nil {
return "", err
}
waitForAgentToConnect := time.Duration(settings.EdgeAgentCheckinInterval) * time.Second
time.Sleep(waitForAgentToConnect * 2)
}
endpointURL = fmt.Sprintf("http://127.0.0.1:%d", tunnel.Port)
}
var stderr bytes.Buffer
cmd := exec.Command(command, args...)
cmd.Stderr = &stderr
transport := &http.Transport{}
output, err := cmd.Output()
if endpoint.TLSConfig.TLS {
tlsConfig, err := crypto.CreateTLSConfigurationFromDisk(endpoint.TLSConfig.TLSCACertPath, endpoint.TLSConfig.TLSCertPath, endpoint.TLSConfig.TLSKeyPath, endpoint.TLSConfig.TLSSkipVerify)
if err != nil {
return "", err
}
transport.TLSClientConfig = tlsConfig
}
httpCli := &http.Client{
Transport: transport,
}
if !strings.HasPrefix(endpointURL, "http") {
endpointURL = fmt.Sprintf("https://%s", endpointURL)
}
url, err := url.Parse(fmt.Sprintf("%s/v2/kubernetes/stack", endpointURL))
if err != nil {
return "", errors.Wrapf(err, "failed to execute kubectl command: %q", stderr.String())
return "", err
}
return string(output), nil
reqPayload, err := json.Marshal(
struct {
StackConfig string
Namespace string
}{
StackConfig: stackConfig,
Namespace: namespace,
})
if err != nil {
return "", err
}
req, err := http.NewRequest(http.MethodPost, url.String(), bytes.NewReader(reqPayload))
if err != nil {
return "", err
}
signature, err := deployer.signatureService.CreateSignature(portainer.PortainerAgentSignatureMessage)
if err != nil {
return "", err
}
req.Header.Set(portainer.PortainerAgentPublicKeyHeader, deployer.signatureService.EncodedPublicKey())
req.Header.Set(portainer.PortainerAgentSignatureHeader, signature)
resp, err := httpCli.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
var errorResponseData struct {
Message string
Details string
}
err = json.NewDecoder(resp.Body).Decode(&errorResponseData)
if err != nil {
output, parseStringErr := ioutil.ReadAll(resp.Body)
if parseStringErr != nil {
return "", parseStringErr
}
return "", fmt.Errorf("Failed parsing, body: %s, error: %w", output, err)
}
return "", fmt.Errorf("Deployment to agent failed: %s", errorResponseData.Details)
}
var responseData struct{ Output string }
err = json.NewDecoder(resp.Body).Decode(&responseData)
if err != nil {
parsedOutput, parseStringErr := ioutil.ReadAll(resp.Body)
if parseStringErr != nil {
return "", parseStringErr
}
return "", fmt.Errorf("Failed decoding, body: %s, err: %w", parsedOutput, err)
}
return responseData.Output, nil
}
// ConvertCompose leverages the kompose binary to deploy a compose compliant manifest.
func (deployer *KubernetesDeployer) ConvertCompose(data []byte) ([]byte, error) {
func (deployer *KubernetesDeployer) ConvertCompose(data string) ([]byte, error) {
command := path.Join(deployer.binaryPath, "kompose")
if runtime.GOOS == "windows" {
command = path.Join(deployer.binaryPath, "kompose.exe")
@@ -142,7 +195,7 @@ func (deployer *KubernetesDeployer) ConvertCompose(data []byte) ([]byte, error)
var stderr bytes.Buffer
cmd := exec.Command(command, args...)
cmd.Stderr = &stderr
cmd.Stdin = bytes.NewReader(data)
cmd.Stdin = strings.NewReader(data)
output, err := cmd.Output()
if err != nil {
@@ -151,12 +204,3 @@ func (deployer *KubernetesDeployer) ConvertCompose(data []byte) ([]byte, error)
return output, nil
}
func (deployer *KubernetesDeployer) getAgentURL(endpoint *portainer.Endpoint) (string, *factory.ProxyServer, error) {
proxy, err := deployer.proxyManager.CreateAgentProxyServer(endpoint)
if err != nil {
return "", nil, err
}
return fmt.Sprintf("http://127.0.0.1:%d/kubernetes", proxy.Port), proxy, nil
}

View File

@@ -13,13 +13,12 @@ import (
"strings"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/internal/stackutils"
)
// SwarmStackManager represents a service for managing stacks.
type SwarmStackManager struct {
binaryPath string
configPath string
dataPath string
signatureService portainer.DigitalSignatureService
fileService portainer.FileService
reverseTunnelService portainer.ReverseTunnelService
@@ -27,16 +26,16 @@ type SwarmStackManager struct {
// NewSwarmStackManager initializes a new SwarmStackManager service.
// It also updates the configuration of the Docker CLI binary.
func NewSwarmStackManager(binaryPath, configPath string, signatureService portainer.DigitalSignatureService, fileService portainer.FileService, reverseTunnelService portainer.ReverseTunnelService) (*SwarmStackManager, error) {
func NewSwarmStackManager(binaryPath, dataPath string, signatureService portainer.DigitalSignatureService, fileService portainer.FileService, reverseTunnelService portainer.ReverseTunnelService) (*SwarmStackManager, error) {
manager := &SwarmStackManager{
binaryPath: binaryPath,
configPath: configPath,
dataPath: dataPath,
signatureService: signatureService,
fileService: fileService,
reverseTunnelService: reverseTunnelService,
}
err := manager.updateDockerCLIConfiguration(manager.configPath)
err := manager.updateDockerCLIConfiguration(dataPath)
if err != nil {
return nil, err
}
@@ -46,7 +45,7 @@ func NewSwarmStackManager(binaryPath, configPath string, signatureService portai
// Login executes the docker login command against a list of registries (including DockerHub).
func (manager *SwarmStackManager) Login(registries []portainer.Registry, endpoint *portainer.Endpoint) {
command, args := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.configPath, endpoint)
command, args := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint)
for _, registry := range registries {
if registry.Authentication {
registryArgs := append(args, "login", "--username", registry.Username, "--password", registry.Password, registry.URL)
@@ -57,35 +56,34 @@ func (manager *SwarmStackManager) Login(registries []portainer.Registry, endpoin
// Logout executes the docker logout command.
func (manager *SwarmStackManager) Logout(endpoint *portainer.Endpoint) error {
command, args := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.configPath, endpoint)
command, args := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint)
args = append(args, "logout")
return runCommandAndCaptureStdErr(command, args, nil, "")
}
// Deploy executes the docker stack deploy command.
func (manager *SwarmStackManager) Deploy(stack *portainer.Stack, prune bool, endpoint *portainer.Endpoint) error {
filePaths := stackutils.GetStackFilePaths(stack)
command, args := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.configPath, endpoint)
stackFilePath := path.Join(stack.ProjectPath, stack.EntryPoint)
command, args := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint)
if prune {
args = append(args, "stack", "deploy", "--prune", "--with-registry-auth")
args = append(args, "stack", "deploy", "--prune", "--with-registry-auth", "--compose-file", stackFilePath, stack.Name)
} else {
args = append(args, "stack", "deploy", "--with-registry-auth")
args = append(args, "stack", "deploy", "--with-registry-auth", "--compose-file", stackFilePath, stack.Name)
}
args = configureFilePaths(args, filePaths)
args = append(args, stack.Name)
env := make([]string, 0)
for _, envvar := range stack.Env {
env = append(env, envvar.Name+"="+envvar.Value)
}
return runCommandAndCaptureStdErr(command, args, env, stack.ProjectPath)
stackFolder := path.Dir(stackFilePath)
return runCommandAndCaptureStdErr(command, args, env, stackFolder)
}
// Remove executes the docker stack rm command.
func (manager *SwarmStackManager) Remove(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
command, args := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.configPath, endpoint)
command, args := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint)
args = append(args, "stack", "rm", stack.Name)
return runCommandAndCaptureStdErr(command, args, nil, "")
}
@@ -109,7 +107,7 @@ func runCommandAndCaptureStdErr(command string, args []string, env []string, wor
return nil
}
func (manager *SwarmStackManager) prepareDockerCommandAndArgs(binaryPath, configPath string, endpoint *portainer.Endpoint) (string, []string) {
func (manager *SwarmStackManager) prepareDockerCommandAndArgs(binaryPath, dataPath string, endpoint *portainer.Endpoint) (string, []string) {
// Assume Linux as a default
command := path.Join(binaryPath, "docker")
@@ -118,7 +116,7 @@ func (manager *SwarmStackManager) prepareDockerCommandAndArgs(binaryPath, config
}
args := make([]string, 0)
args = append(args, "--config", configPath)
args = append(args, "--config", dataPath)
endpointURL := endpoint.URL
if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment {
@@ -145,8 +143,8 @@ func (manager *SwarmStackManager) prepareDockerCommandAndArgs(binaryPath, config
return command, args
}
func (manager *SwarmStackManager) updateDockerCLIConfiguration(configPath string) error {
configFilePath := path.Join(configPath, "config.json")
func (manager *SwarmStackManager) updateDockerCLIConfiguration(dataPath string) error {
configFilePath := path.Join(dataPath, "config.json")
config, err := manager.retrieveConfigurationFromDisk(configFilePath)
if err != nil {
return err
@@ -193,10 +191,3 @@ func (manager *SwarmStackManager) NormalizeStackName(name string) string {
r := regexp.MustCompile("[^a-z0-9]+")
return r.ReplaceAllString(strings.ToLower(name), "")
}
func configureFilePaths(args []string, filePaths []string) []string {
for _, path := range filePaths {
args = append(args, "--compose-file", path)
}
return args
}

View File

@@ -1,15 +0,0 @@
package exec
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestConfigFilePaths(t *testing.T) {
args := []string{"stack", "deploy", "--with-registry-auth"}
filePaths := []string{"dir/file", "dir/file-two", "dir/file-three"}
expected := []string{"stack", "deploy", "--with-registry-auth", "--compose-file", "dir/file", "--compose-file", "dir/file-two", "--compose-file", "dir/file-three"}
output := configureFilePaths(args, filePaths)
assert.ElementsMatch(t, expected, output, "wrong output file paths")
}

View File

@@ -1,92 +0,0 @@
package filesystem
import (
"io/ioutil"
"os"
"path"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_copyFile_returnsError_whenSourceDoesNotExist(t *testing.T) {
tmpdir, _ := ioutil.TempDir("", "backup")
defer os.RemoveAll(tmpdir)
err := copyFile("does-not-exist", tmpdir)
assert.Error(t, err)
}
func Test_copyFile_shouldMakeAbackup(t *testing.T) {
tmpdir, _ := ioutil.TempDir("", "backup")
defer os.RemoveAll(tmpdir)
content := []byte("content")
ioutil.WriteFile(path.Join(tmpdir, "origin"), content, 0600)
err := copyFile(path.Join(tmpdir, "origin"), path.Join(tmpdir, "copy"))
assert.NoError(t, err)
copyContent, _ := ioutil.ReadFile(path.Join(tmpdir, "copy"))
assert.Equal(t, content, copyContent)
}
func Test_CopyDir_shouldCopyAllFilesAndDirectories(t *testing.T) {
destination, _ := ioutil.TempDir("", "destination")
defer os.RemoveAll(destination)
err := CopyDir("./testdata/copy_test", destination, true)
assert.NoError(t, err)
assert.FileExists(t, filepath.Join(destination, "copy_test", "outer"))
assert.FileExists(t, filepath.Join(destination, "copy_test", "dir", ".dotfile"))
assert.FileExists(t, filepath.Join(destination, "copy_test", "dir", "inner"))
}
func Test_CopyDir_shouldCopyOnlyDirContents(t *testing.T) {
destination, _ := ioutil.TempDir("", "destination")
defer os.RemoveAll(destination)
err := CopyDir("./testdata/copy_test", destination, false)
assert.NoError(t, err)
assert.FileExists(t, filepath.Join(destination, "outer"))
assert.FileExists(t, filepath.Join(destination, "dir", ".dotfile"))
assert.FileExists(t, filepath.Join(destination, "dir", "inner"))
}
func Test_CopyPath_shouldSkipWhenNotExist(t *testing.T) {
tmpdir, _ := ioutil.TempDir("", "backup")
defer os.RemoveAll(tmpdir)
err := CopyPath("does-not-exists", tmpdir)
assert.NoError(t, err)
assert.NoFileExists(t, tmpdir)
}
func Test_CopyPath_shouldCopyFile(t *testing.T) {
tmpdir, _ := ioutil.TempDir("", "backup")
defer os.RemoveAll(tmpdir)
content := []byte("content")
ioutil.WriteFile(path.Join(tmpdir, "file"), content, 0600)
os.MkdirAll(path.Join(tmpdir, "backup"), 0700)
err := CopyPath(path.Join(tmpdir, "file"), path.Join(tmpdir, "backup"))
assert.NoError(t, err)
copyContent, err := ioutil.ReadFile(path.Join(tmpdir, "backup", "file"))
assert.NoError(t, err)
assert.Equal(t, content, copyContent)
}
func Test_CopyPath_shouldCopyDir(t *testing.T) {
destination, _ := ioutil.TempDir("", "destination")
defer os.RemoveAll(destination)
err := CopyPath("./testdata/copy_test", destination)
assert.NoError(t, err)
assert.FileExists(t, filepath.Join(destination, "copy_test", "outer"))
assert.FileExists(t, filepath.Join(destination, "copy_test", "dir", ".dotfile"))
assert.FileExists(t, filepath.Join(destination, "copy_test", "dir", "inner"))
}

View File

@@ -43,8 +43,6 @@ const (
BinaryStorePath = "bin"
// EdgeJobStorePath represents the subfolder where schedule files are stored.
EdgeJobStorePath = "edge_jobs"
// DockerConfigPath represents the subfolder where docker configuration is stored.
DockerConfigPath = "docker_config"
// ExtensionRegistryManagementStorePath represents the subfolder where files related to the
// registry management extension are stored.
ExtensionRegistryManagementStorePath = "extensions"
@@ -52,12 +50,6 @@ const (
CustomTemplateStorePath = "custom_templates"
// TempPath represent the subfolder where temporary files are saved
TempPath = "tmp"
// SSLCertPath represents the default ssl certificates path
SSLCertPath = "certs"
// 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
@@ -82,11 +74,6 @@ func NewService(dataStorePath, fileStorePath string) (*Service, error) {
return nil, err
}
err = service.createDirectoryInStore(SSLCertPath)
if err != nil {
return nil, err
}
err = service.createDirectoryInStore(TLSStorePath)
if err != nil {
return nil, err
@@ -102,11 +89,6 @@ func NewService(dataStorePath, fileStorePath string) (*Service, error) {
return nil, err
}
err = service.createDirectoryInStore(DockerConfigPath)
if err != nil {
return nil, err
}
return service, nil
}
@@ -115,11 +97,6 @@ func (service *Service) GetBinaryFolder() string {
return path.Join(service.fileStorePath, BinaryStorePath)
}
// GetDockerConfigPath returns the full path to the docker config store on the filesystem
func (service *Service) GetDockerConfigPath() string {
return path.Join(service.fileStorePath, DockerConfigPath)
}
// RemoveDirectory removes a directory on the filesystem.
func (service *Service) RemoveDirectory(directoryPath string) error {
return os.RemoveAll(directoryPath)
@@ -131,66 +108,6 @@ func (service *Service) GetStackProjectPath(stackIdentifier string) string {
return path.Join(service.fileStorePath, ComposeStorePath, stackIdentifier)
}
// Copy copies the file on fromFilePath to toFilePath
// if toFilePath exists func will fail unless deleteIfExists is true
func (service *Service) Copy(fromFilePath string, toFilePath string, deleteIfExists bool) error {
exists, err := service.FileExists(fromFilePath)
if err != nil {
return err
}
if !exists {
return errors.New("File doesn't exist")
}
finput, err := os.Open(fromFilePath)
if err != nil {
return err
}
defer finput.Close()
exists, err = service.FileExists(toFilePath)
if err != nil {
return err
}
if exists {
if !deleteIfExists {
return errors.New("Destination file exists")
}
err := os.Remove(toFilePath)
if err != nil {
return err
}
}
foutput, err := os.Create(toFilePath)
if err != nil {
return err
}
defer foutput.Close()
buf := make([]byte, 1024)
for {
n, err := finput.Read(buf)
if err != nil && err != io.EOF {
return err
}
if n == 0 {
break
}
if _, err := foutput.Write(buf[:n]); err != nil {
return err
}
}
return nil
}
// StoreStackFileFromBytes creates a subfolder in the ComposeStorePath and stores a new file from bytes.
// It returns the path to the folder where the file is stored.
func (service *Service) StoreStackFileFromBytes(stackIdentifier, fileName string, data []byte) (string, error) {
@@ -590,58 +507,6 @@ func (service *Service) GetDatastorePath() string {
return service.dataStorePath
}
func (service *Service) wrapFileStore(filepath string) string {
return path.Join(service.fileStorePath, filepath)
}
func defaultCertPathUnderFileStore() (string, string) {
certPath := path.Join(SSLCertPath, DefaultSSLCertFilename)
keyPath := path.Join(SSLCertPath, DefaultSSLKeyFilename)
return certPath, keyPath
}
// GetDefaultSSLCertsPath returns the ssl certs path
func (service *Service) GetDefaultSSLCertsPath() (string, string) {
certPath, keyPath := defaultCertPathUnderFileStore()
return service.wrapFileStore(certPath), service.wrapFileStore(keyPath)
}
// StoreSSLCertPair stores a ssl certificate pair
func (service *Service) StoreSSLCertPair(cert, key []byte) (string, string, error) {
certPath, keyPath := defaultCertPathUnderFileStore()
r := bytes.NewReader(cert)
err := service.createFileInStore(certPath, r)
if err != nil {
return "", "", err
}
r = bytes.NewReader(key)
err = service.createFileInStore(keyPath, r)
if err != nil {
return "", "", err
}
return service.wrapFileStore(certPath), service.wrapFileStore(keyPath), nil
}
// CopySSLCertPair copies a ssl certificate pair
func (service *Service) CopySSLCertPair(certPath, keyPath string) (string, string, error) {
defCertPath, defKeyPath := service.GetDefaultSSLCertsPath()
err := service.Copy(certPath, defCertPath, false)
if err != nil {
return "", "", err
}
err = service.Copy(keyPath, defKeyPath, false)
if err != nil {
return "", "", err
}
return defCertPath, defKeyPath, 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

@@ -1,23 +0,0 @@
package filesystem
import (
"os"
"path/filepath"
"github.com/pkg/errors"
)
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)
}
file, err := os.Create(dst)
if err != nil {
return errors.Wrapf(err, "failed to open a file %q", dst)
}
defer file.Close()
_, err = file.Write(content)
return errors.Wrapf(err, "failed to write a file %q", dst)
}

View File

@@ -1,48 +0,0 @@
package filesystem
import (
"io/ioutil"
"path"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_WriteFile_CanStoreContentInANewFile(t *testing.T) {
tmpDir := t.TempDir()
tmpFilePath := path.Join(tmpDir, "dummy")
content := []byte("content")
err := WriteToFile(tmpFilePath, content)
assert.NoError(t, err)
fileContent, _ := ioutil.ReadFile(tmpFilePath)
assert.Equal(t, content, fileContent)
}
func Test_WriteFile_CanOverwriteExistingFile(t *testing.T) {
tmpDir := t.TempDir()
tmpFilePath := path.Join(tmpDir, "dummy")
err := WriteToFile(tmpFilePath, []byte("content"))
assert.NoError(t, err)
content := []byte("new content")
err = WriteToFile(tmpFilePath, content)
assert.NoError(t, err)
fileContent, _ := ioutil.ReadFile(tmpFilePath)
assert.Equal(t, content, fileContent)
}
func Test_WriteFile_CanWriteANestedPath(t *testing.T) {
tmpDir := t.TempDir()
tmpFilePath := path.Join(tmpDir, "dir", "sub-dir", "dummy")
content := []byte("content")
err := WriteToFile(tmpFilePath, content)
assert.NoError(t, err)
fileContent, _ := ioutil.ReadFile(tmpFilePath)
assert.Equal(t, content, fileContent)
}

View File

@@ -2,17 +2,15 @@ package git
import (
"context"
"encoding/json"
"fmt"
"github.com/pkg/errors"
"github.com/portainer/portainer/api/archive"
"io"
"io/ioutil"
"net/http"
"net/url"
"os"
"strings"
"github.com/pkg/errors"
"github.com/portainer/portainer/api/archive"
)
const (
@@ -39,7 +37,7 @@ type azureDownloader struct {
func NewAzureDownloader(client *http.Client) *azureDownloader {
return &azureDownloader{
client: client,
client: client,
baseUrl: "https://dev.azure.com",
}
}
@@ -102,57 +100,6 @@ func (a *azureDownloader) downloadZipFromAzureDevOps(ctx context.Context, option
return zipFile.Name(), nil
}
func (a *azureDownloader) latestCommitID(ctx context.Context, options fetchOptions) (string, error) {
config, err := parseUrl(options.repositoryUrl)
if err != nil {
return "", errors.WithMessage(err, "failed to parse url")
}
refsUrl, err := a.buildRefsUrl(config, options.referenceName)
if err != nil {
return "", errors.WithMessage(err, "failed to build azure refs url")
}
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 != "" {
req.SetBasicAuth(config.username, config.password)
}
if err != nil {
return "", errors.WithMessage(err, "failed to create a new HTTP request")
}
resp, err := a.client.Do(req)
if err != nil {
return "", errors.WithMessage(err, "failed to make an HTTP request")
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("failed to get repository refs with a status \"%v\"", resp.Status)
}
var refs struct {
Value []struct {
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
}
}
return "", errors.Errorf("could not find ref %q in the repository", options.referenceName)
}
func parseUrl(rawUrl string) (*azureOptions, error) {
if strings.HasPrefix(rawUrl, "https://") || strings.HasPrefix(rawUrl, "http://") {
return parseHttpUrl(rawUrl)
@@ -246,27 +193,6 @@ func (a *azureDownloader) buildDownloadUrl(config *azureOptions, referenceName s
return u.String(), nil
}
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),
url.PathEscape(config.repository))
u, err := url.Parse(rawUrl)
if err != nil {
return "", errors.Wrapf(err, "failed to parse refs url path %s", rawUrl)
}
// filterContains=main&api-version=6.0
q := u.Query()
q.Set("filterContains", formatReferenceName(referenceName))
q.Set("api-version", "6.0")
u.RawQuery = q.Encode()
return u.String(), nil
}
const (
branchPrefix = "refs/heads/"
tagPrefix = "refs/tags/"

View File

@@ -78,18 +78,6 @@ func TestService_ClonePrivateRepository_Azure(t *testing.T) {
assert.FileExists(t, filepath.Join(dst, "README.md"))
}
func TestService_LatestCommitID_Azure(t *testing.T) {
ensureIntegrationTest(t)
pat := getRequiredValue(t, "AZURE_DEVOPS_PAT")
service := NewService()
repositoryUrl := "https://portainer.visualstudio.com/Playground/_git/dev_integration"
id, err := service.LatestCommitID(repositoryUrl, "refs/heads/main", "", pat)
assert.NoError(t, err)
assert.NotEmpty(t, id, "cannot guarantee commit id, but it should be not empty")
}
func getRequiredValue(t *testing.T, name string) string {
value, ok := os.LookupEnv(name)
if !ok {

View File

@@ -2,12 +2,11 @@ package git
import (
"context"
"github.com/stretchr/testify/assert"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_buildDownloadUrl(t *testing.T) {
@@ -28,23 +27,6 @@ func Test_buildDownloadUrl(t *testing.T) {
}
}
func Test_buildRefsUrl(t *testing.T) {
a := NewAzureDownloader(nil)
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/refs?filterContains=main&api-version=6.0")
actualUrl, _ := url.Parse(u)
assert.NoError(t, err)
assert.Equal(t, expectedUrl.Host, actualUrl.Host)
assert.Equal(t, expectedUrl.Scheme, actualUrl.Scheme)
assert.Equal(t, expectedUrl.Path, actualUrl.Path)
assert.Equal(t, expectedUrl.Query(), actualUrl.Query())
}
func Test_parseAzureUrl(t *testing.T) {
type args struct {
url string
@@ -266,110 +248,3 @@ 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 := `{
"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))
}))
defer server.Close()
a := &azureDownloader{
client: server.Client(),
baseUrl: server.URL,
}
tests := []struct {
name string
args fetchOptions
want string
wantErr bool
}{
{
name: "should be able to parse response",
args: fetchOptions{
referenceName: "refs/heads/master",
repositoryUrl: "https://dev.azure.com/Organisation/Project/_git/Repository"},
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 {
t.Run(tt.name, func(t *testing.T) {
id, err := a.latestCommitID(context.Background(), tt.args)
if (err != nil) != tt.wantErr {
t.Errorf("azureDownloader.latestCommitID() error = %v, wantErr %v", err, tt.wantErr)
return
}
assert.Equal(t, tt.want, id)
})
}
}

View File

@@ -6,26 +6,16 @@ import (
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/pkg/errors"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/transport/client"
githttp "github.com/go-git/go-git/v5/plumbing/transport/http"
"github.com/go-git/go-git/v5/storage/memory"
)
type fetchOptions struct {
repositoryUrl string
username string
password string
referenceName string
}
type cloneOptions struct {
repositoryUrl string
username string
@@ -36,7 +26,6 @@ type cloneOptions struct {
type downloader interface {
download(ctx context.Context, dst string, opt cloneOptions) error
latestCommitID(ctx context.Context, opt fetchOptions) (string, error)
}
type gitClient struct {
@@ -47,7 +36,13 @@ func (c gitClient) download(ctx context.Context, dst string, opt cloneOptions) e
gitOptions := git.CloneOptions{
URL: opt.repositoryUrl,
Depth: opt.depth,
Auth: getAuth(opt.username, opt.password),
}
if opt.password != "" || opt.username != "" {
gitOptions.Auth = &githttp.BasicAuth{
Username: opt.username,
Password: opt.password,
}
}
if opt.referenceName != "" {
@@ -67,44 +62,6 @@ func (c gitClient) download(ctx context.Context, dst string, opt cloneOptions) e
return nil
}
func (c gitClient) latestCommitID(ctx context.Context, opt fetchOptions) (string, error) {
remote := git.NewRemote(memory.NewStorage(), &config.RemoteConfig{
Name: "origin",
URLs: []string{opt.repositoryUrl},
})
listOptions := &git.ListOptions{
Auth: getAuth(opt.username, opt.password),
}
refs, err := remote.List(listOptions)
if err != nil {
return "", errors.Wrap(err, "failed to list repository refs")
}
for _, ref := range refs {
if strings.EqualFold(ref.Name().String(), opt.referenceName) {
return ref.Hash().String(), nil
}
}
return "", errors.Errorf("could not find ref %q in the repository", opt.referenceName)
}
func getAuth(username, password string) *githttp.BasicAuth {
if password != "" {
if username == "" {
username = "token"
}
return &githttp.BasicAuth{
Username: username,
Password: password,
}
}
return nil
}
// Service represents a service for managing Git.
type Service struct {
httpsCli *http.Client
@@ -117,7 +74,6 @@ func NewService() *Service {
httpsCli := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
Proxy: http.ProxyFromEnvironment,
},
Timeout: 300 * time.Second,
}
@@ -152,19 +108,3 @@ func (service *Service) cloneRepository(destination string, options cloneOptions
return service.git.download(context.TODO(), destination, options)
}
// LatestCommitID returns SHA1 of the latest commit of the specified reference
func (service *Service) LatestCommitID(repositoryURL, referenceName, username, password string) (string, error) {
options := fetchOptions{
repositoryUrl: repositoryURL,
username: username,
password: password,
referenceName: referenceName,
}
if isAzureUrl(options.repositoryUrl) {
return service.azure.latestCommitID(context.TODO(), options)
}
return service.git.latestCommitID(context.TODO(), options)
}

View File

@@ -12,7 +12,7 @@ import (
func TestService_ClonePrivateRepository_GitHub(t *testing.T) {
ensureIntegrationTest(t)
accessToken := getRequiredValue(t, "GITHUB_PAT")
pat := getRequiredValue(t, "GITHUB_PAT")
username := getRequiredValue(t, "GITHUB_USERNAME")
service := NewService()
@@ -21,20 +21,7 @@ func TestService_ClonePrivateRepository_GitHub(t *testing.T) {
defer os.RemoveAll(dst)
repositoryUrl := "https://github.com/portainer/private-test-repository.git"
err = service.CloneRepository(dst, repositoryUrl, "refs/heads/main", username, accessToken)
err = service.CloneRepository(dst, repositoryUrl, "refs/heads/main", username, pat)
assert.NoError(t, err)
assert.FileExists(t, filepath.Join(dst, "README.md"))
}
func TestService_LatestCommitID_GitHub(t *testing.T) {
ensureIntegrationTest(t)
accessToken := getRequiredValue(t, "GITHUB_PAT")
username := getRequiredValue(t, "GITHUB_USERNAME")
service := NewService()
repositoryUrl := "https://github.com/portainer/private-test-repository.git"
id, err := service.LatestCommitID(repositoryUrl, "refs/heads/main", username, accessToken)
assert.NoError(t, err)
assert.NotEmpty(t, id, "cannot guarantee commit id, but it should be not empty")
}

View File

@@ -105,19 +105,7 @@ func Test_cloneRepository(t *testing.T) {
})
assert.NoError(t, err)
assert.Equal(t, 4, getCommitHistoryLength(t, err, dir), "cloned repo has incorrect depth")
}
func Test_latestCommitID(t *testing.T) {
service := Service{git: gitClient{preserveGitDirectory: true}} // no need for http client since the test access the repo via file system.
repositoryURL := bareRepoDir
referenceName := "refs/heads/main"
id, err := service.LatestCommitID(repositoryURL, referenceName, "", "")
assert.NoError(t, err)
assert.Equal(t, "68dcaa7bd452494043c64252ab90db0f98ecf8d2", id)
assert.Equal(t, 3, getCommitHistoryLength(t, err, dir), "cloned repo has incorrect depth")
}
func getCommitHistoryLength(t *testing.T, err error, dir string) int {
@@ -149,10 +137,6 @@ func (t *testDownloader) download(_ context.Context, _ string, _ cloneOptions) e
return nil
}
func (t *testDownloader) latestCommitID(_ context.Context, _ fetchOptions) (string, error) {
return "", nil
}
func Test_cloneRepository_azure(t *testing.T) {
tests := []struct {
name string

Binary file not shown.

Binary file not shown.

View File

@@ -1,20 +1,10 @@
package gittypes
// RepoConfig represents a configuration for a repo
type RepoConfig struct {
// The repo url
URL string `example:"https://github.com/portainer/portainer.git"`
URL string `example:"https://github.com/portainer/portainer-ee.git"`
// The reference name
ReferenceName string `example:"refs/heads/branch_name"`
// Path to where the config file is in this url/refName
ConfigFilePath string `example:"docker-compose.yml"`
// Git credentials
Authentication *GitAuthentication
// Repository hash
ConfigHash string `example:"bc4c183d756879ea4d173315338110b31004b8e0"`
}
type GitAuthentication struct {
Username string
Password string
}

View File

@@ -3,21 +3,15 @@ module github.com/portainer/portainer/api
go 1.16
require (
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/Microsoft/go-winio v0.4.16
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 // indirect
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a
github.com/boltdb/bolt v1.3.1
github.com/containerd/containerd v1.3.1 // indirect
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 v0.0.0-20191126203649-54d085b857e9
github.com/docker/distribution v2.7.1+incompatible // indirect
github.com/docker/docker v0.7.3-0.20190327010347-be7ac8be2ae0
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/docker/docker v0.0.0-00010101000000-000000000000
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
@@ -27,27 +21,21 @@ require (
github.com/gorilla/websocket v1.4.1
github.com/joho/godotenv v1.3.0
github.com/jpillora/chisel v0.0.0-20190724232113-f3a8df20e389
github.com/json-iterator/go v1.1.10
github.com/json-iterator/go v1.1.8
github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c
github.com/mattn/go-shellwords v1.0.6 // indirect
github.com/mitchellh/mapstructure v1.1.2 // 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.1 // indirect
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-20210906052132-ef24824f7548
github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a
github.com/portainer/docker-compose-wrapper v0.0.0-20210527221011-0a1418224b92
github.com/portainer/libcompose v0.5.3
github.com/portainer/libcrypto v0.0.0-20190723020515-23ebe86ab2c2
github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33
github.com/robfig/cron/v3 v3.0.1
github.com/sirupsen/logrus v1.8.1
github.com/stretchr/testify v1.7.0
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
gopkg.in/alecthomas/kingpin.v2 v2.2.6
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
gotest.tools v2.2.0+incompatible // indirect
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c
k8s.io/api v0.17.2
k8s.io/apimachinery v0.17.2
k8s.io/client-go v0.17.2

View File

@@ -1,8 +1,8 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI=
github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0=
github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA=
@@ -11,31 +11,40 @@ github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxB
github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc=
github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Microsoft/go-winio v0.3.8/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
github.com/Microsoft/go-winio v0.4.16 h1:FtSW/jqD+l4ba5iPBj9CODVtgfYAD8w2wS923g/cFDk=
github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=
github.com/Microsoft/hcsshim v0.8.6 h1:ZfF0+zZeYdzMIVMZHKtDKJvLHj76XCuVae/jNkjj0IA=
github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg=
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 h1:AUNCr9CiJuwrRYS3XieqF+Z9B9gNxo/eANAJCF2eiN4=
github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/andrew-d/go-termutil v0.0.0-20150726205930-009166a695a2 h1:axBiC50cNZOs7ygH5BgQp4N+aYrZ2DNpWZ1KG3VOSOM=
github.com/andrew-d/go-termutil v0.0.0-20150726205930-009166a695a2/go.mod h1:jnzFpU88PccN/tPPhCpnNU8mZphvKxYM9lLNkd8e+os=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 h1:4daAzAu0S6Vi7/lbWECcX0j45yZReDZ56BQsrVBOEEY=
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/containerd/containerd v1.3.1 h1:LdbWxLhkAIxGO7h3mATHkyav06WuDs/yTWxIljJOTks=
github.com/containerd/containerd v1.3.1/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc h1:TP+534wVlf61smEIq1nwLLAjQVEK2EADoW3CX9AuT+8=
github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
@@ -47,16 +56,23 @@ github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9 h1:74lLNRzvsdIlkTgfD
github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/docker/cli v0.0.0-20190711175710-5b38d82aa076/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v0.0.0-20191126203649-54d085b857e9 h1:Q6D6b2iRKhvtL3Wj9p0SyPOvUDJ1ht62mbiBoNJ3Aus=
github.com/docker/cli v0.0.0-20191126203649-54d085b857e9/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker-credential-helpers v0.6.3 h1:zI2p9+1NQYdnG6sMU26EX4aVGlqbInSQxQXLvzJ4RPQ=
github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y=
github.com/docker/engine v1.4.2-0.20200204220554-5f6d6f3f2203 h1:QeBh8wW8pIZKlXxlMOQ8hSCMdJA+2Z/bD/iDyCAS8XU=
github.com/docker/engine v1.4.2-0.20200204220554-5f6d6f3f2203/go.mod h1:3CPr2caMgTHxxIAZgEMd3uLYPDlRvPqCpyeRf6ncPcY=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-connections v0.3.0 h1:3lOnM9cSzgGwx8VfK/NGOW5fLQ0GjIlCkaktF+n1M6o=
github.com/docker/go-connections v0.3.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-metrics v0.0.0-20181218153428-b84716841b82 h1:X0fj836zx99zFu83v/M79DuBn84IL/Syx1SY6Y5ZEMA=
github.com/docker/go-metrics v0.0.0-20181218153428-b84716841b82/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI=
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4=
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96 h1:cenwrSVm+Z7QLSV/BsnenAOcDXdX4cMv4wP0B/5QbPg=
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e h1:p1yVGRW3nmb85p1Sh1ZJSDm4A4iKLS5QNbvUHMgGu/M=
@@ -66,6 +82,7 @@ github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
github.com/evanphx/json-patch v4.2.0+incompatible h1:fUDGZCv/7iAN7u0puUVhvKCcsR6vRfwrJatElLBEf0I=
github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
@@ -85,15 +102,20 @@ github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12 h1:PbK
github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw=
github.com/go-git/go-git/v5 v5.3.0 h1:8WKMtJR2j8RntEXR/uvTKagfEt4GYlwQ7mntE4+0GWc=
github.com/go-git/go-git/v5 v5.3.0/go.mod h1:xdX4bWJ48aOrdhnl2XqHYstHbbp6+LFS4r4X+lNVprw=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-ldap/ldap/v3 v3.1.8 h1:5vU/2jOh9HqprwXp8aF915s9p6Z8wmbSEVF7/gdTFhM=
github.com/go-ldap/ldap/v3 v3.1.8/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0=
github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg=
github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc=
github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d h1:3PaI8p3seN09VjbTYC/QWlUZdZ1qS1zGjy7LH2Wt07I=
github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
@@ -102,6 +124,7 @@ github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfb
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
@@ -119,6 +142,8 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+
github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d h1:7XGaL1e6bYS1yIonGp9761ExpPPV1ui0SAC59Yube9k=
github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/mux v0.0.0-20160317213430-0eeaf8392f5b/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
@@ -149,20 +174,25 @@ github.com/jpillora/requestlog v0.0.0-20181015073026-df8817be5f82/go.mod h1:w8bu
github.com/jpillora/sizestr v0.0.0-20160130011556-e2ea2fa42fb9 h1:0c9jcgBtHRtDU//jTrcCgWG6UHjMZytiq/3WhraNgUM=
github.com/jpillora/sizestr v0.0.0-20160130011556-e2ea2fa42fb9/go.mod h1:1ffp+CRe0eAwwRb0/BownUAjMBsmTLwgAvRbfj9dRwE=
github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.8 h1:QiWkFLKq0T7mpzwOTu6BzNDbfTE8OLrYhVKYMLF46Ok=
github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 h1:DowS9hvgyYSX4TO5NpyC606/Z4SxnNYbT+WX27or6Ck=
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c h1:N7A4JCA2G+j5fuFxCsJqjFU/sZe0mj8H0sSoSwbaikw=
github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c/go.mod h1:Nn5wlyECw3iJrzi0AhIWg+AJUb4PlRQVW4/3XHH1LZA=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v0.0.0-20150511174710-5cf931ef8f76/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -170,6 +200,8 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mattn/go-shellwords v1.0.6 h1:9Jok5pILi5S1MnDirGVTufYGtksUs/V2BWUP3ZkeUUI=
github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
@@ -181,9 +213,10 @@ github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lN
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c h1:nXxl5PrvVm2L/wCy8dQu6DMTwH4oIuGN8GJDAlqDdVE=
github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@@ -193,32 +226,49 @@ github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+
github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME=
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420 h1:Yu3681ykYHDfLoI6XVjL4JWmkE+3TX9yfIWwRCh1kFM=
github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
github.com/opencontainers/image-spec v0.0.0-20170515205857-f03dbe35d449 h1:Aq8iG72akPb/kszE7ksZ5ldV+JYPYii/KZOxlpJF07s=
github.com/opencontainers/image-spec v0.0.0-20170515205857-f03dbe35d449/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/opencontainers/runc v0.0.0-20161109192122-51371867a01c h1:iOMba/KmaXgSX5PFKu1u6s+DZXiq+EzPayawa76w6aA=
github.com/opencontainers/runc v0.0.0-20161109192122-51371867a01c/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6 h1:lNCW6THrCKBiJBpz8kbVGjC7MgdCGKwuvBgc7LoD6sw=
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/portainer/docker-compose-wrapper v0.0.0-20210906052132-ef24824f7548 h1:5I9j0e6f9KG/RV6YBKWyks8LSHheE+ltJgpMyyWYUoo=
github.com/portainer/docker-compose-wrapper v0.0.0-20210906052132-ef24824f7548/go.mod h1:WxDlJWZxCnicdLCPnLNEv7/gRhjeIVuCGmsv+iOPH3c=
github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a h1:qY8TbocN75n5PDl16o0uVr5MevtM5IhdwSelXEd4nFM=
github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a/go.mod h1:n54EEIq+MM0NNtqLeCby8ljL+l275VpolXO0ibHegLE=
github.com/portainer/docker-compose-wrapper v0.0.0-20210527221011-0a1418224b92 h1:Hh7SHCf3SJblVywU0TTn5lpTKsH5W23LAKH5sqWggig=
github.com/portainer/docker-compose-wrapper v0.0.0-20210527221011-0a1418224b92/go.mod h1:PF2O2O4UNYWdtPcp6n/mIKpKk+f1jhFTezS8txbf+XM=
github.com/portainer/libcompose v0.5.3 h1:tE4WcPuGvo+NKeDkDWpwNavNLZ5GHIJ4RvuZXsI9uI8=
github.com/portainer/libcompose v0.5.3/go.mod h1:7SKd/ho69rRKHDFSDUwkbMcol2TMKU5OslDsajr8Ro8=
github.com/portainer/libcrypto v0.0.0-20190723020515-23ebe86ab2c2 h1:0PfgGLys9yHr4rtnirg0W0Cjvv6/DzxBIZk5sV59208=
github.com/portainer/libcrypto v0.0.0-20190723020515-23ebe86ab2c2/go.mod h1:/wIeGwJOMYc1JplE/OvYMO5korce39HddIfI8VKGyAM=
github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33 h1:H8HR2dHdBf8HANSkUyVw4o8+4tegGcd+zyKZ3e599II=
github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33/go.mod h1:Y2TfgviWI4rT2qaOTHr+hq6MdKIE5YjgQAu7qwptTV0=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.1.0 h1:BQ53HtBmfOitExawJ6LokA4x8ov/z0SYYb0+HxJfRI8=
github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.6.0 h1:kRhiuYSXR3+uv2IbVbZhUxK5zVD/2pp3Gd2PpvPkpEo=
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.3 h1:CTwfnzjQ+8dS6MhHHu4YswVAD99sL2wjPqP+VkURmKE=
github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
@@ -233,15 +283,17 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce h1:fb190+cK2Xz/dvi9Hv8eCYJYvIGUTN2/KLq1pT6CjEc=
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4=
github.com/urfave/cli v1.21.0/go.mod h1:lxDj6qX9Q6lWQxIrbrT0nwecwUtRnhVZAJjJZrVUZZQ=
github.com/xanzy/ssh-agent v0.3.0 h1:wUMzuKtKilRgBAD1sUb8gOwwRr2FGoBVumcjoOACClI=
github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/xeipuuv/gojsonschema v1.1.0 h1:ngVtJC9TY/lg0AA/1k48FYhBrhRoFlEmWzsehpNAaZg=
github.com/xeipuuv/gojsonschema v1.1.0/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181015023909-0c41d7ab0a0e/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
@@ -253,15 +305,18 @@ golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181017193950-04a2e542c03f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210326060303-6b1517762897 h1:KrsHThm5nFk34YtATK1LsThyGhGbGe1olrte/HInHvs=
@@ -274,6 +329,8 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456 h1:ng0gs1AKnRRuEMZoTLLlbOd+C17zUDepwGQBb/n+JVg=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
@@ -292,7 +349,9 @@ golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -302,8 +361,9 @@ google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoA
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7 h1:ZUjXAXmrAyrmmCPHgCA/vChHcpsX27MZ3yBonD/z1KE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/grpc v1.19.0 h1:cfg4PD8YEdSFnm7qLV4++93WcmhH2nIUhMjhdCvl3j8=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.22.1 h1:/7cs52RnTJmD43s3uxzlq2U7nqVTd/37viQwMrMNlOM=
google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -325,13 +385,13 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
k8s.io/api v0.17.2 h1:NF1UFXcKN7/OOv1uxdRz3qfra8AHsPav5M93hlV9+Dc=
k8s.io/api v0.17.2/go.mod h1:BS9fjjLc4CMuqfSO8vgbHPKMt5+SF0ET6u/RVDihTo4=
k8s.io/apimachinery v0.17.2 h1:hwDQQFbdRlpnnsR64Asdi55GyCaIP/3WQpMmbNBeWr4=

View File

@@ -5,6 +5,7 @@ import (
"log"
"net/http"
"strings"
"time"
"github.com/asaskevich/govalidator"
httperror "github.com/portainer/libhttp/error"
@@ -130,7 +131,32 @@ func (handler *Handler) authenticateLDAPAndCreateUser(w http.ResponseWriter, use
}
func (handler *Handler) writeToken(w http.ResponseWriter, user *portainer.User) *httperror.HandlerError {
return handler.persistAndWriteToken(w, composeTokenData(user))
tokenData := composeTokenData(user)
user.HasAuthenticatedOnce = true
err := handler.DataStore.User().UpdateUser(user.ID, user)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist user inside the database", err}
}
return handler.persistAndWriteToken(w, tokenData)
}
func (handler *Handler) writeTokenForOAuth(w http.ResponseWriter, user *portainer.User, expiryTime *time.Time) *httperror.HandlerError {
token, err := handler.JWTService.GenerateTokenForOAuth(composeTokenData(user), expiryTime)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to generate JWT token", Err: err}
}
user.HasAuthenticatedOnce = true
err = handler.DataStore.User().UpdateUser(user.ID, user)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist user inside the database", err}
}
return response.JSON(w, &authenticateResponse{JWT: token})
}
func (handler *Handler) persistAndWriteToken(w http.ResponseWriter, tokenData *portainer.TokenData) *httperror.HandlerError {
@@ -201,8 +227,9 @@ func teamMembershipExists(teamID portainer.TeamID, memberships []portainer.TeamM
func composeTokenData(user *portainer.User) *portainer.TokenData {
return &portainer.TokenData{
ID: user.ID,
Username: user.Username,
Role: user.Role,
ID: user.ID,
Username: user.Username,
Role: user.Role,
HasAuthenticatedOnce: user.HasAuthenticatedOnce,
}
}

View File

@@ -4,6 +4,7 @@ import (
"errors"
"log"
"net/http"
"time"
"github.com/asaskevich/govalidator"
httperror "github.com/portainer/libhttp/error"
@@ -25,21 +26,21 @@ func (payload *oauthPayload) Validate(r *http.Request) error {
return nil
}
func (handler *Handler) authenticateOAuth(code string, settings *portainer.OAuthSettings) (string, error) {
func (handler *Handler) authenticateOAuth(code string, settings *portainer.OAuthSettings) (string, *time.Time, error) {
if code == "" {
return "", errors.New("Invalid OAuth authorization code")
return "", nil, errors.New("Invalid OAuth authorization code")
}
if settings == nil {
return "", errors.New("Invalid OAuth configuration")
return "", nil, errors.New("Invalid OAuth configuration")
}
username, err := handler.OAuthService.Authenticate(code, settings)
username, expiryTime, err := handler.OAuthService.Authenticate(code, settings)
if err != nil {
return "", err
return "", nil, err
}
return username, nil
return username, expiryTime, nil
}
// @id ValidateOAuth
@@ -69,7 +70,7 @@ func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) *h
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "OAuth authentication is not enabled", Err: errors.New("OAuth authentication is not enabled")}
}
username, err := handler.authenticateOAuth(payload.Code, &settings.OAuthSettings)
username, expiryTime, err := handler.authenticateOAuth(payload.Code, &settings.OAuthSettings)
if err != nil {
log.Printf("[DEBUG] - OAuth authentication error: %s", err)
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to authenticate through OAuth", Err: httperrors.ErrUnauthorized}
@@ -110,5 +111,5 @@ func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) *h
}
return handler.writeToken(w, user)
return handler.writeTokenForOAuth(w, user, expiryTime)
}

View File

@@ -105,10 +105,9 @@ type customTemplateFromFileContentPayload struct {
Note string `example:"This is my <b>custom</b> template"`
// Platform associated to the template.
// Valid values are: 1 - 'linux', 2 - 'windows'
// Required for Docker stacks
Platform portainer.CustomTemplatePlatform `example:"1" enums:"1,2"`
// Type of created stack (1 - swarm, 2 - compose, 3 - kubernetes)
Type portainer.StackType `example:"1" enums:"1,2,3" validate:"required"`
Platform portainer.CustomTemplatePlatform `example:"1" enums:"1,2" validate:"required"`
// Type of created stack (1 - swarm, 2 - compose)
Type portainer.StackType `example:"1" enums:"1,2" validate:"required"`
// Content of stack file
FileContent string `validate:"required"`
}
@@ -123,10 +122,10 @@ func (payload *customTemplateFromFileContentPayload) Validate(r *http.Request) e
if govalidator.IsNull(payload.FileContent) {
return errors.New("Invalid file content")
}
if payload.Type != portainer.KubernetesStack && payload.Platform != portainer.CustomTemplatePlatformLinux && payload.Platform != portainer.CustomTemplatePlatformWindows {
if payload.Platform != portainer.CustomTemplatePlatformLinux && payload.Platform != portainer.CustomTemplatePlatformWindows {
return errors.New("Invalid custom template platform")
}
if payload.Type != portainer.KubernetesStack && payload.Type != portainer.DockerSwarmStack && payload.Type != portainer.DockerComposeStack {
if payload.Type != portainer.DockerSwarmStack && payload.Type != portainer.DockerComposeStack {
return errors.New("Invalid custom template type")
}
return nil
@@ -172,8 +171,7 @@ type customTemplateFromGitRepositoryPayload struct {
Note string `example:"This is my <b>custom</b> template"`
// Platform associated to the template.
// Valid values are: 1 - 'linux', 2 - 'windows'
// Required for Docker stacks
Platform portainer.CustomTemplatePlatform `example:"1" enums:"1,2"`
Platform portainer.CustomTemplatePlatform `example:"1" enums:"1,2" validate:"required"`
// Type of created stack (1 - swarm, 2 - compose)
Type portainer.StackType `example:"1" enums:"1,2" validate:"required"`
@@ -207,11 +205,6 @@ func (payload *customTemplateFromGitRepositoryPayload) Validate(r *http.Request)
if govalidator.IsNull(payload.ComposeFilePathInRepository) {
payload.ComposeFilePathInRepository = filesystem.ComposeFileDefaultName
}
if payload.Type == portainer.KubernetesStack {
return errors.New("Creating a Kubernetes custom template from git is not supported")
}
if payload.Platform != portainer.CustomTemplatePlatformLinux && payload.Platform != portainer.CustomTemplatePlatformWindows {
return errors.New("Invalid custom template platform")
}
@@ -285,21 +278,20 @@ func (payload *customTemplateFromFileUploadPayload) Validate(r *http.Request) er
note, _ := request.RetrieveMultiPartFormValue(r, "Note", true)
payload.Note = note
platform, _ := request.RetrieveNumericMultiPartFormValue(r, "Platform", true)
templatePlatform := portainer.CustomTemplatePlatform(platform)
if templatePlatform != portainer.CustomTemplatePlatformLinux && templatePlatform != portainer.CustomTemplatePlatformWindows {
return errors.New("Invalid custom template platform")
}
payload.Platform = templatePlatform
typeNumeral, _ := request.RetrieveNumericMultiPartFormValue(r, "Type", true)
templateType := portainer.StackType(typeNumeral)
if templateType != portainer.KubernetesStack && templateType != portainer.DockerSwarmStack && templateType != portainer.DockerComposeStack {
if templateType != portainer.DockerComposeStack && templateType != portainer.DockerSwarmStack {
return errors.New("Invalid custom template type")
}
payload.Type = templateType
platform, _ := request.RetrieveNumericMultiPartFormValue(r, "Platform", true)
templatePlatform := portainer.CustomTemplatePlatform(platform)
if templateType != portainer.KubernetesStack && templatePlatform != portainer.CustomTemplatePlatformLinux && templatePlatform != portainer.CustomTemplatePlatformWindows {
return errors.New("Invalid custom template platform")
}
payload.Platform = templatePlatform
composeFileContent, _, err := request.RetrieveMultiPartFormFile(r, "File")
if err != nil {
return errors.New("Invalid Compose file. Ensure that the Compose file is uploaded correctly")

View File

@@ -2,9 +2,7 @@ package customtemplates
import (
"net/http"
"strconv"
"github.com/pkg/errors"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
@@ -19,16 +17,10 @@ import (
// @tags custom_templates
// @security jwt
// @produce json
// @param type query []int true "Template types" Enums(1,2,3)
// @success 200 {array} portainer.CustomTemplate "Success"
// @failure 500 "Server error"
// @router /custom_templates [get]
func (handler *Handler) customTemplateList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
templateTypes, err := parseTemplateTypes(r)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid Custom template type", err}
}
customTemplates, err := handler.DataStore.CustomTemplate().CustomTemplates()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve custom templates from the database", err}
@@ -60,52 +52,5 @@ func (handler *Handler) customTemplateList(w http.ResponseWriter, r *http.Reques
customTemplates = authorization.FilterAuthorizedCustomTemplates(customTemplates, user, userTeamIDs)
}
customTemplates = filterByType(customTemplates, templateTypes)
return response.JSON(w, customTemplates)
}
func parseTemplateTypes(r *http.Request) ([]portainer.StackType, error) {
err := r.ParseForm()
if err != nil {
return nil, errors.WithMessage(err, "failed to parse request params")
}
types, exist := r.Form["type"]
if !exist {
return []portainer.StackType{}, nil
}
res := []portainer.StackType{}
for _, templateTypeStr := range types {
templateType, err := strconv.Atoi(templateTypeStr)
if err != nil {
return nil, errors.WithMessage(err, "failed parsing template type")
}
res = append(res, portainer.StackType(templateType))
}
return res, nil
}
func filterByType(customTemplates []portainer.CustomTemplate, templateTypes []portainer.StackType) []portainer.CustomTemplate {
if len(templateTypes) == 0 {
return customTemplates
}
typeSet := map[portainer.StackType]bool{}
for _, templateType := range templateTypes {
typeSet[templateType] = true
}
filtered := []portainer.CustomTemplate{}
for _, template := range customTemplates {
if typeSet[template.Type] {
filtered = append(filtered, template)
}
}
return filtered
}

View File

@@ -27,10 +27,9 @@ type customTemplateUpdatePayload struct {
Note string `example:"This is my <b>custom</b> template"`
// Platform associated to the template.
// Valid values are: 1 - 'linux', 2 - 'windows'
// Required for Docker stacks
Platform portainer.CustomTemplatePlatform `example:"1" enums:"1,2"`
// Type of created stack (1 - swarm, 2 - compose, 3 - kubernetes)
Type portainer.StackType `example:"1" enums:"1,2,3" validate:"required"`
Platform portainer.CustomTemplatePlatform `example:"1" enums:"1,2" validate:"required"`
// Type of created stack (1 - swarm, 2 - compose)
Type portainer.StackType `example:"1" enums:"1,2" validate:"required"`
// Content of stack file
FileContent string `validate:"required"`
}
@@ -42,10 +41,10 @@ func (payload *customTemplateUpdatePayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.FileContent) {
return errors.New("Invalid file content")
}
if payload.Type != portainer.KubernetesStack && payload.Platform != portainer.CustomTemplatePlatformLinux && payload.Platform != portainer.CustomTemplatePlatformWindows {
if payload.Platform != portainer.CustomTemplatePlatformLinux && payload.Platform != portainer.CustomTemplatePlatformWindows {
return errors.New("Invalid custom template platform")
}
if payload.Type != portainer.KubernetesStack && payload.Type != portainer.DockerSwarmStack && payload.Type != portainer.DockerComposeStack {
if payload.Type != portainer.DockerComposeStack && payload.Type != portainer.DockerSwarmStack {
return errors.New("Invalid custom template type")
}
if govalidator.IsNull(payload.Description) {

View File

@@ -29,10 +29,6 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToDockerAPI)))
h.PathPrefix("/{id}/kubernetes").Handler(
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToKubernetesAPI)))
h.PathPrefix("/{id}/agent/docker").Handler(
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToDockerAPI)))
h.PathPrefix("/{id}/agent/kubernetes").Handler(
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToKubernetesAPI)))
h.PathPrefix("/{id}/storidge").Handler(
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToStoridgeAPI)))
return h

View File

@@ -3,7 +3,6 @@ package endpointproxy
import (
"errors"
"strconv"
"strings"
"time"
httperror "github.com/portainer/libhttp/error"
@@ -66,12 +65,6 @@ func (handler *Handler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http.
}
id := strconv.Itoa(endpointID)
prefix := "/" + id + "/agent/docker";
if !strings.HasPrefix(r.URL.Path, prefix) {
prefix = "/" + id + "/docker";
}
http.StripPrefix(prefix, proxy).ServeHTTP(w, r)
http.StripPrefix("/"+id+"/docker", proxy).ServeHTTP(w, r)
return nil
}

View File

@@ -65,18 +65,17 @@ func (handler *Handler) proxyRequestsToKubernetesAPI(w http.ResponseWriter, r *h
}
}
// For KubernetesLocalEnvironment
requestPrefix := fmt.Sprintf("/%d/kubernetes", endpointID)
if endpoint.Type == portainer.AgentOnKubernetesEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment {
requestPrefix = fmt.Sprintf("/%d", endpointID)
agentPrefix := fmt.Sprintf("/%d/agent/kubernetes", endpointID)
if strings.HasPrefix(r.URL.Path, agentPrefix) {
requestPrefix = agentPrefix
if isKubernetesRequest(strings.TrimPrefix(r.URL.String(), requestPrefix)) {
requestPrefix = fmt.Sprintf("/%d", endpointID)
}
}
http.StripPrefix(requestPrefix, proxy).ServeHTTP(w, r)
return nil
}
func isKubernetesRequest(requestURL string) bool {
return strings.HasPrefix(requestURL, "/api") || strings.HasPrefix(requestURL, "/healthz")
}

View File

@@ -1,88 +0,0 @@
package endpoints
import (
"encoding/base64"
"errors"
"fmt"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
"net/http"
"regexp"
"strings"
)
// @id EndpointAssociationDelete
// @summary De-association an edge endpoint
// @description De-association an edge endpoint.
// @description **Access policy**: administrator
// @security jwt
// @tags endpoints
// @produce json
// @param id path int true "Endpoint identifier"
// @success 200 {object} portainer.Endpoint "Success"
// @failure 400 "Invalid request"
// @failure 404 "Endpoint not found"
// @failure 500 "Server error"
// @router /api/endpoints/:id/association [put]
func (handler *Handler) endpointAssociationDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
}
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
}
if endpoint.Type != portainer.EdgeAgentOnKubernetesEnvironment && endpoint.Type != portainer.EdgeAgentOnDockerEnvironment {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint type", errors.New("Invalid endpoint type")}
}
endpoint.EdgeID = ""
endpoint.Snapshots = []portainer.DockerSnapshot{}
endpoint.Kubernetes.Snapshots = []portainer.KubernetesSnapshot{}
endpoint.EdgeKey, err = handler.updateEdgeKey(endpoint.EdgeKey)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Invalid EdgeKey", err}
}
err = handler.DataStore.Endpoint().UpdateEndpoint(portainer.EndpointID(endpointID), endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Failed persisting endpoint in database", err}
}
handler.ReverseTunnelService.SetTunnelStatusToIdle(endpoint.ID)
return response.JSON(w, endpoint)
}
func (handler *Handler) updateEdgeKey(edgeKey string) (string, error) {
oldEdgeKeyByte, err := base64.RawStdEncoding.DecodeString(edgeKey)
if err != nil {
return "", err
}
oldEdgeKeyStr := string(oldEdgeKeyByte)
httpPort := getPort(handler.BindAddress)
httpsPort := getPort(handler.BindAddressHTTPS)
// replace "http://" with "https://" and replace ":9000" with ":9443", in the case of default values
// oldEdgeKeyStr example: http://10.116.1.178:9000|10.116.1.178:8000|46:99:4a:8d:a6:de:6a:bd:d8:e2:1c:99:81:60:54:55|52
r := regexp.MustCompile(fmt.Sprintf("^(http://)([^|]+)(:%s)(|.*)", httpPort))
newEdgeKeyStr := r.ReplaceAllString(oldEdgeKeyStr, fmt.Sprintf("https://$2:%s$4", httpsPort))
return base64.RawStdEncoding.EncodeToString([]byte(newEdgeKeyStr)), nil
}
func getPort(url string) string {
items := strings.Split(url, ":")
return items[len(items) - 1]
}

View File

@@ -10,7 +10,7 @@ import (
portainer "github.com/portainer/portainer/api"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/endpointutils"
endpointutils "github.com/portainer/portainer/api/internal/endpoint"
)
// GET request on /endpoints/{id}/registries?namespace

View File

@@ -32,8 +32,6 @@ type Handler struct {
K8sClientFactory *cli.ClientFactory
ComposeStackManager portainer.ComposeStackManager
AuthorizationService *authorization.Service
BindAddress string
BindAddressHTTPS string
}
// NewHandler creates a handler to manage endpoint operations.
@@ -47,8 +45,6 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointCreate))).Methods(http.MethodPost)
h.Handle("/endpoints/{id}/settings",
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointSettingsUpdate))).Methods(http.MethodPut)
h.Handle("/endpoints/{id}/association",
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointAssociationDelete))).Methods(http.MethodDelete)
h.Handle("/endpoints/snapshot",
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointSnapshots))).Methods(http.MethodPost)
h.Handle("/endpoints",

View File

@@ -16,13 +16,11 @@ import (
"github.com/portainer/portainer/api/http/handler/endpointproxy"
"github.com/portainer/portainer/api/http/handler/endpoints"
"github.com/portainer/portainer/api/http/handler/file"
"github.com/portainer/portainer/api/http/handler/kubernetes"
"github.com/portainer/portainer/api/http/handler/motd"
"github.com/portainer/portainer/api/http/handler/registries"
"github.com/portainer/portainer/api/http/handler/resourcecontrols"
"github.com/portainer/portainer/api/http/handler/roles"
"github.com/portainer/portainer/api/http/handler/settings"
"github.com/portainer/portainer/api/http/handler/ssl"
"github.com/portainer/portainer/api/http/handler/stacks"
"github.com/portainer/portainer/api/http/handler/status"
"github.com/portainer/portainer/api/http/handler/tags"
@@ -48,14 +46,12 @@ type Handler struct {
EndpointGroupHandler *endpointgroups.Handler
EndpointHandler *endpoints.Handler
EndpointProxyHandler *endpointproxy.Handler
KubernetesHandler *kubernetes.Handler
FileHandler *file.Handler
MOTDHandler *motd.Handler
RegistryHandler *registries.Handler
ResourceControlHandler *resourcecontrols.Handler
RoleHandler *roles.Handler
SettingsHandler *settings.Handler
SSLHandler *ssl.Handler
StackHandler *stacks.Handler
StatusHandler *status.Handler
TagHandler *tags.Handler
@@ -69,7 +65,7 @@ type Handler struct {
}
// @title PortainerCE API
// @version 2.6.3
// @version 2.1.1
// @description.markdown api-description.md
// @termsOfService
@@ -104,8 +100,6 @@ type Handler struct {
// @tag.description Manage Docker environments
// @tag.name endpoint_groups
// @tag.description Manage endpoint groups
// @tag.name kubernetes
// @tag.description Manage Kubernetes cluster
// @tag.name motd
// @tag.description Fetch the message of the day
// @tag.name registries
@@ -132,8 +126,6 @@ type Handler struct {
// @tag.description Manage App Templates
// @tag.name stacks
// @tag.description Manage stacks
// @tag.name ssl
// @tag.description Manage ssl settings
// @tag.name upload
// @tag.description Upload files
// @tag.name webhooks
@@ -164,8 +156,6 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.StripPrefix("/api", h.EdgeTemplatesHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/endpoint_groups"):
http.StripPrefix("/api", h.EndpointGroupHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/kubernetes"):
http.StripPrefix("/api", h.KubernetesHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/endpoints"):
switch {
case strings.Contains(r.URL.Path, "/docker/"):
@@ -176,8 +166,6 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.StripPrefix("/api/endpoints", h.EndpointProxyHandler).ServeHTTP(w, r)
case strings.Contains(r.URL.Path, "/azure/"):
http.StripPrefix("/api/endpoints", h.EndpointProxyHandler).ServeHTTP(w, r)
case strings.Contains(r.URL.Path, "/agent/"):
http.StripPrefix("/api/endpoints", h.EndpointProxyHandler).ServeHTTP(w, r)
case strings.Contains(r.URL.Path, "/edge/"):
http.StripPrefix("/api/endpoints", h.EndpointEdgeHandler).ServeHTTP(w, r)
default:
@@ -205,8 +193,6 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.StripPrefix("/api", h.UploadHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/users"):
http.StripPrefix("/api", h.UserHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/ssl"):
http.StripPrefix("/api", h.SSLHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/teams"):
http.StripPrefix("/api", h.TeamHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/team_memberships"):

View File

@@ -1,71 +0,0 @@
package kubernetes
import (
"errors"
"net/http"
"github.com/gorilla/mux"
httperror "github.com/portainer/libhttp/error"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/middlewares"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/authorization"
"github.com/portainer/portainer/api/internal/endpointutils"
"github.com/portainer/portainer/api/kubernetes/cli"
)
// Handler is the HTTP handler which will natively deal with to external endpoints.
type Handler struct {
*mux.Router
dataStore portainer.DataStore
kubernetesClientFactory *cli.ClientFactory
authorizationService *authorization.Service
JwtService portainer.JWTService
}
// NewHandler creates a handler to process pre-proxied requests to external APIs.
func NewHandler(bouncer *security.RequestBouncer, authorizationService *authorization.Service, dataStore portainer.DataStore, kubernetesClientFactory *cli.ClientFactory) *Handler {
h := &Handler{
Router: mux.NewRouter(),
dataStore: dataStore,
kubernetesClientFactory: kubernetesClientFactory,
authorizationService: authorizationService,
}
kubeRouter := h.PathPrefix("/kubernetes/{id}").Subrouter()
kubeRouter.Use(bouncer.AuthenticatedAccess)
kubeRouter.Use(middlewares.WithEndpoint(dataStore.Endpoint(), "id"))
kubeRouter.Use(kubeOnlyMiddleware)
kubeRouter.PathPrefix("/config").Handler(
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.getKubernetesConfig))).Methods(http.MethodGet)
kubeRouter.PathPrefix("/nodes_limits").Handler(
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.getKubernetesNodesLimits))).Methods(http.MethodGet)
// namespaces
// in the future this piece of code might be in another package (or a few different packages - namespaces/namespace?)
// to keep it simple, we've decided to leave it like this.
namespaceRouter := kubeRouter.PathPrefix("/namespaces/{namespace}").Subrouter()
namespaceRouter.Handle("/system", bouncer.RestrictedAccess(httperror.LoggerHandler(h.namespacesToggleSystem))).Methods(http.MethodPut)
return h
}
func kubeOnlyMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, request *http.Request) {
endpoint, err := middlewares.FetchEndpoint(request)
if err != nil {
httperror.WriteError(rw, http.StatusInternalServerError, "Unable to find an endpoint on request context", err)
return
}
if !endpointutils.IsKubernetesEndpoint(endpoint) {
errMessage := "Endpoint is not a kubernetes endpoint"
httperror.WriteError(rw, http.StatusBadRequest, errMessage, errors.New(errMessage))
return
}
next.ServeHTTP(rw, request)
})
}

View File

@@ -1,109 +0,0 @@
package kubernetes
import (
"errors"
"fmt"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/http/security"
kcli "github.com/portainer/portainer/api/kubernetes/cli"
"net/http"
)
// @id GetKubernetesConfig
// @summary Generates kubeconfig file enabling client communication with k8s api server
// @description Generates kubeconfig file enabling client communication with k8s api server
// @description **Access policy**: authorized
// @tags kubernetes
// @security jwt
// @accept json
// @produce json
// @param id path int true "Endpoint identifier"
// @success 200 "Success"
// @failure 400 "Invalid request"
// @failure 401 "Unauthorized"
// @failure 403 "Permission denied"
// @failure 404 "Endpoint or ServiceAccount not found"
// @failure 500 "Server error"
// @router /kubernetes/{id}/config [get]
func (handler *Handler) getKubernetesConfig(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
}
endpoint, err := handler.dataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
}
tokenData, err := security.RetrieveTokenData(r)
if err != nil {
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
}
bearerToken, err := handler.JwtService.GenerateTokenForKubeconfig(tokenData)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to generate JWT token", err}
}
cli, err := handler.kubernetesClientFactory.GetKubeClient(endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create Kubernetes client", err}
}
apiServerURL := getProxyUrl(r, endpointID)
config, err := cli.GetKubeConfig(r.Context(), apiServerURL, bearerToken, tokenData)
if err != nil {
return &httperror.HandlerError{http.StatusNotFound, "Unable to generate Kubeconfig", err}
}
filenameBase := fmt.Sprintf("%s-%s", tokenData.Username, endpoint.Name)
contentAcceptHeader := r.Header.Get("Accept")
if contentAcceptHeader == "text/yaml" {
yaml, err := kcli.GenerateYAML(config)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Failed to generate Kubeconfig", err}
}
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; %s.yaml", filenameBase))
return YAML(w, yaml)
}
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; %s.json", filenameBase))
return response.JSON(w, config)
}
// getProxyUrl generates portainer proxy url which acts as proxy to k8s api server
func getProxyUrl(r *http.Request, endpointID int) string {
return fmt.Sprintf("https://%s/api/endpoints/%d/kubernetes", r.Host, endpointID)
}
// YAML writes yaml response as string to writer. Returns a pointer to a HandlerError if encoding fails.
// This could be moved to a more useful place; but that place is most likely not in this project.
// It should actually go in https://github.com/portainer/libhttp - since that is from where we use response.JSON.
// We use `data interface{}` as parameter - since im trying to keep it as close to (or the same as) response.JSON method signature:
// https://github.com/portainer/libhttp/blob/d20481a3da823c619887c440a22fdf4fa8f318f2/response/response.go#L13
func YAML(rw http.ResponseWriter, data interface{}) *httperror.HandlerError {
rw.Header().Set("Content-Type", "text/yaml")
strData, ok := data.(string)
if !ok {
return &httperror.HandlerError{
StatusCode: http.StatusInternalServerError,
Message: "Unable to write YAML response",
Err: errors.New("failed to convert input to string"),
}
}
fmt.Fprint(rw, strData)
return nil
}

View File

@@ -1,52 +0,0 @@
package kubernetes
import (
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
"net/http"
)
// @id getKubernetesNodesLimits
// @summary Get CPU and memory limits of all nodes within k8s cluster
// @description Get CPU and memory limits of all nodes within k8s cluster
// @description **Access policy**: authorized
// @tags kubernetes
// @security jwt
// @accept json
// @produce json
// @param id path int true "Endpoint identifier"
// @success 200 {object} K8sNodesLimits "Success"
// @failure 400 "Invalid request"
// @failure 401 "Unauthorized"
// @failure 403 "Permission denied"
// @failure 404 "Endpoint not found"
// @failure 500 "Server error"
// @router /kubernetes/{id}/nodes_limits [get]
func (handler *Handler) getKubernetesNodesLimits(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
}
endpoint, err := handler.dataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
}
cli, err := handler.kubernetesClientFactory.GetKubeClient(endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create Kubernetes client", err}
}
nodesLimits, err := cli.GetNodesLimits()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve nodes limits", err}
}
return response.JSON(w, nodesLimits)
}

View File

@@ -1,65 +0,0 @@
package kubernetes
import (
"net/http"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api/http/middlewares"
)
type namespacesToggleSystemPayload struct {
// Toggle the system state of this namespace to true or false
System bool `example:"true"`
}
func (payload *namespacesToggleSystemPayload) Validate(r *http.Request) error {
return nil
}
// @id KubernetesNamespacesToggleSystem
// @summary Toggle the system state for a namespace
// @description Toggle the system state for a namespace
// @description **Access policy**: administrator or endpoint admin
// @security jwt
// @tags kubernetes
// @accept json
// @param id path int true "Endpoint identifier"
// @param namespace path string true "Namespace name"
// @param body body namespacesToggleSystemPayload true "Update details"
// @success 200 "Success"
// @failure 400 "Invalid request"
// @failure 404 "Endpoint not found"
// @failure 500 "Server error"
// @router /kubernetes/{id}/namespaces/{namespace}/system [put]
func (handler *Handler) namespacesToggleSystem(rw http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpoint, err := middlewares.FetchEndpoint(r)
if err != nil {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint on request context", err}
}
namespaceName, err := request.RetrieveRouteVariableValue(r, "namespace")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid namespace identifier route variable", err}
}
var payload namespacesToggleSystemPayload
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
kubeClient, err := handler.kubernetesClientFactory.GetKubeClient(endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create kubernetes client", err}
}
err = kubeClient.ToggleSystemState(namespaceName, payload.System)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to toggle system status", err}
}
return response.Empty(rw)
}

View File

@@ -32,8 +32,6 @@ type settingsUpdatePayload struct {
EnableEdgeComputeFeatures *bool `example:"true"`
// The duration of a user session
UserSessionTimeout *string `example:"5m"`
// The expiry of a Kubeconfig
KubeconfigExpiry *string `example:"24h" default:"0"`
// Whether telemetry is enabled
EnableTelemetry *bool `example:"false"`
}
@@ -54,12 +52,6 @@ func (payload *settingsUpdatePayload) Validate(r *http.Request) error {
return errors.New("Invalid user session timeout")
}
}
if payload.KubeconfigExpiry != nil {
_, err := time.ParseDuration(*payload.KubeconfigExpiry)
if err != nil {
return errors.New("Invalid Kubeconfig Expiry")
}
}
return nil
}
@@ -143,10 +135,6 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
settings.EdgeAgentCheckinInterval = *payload.EdgeAgentCheckinInterval
}
if payload.KubeconfigExpiry != nil {
settings.KubeconfigExpiry = *payload.KubeconfigExpiry
}
if payload.UserSessionTimeout != nil {
settings.UserSessionTimeout = *payload.UserSessionTimeout

View File

@@ -1,29 +0,0 @@
package ssl
import (
"net/http"
"github.com/gorilla/mux"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/ssl"
)
// Handler is the HTTP handler used to handle MOTD operations.
type Handler struct {
*mux.Router
SSLService *ssl.Service
}
// NewHandler returns a new Handler
func NewHandler(bouncer *security.RequestBouncer) *Handler {
h := &Handler{
Router: mux.NewRouter(),
}
h.Handle("/ssl",
bouncer.AdminAccess(httperror.LoggerHandler(h.sslInspect))).Methods(http.MethodGet)
h.Handle("/ssl",
bouncer.AdminAccess(httperror.LoggerHandler(h.sslUpdate))).Methods(http.MethodPut)
return h
}

View File

@@ -1,29 +0,0 @@
package ssl
import (
"net/http"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/response"
)
// @id SSLInspect
// @summary Inspect the ssl settings
// @description Retrieve the ssl settings.
// @description **Access policy**: administrator
// @tags ssl
// @security jwt
// @produce json
// @success 200 {object} portainer.SSLSettings "Success"
// @failure 400 "Invalid request"
// @failure 403 "Permission denied to access settings"
// @failure 500 "Server error"
// @router /ssl [get]
func (handler *Handler) sslInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
settings, err := handler.SSLService.GetSSLSettings()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Failed to fetch certificate info", err}
}
return response.JSON(w, settings)
}

View File

@@ -1,62 +0,0 @@
package ssl
import (
"errors"
"net/http"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
)
type sslUpdatePayload struct {
Cert *string
Key *string
HTTPEnabled *bool
}
func (payload *sslUpdatePayload) Validate(r *http.Request) error {
if (payload.Cert == nil || payload.Key == nil) && payload.Cert != payload.Key {
return errors.New("both certificate and key files should be provided")
}
return nil
}
// @id SSLUpdate
// @summary Update the ssl settings
// @description Update the ssl settings.
// @description **Access policy**: administrator
// @tags ssl
// @security jwt
// @accept json
// @produce json
// @param body body sslUpdatePayload true "SSL Settings"
// @success 204 "Success"
// @failure 400 "Invalid request"
// @failure 403 "Permission denied to access settings"
// @failure 500 "Server error"
// @router /ssl [put]
func (handler *Handler) sslUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
var payload sslUpdatePayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
if payload.Cert != nil {
err = handler.SSLService.SetCertificates([]byte(*payload.Cert), []byte(*payload.Key))
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Failed to save certificate", err}
}
}
if payload.HTTPEnabled != nil {
err = handler.SSLService.SetHTTPEnabled(*payload.HTTPEnabled)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Failed to force https", err}
}
}
return response.Empty(w)
}

View File

@@ -1,38 +0,0 @@
package stacks
import (
"log"
"net/http"
"time"
httperror "github.com/portainer/libhttp/error"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/scheduler"
"github.com/portainer/portainer/api/stacks"
)
func startAutoupdate(stackID portainer.StackID, interval string, scheduler *scheduler.Scheduler, stackDeployer stacks.StackDeployer, datastore portainer.DataStore, gitService portainer.GitService) (jobID string, e *httperror.HandlerError) {
d, err := time.ParseDuration(interval)
if err != nil {
return "", &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Unable to parse stack's auto update interval", Err: err}
}
jobID = scheduler.StartJobEvery(d, func() {
if err := stacks.RedeployWhenChanged(stackID, stackDeployer, datastore, gitService); err != nil {
log.Printf("[ERROR] [http,stacks] [message: failed redeploying] [err: %s]\n", err)
}
})
return jobID, nil
}
func stopAutoupdate(stackID portainer.StackID, jobID string, scheduler scheduler.Scheduler) {
if jobID == "" {
return
}
if err := scheduler.StopJob(jobID); err != nil {
log.Printf("[WARN] could not stop the job for the stack %v", stackID)
}
}

View File

@@ -1,6 +1,7 @@
package stacks
import (
"errors"
"fmt"
"net/http"
"path"
@@ -8,12 +9,10 @@ import (
"time"
"github.com/asaskevich/govalidator"
"github.com/pkg/errors"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/filesystem"
gittypes "github.com/portainer/portainer/api/git/types"
"github.com/portainer/portainer/api/http/security"
)
@@ -46,7 +45,7 @@ func (handler *Handler) createComposeStackFromFileContent(w http.ResponseWriter,
payload.Name = handler.ComposeStackManager.NormalizeStackName(payload.Name)
isUnique, err := handler.checkUniqueStackNameInDocker(endpoint, payload.Name, 0, false)
isUnique, err := handler.checkUniqueName(endpoint, payload.Name, 0, false)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err}
}
@@ -101,6 +100,7 @@ func (handler *Handler) createComposeStackFromFileContent(w http.ResponseWriter,
type composeStackFromGitRepositoryPayload struct {
// Name of the stack
Name string `example:"myStack" validate:"required"`
// URL of a Git repository hosting the Stack file
RepositoryURL string `example:"https://github.com/openfaas/faas" validate:"required"`
// Reference name of a Git repository hosting the Stack file
@@ -112,11 +112,8 @@ type composeStackFromGitRepositoryPayload struct {
// Password used in basic authentication. Required when RepositoryAuthentication is true.
RepositoryPassword string `example:"myGitPassword"`
// Path to the Stack file inside the Git repository
ComposeFile string `example:"docker-compose.yml" default:"docker-compose.yml"`
// Applicable when deploying with multiple stack files
AdditionalFiles []string `example:"[nz.compose.yml, uat.compose.yml]"`
// Optional auto update configuration
AutoUpdate *portainer.StackAutoUpdate
ComposeFilePathInRepository string `example:"docker-compose.yml" default:"docker-compose.yml"`
// A list of environment variables used during stack deployment
Env []portainer.Pair
}
@@ -125,18 +122,14 @@ func (payload *composeStackFromGitRepositoryPayload) Validate(r *http.Request) e
if govalidator.IsNull(payload.Name) {
return errors.New("Invalid stack name")
}
if govalidator.IsNull(payload.RepositoryURL) || !govalidator.IsURL(payload.RepositoryURL) {
return errors.New("Invalid repository URL. Must correspond to a valid URL format")
}
if govalidator.IsNull(payload.RepositoryReferenceName) {
payload.RepositoryReferenceName = defaultGitReferenceName
}
if payload.RepositoryAuthentication && govalidator.IsNull(payload.RepositoryPassword) {
return errors.New("Invalid repository credentials. Password must be specified when authentication is enabled")
}
if err := validateStackAutoUpdate(payload.AutoUpdate); err != nil {
return err
if payload.RepositoryAuthentication && (govalidator.IsNull(payload.RepositoryUsername) || govalidator.IsNull(payload.RepositoryPassword)) {
return errors.New("Invalid repository credentials. Username and password must be specified when authentication is enabled")
}
return nil
}
@@ -148,72 +141,42 @@ func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWrite
}
payload.Name = handler.ComposeStackManager.NormalizeStackName(payload.Name)
if payload.ComposeFile == "" {
payload.ComposeFile = filesystem.ComposeFileDefaultName
if payload.ComposeFilePathInRepository == "" {
payload.ComposeFilePathInRepository = filesystem.ComposeFileDefaultName
}
isUnique, err := handler.checkUniqueStackNameInDocker(endpoint, payload.Name, 0, false)
isUnique, err := handler.checkUniqueName(endpoint, payload.Name, 0, false)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check for name collision", Err: err}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err}
}
if !isUnique {
return &httperror.HandlerError{StatusCode: http.StatusConflict, Message: fmt.Sprintf("A stack with the name '%s' already exists", payload.Name), Err: errStackAlreadyExists}
}
//make sure the webhook ID is unique
if payload.AutoUpdate != nil && payload.AutoUpdate.Webhook != "" {
isUnique, err := handler.checkUniqueWebhookID(payload.AutoUpdate.Webhook)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check for webhook ID collision", Err: err}
}
if !isUnique {
return &httperror.HandlerError{StatusCode: http.StatusConflict, Message: fmt.Sprintf("Webhook ID: %s already exists", payload.AutoUpdate.Webhook), Err: errWebhookIDAlreadyExists}
}
errorMessage := fmt.Sprintf("A stack with the name '%s' already exists", payload.Name)
return &httperror.HandlerError{http.StatusConflict, errorMessage, errors.New(errorMessage)}
}
stackID := handler.DataStore.Stack().GetNextIdentifier()
stack := &portainer.Stack{
ID: portainer.StackID(stackID),
Name: payload.Name,
Type: portainer.DockerComposeStack,
EndpointID: endpoint.ID,
EntryPoint: payload.ComposeFile,
AdditionalFiles: payload.AdditionalFiles,
AutoUpdate: payload.AutoUpdate,
Env: payload.Env,
GitConfig: &gittypes.RepoConfig{
URL: payload.RepositoryURL,
ReferenceName: payload.RepositoryReferenceName,
ConfigFilePath: payload.ComposeFile,
},
ID: portainer.StackID(stackID),
Name: payload.Name,
Type: portainer.DockerComposeStack,
EndpointID: endpoint.ID,
EntryPoint: payload.ComposeFilePathInRepository,
Env: payload.Env,
Status: portainer.StackStatusActive,
CreationDate: time.Now().Unix(),
}
if payload.RepositoryAuthentication {
stack.GitConfig.Authentication = &gittypes.GitAuthentication{
Username: payload.RepositoryUsername,
Password: payload.RepositoryPassword,
}
}
projectPath := handler.FileService.GetStackProjectPath(strconv.Itoa(int(stack.ID)))
stack.ProjectPath = projectPath
doCleanUp := true
defer handler.cleanUp(stack, &doCleanUp)
err = handler.clone(projectPath, payload.RepositoryURL, payload.RepositoryReferenceName, payload.RepositoryAuthentication, payload.RepositoryUsername, payload.RepositoryPassword)
err = handler.cloneAndSaveConfig(stack, projectPath, payload.RepositoryURL, payload.RepositoryReferenceName, payload.ComposeFilePathInRepository, payload.RepositoryAuthentication, payload.RepositoryUsername, payload.RepositoryPassword)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to clone git repository", Err: err}
}
commitID, err := handler.latestCommitID(payload.RepositoryURL, payload.RepositoryReferenceName, payload.RepositoryAuthentication, payload.RepositoryUsername, payload.RepositoryPassword)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to fetch git repository id", Err: err}
}
stack.GitConfig.ConfigHash = commitID
config, configErr := handler.createComposeDeployConfig(r, stack, endpoint)
if configErr != nil {
return configErr
@@ -224,15 +187,6 @@ func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWrite
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: err.Error(), Err: err}
}
if payload.AutoUpdate != nil && payload.AutoUpdate.Interval != "" {
jobID, e := startAutoupdate(stack.ID, stack.AutoUpdate.Interval, handler.Scheduler, handler.StackDeployer, handler.DataStore, handler.GitService)
if e != nil {
return e
}
stack.AutoUpdate.JobID = jobID
}
stack.CreatedBy = config.user.Username
err = handler.DataStore.Stack().CreateStack(stack)
@@ -281,7 +235,7 @@ func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter,
payload.Name = handler.ComposeStackManager.NormalizeStackName(payload.Name)
isUnique, err := handler.checkUniqueStackNameInDocker(endpoint, payload.Name, 0, false)
isUnique, err := handler.checkUniqueName(endpoint, payload.Name, 0, false)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check for name collision", Err: err}
}
@@ -377,7 +331,7 @@ func (handler *Handler) createComposeDeployConfig(r *http.Request, stack *portai
func (handler *Handler) deployComposeStack(config *composeStackDeploymentConfig) error {
isAdminOrEndpointAdmin, err := handler.userIsAdminOrEndpointAdmin(config.user, config.endpoint.ID)
if err != nil {
return errors.Wrap(err, "failed to check user priviliges deploying a stack")
return err
}
securitySettings := &config.endpoint.SecuritySettings
@@ -390,19 +344,27 @@ func (handler *Handler) deployComposeStack(config *composeStackDeploymentConfig)
!securitySettings.AllowContainerCapabilitiesForRegularUsers) &&
!isAdminOrEndpointAdmin {
for _, file := range append([]string{config.stack.EntryPoint}, config.stack.AdditionalFiles...) {
path := path.Join(config.stack.ProjectPath, file)
stackContent, err := handler.FileService.GetFileContent(path)
if err != nil {
return errors.Wrapf(err, "failed to get stack file content `%q`", path)
}
composeFilePath := path.Join(config.stack.ProjectPath, config.stack.EntryPoint)
stackContent, err := handler.FileService.GetFileContent(composeFilePath)
if err != nil {
return err
}
err = handler.isValidStackFile(stackContent, securitySettings)
if err != nil {
return errors.Wrap(err, "compose file is invalid")
}
err = handler.isValidStackFile(stackContent, securitySettings)
if err != nil {
return err
}
}
return handler.StackDeployer.DeployComposeStack(config.stack, config.endpoint, config.registries)
handler.stackCreationMutex.Lock()
defer handler.stackCreationMutex.Unlock()
handler.SwarmStackManager.Login(config.registries, config.endpoint)
err = handler.ComposeStackManager.Up(config.stack, config.endpoint)
if err != nil {
return err
}
return handler.SwarmStackManager.Logout(config.endpoint)
}

View File

@@ -1,14 +1,13 @@
package stacks
import (
"fmt"
"errors"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"strconv"
"time"
"github.com/pkg/errors"
"github.com/asaskevich/govalidator"
httperror "github.com/portainer/libhttp/error"
@@ -16,21 +15,17 @@ import (
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/filesystem"
gittypes "github.com/portainer/portainer/api/git/types"
"github.com/portainer/portainer/api/http/client"
"github.com/portainer/portainer/api/internal/stackutils"
k "github.com/portainer/portainer/api/kubernetes"
)
const defaultReferenceName = "refs/heads/master"
type kubernetesStringDeploymentPayload struct {
StackName string
ComposeFormat bool
Namespace string
StackFileContent string
}
type kubernetesGitDeploymentPayload struct {
StackName string
ComposeFormat bool
Namespace string
RepositoryURL string
@@ -38,16 +33,7 @@ type kubernetesGitDeploymentPayload struct {
RepositoryAuthentication bool
RepositoryUsername string
RepositoryPassword string
ManifestFile string
AdditionalFiles []string
AutoUpdate *portainer.StackAutoUpdate
}
type kubernetesManifestURLDeploymentPayload struct {
StackName string
Namespace string
ComposeFormat bool
ManifestURL string
FilePathInRepository string
}
func (payload *kubernetesStringDeploymentPayload) Validate(r *http.Request) error {
@@ -57,9 +43,6 @@ func (payload *kubernetesStringDeploymentPayload) Validate(r *http.Request) erro
if govalidator.IsNull(payload.Namespace) {
return errors.New("Invalid namespace")
}
if govalidator.IsNull(payload.StackName) {
return errors.New("Invalid stack name")
}
return nil
}
@@ -73,27 +56,11 @@ func (payload *kubernetesGitDeploymentPayload) Validate(r *http.Request) error {
if payload.RepositoryAuthentication && govalidator.IsNull(payload.RepositoryPassword) {
return errors.New("Invalid repository credentials. Password must be specified when authentication is enabled")
}
if govalidator.IsNull(payload.ManifestFile) {
return errors.New("Invalid manifest file in repository")
if govalidator.IsNull(payload.FilePathInRepository) {
return errors.New("Invalid file path in repository")
}
if govalidator.IsNull(payload.RepositoryReferenceName) {
payload.RepositoryReferenceName = defaultGitReferenceName
}
if err := validateStackAutoUpdate(payload.AutoUpdate); err != nil {
return err
}
if govalidator.IsNull(payload.StackName) {
return errors.New("Invalid stack name")
}
return nil
}
func (payload *kubernetesManifestURLDeploymentPayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.ManifestURL) || !govalidator.IsURL(payload.ManifestURL) {
return errors.New("Invalid manifest URL")
}
if govalidator.IsNull(payload.StackName) {
return errors.New("Invalid stack name")
payload.RepositoryReferenceName = defaultReferenceName
}
return nil
}
@@ -102,61 +69,33 @@ type createKubernetesStackResponse struct {
Output string `json:"Output"`
}
func (handler *Handler) createKubernetesStackFromFileContent(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError {
func (handler *Handler) createKubernetesStackFromFileContent(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint) *httperror.HandlerError {
var payload kubernetesStringDeploymentPayload
if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
}
user, err := handler.DataStore.User().User(userID)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to load user information from the database", Err: err}
}
isUnique, err := handler.checkUniqueStackName(endpoint, payload.StackName, 0)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check for name collision", Err: err}
}
if !isUnique {
return &httperror.HandlerError{StatusCode: http.StatusConflict, Message: fmt.Sprintf("A stack with the name '%s' already exists", payload.StackName), Err: errStackAlreadyExists}
}
stackID := handler.DataStore.Stack().GetNextIdentifier()
stack := &portainer.Stack{
ID: portainer.StackID(stackID),
Type: portainer.KubernetesStack,
EndpointID: endpoint.ID,
EntryPoint: filesystem.ManifestFileDefaultName,
Name: payload.StackName,
Namespace: payload.Namespace,
Status: portainer.StackStatusActive,
CreationDate: time.Now().Unix(),
CreatedBy: user.Username,
IsComposeFormat: payload.ComposeFormat,
OwnerUserID: user.ID,
ID: portainer.StackID(stackID),
Type: portainer.KubernetesStack,
EndpointID: endpoint.ID,
EntryPoint: filesystem.ManifestFileDefaultName,
Status: portainer.StackStatusActive,
CreationDate: time.Now().Unix(),
}
stackFolder := strconv.Itoa(int(stack.ID))
projectPath, err := handler.FileService.StoreStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent))
if err != nil {
fileType := "Manifest"
if stack.IsComposeFormat {
fileType = "Compose"
}
errMsg := fmt.Sprintf("Unable to persist Kubernetes %s file on disk", fileType)
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: errMsg, Err: err}
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist Kubernetes manifest file on disk", Err: err}
}
stack.ProjectPath = projectPath
doCleanUp := true
defer handler.cleanUp(stack, &doCleanUp)
output, err := handler.deployKubernetesStack(user.ID, endpoint, stack, k.KubeAppLabels{
StackID: stackID,
StackName: stack.Name,
Owner: stack.CreatedBy,
Kind: "content",
})
output, err := handler.deployKubernetesStack(endpoint, payload.StackFileContent, payload.ComposeFormat, payload.Namespace)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to deploy Kubernetes stack", Err: err}
}
@@ -170,67 +109,23 @@ func (handler *Handler) createKubernetesStackFromFileContent(w http.ResponseWrit
Output: output,
}
doCleanUp = false
return response.JSON(w, resp)
}
func (handler *Handler) createKubernetesStackFromGitRepository(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError {
func (handler *Handler) createKubernetesStackFromGitRepository(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint) *httperror.HandlerError {
var payload kubernetesGitDeploymentPayload
if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
}
user, err := handler.DataStore.User().User(userID)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to load user information from the database", Err: err}
}
isUnique, err := handler.checkUniqueStackName(endpoint, payload.StackName, 0)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check for name collision", Err: err}
}
if !isUnique {
return &httperror.HandlerError{StatusCode: http.StatusConflict, Message: fmt.Sprintf("A stack with the name '%s' already exists", payload.StackName), Err: errStackAlreadyExists}
}
//make sure the webhook ID is unique
if payload.AutoUpdate != nil && payload.AutoUpdate.Webhook != "" {
isUnique, err := handler.checkUniqueWebhookID(payload.AutoUpdate.Webhook)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check for webhook ID collision", Err: err}
}
if !isUnique {
return &httperror.HandlerError{StatusCode: http.StatusConflict, Message: fmt.Sprintf("Webhook ID: %s already exists", payload.AutoUpdate.Webhook), Err: errWebhookIDAlreadyExists}
}
}
stackID := handler.DataStore.Stack().GetNextIdentifier()
stack := &portainer.Stack{
ID: portainer.StackID(stackID),
Type: portainer.KubernetesStack,
EndpointID: endpoint.ID,
EntryPoint: payload.ManifestFile,
GitConfig: &gittypes.RepoConfig{
URL: payload.RepositoryURL,
ReferenceName: payload.RepositoryReferenceName,
ConfigFilePath: payload.ManifestFile,
},
Namespace: payload.Namespace,
Name: payload.StackName,
Status: portainer.StackStatusActive,
CreationDate: time.Now().Unix(),
CreatedBy: user.Username,
IsComposeFormat: payload.ComposeFormat,
AutoUpdate: payload.AutoUpdate,
AdditionalFiles: payload.AdditionalFiles,
OwnerUserID: user.ID,
}
if payload.RepositoryAuthentication {
stack.GitConfig.Authentication = &gittypes.GitAuthentication{
Username: payload.RepositoryUsername,
Password: payload.RepositoryPassword,
}
ID: portainer.StackID(stackID),
Type: portainer.KubernetesStack,
EndpointID: endpoint.ID,
EntryPoint: payload.FilePathInRepository,
Status: portainer.StackStatusActive,
CreationDate: time.Now().Unix(),
}
projectPath := handler.FileService.GetStackProjectPath(strconv.Itoa(int(stack.ID)))
@@ -239,44 +134,16 @@ func (handler *Handler) createKubernetesStackFromGitRepository(w http.ResponseWr
doCleanUp := true
defer handler.cleanUp(stack, &doCleanUp)
commitID, err := handler.latestCommitID(payload.RepositoryURL, payload.RepositoryReferenceName, payload.RepositoryAuthentication, payload.RepositoryUsername, payload.RepositoryPassword)
stackFileContent, err := handler.cloneManifestContentFromGitRepo(&payload, stack.ProjectPath)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to fetch git repository id", Err: err}
}
stack.GitConfig.ConfigHash = commitID
repositoryUsername := payload.RepositoryUsername
repositoryPassword := payload.RepositoryPassword
if !payload.RepositoryAuthentication {
repositoryUsername = ""
repositoryPassword = ""
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Failed to process manifest from Git repository", Err: err}
}
err = handler.GitService.CloneRepository(projectPath, payload.RepositoryURL, payload.RepositoryReferenceName, repositoryUsername, repositoryPassword)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Failed to clone git repository", Err: err}
}
output, err := handler.deployKubernetesStack(user.ID, endpoint, stack, k.KubeAppLabels{
StackID: stackID,
StackName: stack.Name,
Owner: stack.CreatedBy,
Kind: "git",
})
output, err := handler.deployKubernetesStack(endpoint, stackFileContent, payload.ComposeFormat, payload.Namespace)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to deploy Kubernetes stack", Err: err}
}
if payload.AutoUpdate != nil && payload.AutoUpdate.Interval != "" {
jobID, e := startAutoupdate(stack.ID, stack.AutoUpdate.Interval, handler.Scheduler, handler.StackDeployer, handler.DataStore, handler.GitService)
if e != nil {
return e
}
stack.AutoUpdate.JobID = jobID
}
err = handler.DataStore.Stack().CreateStack(stack)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist the stack inside the database", Err: err}
@@ -285,90 +152,40 @@ func (handler *Handler) createKubernetesStackFromGitRepository(w http.ResponseWr
resp := &createKubernetesStackResponse{
Output: output,
}
doCleanUp = false
return response.JSON(w, resp)
}
func (handler *Handler) createKubernetesStackFromManifestURL(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError {
var payload kubernetesManifestURLDeploymentPayload
if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
}
user, err := handler.DataStore.User().User(userID)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to load user information from the database", Err: err}
}
isUnique, err := handler.checkUniqueStackName(endpoint, payload.StackName, 0)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check for name collision", Err: err}
}
if !isUnique {
return &httperror.HandlerError{StatusCode: http.StatusConflict, Message: fmt.Sprintf("A stack with the name '%s' already exists", payload.StackName), Err: errStackAlreadyExists}
}
stackID := handler.DataStore.Stack().GetNextIdentifier()
stack := &portainer.Stack{
ID: portainer.StackID(stackID),
Type: portainer.KubernetesStack,
EndpointID: endpoint.ID,
EntryPoint: filesystem.ManifestFileDefaultName,
Namespace: payload.Namespace,
Name: payload.StackName,
Status: portainer.StackStatusActive,
CreationDate: time.Now().Unix(),
CreatedBy: user.Username,
IsComposeFormat: payload.ComposeFormat,
OwnerUserID: user.ID,
}
var manifestContent []byte
manifestContent, err = client.Get(payload.ManifestURL, 30)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve manifest from URL", Err: err}
}
stackFolder := strconv.Itoa(int(stack.ID))
projectPath, err := handler.FileService.StoreStackFileFromBytes(stackFolder, stack.EntryPoint, manifestContent)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist Kubernetes manifest file on disk", Err: err}
}
stack.ProjectPath = projectPath
doCleanUp := true
defer handler.cleanUp(stack, &doCleanUp)
output, err := handler.deployKubernetesStack(user.ID, endpoint, stack, k.KubeAppLabels{
StackID: stackID,
StackName: stack.Name,
Owner: stack.CreatedBy,
Kind: "url",
})
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to deploy Kubernetes stack", Err: err}
}
err = handler.DataStore.Stack().CreateStack(stack)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist the Kubernetes stack inside the database", Err: err}
}
resp := &createKubernetesStackResponse{
Output: output,
}
return response.JSON(w, resp)
}
func (handler *Handler) deployKubernetesStack(userID portainer.UserID, endpoint *portainer.Endpoint, stack *portainer.Stack, appLabels k.KubeAppLabels) (string, error) {
func (handler *Handler) deployKubernetesStack(endpoint *portainer.Endpoint, stackConfig string, composeFormat bool, namespace string) (string, error) {
handler.stackCreationMutex.Lock()
defer handler.stackCreationMutex.Unlock()
manifestFilePaths, tempDir, err := stackutils.CreateTempK8SDeploymentFiles(stack, handler.KubernetesDeployer, appLabels)
if err != nil {
return "", errors.Wrap(err, "failed to create temp kub deployment files")
if composeFormat {
convertedConfig, err := handler.KubernetesDeployer.ConvertCompose(stackConfig)
if err != nil {
return "", err
}
stackConfig = string(convertedConfig)
}
defer os.RemoveAll(tempDir)
return handler.KubernetesDeployer.Deploy(userID, endpoint, manifestFilePaths, stack.Namespace)
return handler.KubernetesDeployer.Deploy(endpoint, stackConfig, namespace)
}
func (handler *Handler) cloneManifestContentFromGitRepo(gitInfo *kubernetesGitDeploymentPayload, projectPath string) (string, error) {
repositoryUsername := gitInfo.RepositoryUsername
repositoryPassword := gitInfo.RepositoryPassword
if !gitInfo.RepositoryAuthentication {
repositoryUsername = ""
repositoryPassword = ""
}
err := handler.GitService.CloneRepository(projectPath, gitInfo.RepositoryURL, gitInfo.RepositoryReferenceName, repositoryUsername, repositoryPassword)
if err != nil {
return "", err
}
content, err := ioutil.ReadFile(filepath.Join(projectPath, gitInfo.FilePathInRepository))
if err != nil {
return "", err
}
return string(content), nil
}

View File

@@ -0,0 +1,64 @@
package stacks
import (
"io/ioutil"
"os"
"path"
"testing"
"github.com/stretchr/testify/assert"
)
type git struct {
content string
}
func (g *git) CloneRepository(destination string, repositoryURL, referenceName, username, password string) error {
return g.ClonePublicRepository(repositoryURL, referenceName, destination)
}
func (g *git) ClonePublicRepository(repositoryURL string, referenceName string, destination string) error {
return ioutil.WriteFile(path.Join(destination, "deployment.yml"), []byte(g.content), 0755)
}
func (g *git) ClonePrivateRepositoryWithBasicAuth(repositoryURL, referenceName string, destination, username, password string) error {
return g.ClonePublicRepository(repositoryURL, referenceName, destination)
}
func TestCloneAndConvertGitRepoFile(t *testing.T) {
dir, err := os.MkdirTemp("", "kube-create-stack")
assert.NoError(t, err, "failed to create a tmp dir")
defer os.RemoveAll(dir)
content := `apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.14.2
ports:
- containerPort: 80`
h := &Handler{
GitService: &git{
content: content,
},
}
gitInfo := &kubernetesGitDeploymentPayload{
FilePathInRepository: "deployment.yml",
}
fileContent, err := h.cloneManifestContentFromGitRepo(gitInfo, dir)
assert.NoError(t, err, "failed to clone or convert the file from Git repo")
assert.Equal(t, content, fileContent)
}

View File

@@ -1,20 +1,18 @@
package stacks
import (
"errors"
"fmt"
"net/http"
"path"
"strconv"
"time"
"github.com/pkg/errors"
"github.com/asaskevich/govalidator"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/filesystem"
gittypes "github.com/portainer/portainer/api/git/types"
"github.com/portainer/portainer/api/http/security"
)
@@ -51,8 +49,7 @@ func (handler *Handler) createSwarmStackFromFileContent(w http.ResponseWriter, r
payload.Name = handler.SwarmStackManager.NormalizeStackName(payload.Name)
isUnique, err := handler.checkUniqueStackNameInDocker(endpoint, payload.Name, 0, true)
isUnique, err := handler.checkUniqueName(endpoint, payload.Name, 0, true)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err}
}
@@ -124,11 +121,7 @@ type swarmStackFromGitRepositoryPayload struct {
// Password used in basic authentication. Required when RepositoryAuthentication is true.
RepositoryPassword string `example:"myGitPassword"`
// Path to the Stack file inside the Git repository
ComposeFile string `example:"docker-compose.yml" default:"docker-compose.yml"`
// Applicable when deploying with multiple stack files
AdditionalFiles []string `example:"[nz.compose.yml, uat.compose.yml]"`
// Optional auto update configuration
AutoUpdate *portainer.StackAutoUpdate
ComposeFilePathInRepository string `example:"docker-compose.yml" default:"docker-compose.yml"`
}
func (payload *swarmStackFromGitRepositoryPayload) Validate(r *http.Request) error {
@@ -141,14 +134,11 @@ func (payload *swarmStackFromGitRepositoryPayload) Validate(r *http.Request) err
if govalidator.IsNull(payload.RepositoryURL) || !govalidator.IsURL(payload.RepositoryURL) {
return errors.New("Invalid repository URL. Must correspond to a valid URL format")
}
if govalidator.IsNull(payload.RepositoryReferenceName) {
payload.RepositoryReferenceName = defaultGitReferenceName
if payload.RepositoryAuthentication && (govalidator.IsNull(payload.RepositoryUsername) || govalidator.IsNull(payload.RepositoryPassword)) {
return errors.New("Invalid repository credentials. Username and password must be specified when authentication is enabled")
}
if payload.RepositoryAuthentication && govalidator.IsNull(payload.RepositoryPassword) {
return errors.New("Invalid repository credentials. Password must be specified when authentication is enabled")
}
if err := validateStackAutoUpdate(payload.AutoUpdate); err != nil {
return err
if govalidator.IsNull(payload.ComposeFilePathInRepository) {
payload.ComposeFilePathInRepository = filesystem.ComposeFileDefaultName
}
return nil
}
@@ -157,74 +147,44 @@ func (handler *Handler) createSwarmStackFromGitRepository(w http.ResponseWriter,
var payload swarmStackFromGitRepositoryPayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
payload.Name = handler.SwarmStackManager.NormalizeStackName(payload.Name)
isUnique, err := handler.checkUniqueStackNameInDocker(endpoint, payload.Name, 0, true)
isUnique, err := handler.checkUniqueName(endpoint, payload.Name, 0, true)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check for name collision", Err: err}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err}
}
if !isUnique {
return &httperror.HandlerError{StatusCode: http.StatusConflict, Message: fmt.Sprintf("A stack with the name '%s' already exists", payload.Name), Err: errStackAlreadyExists}
}
//make sure the webhook ID is unique
if payload.AutoUpdate != nil && payload.AutoUpdate.Webhook != "" {
isUnique, err := handler.checkUniqueWebhookID(payload.AutoUpdate.Webhook)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check for webhook ID collision", Err: err}
}
if !isUnique {
return &httperror.HandlerError{StatusCode: http.StatusConflict, Message: fmt.Sprintf("Webhook ID: %s already exists", payload.AutoUpdate.Webhook), Err: errWebhookIDAlreadyExists}
}
errorMessage := fmt.Sprintf("A stack with the name '%s' is already running", payload.Name)
return &httperror.HandlerError{http.StatusConflict, errorMessage, errors.New(errorMessage)}
}
stackID := handler.DataStore.Stack().GetNextIdentifier()
stack := &portainer.Stack{
ID: portainer.StackID(stackID),
Name: payload.Name,
Type: portainer.DockerSwarmStack,
SwarmID: payload.SwarmID,
EndpointID: endpoint.ID,
EntryPoint: payload.ComposeFile,
AdditionalFiles: payload.AdditionalFiles,
AutoUpdate: payload.AutoUpdate,
GitConfig: &gittypes.RepoConfig{
URL: payload.RepositoryURL,
ReferenceName: payload.RepositoryReferenceName,
ConfigFilePath: payload.ComposeFile,
},
ID: portainer.StackID(stackID),
Name: payload.Name,
Type: portainer.DockerSwarmStack,
SwarmID: payload.SwarmID,
EndpointID: endpoint.ID,
EntryPoint: payload.ComposeFilePathInRepository,
Env: payload.Env,
Status: portainer.StackStatusActive,
CreationDate: time.Now().Unix(),
}
if payload.RepositoryAuthentication {
stack.GitConfig.Authentication = &gittypes.GitAuthentication{
Username: payload.RepositoryUsername,
Password: payload.RepositoryPassword,
}
}
projectPath := handler.FileService.GetStackProjectPath(strconv.Itoa(int(stack.ID)))
stack.ProjectPath = projectPath
doCleanUp := true
defer handler.cleanUp(stack, &doCleanUp)
err = handler.clone(projectPath, payload.RepositoryURL, payload.RepositoryReferenceName, payload.RepositoryAuthentication, payload.RepositoryUsername, payload.RepositoryPassword)
err = handler.cloneAndSaveConfig(stack, projectPath, payload.RepositoryURL, payload.RepositoryReferenceName, payload.ComposeFilePathInRepository, payload.RepositoryAuthentication, payload.RepositoryUsername, payload.RepositoryPassword)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to clone git repository", Err: err}
}
commitID, err := handler.latestCommitID(payload.RepositoryURL, payload.RepositoryReferenceName, payload.RepositoryAuthentication, payload.RepositoryUsername, payload.RepositoryPassword)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to fetch git repository id", Err: err}
}
stack.GitConfig.ConfigHash = commitID
config, configErr := handler.createSwarmDeployConfig(r, stack, endpoint, false)
if configErr != nil {
return configErr
@@ -232,23 +192,14 @@ func (handler *Handler) createSwarmStackFromGitRepository(w http.ResponseWriter,
err = handler.deploySwarmStack(config)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: err.Error(), Err: err}
}
if payload.AutoUpdate != nil && payload.AutoUpdate.Interval != "" {
jobID, e := startAutoupdate(stack.ID, stack.AutoUpdate.Interval, handler.Scheduler, handler.StackDeployer, handler.DataStore, handler.GitService)
if e != nil {
return e
}
stack.AutoUpdate.JobID = jobID
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
}
stack.CreatedBy = config.user.Username
err = handler.DataStore.Stack().CreateStack(stack)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist the stack inside the database", Err: err}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack inside the database", err}
}
doCleanUp = false
@@ -299,8 +250,7 @@ func (handler *Handler) createSwarmStackFromFileUpload(w http.ResponseWriter, r
payload.Name = handler.SwarmStackManager.NormalizeStackName(payload.Name)
isUnique, err := handler.checkUniqueStackNameInDocker(endpoint, payload.Name, 0, true)
isUnique, err := handler.checkUniqueName(endpoint, payload.Name, 0, true)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err}
}
@@ -394,25 +344,39 @@ func (handler *Handler) createSwarmDeployConfig(r *http.Request, stack *portaine
func (handler *Handler) deploySwarmStack(config *swarmStackDeploymentConfig) error {
isAdminOrEndpointAdmin, err := handler.userIsAdminOrEndpointAdmin(config.user, config.endpoint.ID)
if err != nil {
return errors.Wrap(err, "failed to validate user admin privileges")
return err
}
settings := &config.endpoint.SecuritySettings
if !settings.AllowBindMountsForRegularUsers && !isAdminOrEndpointAdmin {
for _, file := range append([]string{config.stack.EntryPoint}, config.stack.AdditionalFiles...) {
path := path.Join(config.stack.ProjectPath, file)
stackContent, err := handler.FileService.GetFileContent(path)
if err != nil {
return errors.WithMessage(err, "failed to get stack file content")
}
composeFilePath := path.Join(config.stack.ProjectPath, config.stack.EntryPoint)
err = handler.isValidStackFile(stackContent, settings)
if err != nil {
return errors.WithMessage(err, "swarm stack file content validation failed")
}
stackContent, err := handler.FileService.GetFileContent(composeFilePath)
if err != nil {
return err
}
err = handler.isValidStackFile(stackContent, settings)
if err != nil {
return err
}
}
return handler.StackDeployer.DeploySwarmStack(config.stack, config.endpoint, config.registries, config.prune)
handler.stackCreationMutex.Lock()
defer handler.stackCreationMutex.Unlock()
handler.SwarmStackManager.Login(config.registries, config.endpoint)
err = handler.SwarmStackManager.Deploy(config.stack, config.prune, config.endpoint)
if err != nil {
return err
}
err = handler.SwarmStackManager.Logout(config.endpoint)
if err != nil {
return err
}
return nil
}

View File

@@ -2,30 +2,23 @@ package stacks
import (
"context"
"fmt"
"errors"
"net/http"
"strings"
"sync"
"github.com/docker/docker/api/types"
"github.com/gorilla/mux"
"github.com/pkg/errors"
httperror "github.com/portainer/libhttp/error"
portainer "github.com/portainer/portainer/api"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/docker"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/authorization"
"github.com/portainer/portainer/api/scheduler"
"github.com/portainer/portainer/api/stacks"
)
const defaultGitReferenceName = "refs/heads/master"
var (
errStackAlreadyExists = errors.New("A stack already exists with this name")
errWebhookIDAlreadyExists = errors.New("A webhook ID already exists")
errStackNotExternal = errors.New("Not an external stack")
errStackAlreadyExists = errors.New("A stack already exists with this name")
errStackNotExternal = errors.New("Not an external stack")
)
// Handler is the HTTP handler used to handle stack operations.
@@ -41,8 +34,6 @@ type Handler struct {
SwarmStackManager portainer.SwarmStackManager
ComposeStackManager portainer.ComposeStackManager
KubernetesDeployer portainer.KubernetesDeployer
Scheduler *scheduler.Scheduler
StackDeployer stacks.StackDeployer
}
// NewHandler creates a handler to manage stack operations.
@@ -66,9 +57,7 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
h.Handle("/stacks/{id}",
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackUpdate))).Methods(http.MethodPut)
h.Handle("/stacks/{id}/git",
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackUpdateGit))).Methods(http.MethodPost)
h.Handle("/stacks/{id}/git/redeploy",
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackGitRedeploy))).Methods(http.MethodPut)
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackUpdateGit))).Methods(http.MethodPut)
h.Handle("/stacks/{id}/file",
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackFile))).Methods(http.MethodGet)
h.Handle("/stacks/{id}/migrate",
@@ -77,9 +66,6 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackStart))).Methods(http.MethodPost)
h.Handle("/stacks/{id}/stop",
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackStop))).Methods(http.MethodPost)
h.Handle("/stacks/webhooks/{webhookID}",
httperror.LoggerHandler(h.webhookInvoke)).Methods(http.MethodPost)
return h
}
@@ -127,7 +113,7 @@ func (handler *Handler) userCanCreateStack(securityContext *security.RestrictedR
return handler.userIsAdminOrEndpointAdmin(user, endpointID)
}
func (handler *Handler) checkUniqueStackName(endpoint *portainer.Endpoint, name string, stackID portainer.StackID) (bool, error) {
func (handler *Handler) checkUniqueName(endpoint *portainer.Endpoint, name string, stackID portainer.StackID, swarmMode bool) (bool, error) {
stacks, err := handler.DataStore.Stack().Stacks()
if err != nil {
return false, err
@@ -139,15 +125,6 @@ func (handler *Handler) checkUniqueStackName(endpoint *portainer.Endpoint, name
}
}
return true, nil
}
func (handler *Handler) checkUniqueStackNameInDocker(endpoint *portainer.Endpoint, name string, stackID portainer.StackID, swarmMode bool) (bool, error) {
isUniqueStackName, err := handler.checkUniqueStackName(endpoint, name, stackID)
if err != nil {
return false, err
}
dockerClient, err := handler.DockerClientFactory.CreateClient(endpoint, "")
if err != nil {
return false, err
@@ -180,36 +157,5 @@ func (handler *Handler) checkUniqueStackNameInDocker(endpoint *portainer.Endpoin
}
}
return isUniqueStackName, nil
}
func (handler *Handler) checkUniqueWebhookID(webhookID string) (bool, error) {
_, err := handler.DataStore.Stack().StackByWebhookID(webhookID)
if err == bolterrors.ErrObjectNotFound {
return true, nil
}
return false, err
}
func (handler *Handler) clone(projectPath, repositoryURL, refName string, auth bool, username, password string) error {
if !auth {
username = ""
password = ""
}
err := handler.GitService.CloneRepository(projectPath, repositoryURL, refName, username, password)
if err != nil {
return fmt.Errorf("unable to clone git repository: %w", err)
}
return nil
}
func (handler *Handler) latestCommitID(repositoryURL, refName string, auth bool, username, password string) (string, error) {
if !auth {
username = ""
password = ""
}
return handler.GitService.LatestCommitID(repositoryURL, refName, username, password)
return true, nil
}

View File

@@ -1,24 +0,0 @@
package stacks
import (
"time"
"github.com/asaskevich/govalidator"
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
)
func validateStackAutoUpdate(autoUpdate *portainer.StackAutoUpdate) error {
if autoUpdate == nil {
return nil
}
if autoUpdate.Webhook != "" && !govalidator.IsUUID(autoUpdate.Webhook) {
return errors.New("invalid Webhook format")
}
if autoUpdate.Interval != "" {
if _, err := time.ParseDuration(autoUpdate.Interval); err != nil {
return errors.New("invalid Interval format")
}
}
return nil
}

View File

@@ -1,42 +0,0 @@
package stacks
import (
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/stretchr/testify/assert"
)
func Test_ValidateStackAutoUpdate(t *testing.T) {
tests := []struct {
name string
value *portainer.StackAutoUpdate
wantErr bool
}{
{
name: "webhook is not a valid UUID",
value: &portainer.StackAutoUpdate{Webhook: "fake-webhook"},
wantErr: true,
},
{
name: "incorrect interval value",
value: &portainer.StackAutoUpdate{Interval: "1dd2hh3mm"},
wantErr: true,
},
{
name: "valid auto update",
value: &portainer.StackAutoUpdate{
Webhook: "8dce8c2f-9ca1-482b-ad20-271e86536ada",
Interval: "5h30m40s10ms",
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateStackAutoUpdate(tt.value)
assert.Equalf(t, tt.wantErr, err != nil, "received %+v", err)
})
}
}

View File

@@ -2,9 +2,6 @@ package stacks
import (
"fmt"
"net/http"
"time"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
@@ -12,6 +9,8 @@ import (
bolterrors "github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/stackutils"
"net/http"
"time"
)
// PUT request on /api/stacks/:id/associate?endpointId=<endpointId>&swarmId=<swarmId>&orphanedRunning=<orphanedRunning>
@@ -88,10 +87,5 @@ func (handler *Handler) stackAssociate(w http.ResponseWriter, r *http.Request) *
stack.ResourceControl = resourceControl
if stack.GitConfig != nil && stack.GitConfig.Authentication != nil && stack.GitConfig.Authentication.Password != "" {
// sanitize password in the http response to minimise possible security leaks
stack.GitConfig.Authentication.Password = ""
}
return response.JSON(w, stack)
}

View File

@@ -1,17 +1,19 @@
package stacks
import (
"errors"
"fmt"
"log"
"net/http"
"github.com/docker/cli/cli/compose/loader"
"github.com/docker/cli/cli/compose/types"
"github.com/pkg/errors"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
gittypes "github.com/portainer/portainer/api/git/types"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/authorization"
"github.com/portainer/portainer/api/internal/endpointutils"
@@ -110,7 +112,7 @@ func (handler *Handler) stackCreate(w http.ResponseWriter, r *http.Request) *htt
case portainer.DockerComposeStack:
return handler.createComposeStack(w, r, method, endpoint, tokenData.ID)
case portainer.KubernetesStack:
return handler.createKubernetesStack(w, r, method, endpoint, tokenData.ID)
return handler.createKubernetesStack(w, r, method, endpoint)
}
return &httperror.HandlerError{http.StatusBadRequest, "Invalid value for query parameter: type. Value must be one of: 1 (Swarm stack) or 2 (Compose stack)", errors.New(request.ErrInvalidQueryParameter)}
@@ -127,7 +129,7 @@ func (handler *Handler) createComposeStack(w http.ResponseWriter, r *http.Reques
return handler.createComposeStackFromFileUpload(w, r, endpoint, userID)
}
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid value for query parameter: method. Value must be one of: string, repository or file", Err: errors.New(request.ErrInvalidQueryParameter)}
return &httperror.HandlerError{http.StatusBadRequest, "Invalid value for query parameter: method. Value must be one of: string, repository or file", errors.New(request.ErrInvalidQueryParameter)}
}
func (handler *Handler) createSwarmStack(w http.ResponseWriter, r *http.Request, method string, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError {
@@ -140,17 +142,15 @@ func (handler *Handler) createSwarmStack(w http.ResponseWriter, r *http.Request,
return handler.createSwarmStackFromFileUpload(w, r, endpoint, userID)
}
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid value for query parameter: method. Value must be one of: string, repository or file", Err: errors.New(request.ErrInvalidQueryParameter)}
return &httperror.HandlerError{http.StatusBadRequest, "Invalid value for query parameter: method. Value must be one of: string, repository or file", errors.New(request.ErrInvalidQueryParameter)}
}
func (handler *Handler) createKubernetesStack(w http.ResponseWriter, r *http.Request, method string, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError {
func (handler *Handler) createKubernetesStack(w http.ResponseWriter, r *http.Request, method string, endpoint *portainer.Endpoint) *httperror.HandlerError {
switch method {
case "string":
return handler.createKubernetesStackFromFileContent(w, r, endpoint, userID)
return handler.createKubernetesStackFromFileContent(w, r, endpoint)
case "repository":
return handler.createKubernetesStackFromGitRepository(w, r, endpoint, userID)
case "url":
return handler.createKubernetesStackFromManifestURL(w, r, endpoint, userID)
return handler.createKubernetesStackFromGitRepository(w, r, endpoint)
}
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid value for query parameter: method. Value must be one of: string or repository", Err: errors.New(request.ErrInvalidQueryParameter)}
}
@@ -232,11 +232,24 @@ func (handler *Handler) decorateStackResponse(w http.ResponseWriter, stack *port
}
stack.ResourceControl = resourceControl
if stack.GitConfig != nil && stack.GitConfig.Authentication != nil && stack.GitConfig.Authentication.Password != "" {
// sanitize password in the http response to minimise possible security leaks
stack.GitConfig.Authentication.Password = ""
}
return response.JSON(w, stack)
}
func (handler *Handler) cloneAndSaveConfig(stack *portainer.Stack, projectPath, repositoryURL, refName, configFilePath string, auth bool, username, password string) error {
if !auth {
username = ""
password = ""
}
err := handler.GitService.CloneRepository(projectPath, repositoryURL, refName, username, password)
if err != nil {
return fmt.Errorf("unable to clone git repository: %w", err)
}
stack.GitConfig = &gittypes.RepoConfig{
URL: repositoryURL,
ReferenceName: refName,
ConfigFilePath: configFilePath,
}
return nil
}

View File

@@ -0,0 +1,29 @@
package stacks
import (
"testing"
portainer "github.com/portainer/portainer/api"
gittypes "github.com/portainer/portainer/api/git/types"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/testhelpers"
"github.com/stretchr/testify/assert"
)
func Test_stackHandler_cloneAndSaveConfig_shouldCallGitCloneAndSaveConfigOnStack(t *testing.T) {
handler := NewHandler(&security.RequestBouncer{})
handler.GitService = testhelpers.NewGitService()
url := "url"
refName := "ref"
configPath := "path"
stack := &portainer.Stack{}
err := handler.cloneAndSaveConfig(stack, "", url, refName, configPath, false, "", "")
assert.NoError(t, err, "clone and save should not fail")
assert.Equal(t, gittypes.RepoConfig{
URL: url,
ReferenceName: refName,
ConfigFilePath: configPath,
}, *stack.GitConfig)
}

View File

@@ -1,12 +1,10 @@
package stacks
import (
"context"
"fmt"
"errors"
"net/http"
"strconv"
"github.com/pkg/errors"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
@@ -35,12 +33,12 @@ import (
func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
stackID, err := request.RetrieveRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid stack identifier route variable", Err: err}
return &httperror.HandlerError{http.StatusBadRequest, "Invalid stack identifier route variable", err}
}
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve info from request context", Err: err}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
}
externalStack, _ := request.RetrieveBooleanQueryParameter(r, "external", true)
@@ -50,81 +48,74 @@ func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *htt
id, err := strconv.Atoi(stackID)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid stack identifier route variable", Err: err}
return &httperror.HandlerError{http.StatusBadRequest, "Invalid stack identifier route variable", err}
}
stack, err := handler.DataStore.Stack().Stack(portainer.StackID(id))
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find a stack with the specified identifier inside the database", Err: err}
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a stack with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find a stack with the specified identifier inside the database", Err: err}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err}
}
endpointID, err := request.RetrieveNumericQueryParameter(r, "endpointId", true)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid query parameter: endpointId", Err: err}
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: endpointId", err}
}
isOrphaned := portainer.EndpointID(endpointID) != stack.EndpointID
if isOrphaned && !securityContext.IsAdmin {
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Permission denied to remove orphaned stack", Err: errors.New("Permission denied to remove orphaned stack")}
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to remove orphaned stack", errors.New("Permission denied to remove orphaned stack")}
}
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find the endpoint associated to the stack inside the database", Err: err}
return &httperror.HandlerError{http.StatusNotFound, "Unable to find the endpoint associated to the stack inside the database", err}
} else if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find the endpoint associated to the stack inside the database", Err: err}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err}
}
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve a resource control associated to the stack", Err: err}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
}
if !isOrphaned {
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Permission denied to access endpoint", Err: err}
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
}
if stack.Type == portainer.DockerSwarmStack || stack.Type == portainer.DockerComposeStack {
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack access", Err: err}
}
if !access {
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Access denied to resource", Err: httperrors.ErrResourceAccessDenied}
}
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
}
if !access {
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied}
}
}
// stop scheduler updates of the stack before removal
if stack.AutoUpdate != nil {
stopAutoupdate(stack.ID, stack.AutoUpdate.JobID, *handler.Scheduler)
}
err = handler.deleteStack(securityContext.UserID, stack, endpoint)
err = handler.deleteStack(stack, endpoint)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: err.Error(), Err: err}
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
}
err = handler.DataStore.Stack().DeleteStack(portainer.StackID(id))
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to remove the stack from the database", Err: err}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the stack from the database", err}
}
if resourceControl != nil {
err = handler.DataStore.ResourceControl().DeleteResourceControl(resourceControl.ID)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to remove the associated resource control from the database", Err: err}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the associated resource control from the database", err}
}
}
err = handler.FileService.RemoveDirectory(stack.ProjectPath)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to remove stack files from disk", Err: err}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove stack files from disk", err}
}
return response.Empty(w)
@@ -133,31 +124,31 @@ func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *htt
func (handler *Handler) deleteExternalStack(r *http.Request, w http.ResponseWriter, stackName string, securityContext *security.RestrictedRequestContext) *httperror.HandlerError {
endpointID, err := request.RetrieveNumericQueryParameter(r, "endpointId", false)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid query parameter: endpointId", Err: err}
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: endpointId", err}
}
if !securityContext.IsAdmin {
return &httperror.HandlerError{StatusCode: http.StatusUnauthorized, Message: "Permission denied to delete the stack", Err: httperrors.ErrUnauthorized}
return &httperror.HandlerError{http.StatusUnauthorized, "Permission denied to delete the stack", httperrors.ErrUnauthorized}
}
stack, err := handler.DataStore.Stack().StackByName(stackName)
if err != nil && err != bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check for stack existence inside the database", Err: err}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for stack existence inside the database", err}
}
if stack != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "A stack with this name exists inside the database. Cannot use external delete method", Err: errors.New("A tag already exists with this name")}
return &httperror.HandlerError{http.StatusBadRequest, "A stack with this name exists inside the database. Cannot use external delete method", errors.New("A tag already exists with this name")}
}
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find the endpoint associated to the stack inside the database", Err: err}
return &httperror.HandlerError{http.StatusNotFound, "Unable to find the endpoint associated to the stack inside the database", err}
} else if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find the endpoint associated to the stack inside the database", Err: err}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err}
}
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Permission denied to access endpoint", Err: err}
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
}
stack = &portainer.Stack{
@@ -165,24 +156,18 @@ func (handler *Handler) deleteExternalStack(r *http.Request, w http.ResponseWrit
Type: portainer.DockerSwarmStack,
}
err = handler.deleteStack(securityContext.UserID, stack, endpoint)
err = handler.deleteStack(stack, endpoint)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to delete stack", Err: err}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to delete stack", err}
}
return response.Empty(w)
}
func (handler *Handler) deleteStack(userID portainer.UserID, stack *portainer.Stack, endpoint *portainer.Endpoint) error {
func (handler *Handler) deleteStack(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
if stack.Type == portainer.DockerSwarmStack {
return handler.SwarmStackManager.Remove(stack, endpoint)
}
if stack.Type == portainer.DockerComposeStack {
return handler.ComposeStackManager.Down(context.TODO(), stack, endpoint)
}
if stack.Type == portainer.KubernetesStack {
out, err := handler.KubernetesDeployer.Remove(userID, endpoint, stackutils.GetStackFilePaths(stack), stack.Namespace)
return errors.WithMessagef(err, "failed to remove kubernetes resources: %q", out)
}
return fmt.Errorf("unsupported stack type: %v", stack.Type)
return handler.ComposeStackManager.Down(stack, endpoint)
}

View File

@@ -66,19 +66,17 @@ func (handler *Handler) stackFile(w http.ResponseWriter, r *http.Request) *httpe
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
}
if stack.Type == portainer.DockerSwarmStack || stack.Type == portainer.DockerComposeStack {
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
}
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
}
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
}
if !access {
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", errors.ErrResourceAccessDenied}
}
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
}
if !access {
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", errors.ErrResourceAccessDenied}
}
}

View File

@@ -1,9 +1,8 @@
package stacks
import (
"net/http"
"github.com/portainer/portainer/api/http/errors"
"net/http"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
@@ -61,29 +60,22 @@ func (handler *Handler) stackInspect(w http.ResponseWriter, r *http.Request) *ht
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
}
if stack.Type == portainer.DockerSwarmStack || stack.Type == portainer.DockerComposeStack {
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
}
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
}
if !access {
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", errors.ErrResourceAccessDenied}
}
if resourceControl != nil {
stack.ResourceControl = resourceControl
}
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
}
}
if stack.GitConfig != nil && stack.GitConfig.Authentication != nil && stack.GitConfig.Authentication.Password != "" {
// sanitize password in the http response to minimise possible security leaks
stack.GitConfig.Authentication.Password = ""
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
}
if !access {
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", errors.ErrResourceAccessDenied}
}
if resourceControl != nil {
stack.ResourceControl = resourceControl
}
}
return response.JSON(w, stack)

View File

@@ -1,9 +1,8 @@
package stacks
import (
"net/http"
httperrors "github.com/portainer/portainer/api/http/errors"
"net/http"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
@@ -81,13 +80,6 @@ func (handler *Handler) stackList(w http.ResponseWriter, r *http.Request) *httpe
stacks = authorization.FilterAuthorizedStacks(stacks, user, userTeamIDs)
}
for _, stack := range stacks {
if stack.GitConfig != nil && stack.GitConfig.Authentication != nil && stack.GitConfig.Authentication.Password != "" {
// sanitize password in the http response to minimise possible security leaks
stack.GitConfig.Authentication.Password = ""
}
}
return response.JSON(w, stacks)
}

View File

@@ -50,52 +50,50 @@ func (payload *stackMigratePayload) Validate(r *http.Request) error {
func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
stackID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid stack identifier route variable", Err: err}
return &httperror.HandlerError{http.StatusBadRequest, "Invalid stack identifier route variable", err}
}
var payload stackMigratePayload
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
stack, err := handler.DataStore.Stack().Stack(portainer.StackID(stackID))
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find a stack with the specified identifier inside the database", Err: err}
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a stack with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find a stack with the specified identifier inside the database", Err: err}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err}
}
endpoint, err := handler.DataStore.Endpoint().Endpoint(stack.EndpointID)
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find an endpoint with the specified identifier inside the database", Err: err}
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find an endpoint with the specified identifier inside the database", Err: err}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
}
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Permission denied to access endpoint", Err: err}
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
}
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
}
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve info from request context", Err: err}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
}
if stack.Type == portainer.DockerSwarmStack || stack.Type == portainer.DockerComposeStack {
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve a resource control associated to the stack", Err: err}
}
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack access", Err: err}
}
if !access {
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Access denied to resource", Err: httperrors.ErrResourceAccessDenied}
}
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
}
if !access {
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied}
}
// TODO: this is a work-around for stacks created with Portainer version >= 1.17.1
@@ -103,7 +101,7 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht
// can use the optional EndpointID query parameter to associate a valid endpoint identifier to the stack.
endpointID, err := request.RetrieveNumericQueryParameter(r, "endpointId", true)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid query parameter: endpointId", Err: err}
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: endpointId", err}
}
if endpointID != int(stack.EndpointID) {
stack.EndpointID = portainer.EndpointID(endpointID)
@@ -111,9 +109,9 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht
targetEndpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(payload.EndpointID))
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find an endpoint with the specified identifier inside the database", Err: err}
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find an endpoint with the specified identifier inside the database", Err: err}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
}
stack.EndpointID = portainer.EndpointID(payload.EndpointID)
@@ -125,17 +123,15 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht
if payload.Name != "" {
stack.Name = payload.Name
}
if stack.Type == portainer.DockerSwarmStack || stack.Type == portainer.DockerComposeStack {
isUnique, err := handler.checkUniqueStackNameInDocker(targetEndpoint, stack.Name, stack.ID, stack.SwarmID != "")
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check for name collision", Err: err}
}
isUnique, err := handler.checkUniqueName(targetEndpoint, stack.Name, stack.ID, stack.SwarmID != "")
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err}
}
if !isUnique {
errorMessage := fmt.Sprintf("A stack with the name '%s' is already running on endpoint '%s'", stack.Name, targetEndpoint.Name)
return &httperror.HandlerError{StatusCode: http.StatusConflict, Message: errorMessage, Err: errors.New(errorMessage)}
}
if !isUnique {
errorMessage := fmt.Sprintf("A stack with the name '%s' is already running on endpoint '%s'", stack.Name, targetEndpoint.Name)
return &httperror.HandlerError{http.StatusConflict, errorMessage, errors.New(errorMessage)}
}
migrationError := handler.migrateStack(r, stack, targetEndpoint)
@@ -144,19 +140,14 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht
}
stack.Name = oldName
err = handler.deleteStack(securityContext.UserID, stack, endpoint)
err = handler.deleteStack(stack, endpoint)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: err.Error(), Err: err}
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
}
err = handler.DataStore.Stack().UpdateStack(stack.ID, stack)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist the stack changes inside the database", Err: err}
}
if stack.GitConfig != nil && stack.GitConfig.Authentication != nil && stack.GitConfig.Authentication.Password != "" {
// sanitize password in the http response to minimise possible security leaks
stack.GitConfig.Authentication.Password = ""
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack changes inside the database", err}
}
return response.JSON(w, stack)
@@ -177,7 +168,7 @@ func (handler *Handler) migrateComposeStack(r *http.Request, stack *portainer.St
err := handler.deployComposeStack(config)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: err.Error(), Err: err}
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
}
return nil
@@ -191,7 +182,7 @@ func (handler *Handler) migrateSwarmStack(r *http.Request, stack *portainer.Stac
err := handler.deploySwarmStack(config)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: err.Error(), Err: err}
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
}
return nil

View File

@@ -1,7 +1,6 @@
package stacks
import (
"context"
"errors"
"fmt"
"net/http"
@@ -33,87 +32,68 @@ import (
func (handler *Handler) stackStart(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
stackID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid stack identifier route variable", Err: err}
return &httperror.HandlerError{http.StatusBadRequest, "Invalid stack identifier route variable", err}
}
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve info from request context", Err: err}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
}
stack, err := handler.DataStore.Stack().Stack(portainer.StackID(stackID))
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find a stack with the specified identifier inside the database", Err: err}
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a stack with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find a stack with the specified identifier inside the database", Err: err}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err}
}
endpoint, err := handler.DataStore.Endpoint().Endpoint(stack.EndpointID)
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find an endpoint with the specified identifier inside the database", Err: err}
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find an endpoint with the specified identifier inside the database", Err: err}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
}
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Permission denied to access endpoint", Err: err}
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
}
if stack.Type == portainer.DockerSwarmStack || stack.Type == portainer.DockerComposeStack {
isUnique, err := handler.checkUniqueName(endpoint, stack.Name, stack.ID, stack.SwarmID != "")
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err}
}
if !isUnique {
errorMessage := fmt.Sprintf("A stack with the name '%s' is already running", stack.Name)
return &httperror.HandlerError{http.StatusConflict, errorMessage, errors.New(errorMessage)}
}
isUnique, err := handler.checkUniqueStackNameInDocker(endpoint, stack.Name, stack.ID, stack.SwarmID != "")
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check for name collision", Err: err}
}
if !isUnique {
errorMessage := fmt.Sprintf("A stack with the name '%s' is already running", stack.Name)
return &httperror.HandlerError{StatusCode: http.StatusConflict, Message: errorMessage, Err: errors.New(errorMessage)}
}
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
}
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve a resource control associated to the stack", Err: err}
}
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack access", Err: err}
}
if !access {
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Access denied to resource", Err: httperrors.ErrResourceAccessDenied}
}
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
}
if !access {
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied}
}
if stack.Status == portainer.StackStatusActive {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Stack is already active", Err: errors.New("Stack is already active")}
}
if stack.AutoUpdate != nil && stack.AutoUpdate.Interval != "" {
stopAutoupdate(stack.ID, stack.AutoUpdate.JobID, *handler.Scheduler)
jobID, e := startAutoupdate(stack.ID, stack.AutoUpdate.Interval, handler.Scheduler, handler.StackDeployer, handler.DataStore, handler.GitService)
if e != nil {
return e
}
stack.AutoUpdate.JobID = jobID
return &httperror.HandlerError{http.StatusBadRequest, "Stack is already active", errors.New("Stack is already active")}
}
err = handler.startStack(stack, endpoint)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to start stack", Err: err}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to start stack", err}
}
stack.Status = portainer.StackStatusActive
err = handler.DataStore.Stack().UpdateStack(stack.ID, stack)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to update stack status", Err: err}
}
if stack.GitConfig != nil && stack.GitConfig.Authentication != nil && stack.GitConfig.Authentication.Password != "" {
// sanitize password in the http response to minimise possible security leaks
stack.GitConfig.Authentication.Password = ""
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update stack status", err}
}
return response.JSON(w, stack)
@@ -122,7 +102,7 @@ func (handler *Handler) stackStart(w http.ResponseWriter, r *http.Request) *http
func (handler *Handler) startStack(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
switch stack.Type {
case portainer.DockerComposeStack:
return handler.ComposeStackManager.Up(context.TODO(), stack, endpoint)
return handler.ComposeStackManager.Up(stack, endpoint)
case portainer.DockerSwarmStack:
return handler.SwarmStackManager.Deploy(stack, true, endpoint)
}

View File

@@ -1,7 +1,6 @@
package stacks
import (
"context"
"errors"
"net/http"
@@ -58,31 +57,23 @@ func (handler *Handler) stackStop(w http.ResponseWriter, r *http.Request) *httpe
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
}
if stack.Type == portainer.DockerSwarmStack || stack.Type == portainer.DockerComposeStack {
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
}
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
}
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
}
if !access {
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied}
}
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
}
if !access {
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied}
}
if stack.Status == portainer.StackStatusInactive {
return &httperror.HandlerError{http.StatusBadRequest, "Stack is already inactive", errors.New("Stack is already inactive")}
}
// stop scheduler updates of the stack before stopping
if stack.AutoUpdate != nil && stack.AutoUpdate.JobID != "" {
stopAutoupdate(stack.ID, stack.AutoUpdate.JobID, *handler.Scheduler)
stack.AutoUpdate.JobID = ""
}
err = handler.stopStack(stack, endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to stop stack", err}
@@ -94,18 +85,13 @@ func (handler *Handler) stackStop(w http.ResponseWriter, r *http.Request) *httpe
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update stack status", err}
}
if stack.GitConfig != nil && stack.GitConfig.Authentication != nil && stack.GitConfig.Authentication.Password != "" {
// sanitize password in the http response to minimise possible security leaks
stack.GitConfig.Authentication.Password = ""
}
return response.JSON(w, stack)
}
func (handler *Handler) stopStack(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
switch stack.Type {
case portainer.DockerComposeStack:
return handler.ComposeStackManager.Down(context.TODO(), stack, endpoint)
return handler.ComposeStackManager.Down(stack, endpoint)
case portainer.DockerSwarmStack:
return handler.SwarmStackManager.Remove(stack, endpoint)
}

View File

@@ -1,12 +1,11 @@
package stacks
import (
"errors"
"net/http"
"strconv"
"time"
"github.com/pkg/errors"
"github.com/asaskevich/govalidator"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
@@ -73,9 +72,9 @@ func (handler *Handler) stackUpdate(w http.ResponseWriter, r *http.Request) *htt
stack, err := handler.DataStore.Stack().Stack(portainer.StackID(stackID))
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find a stack with the specified identifier inside the database", Err: err}
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a stack with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find a stack with the specified identifier inside the database", Err: err}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err}
}
// TODO: this is a work-around for stacks created with Portainer version >= 1.17.1
@@ -83,7 +82,7 @@ func (handler *Handler) stackUpdate(w http.ResponseWriter, r *http.Request) *htt
// can use the optional EndpointID query parameter to associate a valid endpoint identifier to the stack.
endpointID, err := request.RetrieveNumericQueryParameter(r, "endpointId", true)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid query parameter: endpointId", Err: err}
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: endpointId", err}
}
if endpointID != int(stack.EndpointID) {
stack.EndpointID = portainer.EndpointID(endpointID)
@@ -91,36 +90,32 @@ func (handler *Handler) stackUpdate(w http.ResponseWriter, r *http.Request) *htt
endpoint, err := handler.DataStore.Endpoint().Endpoint(stack.EndpointID)
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find the endpoint associated to the stack inside the database", Err: err}
return &httperror.HandlerError{http.StatusNotFound, "Unable to find the endpoint associated to the stack inside the database", err}
} else if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find the endpoint associated to the stack inside the database", Err: err}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err}
}
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Permission denied to access endpoint", Err: err}
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
}
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
}
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve info from request context", Err: err}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
}
//only check resource control when it is a DockerSwarmStack or a DockerComposeStack
if stack.Type == portainer.DockerSwarmStack || stack.Type == portainer.DockerComposeStack {
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve a resource control associated to the stack", Err: err}
}
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack access", Err: err}
}
if !access {
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Access denied to resource", Err: httperrors.ErrResourceAccessDenied}
}
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
}
if !access {
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied}
}
updateError := handler.updateAndDeployStack(r, stack, endpoint)
@@ -128,22 +123,9 @@ func (handler *Handler) stackUpdate(w http.ResponseWriter, r *http.Request) *htt
return updateError
}
user, err := handler.DataStore.User().User(securityContext.UserID)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Cannot find context user", Err: errors.Wrap(err, "failed to fetch the user")}
}
stack.UpdatedBy = user.Username
stack.UpdateDate = time.Now().Unix()
stack.Status = portainer.StackStatusActive
err = handler.DataStore.Stack().UpdateStack(stack.ID, stack)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist the stack changes inside the database", Err: err}
}
if stack.GitConfig != nil && stack.GitConfig.Authentication != nil && stack.GitConfig.Authentication.Password != "" {
// sanitize password in the http response to minimise possible security leaks
stack.GitConfig.Authentication.Password = ""
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack changes inside the database", err}
}
return response.JSON(w, stack)
@@ -152,20 +134,15 @@ func (handler *Handler) stackUpdate(w http.ResponseWriter, r *http.Request) *htt
func (handler *Handler) updateAndDeployStack(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint) *httperror.HandlerError {
if stack.Type == portainer.DockerSwarmStack {
return handler.updateSwarmStack(r, stack, endpoint)
} else if stack.Type == portainer.DockerComposeStack {
return handler.updateComposeStack(r, stack, endpoint)
} else if stack.Type == portainer.KubernetesStack {
return handler.updateKubernetesStack(r, stack, endpoint)
} else {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unsupported stack", Err: errors.Errorf("unsupported stack type: %v", stack.Type)}
}
return handler.updateComposeStack(r, stack, endpoint)
}
func (handler *Handler) updateComposeStack(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint) *httperror.HandlerError {
var payload updateComposeStackPayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
stack.Env = payload.Env
@@ -173,7 +150,7 @@ func (handler *Handler) updateComposeStack(r *http.Request, stack *portainer.Sta
stackFolder := strconv.Itoa(int(stack.ID))
_, err = handler.FileService.StoreStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent))
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist updated Compose file on disk", Err: err}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist updated Compose file on disk", err}
}
config, configErr := handler.createComposeDeployConfig(r, stack, endpoint)
@@ -181,9 +158,13 @@ func (handler *Handler) updateComposeStack(r *http.Request, stack *portainer.Sta
return configErr
}
stack.UpdateDate = time.Now().Unix()
stack.UpdatedBy = config.user.Username
stack.Status = portainer.StackStatusActive
err = handler.deployComposeStack(config)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: err.Error(), Err: err}
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
}
return nil
@@ -193,7 +174,7 @@ func (handler *Handler) updateSwarmStack(r *http.Request, stack *portainer.Stack
var payload updateSwarmStackPayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
stack.Env = payload.Env
@@ -201,7 +182,7 @@ func (handler *Handler) updateSwarmStack(r *http.Request, stack *portainer.Stack
stackFolder := strconv.Itoa(int(stack.ID))
_, err = handler.FileService.StoreStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent))
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist updated Compose file on disk", Err: err}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist updated Compose file on disk", err}
}
config, configErr := handler.createSwarmDeployConfig(r, stack, endpoint, payload.Prune)
@@ -209,9 +190,13 @@ func (handler *Handler) updateSwarmStack(r *http.Request, stack *portainer.Stack
return configErr
}
stack.UpdateDate = time.Now().Unix()
stack.UpdatedBy = config.user.Username
stack.Status = portainer.StackStatusActive
err = handler.deploySwarmStack(config)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: err.Error(), Err: err}
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
}
return nil

View File

@@ -2,7 +2,10 @@ package stacks
import (
"errors"
"fmt"
"log"
"net/http"
"time"
"github.com/asaskevich/govalidator"
httperror "github.com/portainer/libhttp/error"
@@ -10,35 +13,29 @@ import (
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
gittypes "github.com/portainer/portainer/api/git/types"
"github.com/portainer/portainer/api/filesystem"
httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/stackutils"
)
type stackGitUpdatePayload struct {
AutoUpdate *portainer.StackAutoUpdate
Env []portainer.Pair
type updateStackGitPayload struct {
RepositoryReferenceName string
RepositoryAuthentication bool
RepositoryUsername string
RepositoryPassword string
}
func (payload *stackGitUpdatePayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.RepositoryReferenceName) {
payload.RepositoryReferenceName = defaultGitReferenceName
}
if err := validateStackAutoUpdate(payload.AutoUpdate); err != nil {
return err
func (payload *updateStackGitPayload) Validate(r *http.Request) error {
if payload.RepositoryAuthentication && (govalidator.IsNull(payload.RepositoryUsername) || govalidator.IsNull(payload.RepositoryPassword)) {
return errors.New("Invalid repository credentials. Username and password must be specified when authentication is enabled")
}
return nil
}
// @id StackUpdateGit
// @summary Update a stack's Git configs
// @description Update the Git settings in a stack, e.g., RepositoryReferenceName and AutoUpdate
// @summary Redeploy a stack
// @description Pull and redeploy a stack via Git
// @description **Access policy**: restricted
// @tags stacks
// @security jwt
@@ -46,7 +43,7 @@ func (payload *stackGitUpdatePayload) Validate(r *http.Request) error {
// @produce json
// @param id path int true "Stack identifier"
// @param endpointId query int false "Stacks created before version 1.18.0 might not have an associated endpoint identifier. Use this optional parameter to set the endpoint identifier used by the stack."
// @param body body stackGitUpdatePayload true "Git configs for pull and redeploy a stack"
// @param body body updateStackGitPayload true "Git configs for pull and redeploy a stack"
// @success 200 {object} portainer.Stack "Success"
// @failure 400 "Invalid request"
// @failure 403 "Permission denied"
@@ -56,23 +53,18 @@ func (payload *stackGitUpdatePayload) Validate(r *http.Request) error {
func (handler *Handler) stackUpdateGit(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
stackID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid stack identifier route variable", Err: err}
}
var payload stackGitUpdatePayload
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
return &httperror.HandlerError{http.StatusBadRequest, "Invalid stack identifier route variable", err}
}
stack, err := handler.DataStore.Stack().Stack(portainer.StackID(stackID))
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find a stack with the specified identifier inside the database", Err: err}
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a stack with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find a stack with the specified identifier inside the database", Err: err}
} else if stack.GitConfig == nil {
msg := "No Git config in the found stack"
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: msg, Err: errors.New(msg)}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err}
}
if stack.GitConfig == nil {
return &httperror.HandlerError{http.StatusBadRequest, "Stack is not created from git", err}
}
// TODO: this is a work-around for stacks created with Portainer version >= 1.17.1
@@ -80,7 +72,7 @@ func (handler *Handler) stackUpdateGit(w http.ResponseWriter, r *http.Request) *
// can use the optional EndpointID query parameter to associate a valid endpoint identifier to the stack.
endpointID, err := request.RetrieveNumericQueryParameter(r, "endpointId", true)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid query parameter: endpointId", Err: err}
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: endpointId", err}
}
if endpointID != int(stack.EndpointID) {
stack.EndpointID = portainer.EndpointID(endpointID)
@@ -88,77 +80,117 @@ func (handler *Handler) stackUpdateGit(w http.ResponseWriter, r *http.Request) *
endpoint, err := handler.DataStore.Endpoint().Endpoint(stack.EndpointID)
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find the endpoint associated to the stack inside the database", Err: err}
return &httperror.HandlerError{http.StatusNotFound, "Unable to find the endpoint associated to the stack inside the database", err}
} else if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find the endpoint associated to the stack inside the database", Err: err}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err}
}
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Permission denied to access endpoint", Err: err}
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
}
if stack.Type == portainer.DockerSwarmStack || stack.Type == portainer.DockerComposeStack {
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve a resource control associated to the stack", Err: err}
}
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve info from request context", Err: err}
}
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack access", Err: err}
}
if !access {
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Access denied to resource", Err: httperrors.ErrResourceAccessDenied}
}
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
}
//stop the autoupdate job if there is any
if stack.AutoUpdate != nil {
stopAutoupdate(stack.ID, stack.AutoUpdate.JobID, *handler.Scheduler)
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
}
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
}
if !access {
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied}
}
var payload updateStackGitPayload
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
//update retrieved stack data based on the payload
stack.GitConfig.ReferenceName = payload.RepositoryReferenceName
stack.AutoUpdate = payload.AutoUpdate
stack.Env = payload.Env
stack.GitConfig.Authentication = nil
if payload.RepositoryAuthentication {
password := payload.RepositoryPassword
if password == "" && stack.GitConfig != nil && stack.GitConfig.Authentication != nil {
password = stack.GitConfig.Authentication.Password
}
stack.GitConfig.Authentication = &gittypes.GitAuthentication{
Username: payload.RepositoryUsername,
Password: password,
}
backupProjectPath := fmt.Sprintf("%s-old", stack.ProjectPath)
err = filesystem.MoveDirectory(stack.ProjectPath, backupProjectPath)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to move git repository directory", err}
}
if payload.AutoUpdate != nil && payload.AutoUpdate.Interval != "" {
jobID, e := startAutoupdate(stack.ID, stack.AutoUpdate.Interval, handler.Scheduler, handler.StackDeployer, handler.DataStore, handler.GitService)
if e != nil {
return e
}
stack.AutoUpdate.JobID = jobID
repositoryUsername := payload.RepositoryUsername
repositoryPassword := payload.RepositoryPassword
if !payload.RepositoryAuthentication {
repositoryUsername = ""
repositoryPassword = ""
}
err = handler.GitService.CloneRepository(stack.ProjectPath, stack.GitConfig.URL, payload.RepositoryReferenceName, repositoryUsername, repositoryPassword)
if err != nil {
restoreError := filesystem.MoveDirectory(backupProjectPath, stack.ProjectPath)
if restoreError != nil {
log.Printf("[WARN] [http,stacks,git] [error: %s] [message: failed restoring backup folder]", restoreError)
}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to clone git repository", err}
}
defer func() {
err = handler.FileService.RemoveDirectory(backupProjectPath)
if err != nil {
log.Printf("[WARN] [http,stacks,git] [error: %s] [message: unable to remove git repository directory]", err)
}
}()
httpErr := handler.deployStack(r, stack, endpoint)
if httpErr != nil {
return httpErr
}
//save the updated stack to DB
err = handler.DataStore.Stack().UpdateStack(stack.ID, stack)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist the stack changes inside the database", Err: err}
}
if stack.GitConfig != nil && stack.GitConfig.Authentication != nil && stack.GitConfig.Authentication.Password != "" {
// sanitize password in the http response to minimise possible security leaks
stack.GitConfig.Authentication.Password = ""
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack changes inside the database", err}
}
return response.JSON(w, stack)
}
func (handler *Handler) deployStack(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint) *httperror.HandlerError {
if stack.Type == portainer.DockerSwarmStack {
config, httpErr := handler.createSwarmDeployConfig(r, stack, endpoint, false)
if httpErr != nil {
return httpErr
}
err := handler.deploySwarmStack(config)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
}
stack.UpdateDate = time.Now().Unix()
stack.UpdatedBy = config.user.Username
stack.Status = portainer.StackStatusActive
return nil
}
config, httpErr := handler.createComposeDeployConfig(r, stack, endpoint)
if httpErr != nil {
return httpErr
}
err := handler.deployComposeStack(config)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
}
stack.UpdateDate = time.Now().Unix()
stack.UpdatedBy = config.user.Username
stack.Status = portainer.StackStatusActive
return nil
}

View File

@@ -1,236 +0,0 @@
package stacks
import (
"fmt"
"log"
"net/http"
"time"
"github.com/asaskevich/govalidator"
"github.com/pkg/errors"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/filesystem"
httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/stackutils"
k "github.com/portainer/portainer/api/kubernetes"
)
type stackGitRedployPayload struct {
RepositoryReferenceName string
RepositoryAuthentication bool
RepositoryUsername string
RepositoryPassword string
Env []portainer.Pair
}
func (payload *stackGitRedployPayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.RepositoryReferenceName) {
payload.RepositoryReferenceName = defaultGitReferenceName
}
return nil
}
// @id StackGitRedeploy
// @summary Redeploy a stack
// @description Pull and redeploy a stack via Git
// @description **Access policy**: restricted
// @tags stacks
// @security jwt
// @accept json
// @produce json
// @param id path int true "Stack identifier"
// @param endpointId query int false "Stacks created before version 1.18.0 might not have an associated endpoint identifier. Use this optional parameter to set the endpoint identifier used by the stack."
// @param body body stackGitRedployPayload true "Git configs for pull and redeploy a stack"
// @success 200 {object} portainer.Stack "Success"
// @failure 400 "Invalid request"
// @failure 403 "Permission denied"
// @failure 404 "Not found"
// @failure 500 "Server error"
// @router /stacks/:id/git/redeploy [put]
func (handler *Handler) stackGitRedeploy(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
stackID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid stack identifier route variable", Err: err}
}
stack, err := handler.DataStore.Stack().Stack(portainer.StackID(stackID))
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find a stack with the specified identifier inside the database", Err: err}
} else if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find a stack with the specified identifier inside the database", Err: err}
}
if stack.GitConfig == nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Stack is not created from git", Err: err}
}
// TODO: this is a work-around for stacks created with Portainer version >= 1.17.1
// The EndpointID property is not available for these stacks, this API endpoint
// can use the optional EndpointID query parameter to associate a valid endpoint identifier to the stack.
endpointID, err := request.RetrieveNumericQueryParameter(r, "endpointId", true)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid query parameter: endpointId", Err: err}
}
if endpointID != int(stack.EndpointID) {
stack.EndpointID = portainer.EndpointID(endpointID)
}
endpoint, err := handler.DataStore.Endpoint().Endpoint(stack.EndpointID)
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find the endpoint associated to the stack inside the database", Err: err}
} else if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find the endpoint associated to the stack inside the database", Err: err}
}
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Permission denied to access endpoint", Err: err}
}
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve info from request context", Err: err}
}
//only check resource control when it is a DockerSwarmStack or a DockerComposeStack
if stack.Type == portainer.DockerSwarmStack || stack.Type == portainer.DockerComposeStack {
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve a resource control associated to the stack", Err: err}
}
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack access", Err: err}
}
if !access {
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Access denied to resource", Err: httperrors.ErrResourceAccessDenied}
}
}
var payload stackGitRedployPayload
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
}
stack.GitConfig.ReferenceName = payload.RepositoryReferenceName
stack.Env = payload.Env
backupProjectPath := fmt.Sprintf("%s-old", stack.ProjectPath)
err = filesystem.MoveDirectory(stack.ProjectPath, backupProjectPath)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to move git repository directory", Err: err}
}
repositoryUsername := ""
repositoryPassword := ""
if payload.RepositoryAuthentication {
repositoryPassword = payload.RepositoryPassword
if repositoryPassword == "" && stack.GitConfig != nil && stack.GitConfig.Authentication != nil {
repositoryPassword = stack.GitConfig.Authentication.Password
}
repositoryUsername = payload.RepositoryUsername
}
err = handler.GitService.CloneRepository(stack.ProjectPath, stack.GitConfig.URL, payload.RepositoryReferenceName, repositoryUsername, repositoryPassword)
if err != nil {
restoreError := filesystem.MoveDirectory(backupProjectPath, stack.ProjectPath)
if restoreError != nil {
log.Printf("[WARN] [http,stacks,git] [error: %s] [message: failed restoring backup folder]", restoreError)
}
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to clone git repository", Err: err}
}
defer func() {
err = handler.FileService.RemoveDirectory(backupProjectPath)
if err != nil {
log.Printf("[WARN] [http,stacks,git] [error: %s] [message: unable to remove git repository directory]", err)
}
}()
httpErr := handler.deployStack(r, stack, endpoint)
if httpErr != nil {
return httpErr
}
newHash, err := handler.GitService.LatestCommitID(stack.GitConfig.URL, stack.GitConfig.ReferenceName, repositoryUsername, repositoryPassword)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable get latest commit id", Err: errors.WithMessagef(err, "failed to fetch latest commit id of the stack %v", stack.ID)}
}
stack.GitConfig.ConfigHash = newHash
user, err := handler.DataStore.User().User(securityContext.UserID)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Cannot find context user", Err: errors.Wrap(err, "failed to fetch the user")}
}
stack.UpdatedBy = user.Username
stack.UpdateDate = time.Now().Unix()
stack.Status = portainer.StackStatusActive
err = handler.DataStore.Stack().UpdateStack(stack.ID, stack)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist the stack changes inside the database", Err: errors.Wrap(err, "failed to update the stack")}
}
if stack.GitConfig != nil && stack.GitConfig.Authentication != nil && stack.GitConfig.Authentication.Password != "" {
// sanitize password in the http response to minimise possible security leaks
stack.GitConfig.Authentication.Password = ""
}
return response.JSON(w, stack)
}
func (handler *Handler) deployStack(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint) *httperror.HandlerError {
switch stack.Type {
case portainer.DockerSwarmStack:
config, httpErr := handler.createSwarmDeployConfig(r, stack, endpoint, false)
if httpErr != nil {
return httpErr
}
if err := handler.deploySwarmStack(config); err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: err.Error(), Err: err}
}
case portainer.DockerComposeStack:
config, httpErr := handler.createComposeDeployConfig(r, stack, endpoint)
if httpErr != nil {
return httpErr
}
if err := handler.deployComposeStack(config); err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: err.Error(), Err: err}
}
case portainer.KubernetesStack:
if stack.Namespace == "" {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Invalid namespace", Err: errors.New("Namespace must not be empty when redeploying kubernetes stacks")}
}
tokenData, err := security.RetrieveTokenData(r)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Failed to retrieve user token data", Err: err}
}
_, err = handler.deployKubernetesStack(tokenData.ID, endpoint, stack, k.KubeAppLabels{
StackID: int(stack.ID),
StackName: stack.Name,
Owner: stack.CreatedBy,
Kind: "git",
})
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to redeploy Kubernetes stack", Err: errors.WithMessage(err, "failed to deploy kube application")}
}
default:
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unsupported stack", Err: errors.Errorf("unsupported stack type: %v", stack.Type)}
}
return nil
}

View File

@@ -1,139 +0,0 @@
package stacks
import (
"fmt"
"io/ioutil"
"net/http"
"os"
"path"
"strconv"
"github.com/asaskevich/govalidator"
"github.com/pkg/errors"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/filesystem"
gittypes "github.com/portainer/portainer/api/git/types"
"github.com/portainer/portainer/api/http/security"
k "github.com/portainer/portainer/api/kubernetes"
)
type kubernetesFileStackUpdatePayload struct {
StackFileContent string
}
type kubernetesGitStackUpdatePayload struct {
RepositoryReferenceName string
RepositoryAuthentication bool
RepositoryUsername string
RepositoryPassword string
AutoUpdate *portainer.StackAutoUpdate
}
func (payload *kubernetesFileStackUpdatePayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.StackFileContent) {
return errors.New("Invalid stack file content")
}
return nil
}
func (payload *kubernetesGitStackUpdatePayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.RepositoryReferenceName) {
payload.RepositoryReferenceName = defaultGitReferenceName
}
if err := validateStackAutoUpdate(payload.AutoUpdate); err != nil {
return err
}
return nil
}
func (handler *Handler) updateKubernetesStack(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint) *httperror.HandlerError {
if stack.GitConfig != nil {
//stop the autoupdate job if there is any
if stack.AutoUpdate != nil {
stopAutoupdate(stack.ID, stack.AutoUpdate.JobID, *handler.Scheduler)
}
var payload kubernetesGitStackUpdatePayload
if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
}
stack.GitConfig.ReferenceName = payload.RepositoryReferenceName
stack.AutoUpdate = payload.AutoUpdate
if payload.RepositoryAuthentication {
password := payload.RepositoryPassword
if password == "" && stack.GitConfig != nil && stack.GitConfig.Authentication != nil {
password = stack.GitConfig.Authentication.Password
}
stack.GitConfig.Authentication = &gittypes.GitAuthentication{
Username: payload.RepositoryUsername,
Password: password,
}
} else {
stack.GitConfig.Authentication = nil
}
if payload.AutoUpdate != nil && payload.AutoUpdate.Interval != "" {
jobID, e := startAutoupdate(stack.ID, stack.AutoUpdate.Interval, handler.Scheduler, handler.StackDeployer, handler.DataStore, handler.GitService)
if e != nil {
return e
}
stack.AutoUpdate.JobID = jobID
}
return nil
}
var payload kubernetesFileStackUpdatePayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
}
tokenData, err := security.RetrieveTokenData(r)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Failed to retrieve user token data", Err: err}
}
tempFileDir, _ := ioutil.TempDir("", "kub_file_content")
defer os.RemoveAll(tempFileDir)
if err := filesystem.WriteToFile(path.Join(tempFileDir, stack.EntryPoint), []byte(payload.StackFileContent)); err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Failed to persist deployment file in a temp directory", Err: err}
}
//use temp dir as the stack project path for deployment
//so if the deployment failed, the original file won't be over-written
stack.ProjectPath = tempFileDir
_, err = handler.deployKubernetesStack(tokenData.ID, endpoint, stack, k.KubeAppLabels{
StackID: int(stack.ID),
StackName: stack.Name,
Owner: stack.CreatedBy,
Kind: "content",
})
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to deploy Kubernetes stack via file content", Err: err}
}
stackFolder := strconv.Itoa(int(stack.ID))
projectPath, err := handler.FileService.StoreStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent))
if err != nil {
fileType := "Manifest"
if stack.IsComposeFormat {
fileType = "Compose"
}
errMsg := fmt.Sprintf("Unable to persist Kubernetes %s file on disk", fileType)
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: errMsg, Err: err}
}
stack.ProjectPath = projectPath
return nil
}

View File

@@ -1,54 +0,0 @@
package stacks
import (
"log"
"net/http"
"github.com/gofrs/uuid"
"github.com/portainer/libhttp/response"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/stacks"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
)
func (handler *Handler) webhookInvoke(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
webhookID, err := retrieveUUIDRouteVariableValue(r, "webhookID")
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid webhook identifier route variable", Err: err}
}
stack, err := handler.DataStore.Stack().StackByWebhookID(webhookID.String())
if err != nil {
statusCode := http.StatusInternalServerError
if err == bolterrors.ErrObjectNotFound {
statusCode = http.StatusNotFound
}
return &httperror.HandlerError{StatusCode: statusCode, Message: "Unable to find the stack by webhook ID", Err: err}
}
if err = stacks.RedeployWhenChanged(stack.ID, handler.StackDeployer, handler.DataStore, handler.GitService); err != nil {
log.Printf("[ERROR] %s\n", err)
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Failed to update the stack", Err: err}
}
return response.Empty(w)
}
func retrieveUUIDRouteVariableValue(r *http.Request, name string) (uuid.UUID, error) {
webhookID, err := request.RetrieveRouteVariableValue(r, name)
if err != nil {
return uuid.Nil, err
}
uid, err := uuid.FromString(webhookID)
if err != nil {
return uuid.Nil, err
}
return uid, nil
}

View File

@@ -1,59 +0,0 @@
package stacks
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/gofrs/uuid"
"github.com/stretchr/testify/assert"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/bolttest"
)
func TestHandler_webhookInvoke(t *testing.T) {
store, teardown := bolttest.MustNewTestStore(true)
defer teardown()
webhookID := newGuidString(t)
store.StackService.CreateStack(&portainer.Stack{
AutoUpdate: &portainer.StackAutoUpdate{
Webhook: webhookID,
},
})
h := NewHandler(nil)
h.DataStore = store
t.Run("invalid uuid results in http.StatusBadRequest", func(t *testing.T) {
w := httptest.NewRecorder()
req := newRequest("notuuid")
h.Router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
})
t.Run("registered webhook ID in http.StatusNoContent", func(t *testing.T) {
w := httptest.NewRecorder()
req := newRequest(webhookID)
h.Router.ServeHTTP(w, req)
assert.Equal(t, http.StatusNoContent, w.Code)
})
t.Run("unregistered webhook ID in http.StatusNotFound", func(t *testing.T) {
w := httptest.NewRecorder()
req := newRequest(newGuidString(t))
h.Router.ServeHTTP(w, req)
assert.Equal(t, http.StatusNotFound, w.Code)
})
}
func newGuidString(t *testing.T) string {
uuid, err := uuid.NewV4()
assert.NoError(t, err)
return uuid.String()
}
func newRequest(webhookID string) *http.Request {
return httptest.NewRequest(http.MethodPost, "/stacks/webhooks/"+webhookID, nil)
}

View File

@@ -17,7 +17,6 @@ import (
type userUpdatePayload struct {
Username string `validate:"required" example:"bob"`
Password string `validate:"required" example:"cg9Wgky3"`
UserTheme string `example:"dark"`
// User role (1 for administrator account and 2 for regular account)
Role int `validate:"required" enums:"1,2" example:"2"`
}
@@ -105,10 +104,6 @@ func (handler *Handler) userUpdate(w http.ResponseWriter, r *http.Request) *http
user.Role = portainer.UserRole(payload.Role)
}
if payload.UserTheme != "" {
user.UserTheme = payload.UserTheme
}
err = handler.DataStore.User().UpdateUser(user.ID, user)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist user changes inside the database", err}

View File

@@ -5,7 +5,6 @@ import (
"github.com/gorilla/websocket"
httperror "github.com/portainer/libhttp/error"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/kubernetes/cli"
)
@@ -13,22 +12,20 @@ import (
// Handler is the HTTP handler used to handle websocket operations.
type Handler struct {
*mux.Router
DataStore portainer.DataStore
SignatureService portainer.DigitalSignatureService
ReverseTunnelService portainer.ReverseTunnelService
KubernetesClientFactory *cli.ClientFactory
requestBouncer *security.RequestBouncer
connectionUpgrader websocket.Upgrader
kubernetesTokenCacheManager *kubernetes.TokenCacheManager
DataStore portainer.DataStore
SignatureService portainer.DigitalSignatureService
ReverseTunnelService portainer.ReverseTunnelService
KubernetesClientFactory *cli.ClientFactory
requestBouncer *security.RequestBouncer
connectionUpgrader websocket.Upgrader
}
// NewHandler creates a handler to manage websocket operations.
func NewHandler(kubernetesTokenCacheManager *kubernetes.TokenCacheManager, bouncer *security.RequestBouncer) *Handler {
func NewHandler(bouncer *security.RequestBouncer) *Handler {
h := &Handler{
Router: mux.NewRouter(),
connectionUpgrader: websocket.Upgrader{},
requestBouncer: bouncer,
kubernetesTokenCacheManager: kubernetesTokenCacheManager,
}
h.PathPrefix("/websocket/exec").Handler(
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.websocketExec)))
@@ -36,7 +33,5 @@ func NewHandler(kubernetesTokenCacheManager *kubernetes.TokenCacheManager, bounc
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.websocketAttach)))
h.PathPrefix("/websocket/pod").Handler(
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.websocketPodExec)))
h.PathPrefix("/websocket/kubernetes-shell").Handler(
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.websocketShellPodExec)))
return h
}

View File

@@ -1,20 +1,16 @@
package websocket
import (
"fmt"
"io"
"log"
"net/http"
"strings"
"github.com/portainer/portainer/api/http/security"
"github.com/gorilla/websocket"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
portainer "github.com/portainer/portainer/api"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
)
// @summary Execute a websocket on pod
@@ -74,14 +70,8 @@ func (handler *Handler) websocketPodExec(w http.ResponseWriter, r *http.Request)
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
}
serviceAccountToken, isAdminToken, err := handler.getToken(r, endpoint, false)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to get user service account token", err}
}
params := &webSocketRequestParams{
endpoint: endpoint,
token: serviceAccountToken,
}
r.Header.Del("Origin")
@@ -100,28 +90,6 @@ func (handler *Handler) websocketPodExec(w http.ResponseWriter, r *http.Request)
return nil
}
cli, err := handler.KubernetesClientFactory.GetKubeClient(endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create Kubernetes client", err}
}
handlerErr := handler.hijackPodExecStartOperation(w, r, cli, serviceAccountToken, isAdminToken, endpoint, namespace, podName, containerName, command)
if handlerErr != nil {
return handlerErr
}
return nil
}
func (handler *Handler) hijackPodExecStartOperation(
w http.ResponseWriter,
r *http.Request,
cli portainer.KubeClient,
serviceAccountToken string,
isAdminToken bool,
endpoint *portainer.Endpoint,
namespace, podName, containerName, command string,
) *httperror.HandlerError {
commandArray := strings.Split(command, " ")
websocketConn, err := handler.connectionUpgrader.Upgrade(w, r, nil)
@@ -135,57 +103,24 @@ func (handler *Handler) hijackPodExecStartOperation(
stdoutReader, stdoutWriter := io.Pipe()
defer stdoutWriter.Close()
// errorChan is used to propagate errors from the go routines to the caller.
errorChan := make(chan error, 1)
go streamFromWebsocketToWriter(websocketConn, stdinWriter, errorChan)
go streamFromReaderToWebsocket(websocketConn, stdoutReader, errorChan)
// StartExecProcess is a blocking operation which streams IO to/from pod;
// this must execute in asynchronously, since the websocketConn could return errors (e.g. client disconnects) before
// the blocking operation is completed.
go cli.StartExecProcess(serviceAccountToken, isAdminToken, namespace, podName, containerName, commandArray, stdinReader, stdoutWriter, errorChan)
cli, err := handler.KubernetesClientFactory.GetKubeClient(endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create Kubernetes client", err}
}
err = cli.StartExecProcess(namespace, podName, containerName, commandArray, stdinReader, stdoutWriter)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to start exec process inside container", err}
}
err = <-errorChan
// websocket client successfully disconnected
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseNoStatusReceived) {
log.Printf("websocket error: %s \n", err.Error())
return nil
}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to start exec process inside container", err}
}
func (handler *Handler) getToken(request *http.Request, endpoint *portainer.Endpoint, setLocalAdminToken bool) (string, bool, error) {
tokenData, err := security.RetrieveTokenData(request)
if err != nil {
return "", false, err
}
kubecli, err := handler.KubernetesClientFactory.GetKubeClient(endpoint)
if err != nil {
return "", false, err
}
tokenCache := handler.kubernetesTokenCacheManager.GetOrCreateTokenCache(int(endpoint.ID))
tokenManager, err := kubernetes.NewTokenManager(kubecli, handler.DataStore, tokenCache, setLocalAdminToken)
if err != nil {
return "", false, err
}
if tokenData.Role == portainer.AdministratorRole {
return tokenManager.GetAdminServiceAccountToken(), true, nil
}
token, err := tokenManager.GetUserServiceAccountToken(int(tokenData.ID), endpoint.ID)
if err != nil {
return "", false, err
}
if token == "" {
return "", false, fmt.Errorf("can not get a valid user service account token")
}
return token, false, nil
return nil
}

View File

@@ -22,16 +22,8 @@ func (handler *Handler) proxyEdgeAgentWebsocketRequest(w http.ResponseWriter, r
endpointURL.Scheme = "ws"
proxy := websocketproxy.NewProxy(endpointURL)
signature, err := handler.SignatureService.CreateSignature(portainer.PortainerAgentSignatureMessage)
if err != nil {
return err
}
proxy.Director = func(incoming *http.Request, out http.Header) {
out.Set(portainer.PortainerAgentPublicKeyHeader, handler.SignatureService.EncodedPublicKey())
out.Set(portainer.PortainerAgentSignatureHeader, signature)
out.Set(portainer.PortainerAgentTargetHeader, params.nodeName)
out.Set(portainer.PortainerAgentKubernetesSATokenHeader, params.token)
}
handler.ReverseTunnelService.SetTunnelStatusToActive(params.endpoint.ID)
@@ -72,7 +64,6 @@ func (handler *Handler) proxyAgentWebsocketRequest(w http.ResponseWriter, r *htt
out.Set(portainer.PortainerAgentPublicKeyHeader, handler.SignatureService.EncodedPublicKey())
out.Set(portainer.PortainerAgentSignatureHeader, signature)
out.Set(portainer.PortainerAgentTargetHeader, params.nodeName)
out.Set(portainer.PortainerAgentKubernetesSATokenHeader, params.token)
}
proxy.ServeHTTP(w, r)

View File

@@ -1,106 +0,0 @@
package websocket
import (
"net/http"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
portainer "github.com/portainer/portainer/api"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/http/security"
)
// websocketShellPodExec handles GET requests on /websocket/pod?token=<token>&endpointId=<endpointID>
// The request will be upgraded to the websocket protocol.
// Authentication and access is controlled via the mandatory token query parameter.
// The request will proxy input from the client to the pod via long-lived websocket connection.
// The following query parameters are mandatory:
// * token: JWT token used for authentication against this endpoint
// * endpointId: endpoint ID of the endpoint where the resource is located
func (handler *Handler) websocketShellPodExec(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpointID, err := request.RetrieveNumericQueryParameter(r, "endpointId", false)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: endpointId", err}
}
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find the endpoint associated to the stack inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err}
}
tokenData, err := security.RetrieveTokenData(r)
if err != nil {
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
}
cli, err := handler.KubernetesClientFactory.GetKubeClient(endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create Kubernetes client", err}
}
serviceAccount, err := cli.GetServiceAccount(tokenData)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find serviceaccount associated with user", err}
}
shellPod, err := cli.CreateUserShellPod(r.Context(), serviceAccount.Name)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create user shell", err}
}
// Modifying request params mid-flight before forewarding to K8s API server (websocket)
q := r.URL.Query()
q.Add("namespace", shellPod.Namespace)
q.Add("podName", shellPod.PodName)
q.Add("containerName", shellPod.ContainerName)
q.Add("command", shellPod.ShellExecCommand)
r.URL.RawQuery = q.Encode()
// Modify url path mid-flight before forewarding to k8s API server (websocket)
r.URL.Path = "/websocket/pod"
/*
Note: The following websocket proxying logic is duplicated from `api/http/handler/websocket/pod.go`
*/
params := &webSocketRequestParams{
endpoint: endpoint,
}
r.Header.Del("Origin")
if endpoint.Type == portainer.AgentOnKubernetesEnvironment {
err := handler.proxyAgentWebsocketRequest(w, r, params)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to proxy websocket request to agent", err}
}
return nil
} else if endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment {
err := handler.proxyEdgeAgentWebsocketRequest(w, r, params)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to proxy websocket request to Edge agent", err}
}
return nil
}
handlerErr := handler.hijackPodExecStartOperation(
w,
r,
cli,
"",
true,
endpoint,
shellPod.Namespace,
shellPod.PodName,
shellPod.ContainerName,
shellPod.ShellExecCommand,
)
if handlerErr != nil {
return handlerErr
}
return nil
}

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