Compare commits
10 Commits
chore/redu
...
fix3272-ba
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c15dc7910d | ||
|
|
223f742ee1 | ||
|
|
85a4e70b87 | ||
|
|
ed003ffaaf | ||
|
|
b3cf11ec22 | ||
|
|
dfb870105c | ||
|
|
2f12cbf083 | ||
|
|
64251b3e88 | ||
|
|
5fcace6b01 | ||
|
|
a5438cc86a |
@@ -23,7 +23,6 @@ import (
|
||||
"github.com/portainer/portainer/api/bolt/tag"
|
||||
"github.com/portainer/portainer/api/bolt/team"
|
||||
"github.com/portainer/portainer/api/bolt/teammembership"
|
||||
"github.com/portainer/portainer/api/bolt/template"
|
||||
"github.com/portainer/portainer/api/bolt/user"
|
||||
"github.com/portainer/portainer/api/bolt/version"
|
||||
"github.com/portainer/portainer/api/bolt/webhook"
|
||||
@@ -38,7 +37,7 @@ const (
|
||||
type Store struct {
|
||||
path string
|
||||
db *bolt.DB
|
||||
checkForDataMigration bool
|
||||
isNew bool
|
||||
fileService portainer.FileService
|
||||
RoleService *role.Service
|
||||
DockerHubService *dockerhub.Service
|
||||
@@ -52,7 +51,6 @@ type Store struct {
|
||||
TagService *tag.Service
|
||||
TeamMembershipService *teammembership.Service
|
||||
TeamService *team.Service
|
||||
TemplateService *template.Service
|
||||
TunnelServerService *tunnelserver.Service
|
||||
UserService *user.Service
|
||||
VersionService *version.Service
|
||||
@@ -65,6 +63,7 @@ func NewStore(storePath string, fileService portainer.FileService) (*Store, erro
|
||||
store := &Store{
|
||||
path: storePath,
|
||||
fileService: fileService,
|
||||
isNew: true,
|
||||
}
|
||||
|
||||
databasePath := path.Join(storePath, databaseFileName)
|
||||
@@ -73,10 +72,8 @@ func NewStore(storePath string, fileService portainer.FileService) (*Store, erro
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !databaseFileExists {
|
||||
store.checkForDataMigration = false
|
||||
} else {
|
||||
store.checkForDataMigration = true
|
||||
if databaseFileExists {
|
||||
store.isNew = false
|
||||
}
|
||||
|
||||
return store, nil
|
||||
@@ -102,9 +99,16 @@ func (store *Store) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsNew returns true if the database was just created and false if it is re-using
|
||||
// existing data.
|
||||
func (store *Store) IsNew() bool {
|
||||
return store.isNew
|
||||
}
|
||||
|
||||
// MigrateData automatically migrate the data based on the DBVersion.
|
||||
// This process is only triggered on an existing database, not if the database was just created.
|
||||
func (store *Store) MigrateData() error {
|
||||
if !store.checkForDataMigration {
|
||||
if store.isNew {
|
||||
return store.VersionService.StoreDBVersion(portainer.DBVersion)
|
||||
}
|
||||
|
||||
@@ -130,7 +134,6 @@ func (store *Store) MigrateData() error {
|
||||
StackService: store.StackService,
|
||||
TagService: store.TagService,
|
||||
TeamMembershipService: store.TeamMembershipService,
|
||||
TemplateService: store.TemplateService,
|
||||
UserService: store.UserService,
|
||||
VersionService: store.VersionService,
|
||||
FileService: store.fileService,
|
||||
@@ -221,12 +224,6 @@ func (store *Store) initServices() error {
|
||||
}
|
||||
store.TeamService = teamService
|
||||
|
||||
templateService, err := template.NewService(store.db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
store.TemplateService = templateService
|
||||
|
||||
tunnelServerService, err := tunnelserver.NewService(store.db)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -4,6 +4,55 @@ import portainer "github.com/portainer/portainer/api"
|
||||
|
||||
// Init creates the default data set.
|
||||
func (store *Store) Init() error {
|
||||
_, err := store.SettingsService.Settings()
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
defaultSettings := &portainer.Settings{
|
||||
AuthenticationMethod: portainer.AuthenticationInternal,
|
||||
BlackListedLabels: make([]portainer.Pair, 0),
|
||||
LDAPSettings: portainer.LDAPSettings{
|
||||
AnonymousMode: true,
|
||||
AutoCreateUsers: true,
|
||||
TLSConfig: portainer.TLSConfiguration{},
|
||||
SearchSettings: []portainer.LDAPSearchSettings{
|
||||
portainer.LDAPSearchSettings{},
|
||||
},
|
||||
GroupSearchSettings: []portainer.LDAPGroupSearchSettings{
|
||||
portainer.LDAPGroupSearchSettings{},
|
||||
},
|
||||
},
|
||||
OAuthSettings: portainer.OAuthSettings{},
|
||||
AllowBindMountsForRegularUsers: true,
|
||||
AllowPrivilegedModeForRegularUsers: true,
|
||||
AllowVolumeBrowserForRegularUsers: false,
|
||||
EnableHostManagementFeatures: false,
|
||||
EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds,
|
||||
TemplatesURL: portainer.DefaultTemplatesURL,
|
||||
}
|
||||
|
||||
err = store.SettingsService.UpdateSettings(defaultSettings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = store.DockerHubService.DockerHub()
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
defaultDockerHub := &portainer.DockerHub{
|
||||
Authentication: false,
|
||||
Username: "",
|
||||
Password: "",
|
||||
}
|
||||
|
||||
err := store.DockerHubService.UpdateDockerHub(defaultDockerHub)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
groups, err := store.EndpointGroupService.EndpointGroups()
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
func (m *Migrator) updateSettingsToDBVersion15() error {
|
||||
legacySettings, err := m.settingsService.Settings()
|
||||
if err != nil {
|
||||
@@ -17,19 +11,6 @@ func (m *Migrator) updateSettingsToDBVersion15() error {
|
||||
}
|
||||
|
||||
func (m *Migrator) updateTemplatesToVersion15() error {
|
||||
legacyTemplates, err := m.templateService.Templates()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, template := range legacyTemplates {
|
||||
template.Logo = strings.Replace(template.Logo, "https://portainer.io/images", portainer.AssetsServerURL, -1)
|
||||
|
||||
err = m.templateService.UpdateTemplate(template.ID, &template)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Removed with the entire template management layer, part of https://github.com/portainer/portainer/issues/3707
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
"github.com/portainer/portainer/api/bolt/stack"
|
||||
"github.com/portainer/portainer/api/bolt/tag"
|
||||
"github.com/portainer/portainer/api/bolt/teammembership"
|
||||
"github.com/portainer/portainer/api/bolt/template"
|
||||
"github.com/portainer/portainer/api/bolt/user"
|
||||
"github.com/portainer/portainer/api/bolt/version"
|
||||
)
|
||||
@@ -35,7 +34,6 @@ type (
|
||||
stackService *stack.Service
|
||||
tagService *tag.Service
|
||||
teamMembershipService *teammembership.Service
|
||||
templateService *template.Service
|
||||
userService *user.Service
|
||||
versionService *version.Service
|
||||
fileService portainer.FileService
|
||||
@@ -56,7 +54,6 @@ type (
|
||||
StackService *stack.Service
|
||||
TagService *tag.Service
|
||||
TeamMembershipService *teammembership.Service
|
||||
TemplateService *template.Service
|
||||
UserService *user.Service
|
||||
VersionService *version.Service
|
||||
FileService portainer.FileService
|
||||
@@ -78,7 +75,6 @@ func NewMigrator(parameters *Parameters) *Migrator {
|
||||
settingsService: parameters.SettingsService,
|
||||
tagService: parameters.TagService,
|
||||
teamMembershipService: parameters.TeamMembershipService,
|
||||
templateService: parameters.TemplateService,
|
||||
stackService: parameters.StackService,
|
||||
userService: parameters.UserService,
|
||||
versionService: parameters.VersionService,
|
||||
@@ -305,7 +301,7 @@ func (m *Migrator) Migrate() error {
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.24.0-dev
|
||||
// Portainer 1.24.0
|
||||
if m.currentDBVersion < 23 {
|
||||
err := m.updateEndointsAndEndpointsGroupsToDBVersion23()
|
||||
if err != nil {
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/internal"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
)
|
||||
|
||||
const (
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
BucketName = "templates"
|
||||
)
|
||||
|
||||
// Service represents a service for managing endpoint data.
|
||||
type Service struct {
|
||||
db *bolt.DB
|
||||
}
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(db *bolt.DB) (*Service, error) {
|
||||
err := internal.CreateBucket(db, BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Service{
|
||||
db: db,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Templates return an array containing all the templates.
|
||||
func (service *Service) Templates() ([]portainer.Template, error) {
|
||||
var templates = make([]portainer.Template, 0)
|
||||
|
||||
err := service.db.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 template portainer.Template
|
||||
err := internal.UnmarshalObject(v, &template)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
templates = append(templates, template)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return templates, err
|
||||
}
|
||||
|
||||
// Template returns a template by ID.
|
||||
func (service *Service) Template(ID portainer.TemplateID) (*portainer.Template, error) {
|
||||
var template portainer.Template
|
||||
identifier := internal.Itob(int(ID))
|
||||
|
||||
err := internal.GetObject(service.db, BucketName, identifier, &template)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &template, nil
|
||||
}
|
||||
|
||||
// CreateTemplate creates a new template.
|
||||
func (service *Service) CreateTemplate(template *portainer.Template) error {
|
||||
return service.db.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(BucketName))
|
||||
|
||||
id, _ := bucket.NextSequence()
|
||||
template.ID = portainer.TemplateID(id)
|
||||
|
||||
data, err := internal.MarshalObject(template)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return bucket.Put(internal.Itob(int(template.ID)), data)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateTemplate saves a template.
|
||||
func (service *Service) UpdateTemplate(ID portainer.TemplateID, template *portainer.Template) error {
|
||||
identifier := internal.Itob(int(ID))
|
||||
return internal.UpdateObject(service.db, BucketName, identifier, template)
|
||||
}
|
||||
|
||||
// DeleteTemplate deletes a template.
|
||||
func (service *Service) DeleteTemplate(ID portainer.TemplateID) error {
|
||||
identifier := internal.Itob(int(ID))
|
||||
return internal.DeleteObject(service.db, BucketName, identifier)
|
||||
}
|
||||
@@ -19,7 +19,6 @@ const (
|
||||
errInvalidEndpointProtocol = portainer.Error("Invalid endpoint protocol: Portainer only supports unix://, npipe:// or tcp://")
|
||||
errSocketOrNamedPipeNotFound = portainer.Error("Unable to locate Unix socket or named pipe")
|
||||
errEndpointsFileNotFound = portainer.Error("Unable to locate external endpoints file")
|
||||
errTemplateFileNotFound = portainer.Error("Unable to locate template file on disk")
|
||||
errInvalidSyncInterval = portainer.Error("Invalid synchronization interval")
|
||||
errInvalidSnapshotInterval = portainer.Error("Invalid snapshot interval")
|
||||
errEndpointExcludeExternal = portainer.Error("Cannot use the -H flag mutually with --external-endpoints")
|
||||
@@ -57,7 +56,6 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
||||
Labels: pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')),
|
||||
Logo: kingpin.Flag("logo", "URL for the logo displayed in the UI").String(),
|
||||
Templates: kingpin.Flag("templates", "URL to the templates definitions.").Short('t').String(),
|
||||
TemplateFile: kingpin.Flag("template-file", "Path to the templates (app) definitions on the filesystem").Default(defaultTemplateFile).String(),
|
||||
}
|
||||
|
||||
kingpin.Parse()
|
||||
@@ -80,12 +78,7 @@ func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
|
||||
return errEndpointExcludeExternal
|
||||
}
|
||||
|
||||
err := validateTemplateFile(*flags.TemplateFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = validateEndpointURL(*flags.EndpointURL)
|
||||
err := validateEndpointURL(*flags.EndpointURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -148,16 +141,6 @@ func validateExternalEndpoints(externalEndpoints string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateTemplateFile(templateFile string) error {
|
||||
if _, err := os.Stat(templateFile); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return errTemplateFileNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateSyncInterval(syncInterval string) error {
|
||||
if syncInterval != defaultSyncInterval {
|
||||
_, err := time.ParseDuration(syncInterval)
|
||||
|
||||
@@ -21,5 +21,4 @@ const (
|
||||
defaultSyncInterval = "60s"
|
||||
defaultSnapshot = "true"
|
||||
defaultSnapshotInterval = "5m"
|
||||
defaultTemplateFile = "/templates.json"
|
||||
)
|
||||
|
||||
@@ -19,5 +19,4 @@ const (
|
||||
defaultSyncInterval = "60s"
|
||||
defaultSnapshot = "true"
|
||||
defaultSnapshotInterval = "5m"
|
||||
defaultTemplateFile = "/templates.json"
|
||||
)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
@@ -236,111 +235,24 @@ func initStatus(endpointManagement, snapshot bool, flags *portainer.CLIFlags) *p
|
||||
}
|
||||
}
|
||||
|
||||
func initDockerHub(dockerHubService portainer.DockerHubService) error {
|
||||
_, err := dockerHubService.DockerHub()
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
dockerhub := &portainer.DockerHub{
|
||||
Authentication: false,
|
||||
Username: "",
|
||||
Password: "",
|
||||
}
|
||||
return dockerHubService.UpdateDockerHub(dockerhub)
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func initSettings(settingsService portainer.SettingsService, flags *portainer.CLIFlags) error {
|
||||
_, err := settingsService.Settings()
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
settings := &portainer.Settings{
|
||||
LogoURL: *flags.Logo,
|
||||
AuthenticationMethod: portainer.AuthenticationInternal,
|
||||
LDAPSettings: portainer.LDAPSettings{
|
||||
AnonymousMode: true,
|
||||
AutoCreateUsers: true,
|
||||
TLSConfig: portainer.TLSConfiguration{},
|
||||
SearchSettings: []portainer.LDAPSearchSettings{
|
||||
portainer.LDAPSearchSettings{},
|
||||
},
|
||||
GroupSearchSettings: []portainer.LDAPGroupSearchSettings{
|
||||
portainer.LDAPGroupSearchSettings{},
|
||||
},
|
||||
},
|
||||
OAuthSettings: portainer.OAuthSettings{},
|
||||
AllowBindMountsForRegularUsers: true,
|
||||
AllowPrivilegedModeForRegularUsers: true,
|
||||
AllowVolumeBrowserForRegularUsers: false,
|
||||
EnableHostManagementFeatures: false,
|
||||
SnapshotInterval: *flags.SnapshotInterval,
|
||||
EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds,
|
||||
}
|
||||
|
||||
if *flags.Templates != "" {
|
||||
settings.TemplatesURL = *flags.Templates
|
||||
}
|
||||
|
||||
if *flags.Labels != nil {
|
||||
settings.BlackListedLabels = *flags.Labels
|
||||
} else {
|
||||
settings.BlackListedLabels = make([]portainer.Pair, 0)
|
||||
}
|
||||
|
||||
return settingsService.UpdateSettings(settings)
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func initTemplates(templateService portainer.TemplateService, fileService portainer.FileService, templateURL, templateFile string) error {
|
||||
if templateURL != "" {
|
||||
log.Printf("Portainer started with the --templates flag. Using external templates, template management will be disabled.")
|
||||
return nil
|
||||
}
|
||||
|
||||
existingTemplates, err := templateService.Templates()
|
||||
func updateSettingsFromFlags(settingsService portainer.SettingsService, flags *portainer.CLIFlags) error {
|
||||
settings, err := settingsService.Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(existingTemplates) != 0 {
|
||||
log.Printf("Templates already registered inside the database. Skipping template import.")
|
||||
return nil
|
||||
settings.LogoURL = *flags.Logo
|
||||
settings.SnapshotInterval = *flags.SnapshotInterval
|
||||
|
||||
if *flags.Templates != "" {
|
||||
settings.TemplatesURL = *flags.Templates
|
||||
}
|
||||
|
||||
templatesJSON, err := fileService.GetFileContent(templateFile)
|
||||
if err != nil {
|
||||
log.Println("Unable to retrieve template definitions via filesystem")
|
||||
return err
|
||||
if *flags.Labels != nil {
|
||||
settings.BlackListedLabels = *flags.Labels
|
||||
}
|
||||
|
||||
var templates []portainer.Template
|
||||
err = json.Unmarshal(templatesJSON, &templates)
|
||||
if err != nil {
|
||||
log.Println("Unable to parse templates file. Please review your template definition file.")
|
||||
return err
|
||||
}
|
||||
|
||||
for _, template := range templates {
|
||||
err := templateService.CreateTemplate(&template)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func retrieveFirstEndpointFromDatabase(endpointService portainer.EndpointService) *portainer.Endpoint {
|
||||
endpoints, err := endpointService.Endpoints()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return &endpoints[0]
|
||||
return settingsService.UpdateSettings(settings)
|
||||
}
|
||||
|
||||
func loadAndParseKeyPair(fileService portainer.FileService, signatureService portainer.DigitalSignatureService) error {
|
||||
@@ -561,14 +473,11 @@ func main() {
|
||||
|
||||
composeStackManager := initComposeStackManager(*flags.Data, reverseTunnelService)
|
||||
|
||||
err = initTemplates(store.TemplateService, fileService, *flags.Templates, *flags.TemplateFile)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = initSettings(store.SettingsService, flags)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
if store.IsNew() {
|
||||
err = updateSettingsFromFlags(store.SettingsService, flags)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
jobScheduler := initJobScheduler()
|
||||
@@ -592,11 +501,6 @@ func main() {
|
||||
|
||||
jobScheduler.Start()
|
||||
|
||||
err = initDockerHub(store.DockerHubService)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
applicationStatus := initStatus(endpointManagement, *flags.Snapshot, flags)
|
||||
|
||||
err = initEndpoint(flags, store.EndpointService, snapshotter)
|
||||
@@ -671,7 +575,6 @@ func main() {
|
||||
StackService: store.StackService,
|
||||
ScheduleService: store.ScheduleService,
|
||||
TagService: store.TagService,
|
||||
TemplateService: store.TemplateService,
|
||||
WebhookService: store.WebhookService,
|
||||
SwarmStackManager: swarmStackManager,
|
||||
ComposeStackManager: composeStackManager,
|
||||
|
||||
@@ -30,8 +30,8 @@ require (
|
||||
github.com/robfig/cron/v3 v3.0.0
|
||||
golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6
|
||||
gopkg.in/asn1-ber.v1 v1.0.0-00010101000000-000000000000 // indirect
|
||||
gopkg.in/ldap.v2 v2.5.1
|
||||
gopkg.in/ldap.v3 v3.1.0
|
||||
gopkg.in/src-d/go-git.v4 v4.13.1
|
||||
)
|
||||
|
||||
|
||||
@@ -175,6 +175,7 @@ github.com/portainer/libcrypto v0.0.0-20190723020515-23ebe86ab2c2 h1:0PfgGLys9yH
|
||||
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/portainer/portainer v0.10.1 h1:I8K345CjGWfUGsVA8c8/gqamwLCC6CIAjxZXSklAFq0=
|
||||
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=
|
||||
@@ -270,6 +271,8 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/ldap.v2 v2.5.1 h1:wiu0okdNfjlBzg6UWvd1Hn8Y+Ux17/u/4nlk4CQr6tU=
|
||||
gopkg.in/ldap.v2 v2.5.1/go.mod h1:oI0cpe/D7HRtBQl8aTg+ZmzFUAvu4lsv3eLXMLGFxWk=
|
||||
gopkg.in/ldap.v3 v3.1.0 h1:DIDWEjI7vQWREh0S8X5/NFPCZ3MCVd55LmXKPW4XLGE=
|
||||
gopkg.in/ldap.v3 v3.1.0/go.mod h1:dQjCc0R0kfyFjIlWNMH1DORwUASZyDxo2Ry1B51dXaQ=
|
||||
gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg=
|
||||
gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98=
|
||||
gopkg.in/src-d/go-git-fixtures.v3 v3.5.0 h1:ivZFOIltbce2Mo8IjzUHAFoq/IylO9WHhNOAJK+LsJg=
|
||||
|
||||
@@ -16,7 +16,6 @@ type publicSettingsResponse struct {
|
||||
AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"`
|
||||
AllowVolumeBrowserForRegularUsers bool `json:"AllowVolumeBrowserForRegularUsers"`
|
||||
EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"`
|
||||
ExternalTemplates bool `json:"ExternalTemplates"`
|
||||
OAuthLoginURI string `json:"OAuthLoginURI"`
|
||||
}
|
||||
|
||||
@@ -34,7 +33,6 @@ func (handler *Handler) settingsPublic(w http.ResponseWriter, r *http.Request) *
|
||||
AllowPrivilegedModeForRegularUsers: settings.AllowPrivilegedModeForRegularUsers,
|
||||
AllowVolumeBrowserForRegularUsers: settings.AllowVolumeBrowserForRegularUsers,
|
||||
EnableHostManagementFeatures: settings.EnableHostManagementFeatures,
|
||||
ExternalTemplates: false,
|
||||
OAuthLoginURI: fmt.Sprintf("%s?response_type=code&client_id=%s&redirect_uri=%s&scope=%s&prompt=login",
|
||||
settings.OAuthSettings.AuthorizationURI,
|
||||
settings.OAuthSettings.ClientID,
|
||||
@@ -42,9 +40,5 @@ func (handler *Handler) settingsPublic(w http.ResponseWriter, r *http.Request) *
|
||||
settings.OAuthSettings.Scopes),
|
||||
}
|
||||
|
||||
if settings.TemplatesURL != "" {
|
||||
publicSettings.ExternalTemplates = true
|
||||
}
|
||||
|
||||
return response.JSON(w, publicSettings)
|
||||
}
|
||||
|
||||
@@ -9,14 +9,9 @@ import (
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
const (
|
||||
errTemplateManagementDisabled = portainer.Error("Template management is disabled")
|
||||
)
|
||||
|
||||
// Handler represents an HTTP API handler for managing templates.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
TemplateService portainer.TemplateService
|
||||
SettingsService portainer.SettingsService
|
||||
}
|
||||
|
||||
@@ -28,29 +23,5 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
|
||||
h.Handle("/templates",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.templateList))).Methods(http.MethodGet)
|
||||
h.Handle("/templates",
|
||||
bouncer.AdminAccess(h.templateManagementCheck(httperror.LoggerHandler(h.templateCreate)))).Methods(http.MethodPost)
|
||||
h.Handle("/templates/{id}",
|
||||
bouncer.RestrictedAccess(h.templateManagementCheck(httperror.LoggerHandler(h.templateInspect)))).Methods(http.MethodGet)
|
||||
h.Handle("/templates/{id}",
|
||||
bouncer.AdminAccess(h.templateManagementCheck(httperror.LoggerHandler(h.templateUpdate)))).Methods(http.MethodPut)
|
||||
h.Handle("/templates/{id}",
|
||||
bouncer.AdminAccess(h.templateManagementCheck(httperror.LoggerHandler(h.templateDelete)))).Methods(http.MethodDelete)
|
||||
return h
|
||||
}
|
||||
|
||||
func (handler *Handler) templateManagementCheck(next http.Handler) http.Handler {
|
||||
return httperror.LoggerHandler(func(rw http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
settings, err := handler.SettingsService.Settings()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
|
||||
}
|
||||
|
||||
if settings.TemplatesURL != "" {
|
||||
return &httperror.HandlerError{http.StatusServiceUnavailable, "Portainer is configured to use external templates, template management is disabled", errTemplateManagementDisabled}
|
||||
}
|
||||
|
||||
next.ServeHTTP(rw, r)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
package templates
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
)
|
||||
|
||||
type templateCreatePayload struct {
|
||||
// Mandatory
|
||||
Type int
|
||||
Title string
|
||||
Description string
|
||||
AdministratorOnly bool
|
||||
|
||||
// Opt stack/container
|
||||
Name string
|
||||
Logo string
|
||||
Note string
|
||||
Platform string
|
||||
Categories []string
|
||||
Env []portainer.TemplateEnv
|
||||
|
||||
// Mandatory container
|
||||
Image string
|
||||
|
||||
// Mandatory stack
|
||||
Repository portainer.TemplateRepository
|
||||
|
||||
// Opt container
|
||||
Registry string
|
||||
Command string
|
||||
Network string
|
||||
Volumes []portainer.TemplateVolume
|
||||
Ports []string
|
||||
Labels []portainer.Pair
|
||||
Privileged bool
|
||||
Interactive bool
|
||||
RestartPolicy string
|
||||
Hostname string
|
||||
}
|
||||
|
||||
func (payload *templateCreatePayload) Validate(r *http.Request) error {
|
||||
if payload.Type == 0 || (payload.Type != 1 && payload.Type != 2 && payload.Type != 3) {
|
||||
return portainer.Error("Invalid template type. Valid values are: 1 (container), 2 (Swarm stack template) or 3 (Compose stack template).")
|
||||
}
|
||||
if govalidator.IsNull(payload.Title) {
|
||||
return portainer.Error("Invalid template title")
|
||||
}
|
||||
if govalidator.IsNull(payload.Description) {
|
||||
return portainer.Error("Invalid template description")
|
||||
}
|
||||
|
||||
if payload.Type == 1 {
|
||||
if govalidator.IsNull(payload.Image) {
|
||||
return portainer.Error("Invalid template image")
|
||||
}
|
||||
}
|
||||
|
||||
if payload.Type == 2 || payload.Type == 3 {
|
||||
if govalidator.IsNull(payload.Repository.URL) {
|
||||
return portainer.Error("Invalid template repository URL")
|
||||
}
|
||||
if govalidator.IsNull(payload.Repository.StackFile) {
|
||||
payload.Repository.StackFile = filesystem.ComposeFileDefaultName
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// POST request on /api/templates
|
||||
func (handler *Handler) templateCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
var payload templateCreatePayload
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
template := &portainer.Template{
|
||||
Type: portainer.TemplateType(payload.Type),
|
||||
Title: payload.Title,
|
||||
Description: payload.Description,
|
||||
AdministratorOnly: payload.AdministratorOnly,
|
||||
Name: payload.Name,
|
||||
Logo: payload.Logo,
|
||||
Note: payload.Note,
|
||||
Platform: payload.Platform,
|
||||
Categories: payload.Categories,
|
||||
Env: payload.Env,
|
||||
}
|
||||
|
||||
if template.Type == portainer.ContainerTemplate {
|
||||
template.Image = payload.Image
|
||||
template.Registry = payload.Registry
|
||||
template.Command = payload.Command
|
||||
template.Network = payload.Network
|
||||
template.Volumes = payload.Volumes
|
||||
template.Ports = payload.Ports
|
||||
template.Labels = payload.Labels
|
||||
template.Privileged = payload.Privileged
|
||||
template.Interactive = payload.Interactive
|
||||
template.RestartPolicy = payload.RestartPolicy
|
||||
template.Hostname = payload.Hostname
|
||||
}
|
||||
|
||||
if template.Type == portainer.SwarmStackTemplate || template.Type == portainer.ComposeStackTemplate {
|
||||
template.Repository = payload.Repository
|
||||
}
|
||||
|
||||
err = handler.TemplateService.CreateTemplate(template)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the template inside the database", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, template)
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package templates
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
// DELETE request on /api/templates/:id
|
||||
func (handler *Handler) templateDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
id, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid template identifier route variable", err}
|
||||
}
|
||||
|
||||
err = handler.TemplateService.DeleteTemplate(portainer.TemplateID(id))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the template from the database", err}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package templates
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
// GET request on /api/templates/:id
|
||||
func (handler *Handler) templateInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
templateID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid template identifier route variable", err}
|
||||
}
|
||||
|
||||
template, err := handler.TemplateService.Template(portainer.TemplateID(templateID))
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a template with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a template with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, template)
|
||||
}
|
||||
@@ -1,14 +1,10 @@
|
||||
package templates
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/client"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
// GET request on /api/templates
|
||||
@@ -18,30 +14,17 @@ func (handler *Handler) templateList(w http.ResponseWriter, r *http.Request) *ht
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
|
||||
}
|
||||
|
||||
var templates []portainer.Template
|
||||
if settings.TemplatesURL == "" {
|
||||
templates, err = handler.TemplateService.Templates()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve templates from the database", err}
|
||||
}
|
||||
} else {
|
||||
var templateData []byte
|
||||
templateData, err = client.Get(settings.TemplatesURL, 0)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve external templates", err}
|
||||
}
|
||||
|
||||
err = json.Unmarshal(templateData, &templates)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to parse external templates", err}
|
||||
}
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
resp, err := http.Get(settings.TemplatesURL)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve templates via the network", err}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, err = io.Copy(w, resp.Body)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to write templates from templates URL", err}
|
||||
}
|
||||
|
||||
filteredTemplates := security.FilterTemplates(templates, securityContext)
|
||||
return response.JSON(w, filteredTemplates)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,164 +0,0 @@
|
||||
package templates
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
type templateUpdatePayload struct {
|
||||
Title *string
|
||||
Description *string
|
||||
AdministratorOnly *bool
|
||||
Name *string
|
||||
Logo *string
|
||||
Note *string
|
||||
Platform *string
|
||||
Categories []string
|
||||
Env []portainer.TemplateEnv
|
||||
Image *string
|
||||
Registry *string
|
||||
Repository portainer.TemplateRepository
|
||||
Command *string
|
||||
Network *string
|
||||
Volumes []portainer.TemplateVolume
|
||||
Ports []string
|
||||
Labels []portainer.Pair
|
||||
Privileged *bool
|
||||
Interactive *bool
|
||||
RestartPolicy *string
|
||||
Hostname *string
|
||||
}
|
||||
|
||||
func (payload *templateUpdatePayload) Validate(r *http.Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// PUT request on /api/templates/:id
|
||||
func (handler *Handler) templateUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
templateID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid template identifier route variable", err}
|
||||
}
|
||||
|
||||
template, err := handler.TemplateService.Template(portainer.TemplateID(templateID))
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a template with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a template with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
var payload templateUpdatePayload
|
||||
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
updateTemplate(template, &payload)
|
||||
|
||||
err = handler.TemplateService.UpdateTemplate(template.ID, template)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to persist template changes inside the database", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, template)
|
||||
}
|
||||
|
||||
func updateContainerProperties(template *portainer.Template, payload *templateUpdatePayload) {
|
||||
if payload.Image != nil {
|
||||
template.Image = *payload.Image
|
||||
}
|
||||
|
||||
if payload.Registry != nil {
|
||||
template.Registry = *payload.Registry
|
||||
}
|
||||
|
||||
if payload.Command != nil {
|
||||
template.Command = *payload.Command
|
||||
}
|
||||
|
||||
if payload.Network != nil {
|
||||
template.Network = *payload.Network
|
||||
}
|
||||
|
||||
if payload.Volumes != nil {
|
||||
template.Volumes = payload.Volumes
|
||||
}
|
||||
|
||||
if payload.Ports != nil {
|
||||
template.Ports = payload.Ports
|
||||
}
|
||||
|
||||
if payload.Labels != nil {
|
||||
template.Labels = payload.Labels
|
||||
}
|
||||
|
||||
if payload.Privileged != nil {
|
||||
template.Privileged = *payload.Privileged
|
||||
}
|
||||
|
||||
if payload.Interactive != nil {
|
||||
template.Interactive = *payload.Interactive
|
||||
}
|
||||
|
||||
if payload.RestartPolicy != nil {
|
||||
template.RestartPolicy = *payload.RestartPolicy
|
||||
}
|
||||
|
||||
if payload.Hostname != nil {
|
||||
template.Hostname = *payload.Hostname
|
||||
}
|
||||
}
|
||||
|
||||
func updateStackProperties(template *portainer.Template, payload *templateUpdatePayload) {
|
||||
if payload.Repository.URL != "" && payload.Repository.StackFile != "" {
|
||||
template.Repository = payload.Repository
|
||||
}
|
||||
}
|
||||
|
||||
func updateTemplate(template *portainer.Template, payload *templateUpdatePayload) {
|
||||
if payload.Title != nil {
|
||||
template.Title = *payload.Title
|
||||
}
|
||||
|
||||
if payload.Description != nil {
|
||||
template.Description = *payload.Description
|
||||
}
|
||||
|
||||
if payload.Name != nil {
|
||||
template.Name = *payload.Name
|
||||
}
|
||||
|
||||
if payload.Logo != nil {
|
||||
template.Logo = *payload.Logo
|
||||
}
|
||||
|
||||
if payload.Note != nil {
|
||||
template.Note = *payload.Note
|
||||
}
|
||||
|
||||
if payload.Platform != nil {
|
||||
template.Platform = *payload.Platform
|
||||
}
|
||||
|
||||
if payload.Categories != nil {
|
||||
template.Categories = payload.Categories
|
||||
}
|
||||
|
||||
if payload.Env != nil {
|
||||
template.Env = payload.Env
|
||||
}
|
||||
|
||||
if payload.AdministratorOnly != nil {
|
||||
template.AdministratorOnly = *payload.AdministratorOnly
|
||||
}
|
||||
|
||||
if template.Type == portainer.ContainerTemplate {
|
||||
updateContainerProperties(template, payload)
|
||||
} else if template.Type == portainer.SwarmStackTemplate || template.Type == portainer.ComposeStackTemplate {
|
||||
updateStackProperties(template, payload)
|
||||
}
|
||||
}
|
||||
@@ -273,7 +273,7 @@ func (transport *Transport) proxyServiceRequest(request *http.Request) (*http.Re
|
||||
func (transport *Transport) proxyVolumeRequest(request *http.Request) (*http.Response, error) {
|
||||
switch requestPath := request.URL.Path; requestPath {
|
||||
case "/volumes/create":
|
||||
return transport.decorateGenericResourceCreationOperation(request, volumeObjectIdentifier, portainer.VolumeResourceControl)
|
||||
return transport.decorateVolumeResourceCreationOperation(request, volumeObjectIdentifier, portainer.VolumeResourceControl)
|
||||
|
||||
case "/volumes/prune":
|
||||
return transport.administratorOperation(request)
|
||||
|
||||
@@ -2,12 +2,14 @@ package docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/docker/docker/client"
|
||||
|
||||
"github.com/portainer/portainer/api"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -79,6 +81,43 @@ func (transport *Transport) volumeInspectOperation(response *http.Response, exec
|
||||
return transport.applyAccessControlOnResource(resourceOperationParameters, responseObject, response, executor)
|
||||
}
|
||||
|
||||
func (transport *Transport) decorateVolumeResourceCreationOperation(request *http.Request, resourceIdentifierAttribute string, resourceType portainer.ResourceControlType) (*http.Response, error) {
|
||||
tokenData, err := security.RetrieveTokenData(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
volumeID := request.Header.Get("X-Portainer-VolumeName")
|
||||
|
||||
if volumeID != "" {
|
||||
cli := transport.dockerClient
|
||||
agentTargetHeader := request.Header.Get(portainer.PortainerAgentTargetHeader)
|
||||
if agentTargetHeader != "" {
|
||||
dockerClient, err := transport.dockerClientFactory.CreateClient(transport.endpoint, agentTargetHeader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer dockerClient.Close()
|
||||
cli = dockerClient
|
||||
}
|
||||
|
||||
_, err := cli.VolumeInspect(context.Background(), volumeID)
|
||||
if err == nil {
|
||||
return nil, errors.New("Creation error: volume already exists")
|
||||
}
|
||||
}
|
||||
|
||||
response, err := transport.executeDockerRequest(request)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
|
||||
if response.StatusCode == http.StatusCreated && volumeID != "" {
|
||||
err = transport.decorateGenericResourceCreationResponse(response, resourceIdentifierAttribute, resourceType, tokenData.ID)
|
||||
}
|
||||
return response, err
|
||||
}
|
||||
|
||||
// selectorVolumeLabels retrieve the labels object associated to the volume object.
|
||||
// Labels are available under the "Labels" property.
|
||||
// API schema references:
|
||||
|
||||
@@ -79,24 +79,6 @@ func FilterRegistries(registries []portainer.Registry, context *RestrictedReques
|
||||
return filteredRegistries
|
||||
}
|
||||
|
||||
// FilterTemplates filters templates based on the user role.
|
||||
// Non-administrator template do not have access to templates where the AdministratorOnly flag is set to true.
|
||||
func FilterTemplates(templates []portainer.Template, context *RestrictedRequestContext) []portainer.Template {
|
||||
filteredTemplates := templates
|
||||
|
||||
if !context.IsAdmin {
|
||||
filteredTemplates = make([]portainer.Template, 0)
|
||||
|
||||
for _, template := range templates {
|
||||
if !template.AdministratorOnly {
|
||||
filteredTemplates = append(filteredTemplates, template)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filteredTemplates
|
||||
}
|
||||
|
||||
// FilterEndpoints filters endpoints based on user role and team memberships.
|
||||
// Non administrator users only have access to authorized endpoints (can be inherited via endoint groups).
|
||||
func FilterEndpoints(endpoints []portainer.Endpoint, groups []portainer.EndpointGroup, context *RestrictedRequestContext) []portainer.Endpoint {
|
||||
|
||||
@@ -71,7 +71,6 @@ type Server struct {
|
||||
TagService portainer.TagService
|
||||
TeamService portainer.TeamService
|
||||
TeamMembershipService portainer.TeamMembershipService
|
||||
TemplateService portainer.TemplateService
|
||||
UserService portainer.UserService
|
||||
WebhookService portainer.WebhookService
|
||||
Handler *handler.Handler
|
||||
@@ -239,7 +238,6 @@ func (server *Server) Start() error {
|
||||
var supportHandler = support.NewHandler(requestBouncer)
|
||||
|
||||
var templatesHandler = templates.NewHandler(requestBouncer)
|
||||
templatesHandler.TemplateService = server.TemplateService
|
||||
templatesHandler.SettingsService = server.SettingsService
|
||||
|
||||
var uploadHandler = upload.NewHandler(requestBouncer)
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/crypto"
|
||||
|
||||
"gopkg.in/ldap.v2"
|
||||
"gopkg.in/ldap.v3"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -48,7 +48,6 @@ type (
|
||||
NoAuth *bool
|
||||
NoAnalytics *bool
|
||||
Templates *string
|
||||
TemplateFile *string
|
||||
TLS *bool
|
||||
TLSSkipVerify *bool
|
||||
TLSCacert *string
|
||||
@@ -73,6 +72,7 @@ type (
|
||||
Open() error
|
||||
Init() error
|
||||
Close() error
|
||||
IsNew() bool
|
||||
MigrateData() error
|
||||
}
|
||||
|
||||
@@ -881,15 +881,6 @@ type (
|
||||
DeleteTeamMembershipByTeamID(teamID TeamID) error
|
||||
}
|
||||
|
||||
// TemplateService represents a service for managing template data
|
||||
TemplateService interface {
|
||||
Templates() ([]Template, error)
|
||||
Template(ID TemplateID) (*Template, error)
|
||||
CreateTemplate(template *Template) error
|
||||
UpdateTemplate(ID TemplateID, template *Template) error
|
||||
DeleteTemplate(ID TemplateID) error
|
||||
}
|
||||
|
||||
// TunnelServerService represents a service for managing data associated to the tunnel server
|
||||
TunnelServerService interface {
|
||||
Info() (*TunnelServerInfo, error)
|
||||
@@ -926,7 +917,7 @@ type (
|
||||
|
||||
const (
|
||||
// APIVersion is the version number of the Portainer API
|
||||
APIVersion = "1.24.0-dev"
|
||||
APIVersion = "2.0.0-dev"
|
||||
// DBVersion is the version number of the Portainer database
|
||||
DBVersion = 23
|
||||
// AssetsServerURL represents the URL of the Portainer asset server
|
||||
@@ -958,6 +949,8 @@ const (
|
||||
DefaultEdgeAgentCheckinIntervalInSeconds = 5
|
||||
// LocalExtensionManifestFile represents the name of the local manifest file for extensions
|
||||
LocalExtensionManifestFile = "/extensions.json"
|
||||
// DefaultTemplatesURL represents the URL to the official templates supported by Portainer
|
||||
DefaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates-2.0.json"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -54,7 +54,7 @@ info:
|
||||
|
||||
**NOTE**: You can find more information on how to query the Docker API in the [Docker official documentation](https://docs.docker.com/engine/api/v1.30/) as well as in [this Portainer example](https://gist.github.com/deviantony/77026d402366b4b43fa5918d41bc42f8).
|
||||
|
||||
version: "1.24.0-dev"
|
||||
version: "2.0.0-dev"
|
||||
title: "Portainer API"
|
||||
contact:
|
||||
email: "info@portainer.io"
|
||||
@@ -3174,7 +3174,7 @@ definitions:
|
||||
description: "Is analytics enabled"
|
||||
Version:
|
||||
type: "string"
|
||||
example: "1.24.0-dev"
|
||||
example: "2.0.0-dev"
|
||||
description: "Portainer API version"
|
||||
PublicSettingsInspectResponse:
|
||||
type: "object"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"packageName": "portainer",
|
||||
"packageVersion": "1.24.0-dev",
|
||||
"packageVersion": "2.0.0-dev",
|
||||
"projectName": "portainer"
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ angular
|
||||
.constant('API_ENDPOINT_TEAM_MEMBERSHIPS', 'api/team_memberships')
|
||||
.constant('API_ENDPOINT_TEMPLATES', 'api/templates')
|
||||
.constant('API_ENDPOINT_WEBHOOKS', 'api/webhooks')
|
||||
.constant('DEFAULT_TEMPLATES_URL', 'https://raw.githubusercontent.com/portainer/templates/master/templates.json')
|
||||
.constant('PAGINATION_MAX_ITEMS', 10)
|
||||
.constant('APPLICATION_CACHE_VALIDITY', 3600)
|
||||
.constant('CONSOLE_COMMANDS_LABEL_PREFIX', 'io.portainer.commands.')
|
||||
|
||||
@@ -7,6 +7,11 @@ angular.module('portainer.docker').factory('Volume', [
|
||||
'VolumesInterceptor',
|
||||
function VolumeFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, VolumesInterceptor) {
|
||||
'use strict';
|
||||
|
||||
function addVolumeNameToHeader(config) {
|
||||
return config.data.Name;
|
||||
}
|
||||
|
||||
return $resource(
|
||||
API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/volumes/:id/:action',
|
||||
{
|
||||
@@ -15,7 +20,13 @@ angular.module('portainer.docker').factory('Volume', [
|
||||
{
|
||||
query: { method: 'GET', interceptor: VolumesInterceptor, timeout: 15000 },
|
||||
get: { method: 'GET', params: { id: '@id' } },
|
||||
create: { method: 'POST', params: { action: 'create' }, transformResponse: genericHandler, ignoreLoadingBar: true },
|
||||
create: {
|
||||
method: 'POST',
|
||||
params: { action: 'create' },
|
||||
transformResponse: genericHandler,
|
||||
ignoreLoadingBar: true,
|
||||
headers: { 'X-Portainer-VolumeName': addVolumeNameToHeader },
|
||||
},
|
||||
remove: {
|
||||
method: 'DELETE',
|
||||
transformResponse: genericHandler,
|
||||
|
||||
@@ -533,28 +533,6 @@ angular.module('portainer.app', []).config([
|
||||
},
|
||||
};
|
||||
|
||||
var template = {
|
||||
name: 'portainer.templates.template',
|
||||
url: '/:id',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: './views/templates/edit/template.html',
|
||||
controller: 'TemplateController',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
var templateCreation = {
|
||||
name: 'portainer.templates.new',
|
||||
url: '/new',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: './views/templates/create/createtemplate.html',
|
||||
controller: 'CreateTemplateController',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
$stateRegistryProvider.register(root);
|
||||
$stateRegistryProvider.register(portainer);
|
||||
$stateRegistryProvider.register(about);
|
||||
@@ -595,7 +573,5 @@ angular.module('portainer.app', []).config([
|
||||
$stateRegistryProvider.register(teams);
|
||||
$stateRegistryProvider.register(team);
|
||||
$stateRegistryProvider.register(templates);
|
||||
$stateRegistryProvider.register(template);
|
||||
$stateRegistryProvider.register(templateCreation);
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -3,8 +3,5 @@ angular.module('portainer.app').component('templateItem', {
|
||||
bindings: {
|
||||
model: '=',
|
||||
onSelect: '<',
|
||||
onDelete: '<',
|
||||
showUpdateAction: '<',
|
||||
showDeleteAction: '<',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -28,15 +28,6 @@
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<span class="text-small">
|
||||
<a ui-sref="portainer.templates.template({ id: $ctrl.model.Id })" class="btn btn-xs btn-primary" ng-click="$event.stopPropagation();" ng-if="$ctrl.showUpdateAction">
|
||||
<i class="fa fa-edit" aria-hidden="true"></i>
|
||||
Update
|
||||
</a>
|
||||
<btn class="btn btn-xs btn-danger" ng-click="$event.stopPropagation(); $ctrl.onDelete($ctrl.model)" ng-if="$ctrl.showDeleteAction">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i> Delete
|
||||
</btn>
|
||||
</span>
|
||||
</div>
|
||||
<!-- !blocklist-item-line1 -->
|
||||
<!-- blocklist-item-line2 -->
|
||||
|
||||
@@ -7,10 +7,6 @@ angular.module('portainer.app').component('templateList', {
|
||||
templates: '<',
|
||||
tableKey: '@',
|
||||
selectAction: '<',
|
||||
deleteAction: '<',
|
||||
showSwarmStacks: '<',
|
||||
showAddAction: '<',
|
||||
showUpdateAction: '<',
|
||||
showDeleteAction: '<',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -49,10 +49,7 @@
|
||||
<template-item
|
||||
ng-repeat="template in $ctrl.templates | filter: $ctrl.filterByType | filter:$ctrl.filterByCategory | filter:$ctrl.state.textFilter"
|
||||
model="template"
|
||||
show-update-action="$ctrl.showUpdateAction"
|
||||
show-delete-action="$ctrl.showDeleteAction"
|
||||
on-select="($ctrl.selectAction)"
|
||||
on-delete="($ctrl.deleteAction)"
|
||||
></template-item>
|
||||
<div ng-if="!$ctrl.templates" class="text-center text-muted">
|
||||
Loading...
|
||||
|
||||
@@ -9,7 +9,6 @@ export function SettingsViewModel(data) {
|
||||
this.AllowVolumeBrowserForRegularUsers = data.AllowVolumeBrowserForRegularUsers;
|
||||
this.SnapshotInterval = data.SnapshotInterval;
|
||||
this.TemplatesURL = data.TemplatesURL;
|
||||
this.ExternalTemplates = data.ExternalTemplates;
|
||||
this.EnableHostManagementFeatures = data.EnableHostManagementFeatures;
|
||||
this.EdgeAgentCheckinInterval = data.EdgeAgentCheckinInterval;
|
||||
}
|
||||
@@ -20,7 +19,6 @@ export function PublicSettingsViewModel(settings) {
|
||||
this.AllowVolumeBrowserForRegularUsers = settings.AllowVolumeBrowserForRegularUsers;
|
||||
this.AuthenticationMethod = settings.AuthenticationMethod;
|
||||
this.EnableHostManagementFeatures = settings.EnableHostManagementFeatures;
|
||||
this.ExternalTemplates = settings.ExternalTemplates;
|
||||
this.LogoURL = settings.LogoURL;
|
||||
this.OAuthLoginURI = settings.OAuthLoginURI;
|
||||
}
|
||||
|
||||
@@ -1,84 +1,44 @@
|
||||
import _ from 'lodash-es';
|
||||
import { PorImageRegistryModel } from 'Docker/models/porImageRegistry';
|
||||
|
||||
export function TemplateDefaultModel() {
|
||||
this.Type = 1;
|
||||
this.AdministratorOnly = false;
|
||||
this.Title = '';
|
||||
this.Description = '';
|
||||
this.Volumes = [];
|
||||
this.Ports = [];
|
||||
this.Env = [];
|
||||
this.Labels = [];
|
||||
this.RestartPolicy = 'always';
|
||||
this.RegistryModel = new PorImageRegistryModel();
|
||||
}
|
||||
|
||||
export function TemplateCreateRequest(model) {
|
||||
this.Type = model.Type;
|
||||
this.Name = model.Name;
|
||||
this.Hostname = model.Hostname;
|
||||
this.Title = model.Title;
|
||||
this.Description = model.Description;
|
||||
this.Note = model.Note;
|
||||
this.Categories = model.Categories;
|
||||
this.Platform = model.Platform;
|
||||
this.Logo = model.Logo;
|
||||
this.Image = model.RegistryModel.Image;
|
||||
this.Registry = model.RegistryModel.Registry.URL;
|
||||
this.Command = model.Command;
|
||||
this.Network = model.Network && model.Network.Name;
|
||||
this.Privileged = model.Privileged;
|
||||
this.Interactive = model.Interactive;
|
||||
this.RestartPolicy = model.RestartPolicy;
|
||||
this.Labels = model.Labels;
|
||||
this.Repository = model.Repository;
|
||||
this.Env = model.Env;
|
||||
this.AdministratorOnly = model.AdministratorOnly;
|
||||
|
||||
this.Ports = [];
|
||||
for (var i = 0; i < model.Ports.length; i++) {
|
||||
var binding = model.Ports[i];
|
||||
if (binding.containerPort && binding.protocol) {
|
||||
var port = binding.hostPort ? binding.hostPort + ':' + binding.containerPort + '/' + binding.protocol : binding.containerPort + '/' + binding.protocol;
|
||||
this.Ports.push(port);
|
||||
export class TemplateViewModel {
|
||||
constructor(data, version) {
|
||||
switch (version) {
|
||||
case '2':
|
||||
this.setTemplatesV2(data);
|
||||
break;
|
||||
default:
|
||||
throw new Error('Unsupported template version');
|
||||
}
|
||||
}
|
||||
|
||||
this.Volumes = model.Volumes;
|
||||
}
|
||||
|
||||
export function TemplateUpdateRequest(model) {
|
||||
TemplateCreateRequest.call(this, model);
|
||||
this.id = model.Id;
|
||||
}
|
||||
|
||||
export function TemplateViewModel(data) {
|
||||
this.Id = data.Id;
|
||||
this.Title = data.title;
|
||||
this.Type = data.type;
|
||||
this.Description = data.description;
|
||||
this.AdministratorOnly = data.AdministratorOnly;
|
||||
this.Name = data.name;
|
||||
this.Note = data.note;
|
||||
this.Categories = data.categories ? data.categories : [];
|
||||
this.Platform = data.platform ? data.platform : '';
|
||||
this.Logo = data.logo;
|
||||
this.Repository = data.repository;
|
||||
this.Hostname = data.hostname;
|
||||
this.RegistryModel = new PorImageRegistryModel();
|
||||
this.RegistryModel.Image = data.image;
|
||||
this.RegistryModel.Registry.URL = data.registry || '';
|
||||
this.Command = data.command ? data.command : '';
|
||||
this.Network = data.network ? data.network : '';
|
||||
this.Privileged = data.privileged ? data.privileged : false;
|
||||
this.Interactive = data.interactive ? data.interactive : false;
|
||||
this.RestartPolicy = data.restart_policy ? data.restart_policy : 'always';
|
||||
this.Labels = data.labels ? data.labels : [];
|
||||
this.Hosts = data.hosts ? data.hosts : [];
|
||||
this.Env = templateEnv(data);
|
||||
this.Volumes = templateVolumes(data);
|
||||
this.Ports = templatePorts(data);
|
||||
setTemplatesV2(data) {
|
||||
this.Id = data.Id;
|
||||
this.Title = data.title;
|
||||
this.Type = data.type;
|
||||
this.Description = data.description;
|
||||
this.AdministratorOnly = data.AdministratorOnly;
|
||||
this.Name = data.name;
|
||||
this.Note = data.note;
|
||||
this.Categories = data.categories ? data.categories : [];
|
||||
this.Platform = data.platform ? data.platform : '';
|
||||
this.Logo = data.logo;
|
||||
this.Repository = data.repository;
|
||||
this.Hostname = data.hostname;
|
||||
this.RegistryModel = new PorImageRegistryModel();
|
||||
this.RegistryModel.Image = data.image;
|
||||
this.RegistryModel.Registry.URL = data.registry || '';
|
||||
this.Command = data.command ? data.command : '';
|
||||
this.Network = data.network ? data.network : '';
|
||||
this.Privileged = data.privileged ? data.privileged : false;
|
||||
this.Interactive = data.interactive ? data.interactive : false;
|
||||
this.RestartPolicy = data.restart_policy ? data.restart_policy : 'always';
|
||||
this.Labels = data.labels ? data.labels : [];
|
||||
this.Hosts = data.hosts ? data.hosts : [];
|
||||
this.Env = templateEnv(data);
|
||||
this.Volumes = templateVolumes(data);
|
||||
this.Ports = templatePorts(data);
|
||||
}
|
||||
}
|
||||
|
||||
function templatePorts(data) {
|
||||
|
||||
@@ -6,11 +6,7 @@ angular.module('portainer.app').factory('Templates', [
|
||||
API_ENDPOINT_TEMPLATES + '/:id',
|
||||
{},
|
||||
{
|
||||
create: { method: 'POST' },
|
||||
query: { method: 'GET', isArray: true },
|
||||
get: { method: 'GET', params: { id: '@id' } },
|
||||
update: { method: 'PUT', params: { id: '@id' } },
|
||||
remove: { method: 'DELETE', params: { id: '@id' } },
|
||||
query: { method: 'GET' },
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { TemplateViewModel, TemplateCreateRequest, TemplateUpdateRequest } from '../../models/template';
|
||||
import _ from 'lodash-es';
|
||||
import { TemplateViewModel } from '../../models/template';
|
||||
|
||||
angular.module('portainer.app').factory('TemplateService', [
|
||||
'$q',
|
||||
@@ -13,7 +14,7 @@ angular.module('portainer.app').factory('TemplateService', [
|
||||
var service = {};
|
||||
|
||||
service.templates = function () {
|
||||
var deferred = $q.defer();
|
||||
const deferred = $q.defer();
|
||||
|
||||
$q.all({
|
||||
templates: Templates.query().$promise,
|
||||
@@ -21,12 +22,17 @@ angular.module('portainer.app').factory('TemplateService', [
|
||||
dockerhub: DockerHubService.dockerhub(),
|
||||
})
|
||||
.then(function success(data) {
|
||||
const templates = data.templates.map(function (item) {
|
||||
const res = new TemplateViewModel(item);
|
||||
const registry = RegistryService.retrievePorRegistryModelFromRepositoryWithRegistries(res.RegistryModel.Registry.URL, data.registries, data.dockerhub);
|
||||
registry.Image = res.RegistryModel.Image;
|
||||
res.RegistryModel = registry;
|
||||
return res;
|
||||
const version = data.templates.version;
|
||||
const templates = _.map(data.templates.templates, (item) => {
|
||||
try {
|
||||
const template = new TemplateViewModel(item, version);
|
||||
const registry = RegistryService.retrievePorRegistryModelFromRepositoryWithRegistries(template.RegistryModel.Registry.URL, data.registries, data.dockerhub);
|
||||
registry.Image = template.RegistryModel.Image;
|
||||
template.RegistryModel = registry;
|
||||
return template;
|
||||
} catch (err) {
|
||||
deferred.reject({ msg: 'Unable to retrieve templates', err: err });
|
||||
}
|
||||
});
|
||||
deferred.resolve(templates);
|
||||
})
|
||||
@@ -37,40 +43,6 @@ angular.module('portainer.app').factory('TemplateService', [
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
service.template = function (id) {
|
||||
var deferred = $q.defer();
|
||||
let template;
|
||||
Templates.get({ id: id })
|
||||
.$promise.then(function success(data) {
|
||||
template = new TemplateViewModel(data);
|
||||
return RegistryService.retrievePorRegistryModelFromRepository(template.RegistryModel.Registry.URL);
|
||||
})
|
||||
.then((registry) => {
|
||||
registry.Image = template.RegistryModel.Image;
|
||||
template.RegistryModel = registry;
|
||||
deferred.resolve(template);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject({ msg: 'Unable to retrieve template details', err: err });
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
service.delete = function (id) {
|
||||
return Templates.remove({ id: id }).$promise;
|
||||
};
|
||||
|
||||
service.create = function (model) {
|
||||
var payload = new TemplateCreateRequest(model);
|
||||
return Templates.create(payload).$promise;
|
||||
};
|
||||
|
||||
service.update = function (model) {
|
||||
var payload = new TemplateUpdateRequest(model);
|
||||
return Templates.update(payload).$promise;
|
||||
};
|
||||
|
||||
service.createTemplateConfiguration = function (template, containerName, network) {
|
||||
var imageConfiguration = ImageHelper.createImageConfigForContainer(template.RegistryModel);
|
||||
var containerConfiguration = createContainerConfiguration(template, containerName, network);
|
||||
|
||||
@@ -46,16 +46,7 @@
|
||||
<div class="col-sm-12 form-section-title">
|
||||
App Templates
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label for="toggle_templates" class="control-label text-left">
|
||||
Use external templates
|
||||
<portainer-tooltip position="bottom" message="When using external templates, in-app template management will be disabled."></portainer-tooltip>
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" name="toggle_templates" ng-model="formValues.externalTemplates" /><i></i> </label>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="formValues.externalTemplates">
|
||||
<div>
|
||||
<div class="form-group">
|
||||
<span class="col-sm-12 text-muted small">
|
||||
You can specify the URL to your own template definitions file here. See
|
||||
@@ -67,7 +58,7 @@
|
||||
URL
|
||||
</label>
|
||||
<div class="col-sm-11">
|
||||
<input type="text" class="form-control" ng-model="settings.TemplatesURL" id="templates_url" placeholder="https://myserver.mydomain/templates.json" />
|
||||
<input type="text" class="form-control" ng-model="settings.TemplatesURL" id="templates_url" placeholder="https://myserver.mydomain/templates.json" required/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -149,7 +140,7 @@
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
ng-click="saveApplicationSettings()"
|
||||
ng-disabled="state.actionInProgress"
|
||||
ng-disabled="state.actionInProgress || !settings.TemplatesURL"
|
||||
button-spinner="state.actionInProgress"
|
||||
>
|
||||
<span ng-hide="state.actionInProgress">Save settings</span>
|
||||
|
||||
@@ -25,7 +25,6 @@ angular.module('portainer.app').controller('SettingsController', [
|
||||
|
||||
$scope.formValues = {
|
||||
customLogo: false,
|
||||
externalTemplates: false,
|
||||
restrictBindMounts: false,
|
||||
restrictPrivilegedMode: false,
|
||||
labelName: '',
|
||||
@@ -59,10 +58,6 @@ angular.module('portainer.app').controller('SettingsController', [
|
||||
settings.LogoURL = '';
|
||||
}
|
||||
|
||||
if (!$scope.formValues.externalTemplates) {
|
||||
settings.TemplatesURL = '';
|
||||
}
|
||||
|
||||
settings.AllowBindMountsForRegularUsers = !$scope.formValues.restrictBindMounts;
|
||||
settings.AllowPrivilegedModeForRegularUsers = !$scope.formValues.restrictPrivilegedMode;
|
||||
settings.AllowVolumeBrowserForRegularUsers = $scope.formValues.enableVolumeBrowser;
|
||||
@@ -95,12 +90,10 @@ angular.module('portainer.app').controller('SettingsController', [
|
||||
.then(function success(data) {
|
||||
var settings = data;
|
||||
$scope.settings = settings;
|
||||
|
||||
if (settings.LogoURL !== '') {
|
||||
$scope.formValues.customLogo = true;
|
||||
}
|
||||
if (settings.TemplatesURL !== '') {
|
||||
$scope.formValues.externalTemplates = true;
|
||||
}
|
||||
$scope.formValues.restrictBindMounts = !settings.AllowBindMountsForRegularUsers;
|
||||
$scope.formValues.restrictPrivilegedMode = !settings.AllowPrivilegedModeForRegularUsers;
|
||||
$scope.formValues.enableVolumeBrowser = settings.AllowVolumeBrowserForRegularUsers;
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import { TemplateDefaultModel } from '../../../models/template';
|
||||
|
||||
angular.module('portainer.app').controller('CreateTemplateController', [
|
||||
'$q',
|
||||
'$scope',
|
||||
'$state',
|
||||
'TemplateService',
|
||||
'TemplateHelper',
|
||||
'NetworkService',
|
||||
'Notifications',
|
||||
function ($q, $scope, $state, TemplateService, TemplateHelper, NetworkService, Notifications) {
|
||||
$scope.state = {
|
||||
actionInProgress: false,
|
||||
};
|
||||
|
||||
$scope.create = function () {
|
||||
var model = $scope.model;
|
||||
|
||||
$scope.state.actionInProgress = true;
|
||||
TemplateService.create(model)
|
||||
.then(function success() {
|
||||
Notifications.success('Template successfully created', model.Title);
|
||||
$state.go('portainer.templates');
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to create template');
|
||||
})
|
||||
.finally(function final() {
|
||||
$scope.state.actionInProgress = false;
|
||||
});
|
||||
};
|
||||
|
||||
function initView() {
|
||||
$scope.model = new TemplateDefaultModel();
|
||||
var provider = $scope.applicationState.endpoint.mode.provider;
|
||||
var apiVersion = $scope.applicationState.endpoint.apiVersion;
|
||||
|
||||
$q.all({
|
||||
templates: TemplateService.templates(),
|
||||
networks: NetworkService.networks(provider === 'DOCKER_STANDALONE' || provider === 'DOCKER_SWARM_MODE', false, provider === 'DOCKER_SWARM_MODE' && apiVersion >= 1.25),
|
||||
})
|
||||
.then(function success(data) {
|
||||
$scope.categories = TemplateHelper.getUniqueCategories(data.templates);
|
||||
$scope.networks = data.networks;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve template details');
|
||||
});
|
||||
}
|
||||
|
||||
initView();
|
||||
},
|
||||
]);
|
||||
@@ -1,22 +0,0 @@
|
||||
<rd-header>
|
||||
<rd-header-title title-text="Create template"></rd-header-title>
|
||||
<rd-header-content> <a ui-sref="portainer.templates">Templates</a> > Add template </rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<template-form
|
||||
model="model"
|
||||
categories="categories"
|
||||
networks="networks"
|
||||
form-action="create"
|
||||
show-type-selector="true"
|
||||
form-action-label="Create the template"
|
||||
action-in-progress="state.actionInProgress"
|
||||
></template-form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,26 +0,0 @@
|
||||
<rd-header>
|
||||
<rd-header-title title-text="Template details">
|
||||
<a data-toggle="tooltip" title-text="Refresh" ui-sref="portainer.templates.template({id: template.Id})" ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-sync" aria-hidden="true"></i>
|
||||
</a>
|
||||
</rd-header-title>
|
||||
<rd-header-content> <a ui-sref="portainer.templates">Templates</a> > {{ ::template.Title }} </rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<template-form
|
||||
model="template"
|
||||
categories="categories"
|
||||
networks="networks"
|
||||
form-action="update"
|
||||
show-type-selector="false"
|
||||
form-action-label="Update the template"
|
||||
action-in-progress="state.actionInProgress"
|
||||
></template-form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,66 +0,0 @@
|
||||
import _ from 'lodash-es';
|
||||
|
||||
angular.module('portainer.app').controller('TemplateController', [
|
||||
'$q',
|
||||
'$scope',
|
||||
'$state',
|
||||
'$transition$',
|
||||
'TemplateService',
|
||||
'TemplateHelper',
|
||||
'NetworkService',
|
||||
'Notifications',
|
||||
function ($q, $scope, $state, $transition$, TemplateService, TemplateHelper, NetworkService, Notifications) {
|
||||
$scope.state = {
|
||||
actionInProgress: false,
|
||||
};
|
||||
|
||||
$scope.update = function () {
|
||||
var model = $scope.template;
|
||||
|
||||
$scope.state.actionInProgress = true;
|
||||
TemplateService.update(model)
|
||||
.then(function success() {
|
||||
Notifications.success('Template successfully updated', model.Title);
|
||||
$state.go('portainer.templates');
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to update template');
|
||||
})
|
||||
.finally(function final() {
|
||||
$scope.state.actionInProgress = false;
|
||||
});
|
||||
};
|
||||
|
||||
function initView() {
|
||||
var provider = $scope.applicationState.endpoint.mode.provider;
|
||||
var apiVersion = $scope.applicationState.endpoint.apiVersion;
|
||||
|
||||
var templateId = $transition$.params().id;
|
||||
$q.all({
|
||||
templates: TemplateService.templates(),
|
||||
template: TemplateService.template(templateId),
|
||||
networks: NetworkService.networks(provider === 'DOCKER_STANDALONE' || provider === 'DOCKER_SWARM_MODE', false, provider === 'DOCKER_SWARM_MODE' && apiVersion >= 1.25),
|
||||
})
|
||||
.then(function success(data) {
|
||||
var template = data.template;
|
||||
if (template.Network) {
|
||||
template.Network = _.find(data.networks, function (o) {
|
||||
return o.Name === template.Network;
|
||||
});
|
||||
} else {
|
||||
template.Network = _.find(data.networks, function (o) {
|
||||
return o.Name === 'bridge';
|
||||
});
|
||||
}
|
||||
$scope.categories = TemplateHelper.getUniqueCategories(data.templates);
|
||||
$scope.template = data.template;
|
||||
$scope.networks = data.networks;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve template details');
|
||||
});
|
||||
}
|
||||
|
||||
initView();
|
||||
},
|
||||
]);
|
||||
@@ -368,10 +368,6 @@
|
||||
templates="templates"
|
||||
table-key="templates"
|
||||
select-action="selectTemplate"
|
||||
delete-action="deleteTemplate"
|
||||
show-add-action="state.templateManagement && isAdmin"
|
||||
show-update-action="state.templateManagement && isAdmin"
|
||||
show-delete-action="state.templateManagement && isAdmin"
|
||||
show-swarm-stacks="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER' && applicationState.endpoint.apiVersion >= 1.25"
|
||||
></template-list>
|
||||
</div>
|
||||
|
||||
@@ -20,7 +20,6 @@ angular.module('portainer.app').controller('TemplatesController', [
|
||||
'SettingsService',
|
||||
'StackService',
|
||||
'EndpointProvider',
|
||||
'ModalService',
|
||||
function (
|
||||
$scope,
|
||||
$q,
|
||||
@@ -39,15 +38,13 @@ angular.module('portainer.app').controller('TemplatesController', [
|
||||
FormValidator,
|
||||
SettingsService,
|
||||
StackService,
|
||||
EndpointProvider,
|
||||
ModalService
|
||||
EndpointProvider
|
||||
) {
|
||||
$scope.state = {
|
||||
selectedTemplate: null,
|
||||
showAdvancedOptions: false,
|
||||
formValidationError: '',
|
||||
actionInProgress: false,
|
||||
templateManagement: true,
|
||||
};
|
||||
|
||||
$scope.formValues = {
|
||||
@@ -255,27 +252,6 @@ angular.module('portainer.app').controller('TemplatesController', [
|
||||
return TemplateService.createTemplateConfiguration(template, name, network);
|
||||
}
|
||||
|
||||
$scope.deleteTemplate = function (template) {
|
||||
ModalService.confirmDeletion('Do you want to delete this template?', function onConfirm(confirmed) {
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
deleteTemplate(template);
|
||||
});
|
||||
};
|
||||
|
||||
function deleteTemplate(template) {
|
||||
TemplateService.delete(template.Id)
|
||||
.then(function success() {
|
||||
Notifications.success('Template successfully deleted');
|
||||
var idx = $scope.templates.indexOf(template);
|
||||
$scope.templates.splice(idx, 1);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to remove template');
|
||||
});
|
||||
}
|
||||
|
||||
function initView() {
|
||||
$scope.isAdmin = Authentication.isAdmin();
|
||||
|
||||
@@ -300,7 +276,6 @@ angular.module('portainer.app').controller('TemplatesController', [
|
||||
$scope.availableNetworks = networks;
|
||||
var settings = data.settings;
|
||||
$scope.allowBindMounts = settings.AllowBindMountsForRegularUsers;
|
||||
$scope.state.templateManagement = !settings.ExternalTemplates;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
$scope.templates = [];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Name: portainer
|
||||
Version: 1.24.0-dev
|
||||
Version: 2.0.0-dev
|
||||
Release: 0
|
||||
License: Zlib
|
||||
Summary: A lightweight docker management UI
|
||||
|
||||
@@ -105,11 +105,6 @@ gruntfile_cfg.eslint = {
|
||||
gruntfile_cfg.copy = {
|
||||
assets: {
|
||||
files: [
|
||||
{
|
||||
dest: '<%= root %>/',
|
||||
src: 'templates.json',
|
||||
cwd: '',
|
||||
},
|
||||
{
|
||||
dest: '<%= root %>/',
|
||||
src: 'extensions.json',
|
||||
@@ -154,7 +149,7 @@ function shell_run_container() {
|
||||
'docker rm -f portainer',
|
||||
'docker run -d -p 8000:8000 -p 9000:9000 -v $(pwd)/dist:/app -v ' +
|
||||
portainer_data +
|
||||
':/data -v /var/run/docker.sock:/var/run/docker.sock:z --name portainer portainer/base /app/portainer --no-analytics --template-file /app/templates.json',
|
||||
':/data -v /var/run/docker.sock:/var/run/docker.sock:z --name portainer portainer/base /app/portainer --no-analytics',
|
||||
].join(';');
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"author": "Portainer.io",
|
||||
"name": "portainer",
|
||||
"homepage": "http://portainer.io",
|
||||
"version": "1.24.0-dev",
|
||||
"version": "2.0.0-dev",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git@github.com:portainer/portainer.git"
|
||||
|
||||
891
templates.json
891
templates.json
@@ -1,891 +0,0 @@
|
||||
[
|
||||
{
|
||||
"type": 1,
|
||||
"title": "Registry",
|
||||
"description": "Docker image registry",
|
||||
"categories": ["docker"],
|
||||
"platform": "linux",
|
||||
"logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/registry.png",
|
||||
"image": "registry:latest",
|
||||
"ports": [
|
||||
"5000/tcp"
|
||||
],
|
||||
"volumes": [{ "container": "/var/lib/registry"}]
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"title": "Nginx",
|
||||
"description": "High performance web server",
|
||||
"categories": ["webserver"],
|
||||
"platform": "linux",
|
||||
"logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/nginx.png",
|
||||
"image": "nginx:latest",
|
||||
"ports": [
|
||||
"80/tcp",
|
||||
"443/tcp"
|
||||
],
|
||||
"volumes": [{"container": "/etc/nginx"}, {"container": "/usr/share/nginx/html"}]
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"title": "Httpd",
|
||||
"description": "Open-source HTTP server",
|
||||
"categories": ["webserver"],
|
||||
"platform": "linux",
|
||||
"logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/httpd.png",
|
||||
"image": "httpd:latest",
|
||||
"ports": [
|
||||
"80/tcp"
|
||||
],
|
||||
"volumes": [{"container": "/usr/local/apache2/htdocs/"}]
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"title": "Caddy",
|
||||
"description": "HTTP/2 web server with automatic HTTPS",
|
||||
"categories": ["webserver"],
|
||||
"platform": "linux",
|
||||
"logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/caddy.png",
|
||||
"image": "abiosoft/caddy:latest",
|
||||
"ports": [
|
||||
"80/tcp", "443/tcp", "2015/tcp"
|
||||
],
|
||||
"volumes": [{"container": "/root/.caddy"}]
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"title": "MySQL",
|
||||
"description": "The most popular open-source database",
|
||||
"categories": ["database"],
|
||||
"platform": "linux",
|
||||
"logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/mysql.png",
|
||||
"image": "mysql:latest",
|
||||
"env": [
|
||||
{
|
||||
"name": "MYSQL_ROOT_PASSWORD",
|
||||
"label": "Root password"
|
||||
}
|
||||
],
|
||||
"ports": [
|
||||
"3306/tcp"
|
||||
],
|
||||
"volumes": [{"container": "/var/lib/mysql"}]
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"title": "MariaDB",
|
||||
"description": "Performance beyond MySQL",
|
||||
"categories": ["database"],
|
||||
"platform": "linux",
|
||||
"logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/mariadb.png",
|
||||
"image": "mariadb:latest",
|
||||
"env": [
|
||||
{
|
||||
"name": "MYSQL_ROOT_PASSWORD",
|
||||
"label": "Root password"
|
||||
}
|
||||
],
|
||||
"ports": [
|
||||
"3306/tcp"
|
||||
],
|
||||
"volumes": [{"container": "/var/lib/mysql"}]
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"title": "PostgreSQL",
|
||||
"description": "The most advanced open-source database",
|
||||
"categories": ["database"],
|
||||
"platform": "linux",
|
||||
"logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/postgres.png",
|
||||
"image": "postgres:latest",
|
||||
"env": [
|
||||
{
|
||||
"name": "POSTGRES_USER",
|
||||
"label": "Superuser"
|
||||
},
|
||||
{
|
||||
"name": "POSTGRES_PASSWORD",
|
||||
"label": "Superuser password"
|
||||
}
|
||||
],
|
||||
"ports": [
|
||||
"5432/tcp"
|
||||
],
|
||||
"volumes": [{"container": "/var/lib/postgresql/data"}]
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"title": "Mongo",
|
||||
"description": "Open-source document-oriented database",
|
||||
"categories": ["database"],
|
||||
"platform": "linux",
|
||||
"logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/mongo.png",
|
||||
"image": "mongo:latest",
|
||||
"ports": [
|
||||
"27017/tcp"
|
||||
],
|
||||
"volumes": [{"container": "/data/db"}]
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"title": "CockroachDB",
|
||||
"description": "An open-source, survivable, strongly consistent, scale-out SQL database",
|
||||
"categories": ["database"],
|
||||
"platform": "linux",
|
||||
"logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/cockroachdb.png",
|
||||
"image": "cockroachdb/cockroach:latest",
|
||||
"ports": [
|
||||
"26257/tcp",
|
||||
"8080/tcp"
|
||||
],
|
||||
"volumes": [{"container": "/cockroach/cockroach-data"}],
|
||||
"command": "start --insecure"
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"title": "CrateDB",
|
||||
"description": "An open-source distributed SQL database",
|
||||
"categories": ["database"],
|
||||
"platform": "linux",
|
||||
"logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/cratedb.png",
|
||||
"image": "crate:latest",
|
||||
"ports": [
|
||||
"4200/tcp",
|
||||
"4300/tcp"
|
||||
],
|
||||
"volumes": [{"container": "/data"}]
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"title": "Elasticsearch",
|
||||
"description": "Open-source search and analytics engine",
|
||||
"categories": ["database"],
|
||||
"platform": "linux",
|
||||
"logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/elasticsearch.png",
|
||||
"image": "elasticsearch:latest",
|
||||
"ports": [
|
||||
"9200/tcp",
|
||||
"9300/tcp"
|
||||
],
|
||||
"volumes": [{"container": "/usr/share/elasticsearch/data"}]
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"title": "Gitlab CE",
|
||||
"description": "Open-source end-to-end software development platform",
|
||||
"note": "Default username is <b>root</b>. Check the <a href=\"https://docs.gitlab.com/omnibus/docker/README.html#after-starting-a-container\" target=\"_blank\">Gitlab documentation</a> to get started.",
|
||||
"categories": ["development", "project-management"],
|
||||
"platform": "linux",
|
||||
"logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/gitlab_ce.png",
|
||||
"image": "gitlab/gitlab-ce:latest",
|
||||
"ports": [
|
||||
"80/tcp",
|
||||
"443/tcp",
|
||||
"22/tcp"
|
||||
],
|
||||
"volumes": [
|
||||
{ "container": "/etc/gitlab" },
|
||||
{ "container": "/var/log/gitlab" },
|
||||
{ "container": "/var/opt/gitlab" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"title": "Minio",
|
||||
"description": "A distributed object storage server built for cloud applications and devops",
|
||||
"categories": ["storage"],
|
||||
"platform": "linux",
|
||||
"logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/minio.png",
|
||||
"image": "minio/minio:latest",
|
||||
"ports": [
|
||||
"9000/tcp"
|
||||
],
|
||||
"env": [
|
||||
{
|
||||
"name": "MINIO_ACCESS_KEY",
|
||||
"label": "Minio access key"
|
||||
},
|
||||
{
|
||||
"name": "MINIO_SECRET_KEY",
|
||||
"label": "Minio secret key"
|
||||
}
|
||||
],
|
||||
"volumes": [{"container": "/data"}, {"container": "/root/.minio"}],
|
||||
"command": "server /data"
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"title": "Scality S3",
|
||||
"description": "Standalone AWS S3 protocol server",
|
||||
"categories": ["storage"],
|
||||
"platform": "linux",
|
||||
"logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/scality-s3.png",
|
||||
"image": "scality/s3server",
|
||||
"ports": [
|
||||
"8000/tcp"
|
||||
],
|
||||
"env": [
|
||||
{
|
||||
"name": "SCALITY_ACCESS_KEY",
|
||||
"label": "Scality S3 access key"
|
||||
},
|
||||
{
|
||||
"name": "SCALITY_SECRET_KEY",
|
||||
"label": "Scality S3 secret key"
|
||||
}
|
||||
],
|
||||
"volumes": [{"container": "/usr/src/app/localData"}, {"container": "/usr/src/app/localMetadata"}]
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"title": "SQL Server",
|
||||
"description": "Microsoft SQL Server on Linux",
|
||||
"categories": ["database"],
|
||||
"platform": "linux",
|
||||
"note": "Password needs to include at least 8 characters including uppercase, lowercase letters, base-10 digits and/or non-alphanumeric symbols.",
|
||||
"logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/microsoft.png",
|
||||
"image": "microsoft/mssql-server-linux:2017-GA",
|
||||
"ports": [
|
||||
"1433/tcp"
|
||||
],
|
||||
"env": [
|
||||
{
|
||||
"name": "ACCEPT_EULA",
|
||||
"set": "Y"
|
||||
},
|
||||
{
|
||||
"name": "SA_PASSWORD",
|
||||
"label": "SA password"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"title": "SQL Server",
|
||||
"description": "Microsoft SQL Server Developer for Windows containers",
|
||||
"categories": ["database"],
|
||||
"platform": "windows",
|
||||
"note": "Password needs to include at least 8 characters including uppercase, lowercase letters, base-10 digits and/or non-alphanumeric symbols.",
|
||||
"logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/microsoft.png",
|
||||
"image": "microsoft/mssql-server-windows-developer:latest",
|
||||
"ports": [
|
||||
"1433/tcp"
|
||||
],
|
||||
"env": [
|
||||
{
|
||||
"name": "ACCEPT_EULA",
|
||||
"set": "Y"
|
||||
},
|
||||
{
|
||||
"name": "sa_password",
|
||||
"label": "SA password"
|
||||
}
|
||||
],
|
||||
"volumes": [{"container": "C:/temp/"}]
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"title": "SQL Server Express",
|
||||
"description": "Microsoft SQL Server Express for Windows containers",
|
||||
"categories": ["database"],
|
||||
"platform": "windows",
|
||||
"note": "Password needs to include at least 8 characters including uppercase, lowercase letters, base-10 digits and/or non-alphanumeric symbols.",
|
||||
"logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/microsoft.png",
|
||||
"image": "microsoft/mssql-server-windows-express:latest",
|
||||
"ports": [
|
||||
"1433/tcp"
|
||||
],
|
||||
"env": [
|
||||
{
|
||||
"name": "ACCEPT_EULA",
|
||||
"set": "Y"
|
||||
},
|
||||
{
|
||||
"name": "sa_password",
|
||||
"label": "SA password"
|
||||
}
|
||||
],
|
||||
"volumes": [{"container": "C:/temp/"}]
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"title": "IronFunctions API",
|
||||
"description": "Open-source serverless computing platform",
|
||||
"categories": ["serverless"],
|
||||
"platform": "linux",
|
||||
"logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/ironfunctions.png",
|
||||
"image": "iron/functions:latest",
|
||||
"ports": [
|
||||
"8080/tcp"
|
||||
],
|
||||
"volumes": [{"container": "/app/data"}],
|
||||
"privileged": true
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"title": "IronFunctions UI",
|
||||
"description": "Open-source user interface for IronFunctions",
|
||||
"categories": ["serverless"],
|
||||
"platform": "linux",
|
||||
"logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/ironfunctions.png",
|
||||
"image": "iron/functions-ui:latest",
|
||||
"ports": [
|
||||
"4000/tcp"
|
||||
],
|
||||
"volumes": [{"container": "/app/data"}],
|
||||
"env": [
|
||||
{
|
||||
"name": "API_URL",
|
||||
"label": "API URL"
|
||||
}
|
||||
],
|
||||
"privileged": true
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"title": "Solr",
|
||||
"description": "Open-source enterprise search platform",
|
||||
"categories": ["search-engine"],
|
||||
"platform": "linux",
|
||||
"logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/solr.png",
|
||||
"image": "solr:latest",
|
||||
"ports": [
|
||||
"8983/tcp"
|
||||
],
|
||||
"volumes": [{"container": "/opt/solr/mydata"}]
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"title": "Redis",
|
||||
"description": "Open-source in-memory data structure store",
|
||||
"categories": ["database"],
|
||||
"platform": "linux",
|
||||
"logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/redis.png",
|
||||
"image": "redis:latest",
|
||||
"ports": [
|
||||
"6379/tcp"
|
||||
],
|
||||
"volumes": [{"container": "/data"}]
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"title": "RabbitMQ",
|
||||
"description": "Highly reliable enterprise messaging system",
|
||||
"categories": ["messaging"],
|
||||
"platform": "linux",
|
||||
"logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/rabbitmq.png",
|
||||
"image": "rabbitmq:latest",
|
||||
"ports": [
|
||||
"5671/tcp",
|
||||
"5672/tcp"
|
||||
],
|
||||
"volumes": [{"container": "/var/lib/rabbitmq"}]
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"title": "Ghost",
|
||||
"description": "Free and open-source blogging platform",
|
||||
"categories": ["blog"],
|
||||
"note": "Access the blog management interface under <code>/ghost/</code>.",
|
||||
"platform": "linux",
|
||||
"logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/ghost.png",
|
||||
"image": "ghost:latest",
|
||||
"ports": [
|
||||
"2368/tcp"
|
||||
],
|
||||
"volumes": [{"container": "/var/lib/ghost/content"}]
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"title": "Plesk",
|
||||
"description": "WebOps platform and hosting control panel",
|
||||
"categories": ["CMS"],
|
||||
"platform": "linux",
|
||||
"note": "Default credentials: admin / changeme",
|
||||
"logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/plesk.png",
|
||||
"image": "plesk/plesk:preview",
|
||||
"ports": [
|
||||
"21/tcp", "80/tcp", "443/tcp", "8880/tcp", "8443/tcp", "8447/tcp"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"title": "Joomla",
|
||||
"description": "Another free and open-source CMS",
|
||||
"categories": ["CMS"],
|
||||
"platform": "linux",
|
||||
"logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/joomla.png",
|
||||
"image": "joomla:latest",
|
||||
"env": [
|
||||
{
|
||||
"name": "JOOMLA_DB_HOST",
|
||||
"label": "MySQL database host",
|
||||
"type": "container"
|
||||
},
|
||||
{
|
||||
"name": "JOOMLA_DB_PASSWORD",
|
||||
"label": "Database password"
|
||||
}
|
||||
],
|
||||
"ports": [
|
||||
"80/tcp"
|
||||
],
|
||||
"volumes": [{"container": "/var/www/html"}]
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"title": "Drupal",
|
||||
"description": "Open-source content management framework",
|
||||
"categories": ["CMS"],
|
||||
"platform": "linux",
|
||||
"logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/drupal.png",
|
||||
"image": "drupal:latest",
|
||||
"ports": [
|
||||
"80/tcp"
|
||||
],
|
||||
"volumes": [{"container": "/var/www/html"}]
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"title": "Plone",
|
||||
"description": "A free and open-source CMS built on top of Zope",
|
||||
"categories": ["CMS"],
|
||||
"platform": "linux",
|
||||
"logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/plone.png",
|
||||
"image": "plone:latest",
|
||||
"ports": [
|
||||
"8080/tcp"
|
||||
],
|
||||
"volumes": [{"container": "/data"}]
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"title": "Magento 2",
|
||||
"description": "Open-source e-commerce platform",
|
||||
"categories": ["CMS"],
|
||||
"platform": "linux",
|
||||
"logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/magento.png",
|
||||
"image": "alankent/gsd:latest",
|
||||
"ports": [
|
||||
"80/tcp",
|
||||
"3000/tcp",
|
||||
"3001/tcp"
|
||||
],
|
||||
"volumes": [{"container": "/var/www/html/"}]
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"title": "Sematext Docker Agent",
|
||||
"description": "Collect logs, metrics and docker events",
|
||||
"categories": ["Log Management", "Monitoring"],
|
||||
"platform": "linux",
|
||||
"logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/sematext_agent.png",
|
||||
"image": "sematext/sematext-agent-docker:latest",
|
||||
"name": "sematext-agent",
|
||||
"privileged": true,
|
||||
"env": [
|
||||
{
|
||||
"name": "LOGSENE_TOKEN",
|
||||
"label": "Logs token"
|
||||
},
|
||||
{
|
||||
"name": "SPM_TOKEN",
|
||||
"label": "SPM monitoring token"
|
||||
}
|
||||
],
|
||||
"volumes": [
|
||||
{
|
||||
"container": "/var/run/docker.sock",
|
||||
"bind": "/var/run/docker.sock"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"title": "Datadog agent",
|
||||
"description": "Collect events and metrics",
|
||||
"categories": ["Monitoring"],
|
||||
"platform": "linux",
|
||||
"logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/datadog_agent.png",
|
||||
"image": "datadog/agent:latest",
|
||||
"env": [
|
||||
{
|
||||
"name": "DD_API_KEY",
|
||||
"label": "Datadog API key"
|
||||
}
|
||||
],
|
||||
"volumes": [
|
||||
{
|
||||
"container": "/var/run/docker.sock",
|
||||
"bind": "/var/run/docker.sock",
|
||||
"readonly": true
|
||||
},
|
||||
{
|
||||
"container": "/host/sys/fs/cgroup",
|
||||
"bind": "/sys/fs/cgroup",
|
||||
"readonly": true
|
||||
},
|
||||
{
|
||||
"container": "/host/proc",
|
||||
"bind": "/proc",
|
||||
"readonly": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"title": "Mautic",
|
||||
"description": "Open-source marketing automation platform",
|
||||
"categories": ["marketing"],
|
||||
"platform": "linux",
|
||||
"logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/mautic.png",
|
||||
"image": "mautic/mautic:latest",
|
||||
"env": [
|
||||
{
|
||||
"name": "MAUTIC_DB_HOST",
|
||||
"label": "MySQL database host",
|
||||
"type": "container"
|
||||
},
|
||||
{
|
||||
"name": "MAUTIC_DB_PASSWORD",
|
||||
"label": "Database password"
|
||||
}
|
||||
],
|
||||
"ports": [
|
||||
"80/tcp"
|
||||
],
|
||||
"volumes": [{"container": "/var/www/html"}]
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"title": "Wowza",
|
||||
"description": "Streaming media server",
|
||||
"categories": ["streaming"],
|
||||
"platform": "linux",
|
||||
"logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/wowza.png",
|
||||
"image": "sameersbn/wowza:4.1.2-8",
|
||||
"env": [
|
||||
{
|
||||
"name": "WOWZA_ACCEPT_LICENSE",
|
||||
"label": "Agree to Wowza EULA",
|
||||
"set": "yes"
|
||||
},
|
||||
{
|
||||
"name": "WOWZA_KEY",
|
||||
"label": "License key"
|
||||
}
|
||||
],
|
||||
"ports": [
|
||||
"1935/tcp",
|
||||
"8086/tcp",
|
||||
"8087/tcp",
|
||||
"8088/tcp"
|
||||
],
|
||||
"volumes": [{"container": "/var/lib/wowza"}]
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"title": "Jenkins",
|
||||
"description": "Open-source continuous integration tool",
|
||||
"categories": ["continuous-integration"],
|
||||
"platform": "linux",
|
||||
"logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/jenkins.png",
|
||||
"image": "jenkins/jenkins:lts",
|
||||
"ports": [
|
||||
"8080/tcp",
|
||||
"50000/tcp"
|
||||
],
|
||||
"env": [
|
||||
{
|
||||
"name": "JENKINS_OPTS",
|
||||
"label": "Jenkins options"
|
||||
}
|
||||
],
|
||||
"volumes": [{"container": "/var/jenkins_home"}]
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"title": "Redmine",
|
||||
"description": "Open-source project management tool",
|
||||
"categories": ["project-management"],
|
||||
"platform": "linux",
|
||||
"logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/redmine.png",
|
||||
"image": "redmine:latest",
|
||||
"ports": [
|
||||
"3000/tcp"
|
||||
],
|
||||
"volumes": [{"container": "/usr/src/redmine/files"}]
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"title": "Odoo",
|
||||
"description": "Open-source business apps",
|
||||
"categories": ["project-management"],
|
||||
"platform": "linux",
|
||||
"logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/odoo.png",
|
||||
"image": "odoo:latest",
|
||||
"env": [
|
||||
{
|
||||
"name": "HOST",
|
||||
"label": "PostgreSQL database host",
|
||||
"type": "container"
|
||||
},
|
||||
{
|
||||
"name": "USER",
|
||||
"label": "Database user"
|
||||
},
|
||||
{
|
||||
"name": "PASSWORD",
|
||||
"label": "Database password"
|
||||
}
|
||||
],
|
||||
"ports": [
|
||||
"8069/tcp"
|
||||
],
|
||||
"volumes": [{"container": "/var/lib/odoo"}, {"container": "/mnt/extra-addons"}]
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"title": "Urbackup",
|
||||
"description": "Open-source network backup",
|
||||
"categories": ["backup"],
|
||||
"platform": "linux",
|
||||
"note": "This application web interface is exposed on the port 55414 inside the container.",
|
||||
"logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/urbackup.png",
|
||||
"image": "cfstras/urbackup",
|
||||
"ports": [
|
||||
"55413/tcp", "55414/tcp", "55415/tcp", "35622/tcp"
|
||||
],
|
||||
"volumes": [{"container": "/var/urbackup"}]
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"title": "File browser",
|
||||
"description": "A web file manager",
|
||||
"note": "Default credentials: admin/admin",
|
||||
"categories": ["filesystem", "storage"],
|
||||
"platform": "linux",
|
||||
"logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/filebrowser.png",
|
||||
"image": "filebrowser/filebrowser:latest",
|
||||
"ports": [
|
||||
"80/tcp"
|
||||
],
|
||||
"volumes": [{"container": "/data"}, {"container": "/srv"}],
|
||||
"command": "--port 80 --database /data/database.db --scope /srv"
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"title": "CommandBox",
|
||||
"description": "ColdFusion (CFML) CLI",
|
||||
"categories": ["development"],
|
||||
"platform": "linux",
|
||||
"logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/ortussolutions-commandbox.png",
|
||||
"image": "ortussolutions/commandbox:latest",
|
||||
"env": [
|
||||
{
|
||||
"name": "CFENGINE",
|
||||
"set": "lucee@4.5"
|
||||
}
|
||||
],
|
||||
"ports": [
|
||||
"8080/tcp", "8443/tcp"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"title": "ContentBox",
|
||||
"description": "Open-source modular CMS",
|
||||
"categories": ["CMS"],
|
||||
"platform": "linux",
|
||||
"logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/ortussolutions-contentbox.png",
|
||||
"image": "ortussolutions/contentbox:latest",
|
||||
"env": [
|
||||
{
|
||||
"name": "express",
|
||||
"set": "true"
|
||||
},
|
||||
{
|
||||
"name": "install",
|
||||
"set": "true"
|
||||
},
|
||||
{
|
||||
"name": "CFENGINE",
|
||||
"set": "lucee@4.5"
|
||||
}
|
||||
],
|
||||
"ports": [
|
||||
"8080/tcp", "8443/tcp"
|
||||
],
|
||||
"volumes": [{"container": "/data/contentbox/db"}, {"container": "/app/includes/shared/media"}]
|
||||
},
|
||||
{
|
||||
"type": 2,
|
||||
"title": "Portainer Agent",
|
||||
"description": "Manage all the resources in your Swarm cluster",
|
||||
"note": "The agent will be deployed globally inside your cluster and available on port 9001.",
|
||||
"categories": ["portainer"],
|
||||
"platform": "linux",
|
||||
"logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/portainer.png",
|
||||
"repository": {
|
||||
"url": "https://github.com/portainer/templates",
|
||||
"stackfile": "stacks/portainer-agent/docker-stack.yml"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": 2,
|
||||
"title": "OpenFaaS",
|
||||
"name": "func",
|
||||
"description": "Serverless functions made simple",
|
||||
"note": "Deploys the API gateway and sample functions. You can access the UI on port 8080. <b>Warning</b>: the name of the stack must be 'func'.",
|
||||
"categories": ["serverless"],
|
||||
"platform": "linux",
|
||||
"logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/openfaas.png",
|
||||
"repository": {
|
||||
"url": "https://github.com/openfaas/faas",
|
||||
"stackfile": "docker-compose.yml"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": 2,
|
||||
"title": "IronFunctions",
|
||||
"description": "Open-source serverless computing platform",
|
||||
"note": "Deploys the IronFunctions API and UI.",
|
||||
"categories": ["serverless"],
|
||||
"platform": "linux",
|
||||
"logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/ironfunctions.png",
|
||||
"repository": {
|
||||
"url": "https://github.com/portainer/templates",
|
||||
"stackfile": "stacks/ironfunctions/docker-stack.yml"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": 2,
|
||||
"title": "CockroachDB",
|
||||
"description": "CockroachDB cluster",
|
||||
"note": "Deploys an insecure CockroachDB cluster, please refer to <a href=\"https://www.cockroachlabs.com/docs/stable/orchestrate-cockroachdb-with-docker-swarm.html\" target=\"_blank\">CockroachDB documentation</a> for production deployments.",
|
||||
"categories": ["database"],
|
||||
"platform": "linux",
|
||||
"logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/cockroachdb.png",
|
||||
"repository": {
|
||||
"url": "https://github.com/portainer/templates",
|
||||
"stackfile": "stacks/cockroachdb/docker-stack.yml"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": 2,
|
||||
"title": "Wordpress",
|
||||
"description": "Wordpress setup with a MySQL database",
|
||||
"note": "Deploys a Wordpress instance connected to a MySQL database.",
|
||||
"categories": ["CMS"],
|
||||
"platform": "linux",
|
||||
"logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/wordpress.png",
|
||||
"repository": {
|
||||
"url": "https://github.com/portainer/templates",
|
||||
"stackfile": "stacks/wordpress/docker-stack.yml"
|
||||
},
|
||||
"env": [
|
||||
{
|
||||
"name": "MYSQL_DATABASE_PASSWORD",
|
||||
"label": "Database root password",
|
||||
"description": "Password used by the MySQL root user."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": 3,
|
||||
"title": "Wordpress",
|
||||
"description": "Wordpress setup with a MySQL database",
|
||||
"note": "Deploys a Wordpress instance connected to a MySQL database.",
|
||||
"categories": ["CMS"],
|
||||
"platform": "linux",
|
||||
"logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/wordpress.png",
|
||||
"repository": {
|
||||
"url": "https://github.com/portainer/templates",
|
||||
"stackfile": "stacks/wordpress/docker-compose.yml"
|
||||
},
|
||||
"env": [
|
||||
{
|
||||
"name": "MYSQL_DATABASE_PASSWORD",
|
||||
"label": "Database root password",
|
||||
"description": "Password used by the MySQL root user."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": 2,
|
||||
"title": "Microsoft OMS Agent",
|
||||
"description": "Microsoft Operations Management Suite Linux agent.",
|
||||
"categories": ["OPS"],
|
||||
"platform": "linux",
|
||||
"logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/microsoft.png",
|
||||
"repository": {
|
||||
"url": "https://github.com/portainer/templates",
|
||||
"stackfile": "stacks/microsoft-oms/docker-stack.yml"
|
||||
},
|
||||
"env": [
|
||||
{
|
||||
"name": "AZURE_WORKSPACE_ID",
|
||||
"label": "Workspace ID",
|
||||
"description": "Azure Workspace ID"
|
||||
},
|
||||
{
|
||||
"name": "AZURE_PRIMARY_KEY",
|
||||
"label": "Primary key",
|
||||
"description": "Azure primary key"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Sematext Docker Agent",
|
||||
"type": 2,
|
||||
"categories": ["Log Management", "Monitoring"],
|
||||
"description": "Collect logs, metrics and docker events",
|
||||
"logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/sematext_agent.png",
|
||||
"platform": "linux",
|
||||
"repository": {
|
||||
"url": "https://github.com/portainer/templates",
|
||||
"stackfile": "stacks/sematext-agent-docker/docker-stack.yml"
|
||||
},
|
||||
"env": [
|
||||
{
|
||||
"name": "LOGSENE_TOKEN",
|
||||
"label": "Logs token"
|
||||
},
|
||||
{
|
||||
"name": "SPM_TOKEN",
|
||||
"label": "SPM monitoring token"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Datadog agent",
|
||||
"type": 2,
|
||||
"categories": ["Monitoring"],
|
||||
"description": "Collect events and metrics",
|
||||
"logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/datadog_agent.png",
|
||||
"platform": "linux",
|
||||
"repository": {
|
||||
"url": "https://github.com/portainer/templates",
|
||||
"stackfile": "stacks/datadog-agent/docker-stack.yml"
|
||||
},
|
||||
"env": [
|
||||
{
|
||||
"name": "API_KEY",
|
||||
"label": "Datadog API key"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"title": "Sonatype Nexus3",
|
||||
"description": "Sonatype Nexus3 registry manager",
|
||||
"categories": ["docker"],
|
||||
"platform": "linux",
|
||||
"logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/sonatype.png",
|
||||
"image": "sonatype/nexus3:latest",
|
||||
"ports": [
|
||||
"8081/tcp"
|
||||
],
|
||||
"volumes": [{ "container": "/nexus-data"}]
|
||||
}
|
||||
]
|
||||
Reference in New Issue
Block a user