Compare commits

...

10 Commits

Author SHA1 Message Date
Maxime Bajeux
c15dc7910d fix(volumes): add unicity check on volumes 2020-04-30 16:00:55 +02:00
Maxime Bajeux
223f742ee1 fix(volume): add unicity check on creation 2020-04-29 02:20:03 +02:00
Anthony Lapenna
85a4e70b87 feat(templates): leftovers cleanup (#3762)
* feat(templates): leftovers cleanup

* feat(templates): update CLIFlags structure
2020-04-27 17:58:24 +12:00
Anthony Lapenna
ed003ffaaf Merge branch '2.0' of github.com:portainer/portainer into 2.0 2020-04-27 14:34:03 +12:00
Anthony Lapenna
b3cf11ec22 Merge branch 'develop' into 2.0 2020-04-27 14:33:48 +12:00
Simone Cattaneo
dfb870105c fix(api): updated LDAP library to v3 (portainer#3244) (#3386)
Co-authored-by: Anthony Lapenna <anthony.lapenna@portainer.io>
2020-04-27 14:14:27 +12:00
Anthony Lapenna
2f12cbf083 chore(version): bump version number 2020-04-21 12:10:26 +12:00
Maxime Bajeux
64251b3e88 feat(templates): support templates versioning (#3729)
* feat(templates): Support templates versioning format

* Update app/portainer/models/template.js

Co-authored-by: Anthony Lapenna <anthony.lapenna@portainer.io>
2020-04-21 12:05:30 +12:00
Anthony Lapenna
5fcace6b01 feat(templates): fix an issue with templates initialization and update settings view 2020-04-16 12:22:08 +12:00
Anthony Lapenna
a5438cc86a feat(templates): remove template management features (#3719)
* feat(api): remove template management features

* feat(templates): remove template management features
2020-04-15 17:49:34 +12:00
49 changed files with 213 additions and 1991 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -21,5 +21,4 @@ const (
defaultSyncInterval = "60s"
defaultSnapshot = "true"
defaultSnapshotInterval = "5m"
defaultTemplateFile = "/templates.json"
)

View File

@@ -19,5 +19,4 @@ const (
defaultSyncInterval = "60s"
defaultSnapshot = "true"
defaultSnapshotInterval = "5m"
defaultTemplateFile = "/templates.json"
)

View File

@@ -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,

View File

@@ -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
)

View File

@@ -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=

View File

@@ -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)
}

View File

@@ -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
})
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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)
}
}

View File

@@ -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)

View File

@@ -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:

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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 (

View File

@@ -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 (

View File

@@ -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"

View File

@@ -1,5 +1,5 @@
{
"packageName": "portainer",
"packageVersion": "1.24.0-dev",
"packageVersion": "2.0.0-dev",
"projectName": "portainer"
}

View File

@@ -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.')

View File

@@ -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,

View File

@@ -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);
},
]);

View File

@@ -3,8 +3,5 @@ angular.module('portainer.app').component('templateItem', {
bindings: {
model: '=',
onSelect: '<',
onDelete: '<',
showUpdateAction: '<',
showDeleteAction: '<',
},
});

View File

@@ -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 -->

View File

@@ -7,10 +7,6 @@ angular.module('portainer.app').component('templateList', {
templates: '<',
tableKey: '@',
selectAction: '<',
deleteAction: '<',
showSwarmStacks: '<',
showAddAction: '<',
showUpdateAction: '<',
showDeleteAction: '<',
},
});

View File

@@ -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...

View File

@@ -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;
}

View File

@@ -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) {

View File

@@ -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' },
}
);
},

View File

@@ -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);

View File

@@ -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>

View File

@@ -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;

View File

@@ -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();
},
]);

View File

@@ -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> &gt; 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>

View File

@@ -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> &gt; {{ ::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>

View File

@@ -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();
},
]);

View File

@@ -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>

View File

@@ -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 = [];

View File

@@ -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

View File

@@ -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(';');
}

View File

@@ -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"

View File

@@ -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"}]
}
]