mirror of
https://github.com/portainer/portainer.git
synced 2025-07-25 08:19:40 +02:00
feat(templates): introduce templates management (#2017)
This commit is contained in:
parent
e7939a5384
commit
61c285bd2e
63 changed files with 3489 additions and 637 deletions
|
@ -18,6 +18,7 @@ import (
|
|||
"github.com/portainer/portainer/bolt/tag"
|
||||
"github.com/portainer/portainer/bolt/team"
|
||||
"github.com/portainer/portainer/bolt/teammembership"
|
||||
"github.com/portainer/portainer/bolt/template"
|
||||
"github.com/portainer/portainer/bolt/user"
|
||||
"github.com/portainer/portainer/bolt/version"
|
||||
)
|
||||
|
@ -43,6 +44,7 @@ type Store struct {
|
|||
TagService *tag.Service
|
||||
TeamMembershipService *teammembership.Service
|
||||
TeamService *team.Service
|
||||
TemplateService *template.Service
|
||||
UserService *user.Service
|
||||
VersionService *version.Service
|
||||
}
|
||||
|
@ -212,6 +214,12 @@ func (store *Store) initServices() error {
|
|||
}
|
||||
store.TeamService = teamService
|
||||
|
||||
templateService, err := template.NewService(store.db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
store.TemplateService = templateService
|
||||
|
||||
userService, err := user.NewService(store.db)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
95
api/bolt/template/template.go
Normal file
95
api/bolt/template/template.go
Normal file
|
@ -0,0 +1,95 @@
|
|||
package template
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/bolt/internal"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
)
|
||||
|
||||
const (
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
BucketName = "templates"
|
||||
)
|
||||
|
||||
// Service represents a service for managing endpoint data.
|
||||
type Service struct {
|
||||
db *bolt.DB
|
||||
}
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(db *bolt.DB) (*Service, error) {
|
||||
err := internal.CreateBucket(db, BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Service{
|
||||
db: db,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Templates return an array containing all the templates.
|
||||
func (service *Service) Templates() ([]portainer.Template, error) {
|
||||
var templates = make([]portainer.Template, 0)
|
||||
|
||||
err := service.db.View(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(BucketName))
|
||||
|
||||
cursor := bucket.Cursor()
|
||||
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||
var template portainer.Template
|
||||
err := internal.UnmarshalObject(v, &template)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
templates = append(templates, template)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return templates, err
|
||||
}
|
||||
|
||||
// Template returns a template by ID.
|
||||
func (service *Service) Template(ID portainer.TemplateID) (*portainer.Template, error) {
|
||||
var template portainer.Template
|
||||
identifier := internal.Itob(int(ID))
|
||||
|
||||
err := internal.GetObject(service.db, BucketName, identifier, &template)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &template, nil
|
||||
}
|
||||
|
||||
// CreateTemplate creates a new template.
|
||||
func (service *Service) CreateTemplate(template *portainer.Template) error {
|
||||
return service.db.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(BucketName))
|
||||
|
||||
id, _ := bucket.NextSequence()
|
||||
template.ID = portainer.TemplateID(id)
|
||||
|
||||
data, err := internal.MarshalObject(template)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return bucket.Put(internal.Itob(int(template.ID)), data)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateTemplate saves a template.
|
||||
func (service *Service) UpdateTemplate(ID portainer.TemplateID, template *portainer.Template) error {
|
||||
identifier := internal.Itob(int(ID))
|
||||
return internal.UpdateObject(service.db, BucketName, identifier, template)
|
||||
}
|
||||
|
||||
// DeleteTemplate deletes a template.
|
||||
func (service *Service) DeleteTemplate(ID portainer.TemplateID) error {
|
||||
identifier := internal.Itob(int(ID))
|
||||
return internal.DeleteObject(service.db, BucketName, identifier)
|
||||
}
|
|
@ -19,6 +19,7 @@ const (
|
|||
errInvalidEndpointProtocol = portainer.Error("Invalid endpoint protocol: Portainer only supports unix:// or tcp://")
|
||||
errSocketNotFound = portainer.Error("Unable to locate Unix socket")
|
||||
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")
|
||||
errEndpointExcludeExternal = portainer.Error("Cannot use the -H flag mutually with --external-endpoints")
|
||||
errNoAuthExcludeAdminPassword = portainer.Error("Cannot use --no-auth with --admin-password or --admin-password-file")
|
||||
|
@ -50,7 +51,8 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
|||
AdminPasswordFile: kingpin.Flag("admin-password-file", "Path to the file containing the password for the admin user").String(),
|
||||
Labels: pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')),
|
||||
Logo: kingpin.Flag("logo", "URL for the logo displayed in the UI").String(),
|
||||
Templates: kingpin.Flag("templates", "URL to the templates (apps) definitions").Short('t').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()
|
||||
|
@ -73,7 +75,12 @@ func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
|
|||
return errEndpointExcludeExternal
|
||||
}
|
||||
|
||||
err := validateEndpointURL(*flags.EndpointURL)
|
||||
err := validateTemplateFile(*flags.TemplateFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = validateEndpointURL(*flags.EndpointURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -130,6 +137,16 @@ 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)
|
||||
|
|
|
@ -17,4 +17,5 @@ const (
|
|||
defaultSSLCertPath = "/certs/portainer.crt"
|
||||
defaultSSLKeyPath = "/certs/portainer.key"
|
||||
defaultSyncInterval = "60s"
|
||||
defaultTemplateFile = "/templates.json"
|
||||
)
|
||||
|
|
|
@ -15,4 +15,5 @@ const (
|
|||
defaultSSLCertPath = "C:\\certs\\portainer.crt"
|
||||
defaultSSLKeyPath = "C:\\certs\\portainer.key"
|
||||
defaultSyncInterval = "60s"
|
||||
defaultTemplateFile = "C:\\templates.json"
|
||||
)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package main // import "github.com/portainer/portainer"
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
|
@ -143,9 +144,8 @@ func initSettings(settingsService portainer.SettingsService, flags *portainer.CL
|
|||
_, err := settingsService.Settings()
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
settings := &portainer.Settings{
|
||||
LogoURL: *flags.Logo,
|
||||
DisplayExternalContributors: false,
|
||||
AuthenticationMethod: portainer.AuthenticationInternal,
|
||||
LogoURL: *flags.Logo,
|
||||
AuthenticationMethod: portainer.AuthenticationInternal,
|
||||
LDAPSettings: portainer.LDAPSettings{
|
||||
TLSConfig: portainer.TLSConfiguration{},
|
||||
SearchSettings: []portainer.LDAPSearchSettings{
|
||||
|
@ -156,12 +156,6 @@ func initSettings(settingsService portainer.SettingsService, flags *portainer.CL
|
|||
AllowPrivilegedModeForRegularUsers: true,
|
||||
}
|
||||
|
||||
if *flags.Templates != "" {
|
||||
settings.TemplatesURL = *flags.Templates
|
||||
} else {
|
||||
settings.TemplatesURL = portainer.DefaultTemplatesURL
|
||||
}
|
||||
|
||||
if *flags.Labels != nil {
|
||||
settings.BlackListedLabels = *flags.Labels
|
||||
} else {
|
||||
|
@ -176,6 +170,58 @@ func initSettings(settingsService portainer.SettingsService, flags *portainer.CL
|
|||
return nil
|
||||
}
|
||||
|
||||
func initTemplates(templateService portainer.TemplateService, fileService portainer.FileService, templateURL, templateFile string) error {
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
var templatesJSON []byte
|
||||
if templateURL == "" {
|
||||
return loadTemplatesFromFile(fileService, templateService, templateFile)
|
||||
}
|
||||
|
||||
templatesJSON, err = client.Get(templateURL)
|
||||
if err != nil {
|
||||
log.Println("Unable to retrieve templates via HTTP")
|
||||
return err
|
||||
}
|
||||
|
||||
return unmarshalAndPersistTemplates(templateService, templatesJSON)
|
||||
}
|
||||
|
||||
func loadTemplatesFromFile(fileService portainer.FileService, templateService portainer.TemplateService, templateFile string) error {
|
||||
templatesJSON, err := fileService.GetFileContent(templateFile)
|
||||
if err != nil {
|
||||
log.Println("Unable to retrieve template via filesystem")
|
||||
return err
|
||||
}
|
||||
return unmarshalAndPersistTemplates(templateService, templatesJSON)
|
||||
}
|
||||
|
||||
func unmarshalAndPersistTemplates(templateService portainer.TemplateService, templateData []byte) error {
|
||||
var templates []portainer.Template
|
||||
err := json.Unmarshal(templateData, &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 {
|
||||
|
@ -334,6 +380,11 @@ func main() {
|
|||
|
||||
composeStackManager := initComposeStackManager(*flags.Data)
|
||||
|
||||
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)
|
||||
|
@ -357,7 +408,7 @@ func main() {
|
|||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
adminPasswordHash, err = cryptoService.Hash(content)
|
||||
adminPasswordHash, err = cryptoService.Hash(string(content))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
@ -404,6 +455,7 @@ func main() {
|
|||
DockerHubService: store.DockerHubService,
|
||||
StackService: store.StackService,
|
||||
TagService: store.TagService,
|
||||
TemplateService: store.TemplateService,
|
||||
SwarmStackManager: swarmStackManager,
|
||||
ComposeStackManager: composeStackManager,
|
||||
CryptoService: cryptoService,
|
||||
|
|
|
@ -169,7 +169,7 @@ func (manager *SwarmStackManager) retrieveConfigurationFromDisk(path string) (ma
|
|||
return make(map[string]interface{}), nil
|
||||
}
|
||||
|
||||
err = json.Unmarshal([]byte(raw), &config)
|
||||
err = json.Unmarshal(raw, &config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -176,14 +176,14 @@ func (service *Service) DeleteTLSFile(folder string, fileType portainer.TLSFileT
|
|||
return nil
|
||||
}
|
||||
|
||||
// GetFileContent returns a string content from file.
|
||||
func (service *Service) GetFileContent(filePath string) (string, error) {
|
||||
// GetFileContent returns the content of a file as bytes.
|
||||
func (service *Service) GetFileContent(filePath string) ([]byte, error) {
|
||||
content, err := ioutil.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return string(content), nil
|
||||
return content, nil
|
||||
}
|
||||
|
||||
// Rename renames a file or directory
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
@ -61,6 +62,27 @@ func (client *HTTPClient) ExecuteAzureAuthenticationRequest(credentials *portain
|
|||
return &token, nil
|
||||
}
|
||||
|
||||
// Get executes a simple HTTP GET to the specified URL and returns
|
||||
// the content of the response body.
|
||||
func Get(url string) ([]byte, error) {
|
||||
client := &http.Client{
|
||||
Timeout: time.Second * 3,
|
||||
}
|
||||
|
||||
response, err := client.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
|
||||
// ExecutePingOperation will send a SystemPing operation HTTP request to a Docker environment
|
||||
// using the specified host and optional TLS configuration.
|
||||
// It uses a new Http.Client for each operation.
|
||||
|
|
|
@ -10,7 +10,6 @@ import (
|
|||
|
||||
type publicSettingsResponse struct {
|
||||
LogoURL string `json:"LogoURL"`
|
||||
DisplayExternalContributors bool `json:"DisplayExternalContributors"`
|
||||
AuthenticationMethod portainer.AuthenticationMethod `json:"AuthenticationMethod"`
|
||||
AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"`
|
||||
AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"`
|
||||
|
@ -25,7 +24,6 @@ func (handler *Handler) settingsPublic(w http.ResponseWriter, r *http.Request) *
|
|||
|
||||
publicSettings := &publicSettingsResponse{
|
||||
LogoURL: settings.LogoURL,
|
||||
DisplayExternalContributors: settings.DisplayExternalContributors,
|
||||
AuthenticationMethod: settings.AuthenticationMethod,
|
||||
AllowBindMountsForRegularUsers: settings.AllowBindMountsForRegularUsers,
|
||||
AllowPrivilegedModeForRegularUsers: settings.AllowPrivilegedModeForRegularUsers,
|
||||
|
|
|
@ -12,10 +12,8 @@ import (
|
|||
)
|
||||
|
||||
type settingsUpdatePayload struct {
|
||||
TemplatesURL string
|
||||
LogoURL string
|
||||
BlackListedLabels []portainer.Pair
|
||||
DisplayExternalContributors bool
|
||||
AuthenticationMethod int
|
||||
LDAPSettings portainer.LDAPSettings
|
||||
AllowBindMountsForRegularUsers bool
|
||||
|
@ -23,9 +21,6 @@ type settingsUpdatePayload struct {
|
|||
}
|
||||
|
||||
func (payload *settingsUpdatePayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.TemplatesURL) || !govalidator.IsURL(payload.TemplatesURL) {
|
||||
return portainer.Error("Invalid templates URL. Must correspond to a valid URL format")
|
||||
}
|
||||
if payload.AuthenticationMethod == 0 {
|
||||
return portainer.Error("Invalid authentication method")
|
||||
}
|
||||
|
@ -47,10 +42,8 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
|
|||
}
|
||||
|
||||
settings := &portainer.Settings{
|
||||
TemplatesURL: payload.TemplatesURL,
|
||||
LogoURL: payload.LogoURL,
|
||||
BlackListedLabels: payload.BlackListedLabels,
|
||||
DisplayExternalContributors: payload.DisplayExternalContributors,
|
||||
LDAPSettings: payload.LDAPSettings,
|
||||
AllowBindMountsForRegularUsers: payload.AllowBindMountsForRegularUsers,
|
||||
AllowPrivilegedModeForRegularUsers: payload.AllowPrivilegedModeForRegularUsers,
|
||||
|
|
|
@ -54,5 +54,5 @@ func (handler *Handler) stackFile(w http.ResponseWriter, r *http.Request) *httpe
|
|||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Compose file from disk", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, &stackFileResponse{StackFileContent: stackFileContent})
|
||||
return response.JSON(w, &stackFileResponse{StackFileContent: string(stackFileContent)})
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import (
|
|||
"github.com/portainer/portainer/http/response"
|
||||
)
|
||||
|
||||
// DELETE request on /api/tags/:name
|
||||
// DELETE request on /api/tags/:id
|
||||
func (handler *Handler) tagDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
id, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
|
|
|
@ -9,14 +9,10 @@ import (
|
|||
"github.com/portainer/portainer/http/security"
|
||||
)
|
||||
|
||||
const (
|
||||
containerTemplatesURLLinuxServerIo = "https://tools.linuxserver.io/portainer.json"
|
||||
)
|
||||
|
||||
// Handler represents an HTTP API handler for managing templates.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
SettingsService portainer.SettingsService
|
||||
TemplateService portainer.TemplateService
|
||||
}
|
||||
|
||||
// NewHandler returns a new instance of Handler.
|
||||
|
@ -25,6 +21,14 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
|||
Router: mux.NewRouter(),
|
||||
}
|
||||
h.Handle("/templates",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.templateList))).Methods(http.MethodGet)
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.templateList))).Methods(http.MethodGet)
|
||||
h.Handle("/templates",
|
||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.templateCreate))).Methods(http.MethodPost)
|
||||
h.Handle("/templates/{id}",
|
||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.templateInspect))).Methods(http.MethodGet)
|
||||
h.Handle("/templates/{id}",
|
||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.templateUpdate))).Methods(http.MethodPut)
|
||||
h.Handle("/templates/{id}",
|
||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.templateDelete))).Methods(http.MethodDelete)
|
||||
return h
|
||||
}
|
||||
|
|
122
api/http/handler/templates/template_create.go
Normal file
122
api/http/handler/templates/template_create.go
Normal file
|
@ -0,0 +1,122 @@
|
|||
package templates
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/filesystem"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/request"
|
||||
"github.com/portainer/portainer/http/response"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
25
api/http/handler/templates/template_delete.go
Normal file
25
api/http/handler/templates/template_delete.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
package templates
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/request"
|
||||
"github.com/portainer/portainer/http/response"
|
||||
)
|
||||
|
||||
// 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)
|
||||
}
|
27
api/http/handler/templates/template_inspect.go
Normal file
27
api/http/handler/templates/template_inspect.go
Normal file
|
@ -0,0 +1,27 @@
|
|||
package templates
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/request"
|
||||
"github.com/portainer/portainer/http/response"
|
||||
)
|
||||
|
||||
// GET request on /api/templates/:id
|
||||
func (handler *Handler) templateInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
templateID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid template identifier route variable", err}
|
||||
}
|
||||
|
||||
template, err := handler.TemplateService.Template(portainer.TemplateID(templateID))
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a template with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a template with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, template)
|
||||
}
|
|
@ -1,50 +1,26 @@
|
|||
package templates
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/request"
|
||||
"github.com/portainer/portainer/http/response"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
)
|
||||
|
||||
// GET request on /api/templates?key=<key>
|
||||
// GET request on /api/templates
|
||||
func (handler *Handler) templateList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
key, err := request.RetrieveQueryParameter(r, "key", false)
|
||||
templates, err := handler.TemplateService.Templates()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: key", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve templates from the database", err}
|
||||
}
|
||||
|
||||
templatesURL, templateErr := handler.retrieveTemplateURLFromKey(key)
|
||||
if templateErr != nil {
|
||||
return templateErr
|
||||
}
|
||||
|
||||
resp, err := http.Get(templatesURL)
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve templates via the network", err}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to read template response", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||
}
|
||||
|
||||
return response.Bytes(w, body, "application/json")
|
||||
}
|
||||
|
||||
func (handler *Handler) retrieveTemplateURLFromKey(key string) (string, *httperror.HandlerError) {
|
||||
switch key {
|
||||
case "containers":
|
||||
settings, err := handler.SettingsService.Settings()
|
||||
if err != nil {
|
||||
return "", &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
|
||||
}
|
||||
return settings.TemplatesURL, nil
|
||||
case "linuxserver.io":
|
||||
return containerTemplatesURLLinuxServerIo, nil
|
||||
}
|
||||
return "", &httperror.HandlerError{http.StatusBadRequest, "Invalid value for query parameter: key. Value must be one of: containers or linuxserver.io", request.ErrInvalidQueryParameter}
|
||||
filteredTemplates := security.FilterTemplates(templates, securityContext)
|
||||
|
||||
return response.JSON(w, filteredTemplates)
|
||||
}
|
||||
|
|
164
api/http/handler/templates/template_update.go
Normal file
164
api/http/handler/templates/template_update.go
Normal file
|
@ -0,0 +1,164 @@
|
|||
package templates
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/request"
|
||||
"github.com/portainer/portainer/http/response"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -23,10 +23,3 @@ func Empty(rw http.ResponseWriter) *httperror.HandlerError {
|
|||
rw.WriteHeader(http.StatusNoContent)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Bytes write data into rw. It also allows to set the Content-Type header.
|
||||
func Bytes(rw http.ResponseWriter, data []byte, contentType string) *httperror.HandlerError {
|
||||
rw.Header().Set("Content-Type", contentType)
|
||||
rw.Write(data)
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -77,6 +77,24 @@ func FilterRegistries(registries []portainer.Registry, context *RestrictedReques
|
|||
return filteredRegistries
|
||||
}
|
||||
|
||||
// FilterTemplates filters templates based on the user role.
|
||||
// Non-administrato 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 {
|
||||
|
|
|
@ -55,6 +55,7 @@ type Server struct {
|
|||
TagService portainer.TagService
|
||||
TeamService portainer.TeamService
|
||||
TeamMembershipService portainer.TeamMembershipService
|
||||
TemplateService portainer.TemplateService
|
||||
UserService portainer.UserService
|
||||
Handler *handler.Handler
|
||||
SSL bool
|
||||
|
@ -143,7 +144,7 @@ func (server *Server) Start() error {
|
|||
var statusHandler = status.NewHandler(requestBouncer, server.Status)
|
||||
|
||||
var templatesHandler = templates.NewHandler(requestBouncer)
|
||||
templatesHandler.SettingsService = server.SettingsService
|
||||
templatesHandler.TemplateService = server.TemplateService
|
||||
|
||||
var uploadHandler = upload.NewHandler(requestBouncer)
|
||||
uploadHandler.FileService = server.FileService
|
||||
|
|
103
api/portainer.go
103
api/portainer.go
|
@ -21,6 +21,7 @@ type (
|
|||
NoAuth *bool
|
||||
NoAnalytics *bool
|
||||
Templates *string
|
||||
TemplateFile *string
|
||||
TLS *bool
|
||||
TLSSkipVerify *bool
|
||||
TLSCacert *string
|
||||
|
@ -68,16 +69,16 @@ type (
|
|||
|
||||
// Settings represents the application settings.
|
||||
Settings struct {
|
||||
TemplatesURL string `json:"TemplatesURL"`
|
||||
LogoURL string `json:"LogoURL"`
|
||||
BlackListedLabels []Pair `json:"BlackListedLabels"`
|
||||
DisplayExternalContributors bool `json:"DisplayExternalContributors"`
|
||||
AuthenticationMethod AuthenticationMethod `json:"AuthenticationMethod"`
|
||||
LDAPSettings LDAPSettings `json:"LDAPSettings"`
|
||||
AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"`
|
||||
AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"`
|
||||
// Deprecated fields
|
||||
DisplayDonationHeader bool
|
||||
DisplayDonationHeader bool
|
||||
DisplayExternalContributors bool
|
||||
TemplatesURL string
|
||||
}
|
||||
|
||||
// User represents a user account.
|
||||
|
@ -277,6 +278,79 @@ type (
|
|||
Name string `json:"Name"`
|
||||
}
|
||||
|
||||
// TemplateID represents a template identifier.
|
||||
TemplateID int
|
||||
|
||||
// TemplateType represents the type of a template.
|
||||
TemplateType int
|
||||
|
||||
// Template represents an application template.
|
||||
Template struct {
|
||||
// Mandatory container/stack fields
|
||||
ID TemplateID `json:"Id"`
|
||||
Type TemplateType `json:"type"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
AdministratorOnly bool `json:"administrator_only"`
|
||||
|
||||
// Mandatory container fields
|
||||
Image string `json:"image"`
|
||||
|
||||
// Mandatory stack fields
|
||||
Repository TemplateRepository `json:"repository"`
|
||||
|
||||
// Optional stack/container fields
|
||||
Name string `json:"name,omitempty"`
|
||||
Logo string `json:"logo,omitempty"`
|
||||
Env []TemplateEnv `json:"env,omitempty"`
|
||||
Note string `json:"note,omitempty"`
|
||||
Platform string `json:"platform,omitempty"`
|
||||
Categories []string `json:"categories,omitempty"`
|
||||
|
||||
// Optional container fields
|
||||
Registry string `json:"registry,omitempty"`
|
||||
Command string `json:"command,omitempty"`
|
||||
Network string `json:"network,omitempty"`
|
||||
Volumes []TemplateVolume `json:"volumes,omitempty"`
|
||||
Ports []string `json:"ports,omitempty"`
|
||||
Labels []Pair `json:"labels,omitempty"`
|
||||
Privileged bool `json:"privileged,omitempty"`
|
||||
Interactive bool `json:"interactive,omitempty"`
|
||||
RestartPolicy string `json:"restart_policy,omitempty"`
|
||||
Hostname string `json:"hostname,omitempty"`
|
||||
}
|
||||
|
||||
// TemplateEnv represents a template environment variable configuration.
|
||||
TemplateEnv struct {
|
||||
Name string `json:"name"`
|
||||
Label string `json:"label,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Default string `json:"default,omitempty"`
|
||||
Preset bool `json:"preset,omitempty"`
|
||||
Select []TemplateEnvSelect `json:"select,omitempty"`
|
||||
}
|
||||
|
||||
// TemplateVolume represents a template volume configuration.
|
||||
TemplateVolume struct {
|
||||
Container string `json:"container"`
|
||||
Bind string `json:"bind,omitempty"`
|
||||
ReadOnly bool `json:"readonly,omitempty"`
|
||||
}
|
||||
|
||||
// TemplateRepository represents the git repository configuration for a template.
|
||||
TemplateRepository struct {
|
||||
URL string `json:"url"`
|
||||
StackFile string `json:"stackfile"`
|
||||
}
|
||||
|
||||
// TemplateEnvSelect represents text/value pair that will be displayed as a choice for the
|
||||
// template user.
|
||||
TemplateEnvSelect struct {
|
||||
Text string `json:"text"`
|
||||
Value string `json:"value"`
|
||||
Default bool `json:"default"`
|
||||
}
|
||||
|
||||
// ResourceAccessLevel represents the level of control associated to a resource.
|
||||
ResourceAccessLevel int
|
||||
|
||||
|
@ -411,6 +485,15 @@ type (
|
|||
DeleteTag(ID TagID) 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
|
||||
}
|
||||
|
||||
// CryptoService represents a service for encrypting/hashing data.
|
||||
CryptoService interface {
|
||||
Hash(data string) (string, error)
|
||||
|
@ -434,7 +517,7 @@ type (
|
|||
|
||||
// FileService represents a service for managing files.
|
||||
FileService interface {
|
||||
GetFileContent(filePath string) (string, error)
|
||||
GetFileContent(filePath string) ([]byte, error)
|
||||
Rename(oldPath, newPath string) error
|
||||
RemoveDirectory(directoryPath string) error
|
||||
StoreTLSFileFromBytes(folder string, fileType TLSFileType, data []byte) (string, error)
|
||||
|
@ -487,8 +570,6 @@ const (
|
|||
APIVersion = "1.18.2-dev"
|
||||
// DBVersion is the version number of the Portainer database.
|
||||
DBVersion = 12
|
||||
// DefaultTemplatesURL represents the default URL for the templates definitions.
|
||||
DefaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates.json"
|
||||
// PortainerAgentHeader represents the name of the header available in any agent response
|
||||
PortainerAgentHeader = "Portainer-Agent"
|
||||
// PortainerAgentTargetHeader represent the name of the header containing the target node name.
|
||||
|
@ -582,3 +663,13 @@ const (
|
|||
// DockerComposeStack represents a stack managed via docker-compose
|
||||
DockerComposeStack
|
||||
)
|
||||
|
||||
const (
|
||||
_ TemplateType = iota
|
||||
// ContainerTemplate represents a container template
|
||||
ContainerTemplate
|
||||
// SwarmStackTemplate represents a template used to deploy a Swarm stack
|
||||
SwarmStackTemplate
|
||||
// ComposeStackTemplate represents a template used to deploy a Compose stack
|
||||
ComposeStackTemplate
|
||||
)
|
||||
|
|
529
api/swagger.yaml
529
api/swagger.yaml
|
@ -2530,32 +2530,189 @@ paths:
|
|||
get:
|
||||
tags:
|
||||
- "templates"
|
||||
summary: "Retrieve App templates"
|
||||
summary: "List available templates"
|
||||
description: |
|
||||
Retrieve App templates.
|
||||
You can find more information about the format at http://portainer.readthedocs.io/en/stable/templates.html
|
||||
**Access policy**: authenticated
|
||||
List available templates.
|
||||
Administrator templates will not be listed for non-administrator users.
|
||||
**Access policy**: restricted
|
||||
operationId: "TemplateList"
|
||||
produces:
|
||||
- "application/json"
|
||||
parameters:
|
||||
- name: "key"
|
||||
in: "query"
|
||||
required: true
|
||||
description: "Templates key. Valid values are 'container' or 'linuxserver.io'."
|
||||
type: "string"
|
||||
responses:
|
||||
200:
|
||||
description: "Success"
|
||||
schema:
|
||||
$ref: "#/definitions/TemplateListResponse"
|
||||
500:
|
||||
description: "Server error"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
post:
|
||||
tags:
|
||||
- "templates"
|
||||
summary: "Create a new template"
|
||||
description: |
|
||||
Create a new template.
|
||||
**Access policy**: administrator
|
||||
operationId: "TemplateCreate"
|
||||
consumes:
|
||||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
parameters:
|
||||
- in: "body"
|
||||
name: "body"
|
||||
description: "Template details"
|
||||
required: true
|
||||
schema:
|
||||
$ref: "#/definitions/TemplateCreateRequest"
|
||||
responses:
|
||||
200:
|
||||
description: "Success"
|
||||
schema:
|
||||
$ref: "#/definitions/Template"
|
||||
400:
|
||||
description: "Invalid request"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
examples:
|
||||
application/json:
|
||||
err: "Invalid query format"
|
||||
err: "Invalid request data format"
|
||||
403:
|
||||
description: "Unauthorized"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
examples:
|
||||
application/json:
|
||||
err: "Access denied to resource"
|
||||
500:
|
||||
description: "Server error"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
/templates/{id}:
|
||||
get:
|
||||
tags:
|
||||
- "templates"
|
||||
summary: "Inspect a template"
|
||||
description: |
|
||||
Retrieve details about a template.
|
||||
**Access policy**: administrator
|
||||
operationId: "TemplateInspect"
|
||||
produces:
|
||||
- "application/json"
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
description: "Template identifier"
|
||||
required: true
|
||||
type: "integer"
|
||||
responses:
|
||||
200:
|
||||
description: "Success"
|
||||
schema:
|
||||
$ref: "#/definitions/Template"
|
||||
400:
|
||||
description: "Invalid request"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
examples:
|
||||
application/json:
|
||||
err: "Invalid request"
|
||||
403:
|
||||
description: "Unauthorized"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
examples:
|
||||
application/json:
|
||||
err: "Access denied to resource"
|
||||
404:
|
||||
description: "Template not found"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
examples:
|
||||
application/json:
|
||||
err: "Template not found"
|
||||
500:
|
||||
description: "Server error"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
put:
|
||||
tags:
|
||||
- "templates"
|
||||
summary: "Update a template"
|
||||
description: |
|
||||
Update a template.
|
||||
**Access policy**: administrator
|
||||
operationId: "TemplateUpdate"
|
||||
consumes:
|
||||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
description: "Template identifier"
|
||||
required: true
|
||||
type: "integer"
|
||||
- in: "body"
|
||||
name: "body"
|
||||
description: "Template details"
|
||||
required: true
|
||||
schema:
|
||||
$ref: "#/definitions/TemplateUpdateRequest"
|
||||
responses:
|
||||
200:
|
||||
description: "Success"
|
||||
400:
|
||||
description: "Invalid request"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
examples:
|
||||
application/json:
|
||||
err: "Invalid request data format"
|
||||
403:
|
||||
description: "Unauthorized"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
examples:
|
||||
application/json:
|
||||
err: "Access denied to resource"
|
||||
404:
|
||||
description: "Template not found"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
examples:
|
||||
application/json:
|
||||
err: "Template not found"
|
||||
500:
|
||||
description: "Server error"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
delete:
|
||||
tags:
|
||||
- "templates"
|
||||
summary: "Remove a template"
|
||||
description: |
|
||||
Remove a template.
|
||||
**Access policy**: administrator
|
||||
operationId: "TemplateDelete"
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
description: "Template identifier"
|
||||
required: true
|
||||
type: "integer"
|
||||
responses:
|
||||
204:
|
||||
description: "Success"
|
||||
400:
|
||||
description: "Invalid request"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
examples:
|
||||
application/json:
|
||||
err: "Invalid request"
|
||||
500:
|
||||
description: "Server error"
|
||||
schema:
|
||||
|
@ -3602,9 +3759,17 @@ definitions:
|
|||
type: "array"
|
||||
items:
|
||||
$ref: "#/definitions/Template"
|
||||
Template:
|
||||
TemplateCreateRequest:
|
||||
type: "object"
|
||||
required:
|
||||
- "type"
|
||||
- "title"
|
||||
- "description"
|
||||
properties:
|
||||
type:
|
||||
type: "integer"
|
||||
example: 1
|
||||
description: "Template type. Valid values are: 1 (container), 2 (Swarm stack) or 3 (Compose stack)"
|
||||
title:
|
||||
type: "string"
|
||||
example: "Nginx"
|
||||
|
@ -3613,14 +3778,354 @@ definitions:
|
|||
type: "string"
|
||||
example: "High performance web server"
|
||||
description: "Description of the template"
|
||||
administrator_only:
|
||||
type: "boolean"
|
||||
example: true
|
||||
description: "Whether the template should be available to administrators only"
|
||||
image:
|
||||
type: "string"
|
||||
example: "nginx:latest"
|
||||
description: "Image associated to a container template. Mandatory for a container template"
|
||||
repository:
|
||||
$ref: "#/definitions/TemplateRepository"
|
||||
name:
|
||||
type: "string"
|
||||
example: "mystackname"
|
||||
description: "Default name for the stack/container to be used on deployment"
|
||||
logo:
|
||||
type: "string"
|
||||
example: "https://cloudinovasi.id/assets/img/logos/nginx.png"
|
||||
description: "URL of the template's logo"
|
||||
env:
|
||||
type: "array"
|
||||
description: "A list of environment variables used during the template deployment"
|
||||
items:
|
||||
$ref: "#/definitions/TemplateEnv"
|
||||
note:
|
||||
type: "string"
|
||||
example: "This is my <b>custom</b> template"
|
||||
description: "A note that will be displayed in the UI. Supports HTML content"
|
||||
platform:
|
||||
type: "string"
|
||||
example: "linux"
|
||||
description: "Platform associated to the template. Valid values are: 'linux', 'windows' or leave empty for multi-platform"
|
||||
categories:
|
||||
type: "array"
|
||||
description: "A list of categories associated to the template"
|
||||
items:
|
||||
type: "string"
|
||||
exampe: "database"
|
||||
registry:
|
||||
type: "string"
|
||||
example: "quay.io"
|
||||
description: "The URL of a registry associated to the image for a container template"
|
||||
command:
|
||||
type: "string"
|
||||
example: "ls -lah"
|
||||
description: "The command that will be executed in a container template"
|
||||
network:
|
||||
type: "string"
|
||||
example: "mynet"
|
||||
description: "Name of a network that will be used on container deployment if it exists inside the environment"
|
||||
volumes:
|
||||
type: "array"
|
||||
description: "A list of volumes used during the container template deployment"
|
||||
items:
|
||||
$ref: "#/definitions/TemplateVolume"
|
||||
ports:
|
||||
type: "array"
|
||||
description: "A list of ports exposed by the container"
|
||||
items:
|
||||
type: "string"
|
||||
example: "8080:80/tcp"
|
||||
labels:
|
||||
type: "array"
|
||||
description: "Container labels"
|
||||
items:
|
||||
$ref: '#/definitions/Pair'
|
||||
privileged:
|
||||
type: "boolean"
|
||||
example: true
|
||||
description: "Whether the container should be started in privileged mode"
|
||||
interactive:
|
||||
type: "boolean"
|
||||
example: true
|
||||
description: "Whether the container should be started in interactive mode (-i -t equivalent on the CLI)"
|
||||
restart_policy:
|
||||
type: "string"
|
||||
example: "on-failure"
|
||||
description: "Container restart policy"
|
||||
hostname:
|
||||
type: "string"
|
||||
example: "mycontainer"
|
||||
description: "Container hostname"
|
||||
TemplateUpdateRequest:
|
||||
type: "object"
|
||||
properties:
|
||||
type:
|
||||
type: "integer"
|
||||
example: 1
|
||||
description: "Template type. Valid values are: 1 (container), 2 (Swarm stack) or 3 (Compose stack)"
|
||||
title:
|
||||
type: "string"
|
||||
example: "Nginx"
|
||||
description: "Title of the template"
|
||||
description:
|
||||
type: "string"
|
||||
example: "High performance web server"
|
||||
description: "Description of the template"
|
||||
administrator_only:
|
||||
type: "boolean"
|
||||
example: true
|
||||
description: "Whether the template should be available to administrators only"
|
||||
image:
|
||||
type: "string"
|
||||
example: "nginx:latest"
|
||||
description: "The Docker image associated to the template"
|
||||
description: "Image associated to a container template. Mandatory for a container template"
|
||||
repository:
|
||||
$ref: "#/definitions/TemplateRepository"
|
||||
name:
|
||||
type: "string"
|
||||
example: "mystackname"
|
||||
description: "Default name for the stack/container to be used on deployment"
|
||||
logo:
|
||||
type: "string"
|
||||
example: "https://cloudinovasi.id/assets/img/logos/nginx.png"
|
||||
description: "URL of the template's logo"
|
||||
env:
|
||||
type: "array"
|
||||
description: "A list of environment variables used during the template deployment"
|
||||
items:
|
||||
$ref: "#/definitions/TemplateEnv"
|
||||
note:
|
||||
type: "string"
|
||||
example: "This is my <b>custom</b> template"
|
||||
description: "A note that will be displayed in the UI. Supports HTML content"
|
||||
platform:
|
||||
type: "string"
|
||||
example: "linux"
|
||||
description: "Platform associated to the template. Valid values are: 'linux', 'windows' or leave empty for multi-platform"
|
||||
categories:
|
||||
type: "array"
|
||||
description: "A list of categories associated to the template"
|
||||
items:
|
||||
type: "string"
|
||||
exampe: "database"
|
||||
registry:
|
||||
type: "string"
|
||||
example: "quay.io"
|
||||
description: "The URL of a registry associated to the image for a container template"
|
||||
command:
|
||||
type: "string"
|
||||
example: "ls -lah"
|
||||
description: "The command that will be executed in a container template"
|
||||
network:
|
||||
type: "string"
|
||||
example: "mynet"
|
||||
description: "Name of a network that will be used on container deployment if it exists inside the environment"
|
||||
volumes:
|
||||
type: "array"
|
||||
description: "A list of volumes used during the container template deployment"
|
||||
items:
|
||||
$ref: "#/definitions/TemplateVolume"
|
||||
ports:
|
||||
type: "array"
|
||||
description: "A list of ports exposed by the container"
|
||||
items:
|
||||
type: "string"
|
||||
example: "8080:80/tcp"
|
||||
labels:
|
||||
type: "array"
|
||||
description: "Container labels"
|
||||
items:
|
||||
$ref: '#/definitions/Pair'
|
||||
privileged:
|
||||
type: "boolean"
|
||||
example: true
|
||||
description: "Whether the container should be started in privileged mode"
|
||||
interactive:
|
||||
type: "boolean"
|
||||
example: true
|
||||
description: "Whether the container should be started in interactive mode (-i -t equivalent on the CLI)"
|
||||
restart_policy:
|
||||
type: "string"
|
||||
example: "on-failure"
|
||||
description: "Container restart policy"
|
||||
hostname:
|
||||
type: "string"
|
||||
example: "mycontainer"
|
||||
description: "Container hostname"
|
||||
Template:
|
||||
type: "object"
|
||||
properties:
|
||||
id:
|
||||
type: "integer"
|
||||
example: 1
|
||||
description: "Template identifier"
|
||||
type:
|
||||
type: "integer"
|
||||
example: 1
|
||||
description: "Template type. Valid values are: 1 (container), 2 (Swarm stack) or 3 (Compose stack)"
|
||||
title:
|
||||
type: "string"
|
||||
example: "Nginx"
|
||||
description: "Title of the template"
|
||||
description:
|
||||
type: "string"
|
||||
example: "High performance web server"
|
||||
description: "Description of the template"
|
||||
administrator_only:
|
||||
type: "boolean"
|
||||
example: true
|
||||
description: "Whether the template should be available to administrators only"
|
||||
image:
|
||||
type: "string"
|
||||
example: "nginx:latest"
|
||||
description: "Image associated to a container template. Mandatory for a container template"
|
||||
repository:
|
||||
$ref: "#/definitions/TemplateRepository"
|
||||
name:
|
||||
type: "string"
|
||||
example: "mystackname"
|
||||
description: "Default name for the stack/container to be used on deployment"
|
||||
logo:
|
||||
type: "string"
|
||||
example: "https://cloudinovasi.id/assets/img/logos/nginx.png"
|
||||
description: "URL of the template's logo"
|
||||
env:
|
||||
type: "array"
|
||||
description: "A list of environment variables used during the template deployment"
|
||||
items:
|
||||
$ref: "#/definitions/TemplateEnv"
|
||||
note:
|
||||
type: "string"
|
||||
example: "This is my <b>custom</b> template"
|
||||
description: "A note that will be displayed in the UI. Supports HTML content"
|
||||
platform:
|
||||
type: "string"
|
||||
example: "linux"
|
||||
description: "Platform associated to the template. Valid values are: 'linux', 'windows' or leave empty for multi-platform"
|
||||
categories:
|
||||
type: "array"
|
||||
description: "A list of categories associated to the template"
|
||||
items:
|
||||
type: "string"
|
||||
exampe: "database"
|
||||
registry:
|
||||
type: "string"
|
||||
example: "quay.io"
|
||||
description: "The URL of a registry associated to the image for a container template"
|
||||
command:
|
||||
type: "string"
|
||||
example: "ls -lah"
|
||||
description: "The command that will be executed in a container template"
|
||||
network:
|
||||
type: "string"
|
||||
example: "mynet"
|
||||
description: "Name of a network that will be used on container deployment if it exists inside the environment"
|
||||
volumes:
|
||||
type: "array"
|
||||
description: "A list of volumes used during the container template deployment"
|
||||
items:
|
||||
$ref: "#/definitions/TemplateVolume"
|
||||
ports:
|
||||
type: "array"
|
||||
description: "A list of ports exposed by the container"
|
||||
items:
|
||||
type: "string"
|
||||
example: "8080:80/tcp"
|
||||
labels:
|
||||
type: "array"
|
||||
description: "Container labels"
|
||||
items:
|
||||
$ref: '#/definitions/Pair'
|
||||
privileged:
|
||||
type: "boolean"
|
||||
example: true
|
||||
description: "Whether the container should be started in privileged mode"
|
||||
interactive:
|
||||
type: "boolean"
|
||||
example: true
|
||||
description: "Whether the container should be started in interactive mode (-i -t equivalent on the CLI)"
|
||||
restart_policy:
|
||||
type: "string"
|
||||
example: "on-failure"
|
||||
description: "Container restart policy"
|
||||
hostname:
|
||||
type: "string"
|
||||
example: "mycontainer"
|
||||
description: "Container hostname"
|
||||
TemplateVolume:
|
||||
type: "object"
|
||||
properties:
|
||||
container:
|
||||
type: "string"
|
||||
example: "/data"
|
||||
description: "Path inside the container"
|
||||
bind:
|
||||
type: "string"
|
||||
example: "/tmp"
|
||||
description: "Path on the host"
|
||||
readonly:
|
||||
type: "boolean"
|
||||
example: true
|
||||
description: "Whether the volume used should be readonly"
|
||||
TemplateEnv:
|
||||
type: "object"
|
||||
properties:
|
||||
name:
|
||||
type: "string"
|
||||
example: "MYSQL_ROOT_PASSWORD"
|
||||
description: "name of the environment variable"
|
||||
label:
|
||||
type: "string"
|
||||
example: "Root password"
|
||||
description: "Text for the label that will be generated in the UI"
|
||||
description:
|
||||
type: "string"
|
||||
example: "MySQL root account password"
|
||||
description: "Content of the tooltip that will be generated in the UI"
|
||||
default:
|
||||
type: "string"
|
||||
example: "default_value"
|
||||
description: "Default value that will be set for the variable"
|
||||
preset:
|
||||
type: "boolean"
|
||||
example: true
|
||||
description: "If set to true, will not generate any input for this variable in the UI"
|
||||
select:
|
||||
type: "array"
|
||||
description: "A list of name/value that will be used to generate a dropdown in the UI"
|
||||
items:
|
||||
$ref: '#/definitions/TemplateEnvSelect'
|
||||
TemplateEnvSelect:
|
||||
type: "object"
|
||||
properties:
|
||||
text:
|
||||
type: "string"
|
||||
example: "text value"
|
||||
description: "Some text that will displayed as a choice"
|
||||
value:
|
||||
type: "string"
|
||||
example: "value"
|
||||
description: "A value that will be associated to the choice"
|
||||
default:
|
||||
type: "boolean"
|
||||
example: true
|
||||
description: "Will set this choice as the default choice"
|
||||
TemplateRepository:
|
||||
type: "object"
|
||||
required:
|
||||
- "URL"
|
||||
properties:
|
||||
URL:
|
||||
type: "string"
|
||||
example: "https://github.com/portainer/portainer-compose"
|
||||
description: "URL of a git repository used to deploy a stack template. Mandatory for a Swarm/Compose stack template"
|
||||
stackfile:
|
||||
type: "string"
|
||||
example: "./subfolder/docker-compose.yml"
|
||||
description: "Path to the stack file inside the git repository"
|
||||
StackMigrateRequest:
|
||||
type: "object"
|
||||
required:
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue