feat(templates): remove template management features (#3719)

* feat(api): remove template management features

* feat(templates): remove template management features
2.0
Anthony Lapenna 2020-04-15 17:49:34 +12:00 committed by Anthony Lapenna
parent 45f93882d0
commit 5563ff60fc
36 changed files with 26 additions and 965 deletions

View File

@ -26,7 +26,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"
@ -58,7 +57,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
@ -137,7 +135,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,
@ -246,12 +243,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

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

@ -15,7 +15,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"
)
@ -37,7 +36,6 @@ type (
stackService *stack.Service
tagService *tag.Service
teamMembershipService *teammembership.Service
templateService *template.Service
userService *user.Service
versionService *version.Service
fileService portainer.FileService
@ -59,7 +57,6 @@ type (
StackService *stack.Service
TagService *tag.Service
TeamMembershipService *teammembership.Service
TemplateService *template.Service
UserService *user.Service
VersionService *version.Service
FileService portainer.FileService
@ -82,7 +79,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,

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

@ -20,7 +20,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")
@ -58,7 +57,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 App templates definitions on the filesystem (deprecated)").Default(defaultTemplateFile).String(),
}
kingpin.Parse()
@ -83,12 +81,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
}
@ -173,16 +166,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"
@ -276,6 +275,7 @@ func initSettings(settingsService portainer.SettingsService, flags *portainer.CL
EnableHostManagementFeatures: false,
SnapshotInterval: *flags.SnapshotInterval,
EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds,
TemplatesURL: portainer.DefaultTemplatesURL,
}
if *flags.Templates != "" {
@ -296,45 +296,6 @@ func initSettings(settingsService portainer.SettingsService, flags *portainer.CL
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()
if err != nil {
return err
}
if len(existingTemplates) != 0 {
log.Printf("Templates already registered inside the database. Skipping template import.")
return nil
}
templatesJSON, err := fileService.GetFileContent(templateFile)
if err != nil {
log.Println("Unable to retrieve template definitions via filesystem")
return err
}
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 {
@ -561,11 +522,6 @@ 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)
@ -674,7 +630,6 @@ func main() {
StackService: store.StackService,
ScheduleService: store.ScheduleService,
TagService: store.TagService,
TemplateService: store.TemplateService,
WebhookService: store.WebhookService,
SwarmStackManager: swarmStackManager,
ComposeStackManager: composeStackManager,

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=

View File

@ -18,7 +18,7 @@ func (handler *Handler) edgeTemplateList(w http.ResponseWriter, r *http.Request)
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
}
url := portainer.EdgeTemplatesURL
url := portainer.DefaultTemplatesURL
if settings.TemplatesURL != "" {
url = settings.TemplatesURL
}

View File

@ -17,7 +17,6 @@ type publicSettingsResponse struct {
AllowVolumeBrowserForRegularUsers bool `json:"AllowVolumeBrowserForRegularUsers"`
EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"`
EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"`
ExternalTemplates bool `json:"ExternalTemplates"`
OAuthLoginURI string `json:"OAuthLoginURI"`
}
@ -36,7 +35,6 @@ func (handler *Handler) settingsPublic(w http.ResponseWriter, r *http.Request) *
AllowVolumeBrowserForRegularUsers: settings.AllowVolumeBrowserForRegularUsers,
EnableHostManagementFeatures: settings.EnableHostManagementFeatures,
EnableEdgeComputeFeatures: settings.EnableEdgeComputeFeatures,
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,
@ -44,9 +42,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

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

@ -78,7 +78,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
@ -282,7 +281,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

@ -538,7 +538,8 @@ type (
AccessLevel ResourceAccessLevel `json:"AccessLevel"`
}
// Template represents an application template
// Template represents an application template that can be used as an App Template
// or an Edge template
Template struct {
// Mandatory container/stack fields
ID TemplateID `json:"Id"`
@ -553,7 +554,7 @@ type (
// Mandatory stack fields
Repository TemplateRepository `json:"repository"`
// Mandatory edge stack fields
// Mandatory Edge stack fields
StackFile string `json:"stackFile"`
// Optional stack/container fields
@ -943,15 +944,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)
@ -1039,8 +1031,8 @@ const (
DefaultEdgeAgentCheckinIntervalInSeconds = 5
// LocalExtensionManifestFile represents the name of the local manifest file for extensions
LocalExtensionManifestFile = "/extensions.json"
// EdgeTemplatesURL represents the URL used to retrieve Edge templates
EdgeTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates-1.20.0.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

@ -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;
this.EnableEdgeComputeFeatures = data.EnableEdgeComputeFeatures;
@ -21,7 +20,6 @@ export function PublicSettingsViewModel(settings) {
this.AllowVolumeBrowserForRegularUsers = settings.AllowVolumeBrowserForRegularUsers;
this.AuthenticationMethod = settings.AuthenticationMethod;
this.EnableHostManagementFeatures = settings.EnableHostManagementFeatures;
this.ExternalTemplates = settings.ExternalTemplates;
this.EnableEdgeComputeFeatures = settings.EnableEdgeComputeFeatures;
this.LogoURL = settings.LogoURL;
this.OAuthLoginURI = settings.OAuthLoginURI;

View File

@ -1,58 +1,6 @@
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);
}
}
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;

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,4 @@
import { TemplateViewModel, TemplateCreateRequest, TemplateUpdateRequest } from '../../models/template';
import { TemplateViewModel } from '../../models/template';
angular.module('portainer.app').factory('TemplateService', [
'$q',
@ -21,7 +21,7 @@ angular.module('portainer.app').factory('TemplateService', [
dockerhub: DockerHubService.dockerhub(),
})
.then(function success(data) {
const templates = data.templates.map(function (item) {
const templates = data.templates.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;
@ -37,40 +37,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

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

@ -366,10 +366,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

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