Compare commits

...

14 Commits

Author SHA1 Message Date
Anthony Lapenna
5dc6374b10 refactor(azure): remove Azure ACI endpoint support 2020-05-19 15:07:47 +12:00
Anthony Lapenna
74135501ea feat(templates): remove template management features (#3719)
* feat(api): remove template management features

* feat(templates): remove template management features
2020-05-19 15:07:46 +12:00
Anthony Lapenna
3d03184897 refactor(api): remove unused error constant 2020-05-19 15:06:29 +12:00
Chaim Lev-Ari
41722c0304 feat(server): remove external endpoint feature (#3837)
* fix(prettier): auto format html files (#3836)

* refactor(main): remove reference to external endpoints

* refactor(cli): remove parsing of external endpoints param

* refactor(portainer): remove types for external endpoints

* refactor(endpoints): remove warning for external endpoints

* refactor(endpoints): remove endpoint management setting

* refactor(endpoints): remove ref to endpoint management

* fix(main): remove endpoint management
2020-05-18 20:29:37 +12:00
Anthony Lapenna
4a2a11b820 fix(api): update to template file format for Edge templates 2020-05-18 12:20:27 +12:00
Anthony Lapenna
dfc1441d30 feat(cli): remove the --no-snapshot CLI flag (#3814) 2020-05-17 16:21:55 +12:00
Anthony Lapenna
0494c12287 feat(project): remove pulldog 2020-05-17 16:20:43 +12:00
Anthony Lapenna
67d032c5f6 feat(api): bump ldap library version (#3772)
* feat(api): bump ldap library version

* feat(api): fix ldap v3 import
2020-05-17 16:20:07 +12:00
Anthony Lapenna
3bd0f2ec7d feat(templates): leftovers cleanup (#3762)
* feat(templates): leftovers cleanup

* feat(templates): update CLIFlags structure
2020-05-17 16:20:07 +12:00
Simone Cattaneo
45acbbb8fe fix(api): updated LDAP library to v3 (portainer#3244) (#3386)
Co-authored-by: Anthony Lapenna <anthony.lapenna@portainer.io>
2020-05-17 16:20:07 +12:00
Anthony Lapenna
ff74729283 chore(version): bump version number 2020-05-17 16:20:07 +12:00
Maxime Bajeux
dfc7a4354f feat(templates): support templates versioning (#3729)
* feat(templates): Support templates versioning format

* Update app/portainer/models/template.js

Co-authored-by: Anthony Lapenna <anthony.lapenna@portainer.io>
2020-05-17 16:20:07 +12:00
Anthony Lapenna
47662f8fad feat(templates): fix an issue with templates initialization and update settings view 2020-05-17 16:20:05 +12:00
Anthony Lapenna
b01025cc3e feat(templates): remove template management features (#3719)
* feat(api): remove template management features

* feat(templates): remove template management features
2020-05-17 16:18:29 +12:00
116 changed files with 247 additions and 4217 deletions

View File

@@ -5,16 +5,14 @@ import (
"path"
"time"
"github.com/portainer/portainer/api/bolt/edgegroup"
"github.com/portainer/portainer/api/bolt/edgestack"
"github.com/portainer/portainer/api/bolt/endpointrelation"
"github.com/portainer/portainer/api/bolt/tunnelserver"
"github.com/boltdb/bolt"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/dockerhub"
"github.com/portainer/portainer/api/bolt/edgegroup"
"github.com/portainer/portainer/api/bolt/edgestack"
"github.com/portainer/portainer/api/bolt/endpoint"
"github.com/portainer/portainer/api/bolt/endpointgroup"
"github.com/portainer/portainer/api/bolt/endpointrelation"
"github.com/portainer/portainer/api/bolt/extension"
"github.com/portainer/portainer/api/bolt/migrator"
"github.com/portainer/portainer/api/bolt/registry"
@@ -26,7 +24,7 @@ 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/tunnelserver"
"github.com/portainer/portainer/api/bolt/user"
"github.com/portainer/portainer/api/bolt/version"
"github.com/portainer/portainer/api/bolt/webhook"
@@ -41,7 +39,7 @@ const (
type Store struct {
path string
db *bolt.DB
checkForDataMigration bool
isNew bool
fileService portainer.FileService
RoleService *role.Service
DockerHubService *dockerhub.Service
@@ -58,7 +56,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
@@ -71,6 +68,7 @@ func NewStore(storePath string, fileService portainer.FileService) (*Store, erro
store := &Store{
path: storePath,
fileService: fileService,
isNew: true,
}
databasePath := path.Join(storePath, databaseFileName)
@@ -79,10 +77,8 @@ func NewStore(storePath string, fileService portainer.FileService) (*Store, erro
return nil, err
}
if !databaseFileExists {
store.checkForDataMigration = false
} else {
store.checkForDataMigration = true
if databaseFileExists {
store.isNew = false
}
return store, nil
@@ -108,9 +104,16 @@ func (store *Store) Close() error {
return nil
}
// IsNew returns true if the database was just created and false if it is re-using
// existing data.
func (store *Store) IsNew() bool {
return store.isNew
}
// MigrateData automatically migrate the data based on the DBVersion.
// This process is only triggered on an existing database, not if the database was just created.
func (store *Store) MigrateData() error {
if !store.checkForDataMigration {
if store.isNew {
return store.VersionService.StoreDBVersion(portainer.DBVersion)
}
@@ -137,7 +140,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 +248,6 @@ func (store *Store) initServices() error {
}
store.TeamService = teamService
templateService, err := template.NewService(store.db)
if err != nil {
return err
}
store.TemplateService = templateService
tunnelServerService, err := tunnelserver.NewService(store.db)
if err != nil {
return err

View File

@@ -4,6 +4,55 @@ import portainer "github.com/portainer/portainer/api"
// Init creates the default data set.
func (store *Store) Init() error {
_, err := store.SettingsService.Settings()
if err == portainer.ErrObjectNotFound {
defaultSettings := &portainer.Settings{
AuthenticationMethod: portainer.AuthenticationInternal,
BlackListedLabels: make([]portainer.Pair, 0),
LDAPSettings: portainer.LDAPSettings{
AnonymousMode: true,
AutoCreateUsers: true,
TLSConfig: portainer.TLSConfiguration{},
SearchSettings: []portainer.LDAPSearchSettings{
portainer.LDAPSearchSettings{},
},
GroupSearchSettings: []portainer.LDAPGroupSearchSettings{
portainer.LDAPGroupSearchSettings{},
},
},
OAuthSettings: portainer.OAuthSettings{},
AllowBindMountsForRegularUsers: true,
AllowPrivilegedModeForRegularUsers: true,
AllowVolumeBrowserForRegularUsers: false,
EnableHostManagementFeatures: false,
EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds,
TemplatesURL: portainer.DefaultTemplatesURL,
}
err = store.SettingsService.UpdateSettings(defaultSettings)
if err != nil {
return err
}
} else if err != nil {
return err
}
_, err = store.DockerHubService.DockerHub()
if err == portainer.ErrObjectNotFound {
defaultDockerHub := &portainer.DockerHub{
Authentication: false,
Username: "",
Password: "",
}
err := store.DockerHubService.UpdateDockerHub(defaultDockerHub)
if err != nil {
return err
}
} else if err != nil {
return err
}
groups, err := store.EndpointGroupService.EndpointGroups()
if err != nil {
return err

View File

@@ -1,11 +1,5 @@
package migrator
import (
"strings"
"github.com/portainer/portainer/api"
)
func (m *Migrator) updateSettingsToDBVersion15() error {
legacySettings, err := m.settingsService.Settings()
if err != nil {
@@ -17,19 +11,6 @@ func (m *Migrator) updateSettingsToDBVersion15() error {
}
func (m *Migrator) updateTemplatesToVersion15() error {
legacyTemplates, err := m.templateService.Templates()
if err != nil {
return err
}
for _, template := range legacyTemplates {
template.Logo = strings.Replace(template.Logo, "https://portainer.io/images", portainer.AssetsServerURL, -1)
err = m.templateService.UpdateTemplate(template.ID, &template)
if err != nil {
return err
}
}
// Removed with the entire template management layer, part of https://github.com/portainer/portainer/issues/3707
return nil
}

View File

@@ -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,
@@ -309,7 +305,7 @@ func (m *Migrator) Migrate() error {
}
}
// Portainer 1.24.0-dev
// Portainer 1.24.0
if m.currentDBVersion < 23 {
err := m.updateTagsToDBVersion23()
if err != nil {

View File

@@ -1,95 +0,0 @@
package template
import (
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
"github.com/boltdb/bolt"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "templates"
)
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
}, nil
}
// Templates return an array containing all the templates.
func (service *Service) Templates() ([]portainer.Template, error) {
var templates = make([]portainer.Template, 0)
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var template portainer.Template
err := internal.UnmarshalObject(v, &template)
if err != nil {
return err
}
templates = append(templates, template)
}
return nil
})
return templates, err
}
// Template returns a template by ID.
func (service *Service) Template(ID portainer.TemplateID) (*portainer.Template, error) {
var template portainer.Template
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &template)
if err != nil {
return nil, err
}
return &template, nil
}
// CreateTemplate creates a new template.
func (service *Service) CreateTemplate(template *portainer.Template) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()
template.ID = portainer.TemplateID(id)
data, err := internal.MarshalObject(template)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(template.ID)), data)
})
}
// UpdateTemplate saves a template.
func (service *Service) UpdateTemplate(ID portainer.TemplateID, template *portainer.Template) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, template)
}
// DeleteTemplate deletes a template.
func (service *Service) DeleteTemplate(ID portainer.TemplateID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
}

View File

@@ -19,11 +19,7 @@ type Service struct{}
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")
errNoAuthExcludeAdminPassword = portainer.Error("Cannot use --no-auth with --admin-password or --admin-password-file")
errAdminPassExcludeAdminPassFile = portainer.Error("Cannot use --admin-password with --admin-password-file")
)
@@ -39,7 +35,6 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
Assets: kingpin.Flag("assets", "Path to the assets").Default(defaultAssetsDirectory).Short('a').String(),
Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default(defaultDataDirectory).Short('d').String(),
EndpointURL: kingpin.Flag("host", "Endpoint URL").Short('H').String(),
ExternalEndpoints: kingpin.Flag("external-endpoints", "Path to a file defining available endpoints (deprecated)").String(),
NoAuth: kingpin.Flag("no-auth", "Disable authentication (deprecated)").Default(defaultNoAuth).Bool(),
NoAnalytics: kingpin.Flag("no-analytics", "Disable Analytics in app").Default(defaultNoAnalytics).Bool(),
TLS: kingpin.Flag("tlsverify", "TLS support").Default(defaultTLS).Bool(),
@@ -50,15 +45,12 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
SSL: kingpin.Flag("ssl", "Secure Portainer instance using SSL").Default(defaultSSL).Bool(),
SSLCert: kingpin.Flag("sslcert", "Path to the SSL certificate used to secure the Portainer instance").Default(defaultSSLCertPath).String(),
SSLKey: kingpin.Flag("sslkey", "Path to the SSL key used to secure the Portainer instance").Default(defaultSSLKeyPath).String(),
SyncInterval: kingpin.Flag("sync-interval", "Duration between each synchronization via the external endpoints source (deprecated)").Default(defaultSyncInterval).String(),
Snapshot: kingpin.Flag("snapshot", "Start a background job to create endpoint snapshots (deprecated)").Default(defaultSnapshot).Bool(),
SnapshotInterval: kingpin.Flag("snapshot-interval", "Duration between each endpoint snapshot job").Default(defaultSnapshotInterval).String(),
AdminPassword: kingpin.Flag("admin-password", "Hashed admin password").String(),
AdminPasswordFile: kingpin.Flag("admin-password-file", "Path to the file containing the password for the admin user").String(),
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()
@@ -79,26 +71,7 @@ func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
displayDeprecationWarnings(flags)
if *flags.EndpointURL != "" && *flags.ExternalEndpoints != "" {
return errEndpointExcludeExternal
}
err := validateTemplateFile(*flags.TemplateFile)
if err != nil {
return err
}
err = validateEndpointURL(*flags.EndpointURL)
if err != nil {
return err
}
err = validateExternalEndpoints(*flags.ExternalEndpoints)
if err != nil {
return err
}
err = validateSyncInterval(*flags.SyncInterval)
err := validateEndpointURL(*flags.EndpointURL)
if err != nil {
return err
}
@@ -120,25 +93,9 @@ func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
}
func displayDeprecationWarnings(flags *portainer.CLIFlags) {
if *flags.ExternalEndpoints != "" {
log.Println("Warning: the --external-endpoint flag is deprecated and will likely be removed in a future version of Portainer.")
}
if *flags.SyncInterval != defaultSyncInterval {
log.Println("Warning: the --sync-interval flag is deprecated and will likely be removed in a future version of Portainer.")
}
if *flags.NoAuth {
log.Println("Warning: the --no-auth flag is deprecated and will likely be removed in a future version of Portainer.")
}
if !*flags.Snapshot {
log.Println("Warning: the --no-snapshot flag is deprecated and will likely be removed in a future version of Portainer.")
}
if *flags.TemplateFile != "" {
log.Println("Warning: the --template-file flag is deprecated and will likely be removed in a future version of Portainer.")
}
}
func validateEndpointURL(endpointURL string) error {
@@ -161,38 +118,6 @@ func validateEndpointURL(endpointURL string) error {
return nil
}
func validateExternalEndpoints(externalEndpoints string) error {
if externalEndpoints != "" {
if _, err := os.Stat(externalEndpoints); err != nil {
if os.IsNotExist(err) {
return errEndpointsFileNotFound
}
return err
}
}
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)
if err != nil {
return errInvalidSyncInterval
}
}
return nil
}
func validateSnapshotInterval(snapshotInterval string) error {
if snapshotInterval != defaultSnapshotInterval {
_, err := time.ParseDuration(snapshotInterval)

View File

@@ -18,8 +18,5 @@ const (
defaultSSL = "false"
defaultSSLCertPath = "/certs/portainer.crt"
defaultSSLKeyPath = "/certs/portainer.key"
defaultSyncInterval = "60s"
defaultSnapshot = "true"
defaultSnapshotInterval = "5m"
defaultTemplateFile = "/templates.json"
)

View File

@@ -16,8 +16,5 @@ const (
defaultSSL = "false"
defaultSSLCertPath = "C:\\certs\\portainer.crt"
defaultSSLKeyPath = "C:\\certs\\portainer.key"
defaultSyncInterval = "60s"
defaultSnapshot = "true"
defaultSnapshotInterval = "5m"
defaultTemplateFile = "/templates.json"
)

View File

@@ -1,7 +1,6 @@
package main
import (
"encoding/json"
"log"
"os"
"strings"
@@ -158,45 +157,6 @@ func loadSnapshotSystemSchedule(jobScheduler portainer.JobScheduler, snapshotter
return nil
}
func loadEndpointSyncSystemSchedule(jobScheduler portainer.JobScheduler, scheduleService portainer.ScheduleService, endpointService portainer.EndpointService, flags *portainer.CLIFlags) error {
if *flags.ExternalEndpoints == "" {
return nil
}
log.Println("Using external endpoint definition. Endpoint management via the API will be disabled.")
schedules, err := scheduleService.SchedulesByJobType(portainer.EndpointSyncJobType)
if err != nil {
return err
}
if len(schedules) != 0 {
return nil
}
endpointSyncJob := &portainer.EndpointSyncJob{}
endpointSyncSchedule := &portainer.Schedule{
ID: portainer.ScheduleID(scheduleService.GetNextIdentifier()),
Name: "system_endpointsync",
CronExpression: "@every " + *flags.SyncInterval,
Recurring: true,
JobType: portainer.EndpointSyncJobType,
EndpointSyncJob: endpointSyncJob,
Created: time.Now().Unix(),
}
endpointSyncJobContext := cron.NewEndpointSyncJobContext(endpointService, *flags.ExternalEndpoints)
endpointSyncJobRunner := cron.NewEndpointSyncJobRunner(endpointSyncSchedule, endpointSyncJobContext)
err = jobScheduler.ScheduleJob(endpointSyncJobRunner)
if err != nil {
return err
}
return scheduleService.CreateSchedule(endpointSyncSchedule)
}
func loadSchedulesFromDatabase(jobScheduler portainer.JobScheduler, jobService portainer.JobService, scheduleService portainer.ScheduleService, endpointService portainer.EndpointService, fileService portainer.FileService, reverseTunnelService portainer.ReverseTunnelService) error {
schedules, err := scheduleService.Schedules()
if err != nil {
@@ -226,121 +186,32 @@ func loadSchedulesFromDatabase(jobScheduler portainer.JobScheduler, jobService p
return nil
}
func initStatus(endpointManagement, snapshot bool, flags *portainer.CLIFlags) *portainer.Status {
func initStatus(flags *portainer.CLIFlags) *portainer.Status {
return &portainer.Status{
Analytics: !*flags.NoAnalytics,
Authentication: !*flags.NoAuth,
EndpointManagement: endpointManagement,
Snapshot: snapshot,
Version: portainer.APIVersion,
Analytics: !*flags.NoAnalytics,
Authentication: !*flags.NoAuth,
Version: portainer.APIVersion,
}
}
func initDockerHub(dockerHubService portainer.DockerHubService) error {
_, err := dockerHubService.DockerHub()
if err == portainer.ErrObjectNotFound {
dockerhub := &portainer.DockerHub{
Authentication: false,
Username: "",
Password: "",
}
return dockerHubService.UpdateDockerHub(dockerhub)
} else if err != nil {
return err
}
return nil
}
func initSettings(settingsService portainer.SettingsService, flags *portainer.CLIFlags) error {
_, err := settingsService.Settings()
if err == portainer.ErrObjectNotFound {
settings := &portainer.Settings{
LogoURL: *flags.Logo,
AuthenticationMethod: portainer.AuthenticationInternal,
LDAPSettings: portainer.LDAPSettings{
AnonymousMode: true,
AutoCreateUsers: true,
TLSConfig: portainer.TLSConfiguration{},
SearchSettings: []portainer.LDAPSearchSettings{
portainer.LDAPSearchSettings{},
},
GroupSearchSettings: []portainer.LDAPGroupSearchSettings{
portainer.LDAPGroupSearchSettings{},
},
},
OAuthSettings: portainer.OAuthSettings{},
AllowBindMountsForRegularUsers: true,
AllowPrivilegedModeForRegularUsers: true,
AllowVolumeBrowserForRegularUsers: false,
EnableHostManagementFeatures: false,
SnapshotInterval: *flags.SnapshotInterval,
EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds,
}
if *flags.Templates != "" {
settings.TemplatesURL = *flags.Templates
}
if *flags.Labels != nil {
settings.BlackListedLabels = *flags.Labels
} else {
settings.BlackListedLabels = make([]portainer.Pair, 0)
}
return settingsService.UpdateSettings(settings)
} else if err != nil {
return err
}
return nil
}
func initTemplates(templateService portainer.TemplateService, fileService portainer.FileService, templateURL, templateFile string) error {
if templateURL != "" {
log.Printf("Portainer started with the --templates flag. Using external templates, template management will be disabled.")
return nil
}
existingTemplates, err := templateService.Templates()
func updateSettingsFromFlags(settingsService portainer.SettingsService, flags *portainer.CLIFlags) error {
settings, err := settingsService.Settings()
if err != nil {
return err
}
if len(existingTemplates) != 0 {
log.Printf("Templates already registered inside the database. Skipping template import.")
return nil
settings.LogoURL = *flags.Logo
settings.SnapshotInterval = *flags.SnapshotInterval
if *flags.Templates != "" {
settings.TemplatesURL = *flags.Templates
}
templatesJSON, err := fileService.GetFileContent(templateFile)
if err != nil {
log.Println("Unable to retrieve template definitions via filesystem")
return err
if *flags.Labels != nil {
settings.BlackListedLabels = *flags.Labels
}
var templates []portainer.Template
err = json.Unmarshal(templatesJSON, &templates)
if err != nil {
log.Println("Unable to parse templates file. Please review your template definition file.")
return err
}
for _, template := range templates {
err := templateService.CreateTemplate(&template)
if err != nil {
return err
}
}
return nil
}
func retrieveFirstEndpointFromDatabase(endpointService portainer.EndpointService) *portainer.Endpoint {
endpoints, err := endpointService.Endpoints()
if err != nil {
log.Fatal(err)
}
return &endpoints[0]
return settingsService.UpdateSettings(settings)
}
func loadAndParseKeyPair(fileService portainer.FileService, signatureService portainer.DigitalSignatureService) error {
@@ -549,11 +420,6 @@ func main() {
snapshotter := initSnapshotter(clientFactory)
endpointManagement := true
if *flags.ExternalEndpoints != "" {
endpointManagement = false
}
swarmStackManager, err := initSwarmStackManager(*flags.Assets, *flags.Data, digitalSignatureService, fileService, reverseTunnelService)
if err != nil {
log.Fatal(err)
@@ -561,14 +427,11 @@ func main() {
composeStackManager := initComposeStackManager(*flags.Data, reverseTunnelService)
err = initTemplates(store.TemplateService, fileService, *flags.Templates, *flags.TemplateFile)
if err != nil {
log.Fatal(err)
}
err = initSettings(store.SettingsService, flags)
if err != nil {
log.Fatal(err)
if store.IsNew() {
err = updateSettingsFromFlags(store.SettingsService, flags)
if err != nil {
log.Fatal(err)
}
}
jobScheduler := initJobScheduler()
@@ -578,26 +441,14 @@ func main() {
log.Fatal(err)
}
err = loadEndpointSyncSystemSchedule(jobScheduler, store.ScheduleService, store.EndpointService, flags)
err = loadSnapshotSystemSchedule(jobScheduler, snapshotter, store.ScheduleService, store.EndpointService, store.SettingsService)
if err != nil {
log.Fatal(err)
}
if *flags.Snapshot {
err = loadSnapshotSystemSchedule(jobScheduler, snapshotter, store.ScheduleService, store.EndpointService, store.SettingsService)
if err != nil {
log.Fatal(err)
}
}
jobScheduler.Start()
err = initDockerHub(store.DockerHubService)
if err != nil {
log.Fatal(err)
}
applicationStatus := initStatus(endpointManagement, *flags.Snapshot, flags)
applicationStatus := initStatus(flags)
err = initEndpoint(flags, store.EndpointService, snapshotter)
if err != nil {
@@ -656,7 +507,6 @@ func main() {
BindAddress: *flags.Addr,
AssetsPath: *flags.Assets,
AuthDisabled: *flags.NoAuth,
EndpointManagement: endpointManagement,
RoleService: store.RoleService,
UserService: store.UserService,
TeamService: store.TeamService,
@@ -674,7 +524,6 @@ func main() {
StackService: store.StackService,
ScheduleService: store.ScheduleService,
TagService: store.TagService,
TemplateService: store.TemplateService,
WebhookService: store.WebhookService,
SwarmStackManager: swarmStackManager,
ComposeStackManager: composeStackManager,

View File

@@ -1,214 +0,0 @@
package cron
import (
"encoding/json"
"io/ioutil"
"log"
"strings"
"github.com/portainer/portainer/api"
)
// EndpointSyncJobRunner is used to run a EndpointSyncJob
type EndpointSyncJobRunner struct {
schedule *portainer.Schedule
context *EndpointSyncJobContext
}
// EndpointSyncJobContext represents the context of execution of a EndpointSyncJob
type EndpointSyncJobContext struct {
endpointService portainer.EndpointService
endpointFilePath string
}
// NewEndpointSyncJobContext returns a new context that can be used to execute a EndpointSyncJob
func NewEndpointSyncJobContext(endpointService portainer.EndpointService, endpointFilePath string) *EndpointSyncJobContext {
return &EndpointSyncJobContext{
endpointService: endpointService,
endpointFilePath: endpointFilePath,
}
}
// NewEndpointSyncJobRunner returns a new runner that can be scheduled
func NewEndpointSyncJobRunner(schedule *portainer.Schedule, context *EndpointSyncJobContext) *EndpointSyncJobRunner {
return &EndpointSyncJobRunner{
schedule: schedule,
context: context,
}
}
type synchronization struct {
endpointsToCreate []*portainer.Endpoint
endpointsToUpdate []*portainer.Endpoint
endpointsToDelete []*portainer.Endpoint
}
type fileEndpoint struct {
Name string `json:"Name"`
URL string `json:"URL"`
TLS bool `json:"TLS,omitempty"`
TLSSkipVerify bool `json:"TLSSkipVerify,omitempty"`
TLSCACert string `json:"TLSCACert,omitempty"`
TLSCert string `json:"TLSCert,omitempty"`
TLSKey string `json:"TLSKey,omitempty"`
}
// GetSchedule returns the schedule associated to the runner
func (runner *EndpointSyncJobRunner) GetSchedule() *portainer.Schedule {
return runner.schedule
}
// Run triggers the execution of the endpoint synchronization process.
func (runner *EndpointSyncJobRunner) Run() {
data, err := ioutil.ReadFile(runner.context.endpointFilePath)
if endpointSyncError(err) {
return
}
var fileEndpoints []fileEndpoint
err = json.Unmarshal(data, &fileEndpoints)
if endpointSyncError(err) {
return
}
if len(fileEndpoints) == 0 {
log.Println("background job error (endpoint synchronization). External endpoint source is empty")
return
}
storedEndpoints, err := runner.context.endpointService.Endpoints()
if endpointSyncError(err) {
return
}
convertedFileEndpoints := convertFileEndpoints(fileEndpoints)
sync := prepareSyncData(storedEndpoints, convertedFileEndpoints)
if sync.requireSync() {
err = runner.context.endpointService.Synchronize(sync.endpointsToCreate, sync.endpointsToUpdate, sync.endpointsToDelete)
if endpointSyncError(err) {
return
}
log.Printf("Endpoint synchronization ended. [created: %v] [updated: %v] [deleted: %v]", len(sync.endpointsToCreate), len(sync.endpointsToUpdate), len(sync.endpointsToDelete))
}
}
func endpointSyncError(err error) bool {
if err != nil {
log.Printf("background job error (endpoint synchronization). Unable to synchronize endpoints (err=%s)\n", err)
return true
}
return false
}
func isValidEndpoint(endpoint *portainer.Endpoint) bool {
if endpoint.Name != "" && endpoint.URL != "" {
if !strings.HasPrefix(endpoint.URL, "unix://") && !strings.HasPrefix(endpoint.URL, "tcp://") {
return false
}
return true
}
return false
}
func convertFileEndpoints(fileEndpoints []fileEndpoint) []portainer.Endpoint {
convertedEndpoints := make([]portainer.Endpoint, 0)
for _, e := range fileEndpoints {
endpoint := portainer.Endpoint{
Name: e.Name,
URL: e.URL,
TLSConfig: portainer.TLSConfiguration{},
}
if e.TLS {
endpoint.TLSConfig.TLS = true
endpoint.TLSConfig.TLSSkipVerify = e.TLSSkipVerify
endpoint.TLSConfig.TLSCACertPath = e.TLSCACert
endpoint.TLSConfig.TLSCertPath = e.TLSCert
endpoint.TLSConfig.TLSKeyPath = e.TLSKey
}
convertedEndpoints = append(convertedEndpoints, endpoint)
}
return convertedEndpoints
}
func endpointExists(endpoint *portainer.Endpoint, endpoints []portainer.Endpoint) int {
for idx, v := range endpoints {
if endpoint.Name == v.Name && isValidEndpoint(&v) {
return idx
}
}
return -1
}
func mergeEndpointIfRequired(original, updated *portainer.Endpoint) *portainer.Endpoint {
var endpoint *portainer.Endpoint
if original.URL != updated.URL || original.TLSConfig.TLS != updated.TLSConfig.TLS ||
(updated.TLSConfig.TLS && original.TLSConfig.TLSSkipVerify != updated.TLSConfig.TLSSkipVerify) ||
(updated.TLSConfig.TLS && original.TLSConfig.TLSCACertPath != updated.TLSConfig.TLSCACertPath) ||
(updated.TLSConfig.TLS && original.TLSConfig.TLSCertPath != updated.TLSConfig.TLSCertPath) ||
(updated.TLSConfig.TLS && original.TLSConfig.TLSKeyPath != updated.TLSConfig.TLSKeyPath) {
endpoint = original
endpoint.URL = updated.URL
if updated.TLSConfig.TLS {
endpoint.TLSConfig.TLS = true
endpoint.TLSConfig.TLSSkipVerify = updated.TLSConfig.TLSSkipVerify
endpoint.TLSConfig.TLSCACertPath = updated.TLSConfig.TLSCACertPath
endpoint.TLSConfig.TLSCertPath = updated.TLSConfig.TLSCertPath
endpoint.TLSConfig.TLSKeyPath = updated.TLSConfig.TLSKeyPath
} else {
endpoint.TLSConfig.TLS = false
endpoint.TLSConfig.TLSSkipVerify = false
endpoint.TLSConfig.TLSCACertPath = ""
endpoint.TLSConfig.TLSCertPath = ""
endpoint.TLSConfig.TLSKeyPath = ""
}
}
return endpoint
}
func (sync synchronization) requireSync() bool {
if len(sync.endpointsToCreate) != 0 || len(sync.endpointsToUpdate) != 0 || len(sync.endpointsToDelete) != 0 {
return true
}
return false
}
func prepareSyncData(storedEndpoints, fileEndpoints []portainer.Endpoint) *synchronization {
endpointsToCreate := make([]*portainer.Endpoint, 0)
endpointsToUpdate := make([]*portainer.Endpoint, 0)
endpointsToDelete := make([]*portainer.Endpoint, 0)
for idx := range storedEndpoints {
fidx := endpointExists(&storedEndpoints[idx], fileEndpoints)
if fidx != -1 {
endpoint := mergeEndpointIfRequired(&storedEndpoints[idx], &fileEndpoints[fidx])
if endpoint != nil {
log.Printf("New definition for a stored endpoint found in file, updating database. [name: %v] [url: %v]\n", endpoint.Name, endpoint.URL)
endpointsToUpdate = append(endpointsToUpdate, endpoint)
}
} else {
log.Printf("Stored endpoint not found in file (definition might be invalid), removing from database. [name: %v] [url: %v]", storedEndpoints[idx].Name, storedEndpoints[idx].URL)
endpointsToDelete = append(endpointsToDelete, &storedEndpoints[idx])
}
}
for idx, endpoint := range fileEndpoints {
if !isValidEndpoint(&endpoint) {
log.Printf("Invalid file endpoint definition, skipping. [name: %v] [url: %v]", endpoint.Name, endpoint.URL)
continue
}
sidx := endpointExists(&fileEndpoints[idx], storedEndpoints)
if sidx == -1 {
log.Printf("File endpoint not found in database, adding to database. [name: %v] [url: %v]", fileEndpoints[idx].Name, fileEndpoints[idx].URL)
endpointsToCreate = append(endpointsToCreate, &fileEndpoints[idx])
}
}
return &synchronization{
endpointsToCreate: endpointsToCreate,
endpointsToUpdate: endpointsToUpdate,
endpointsToDelete: endpointsToDelete,
}
}

View File

@@ -53,7 +53,7 @@ func (runner *SnapshotJobRunner) Run() {
}
for _, endpoint := range endpoints {
if endpoint.Type == portainer.AzureEnvironment || endpoint.Type == portainer.EdgeAgentEnvironment {
if endpoint.Type == portainer.EdgeAgentEnvironment {
continue
}

View File

@@ -35,9 +35,7 @@ func NewClientFactory(signatureService portainer.DigitalSignatureService, revers
// a specific endpoint configuration. The nodeName parameter can be used
// with an agent enabled endpoint to target a specific node in an agent cluster.
func (factory *ClientFactory) CreateClient(endpoint *portainer.Endpoint, nodeName string) (*client.Client, error) {
if endpoint.Type == portainer.AzureEnvironment {
return nil, unsupportedEnvironmentType
} else if endpoint.Type == portainer.AgentOnDockerEnvironment {
if endpoint.Type == portainer.AgentOnDockerEnvironment {
return createAgentClient(endpoint, factory.signatureService, nodeName)
} else if endpoint.Type == portainer.EdgeAgentEnvironment {
return createEdgeClient(endpoint, factory.reverseTunnelService, nodeName)

View File

@@ -39,11 +39,6 @@ const (
ErrEndpointAccessDenied = Error("Access denied to endpoint")
)
// Azure environment errors
const (
ErrAzureInvalidCredentials = Error("Invalid Azure credentials")
)
// Endpoint group errors.
const (
ErrCannotRemoveDefaultGroup = Error("Cannot remove the default endpoint group")

View File

@@ -13,6 +13,7 @@ require (
github.com/docker/cli v0.0.0-20191126203649-54d085b857e9
github.com/docker/docker v0.0.0-00010101000000-000000000000
github.com/g07cha/defender v0.0.0-20180505193036-5665c627c814
github.com/go-ldap/ldap/v3 v3.1.8
github.com/gofrs/uuid v3.2.0+incompatible
github.com/gorilla/mux v1.7.3
github.com/gorilla/securecookie v1.1.1
@@ -30,11 +31,7 @@ require (
github.com/robfig/cron/v3 v3.0.0
golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1
gopkg.in/alecthomas/kingpin.v2 v2.2.6
gopkg.in/asn1-ber.v1 v1.0.0-00010101000000-000000000000 // indirect
gopkg.in/ldap.v2 v2.5.1
gopkg.in/src-d/go-git.v4 v4.13.1
)
replace github.com/docker/docker => github.com/docker/engine v1.4.2-0.20200204220554-5f6d6f3f2203
replace gopkg.in/asn1-ber.v1 => github.com/go-asn1-ber/asn1-ber v1.3.1

View File

@@ -50,12 +50,8 @@ github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BU
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker-credential-helpers v0.6.3 h1:zI2p9+1NQYdnG6sMU26EX4aVGlqbInSQxQXLvzJ4RPQ=
github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y=
github.com/docker/engine v1.4.2-0.20191127222017-3152f9436292 h1:qQ7mw+CVWpRj5DWBL4CVHtBbGQdlPCj4j1evDh0ethw=
github.com/docker/engine v1.4.2-0.20191127222017-3152f9436292/go.mod h1:3CPr2caMgTHxxIAZgEMd3uLYPDlRvPqCpyeRf6ncPcY=
github.com/docker/engine v1.4.2-0.20200204220554-5f6d6f3f2203 h1:QeBh8wW8pIZKlXxlMOQ8hSCMdJA+2Z/bD/iDyCAS8XU=
github.com/docker/engine v1.4.2-0.20200204220554-5f6d6f3f2203/go.mod h1:3CPr2caMgTHxxIAZgEMd3uLYPDlRvPqCpyeRf6ncPcY=
github.com/docker/engine v1.13.1 h1:Cks33UT9YBW5Xyc3MtGDq2IPgqfJtJ+qkFaxc2b0Euc=
github.com/docker/engine v1.13.1/go.mod h1:3CPr2caMgTHxxIAZgEMd3uLYPDlRvPqCpyeRf6ncPcY=
github.com/docker/go-connections v0.3.0 h1:3lOnM9cSzgGwx8VfK/NGOW5fLQ0GjIlCkaktF+n1M6o=
github.com/docker/go-connections v0.3.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-metrics v0.0.0-20181218153428-b84716841b82 h1:X0fj836zx99zFu83v/M79DuBn84IL/Syx1SY6Y5ZEMA=
@@ -77,6 +73,8 @@ github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aev
github.com/go-asn1-ber/asn1-ber v1.3.1 h1:gvPdv/Hr++TRFCl0UbPFHC54P9N9jgsRPnmnr419Uck=
github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-ldap/ldap/v3 v3.1.8 h1:5vU/2jOh9HqprwXp8aF915s9p6Z8wmbSEVF7/gdTFhM=
github.com/go-ldap/ldap/v3 v3.1.8/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
@@ -140,8 +138,6 @@ github.com/mattn/go-shellwords v1.0.6 h1:9Jok5pILi5S1MnDirGVTufYGtksUs/V2BWUP3Zk
github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/microsoft/go-winio v0.4.8 h1:N4SmTFXUK7/jnn/UG/gm2mrHiYu9LVGvtsvULyody/c=
github.com/microsoft/go-winio v0.4.8/go.mod h1:kcIxxtKZE55DEncT/EOvFiygPobhUWpSDqDb47poQOU=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
@@ -175,6 +171,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=
@@ -247,8 +244,6 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3 h1:4y9KwBHBgBNwDbtu44R5o1fdOCQUEXhbk/P4A9WmJq0=
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200219091948-cb0a6d8edb6c h1:jceGD5YNJGgGMkJz79agzOln1K9TaZUjv5ird16qniQ=
golang.org/x/sys v0.0.0-20200219091948-cb0a6d8edb6c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
@@ -268,8 +263,6 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/ldap.v2 v2.5.1 h1:wiu0okdNfjlBzg6UWvd1Hn8Y+Ux17/u/4nlk4CQr6tU=
gopkg.in/ldap.v2 v2.5.1/go.mod h1:oI0cpe/D7HRtBQl8aTg+ZmzFUAvu4lsv3eLXMLGFxWk=
gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg=
gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98=
gopkg.in/src-d/go-git-fixtures.v3 v3.5.0 h1:ivZFOIltbce2Mo8IjzUHAFoq/IylO9WHhNOAJK+LsJg=

View File

@@ -2,12 +2,9 @@ package client
import (
"crypto/tls"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
"strings"
"time"
@@ -19,55 +16,6 @@ const (
defaultHTTPTimeout = 5
)
// HTTPClient represents a client to send HTTP requests.
type HTTPClient struct {
*http.Client
}
// NewHTTPClient is used to build a new HTTPClient.
func NewHTTPClient() *HTTPClient {
return &HTTPClient{
&http.Client{
Timeout: time.Second * time.Duration(defaultHTTPTimeout),
},
}
}
// AzureAuthenticationResponse represents an Azure API authentication response.
type AzureAuthenticationResponse struct {
AccessToken string `json:"access_token"`
ExpiresOn string `json:"expires_on"`
}
// ExecuteAzureAuthenticationRequest is used to execute an authentication request
// against the Azure API. It re-uses the same http.Client.
func (client *HTTPClient) ExecuteAzureAuthenticationRequest(credentials *portainer.AzureCredentials) (*AzureAuthenticationResponse, error) {
loginURL := fmt.Sprintf("https://login.microsoftonline.com/%s/oauth2/token", credentials.TenantID)
params := url.Values{
"grant_type": {"client_credentials"},
"client_id": {credentials.ApplicationID},
"client_secret": {credentials.AuthenticationKey},
"resource": {"https://management.azure.com/"},
}
response, err := client.PostForm(loginURL, params)
if err != nil {
return nil, err
}
if response.StatusCode != http.StatusOK {
return nil, portainer.ErrAzureInvalidCredentials
}
var token AzureAuthenticationResponse
err = json.NewDecoder(response.Body).Decode(&token)
if err != nil {
return nil, err
}
return &token, nil
}
// Get executes a simple HTTP GET to the specified URL and returns
// the content of the response body. Timeout can be specified via the timeout parameter,
// will default to defaultHTTPTimeout if set to 0.

View File

@@ -2,7 +2,6 @@ package edgetemplates
import (
"encoding/json"
"log"
"net/http"
httperror "github.com/portainer/libhttp/error"
@@ -11,6 +10,11 @@ import (
"github.com/portainer/portainer/api/http/client"
)
type templateFileFormat struct {
Version string `json:"version"`
Templates []portainer.Template `json:"templates"`
}
// GET request on /api/edgetemplates
func (handler *Handler) edgeTemplateList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
settings, err := handler.SettingsService.Settings()
@@ -18,28 +22,27 @@ 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
}
var templateData []byte
templateData, err = client.Get(url, 0)
templateData, err = client.Get(url, 10)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve external templates", err}
}
var templates []portainer.Template
var templateFile templateFileFormat
err = json.Unmarshal(templateData, &templates)
err = json.Unmarshal(templateData, &templateFile)
if err != nil {
log.Printf("[DEBUG] [http,edge,templates] [failed parsing edge templates] [body: %s]", templateData)
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to parse external templates", err}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to parse template file", err}
}
filteredTemplates := []portainer.Template{}
filteredTemplates := make([]portainer.Template, 0)
for _, template := range templates {
for _, template := range templateFile.Templates {
if template.Type == portainer.EdgeStackTemplate {
filteredTemplates = append(filteredTemplates, template)
}

View File

@@ -24,8 +24,6 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
Router: mux.NewRouter(),
requestBouncer: bouncer,
}
h.PathPrefix("/{id}/azure").Handler(
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToAzureAPI)))
h.PathPrefix("/{id}/docker").Handler(
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToDockerAPI)))
h.PathPrefix("/{id}/storidge").Handler(

View File

@@ -1,43 +0,0 @@
package endpointproxy
import (
"strconv"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/portainer/api"
"net/http"
)
func (handler *Handler) proxyRequestsToAzureAPI(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
}
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
}
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, false)
if err != nil {
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
}
var proxy http.Handler
proxy = handler.ProxyManager.GetEndpointProxy(endpoint)
if proxy == nil {
proxy, err = handler.ProxyManager.CreateAndRegisterEndpointProxy(endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create proxy", err}
}
}
id := strconv.Itoa(endpointID)
http.StripPrefix("/"+id+"/azure", proxy).ServeHTTP(w, r)
return nil
}

View File

@@ -18,21 +18,18 @@ import (
)
type endpointCreatePayload struct {
Name string
URL string
EndpointType int
PublicURL string
GroupID int
TLS bool
TLSSkipVerify bool
TLSSkipClientVerify bool
TLSCACertFile []byte
TLSCertFile []byte
TLSKeyFile []byte
AzureApplicationID string
AzureTenantID string
AzureAuthenticationKey string
TagIDs []portainer.TagID
Name string
URL string
EndpointType int
PublicURL string
GroupID int
TLS bool
TLSSkipVerify bool
TLSSkipClientVerify bool
TLSCACertFile []byte
TLSCertFile []byte
TLSKeyFile []byte
TagIDs []portainer.TagID
}
func (payload *endpointCreatePayload) Validate(r *http.Request) error {
@@ -44,7 +41,7 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error {
endpointType, err := request.RetrieveNumericMultiPartFormValue(r, "EndpointType", false)
if err != nil || endpointType == 0 {
return portainer.Error("Invalid endpoint type value. Value must be one of: 1 (Docker environment), 2 (Agent environment), 3 (Azure environment) or 4 (Edge Agent environment)")
return portainer.Error("Invalid endpoint type value. Value must be one of: 1 (Docker environment), 2 (Agent environment) or 4 (Edge Agent environment)")
}
payload.EndpointType = endpointType
@@ -96,45 +93,20 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error {
}
}
switch portainer.EndpointType(payload.EndpointType) {
case portainer.AzureEnvironment:
azureApplicationID, err := request.RetrieveMultiPartFormValue(r, "AzureApplicationID", false)
if err != nil {
return portainer.Error("Invalid Azure application ID")
}
payload.AzureApplicationID = azureApplicationID
azureTenantID, err := request.RetrieveMultiPartFormValue(r, "AzureTenantID", false)
if err != nil {
return portainer.Error("Invalid Azure tenant ID")
}
payload.AzureTenantID = azureTenantID
azureAuthenticationKey, err := request.RetrieveMultiPartFormValue(r, "AzureAuthenticationKey", false)
if err != nil {
return portainer.Error("Invalid Azure authentication key")
}
payload.AzureAuthenticationKey = azureAuthenticationKey
default:
url, err := request.RetrieveMultiPartFormValue(r, "URL", true)
if err != nil {
return portainer.Error("Invalid endpoint URL")
}
payload.URL = url
publicURL, _ := request.RetrieveMultiPartFormValue(r, "PublicURL", true)
payload.PublicURL = publicURL
endpointURL, err := request.RetrieveMultiPartFormValue(r, "URL", true)
if err != nil {
return portainer.Error("Invalid endpoint URL")
}
payload.URL = endpointURL
publicURL, _ := request.RetrieveMultiPartFormValue(r, "PublicURL", true)
payload.PublicURL = publicURL
return nil
}
// POST request on /api/endpoints
func (handler *Handler) endpointCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
if !handler.authorizeEndpointManagement {
return &httperror.HandlerError{http.StatusServiceUnavailable, "Endpoint management is disabled", ErrEndpointManagementDisabled}
}
payload := &endpointCreatePayload{}
err := payload.Validate(r)
if err != nil {
@@ -182,9 +154,7 @@ func (handler *Handler) endpointCreate(w http.ResponseWriter, r *http.Request) *
}
func (handler *Handler) createEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) {
if portainer.EndpointType(payload.EndpointType) == portainer.AzureEnvironment {
return handler.createAzureEndpoint(payload)
} else if portainer.EndpointType(payload.EndpointType) == portainer.EdgeAgentEnvironment {
if portainer.EndpointType(payload.EndpointType) == portainer.EdgeAgentEnvironment {
return handler.createEdgeAgentEndpoint(payload)
}
@@ -194,44 +164,6 @@ func (handler *Handler) createEndpoint(payload *endpointCreatePayload) (*portain
return handler.createUnsecuredEndpoint(payload)
}
func (handler *Handler) createAzureEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) {
credentials := portainer.AzureCredentials{
ApplicationID: payload.AzureApplicationID,
TenantID: payload.AzureTenantID,
AuthenticationKey: payload.AzureAuthenticationKey,
}
httpClient := client.NewHTTPClient()
_, err := httpClient.ExecuteAzureAuthenticationRequest(&credentials)
if err != nil {
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to authenticate against Azure", err}
}
endpointID := handler.EndpointService.GetNextIdentifier()
endpoint := &portainer.Endpoint{
ID: portainer.EndpointID(endpointID),
Name: payload.Name,
URL: "https://management.azure.com",
Type: portainer.AzureEnvironment,
GroupID: portainer.EndpointGroupID(payload.GroupID),
PublicURL: payload.PublicURL,
UserAccessPolicies: portainer.UserAccessPolicies{},
TeamAccessPolicies: portainer.TeamAccessPolicies{},
Extensions: []portainer.EndpointExtension{},
AzureCredentials: credentials,
TagIDs: payload.TagIDs,
Status: portainer.EndpointStatusUp,
Snapshots: []portainer.Snapshot{},
}
err = handler.saveEndpointAndUpdateAuthorizations(endpoint)
if err != nil {
return nil, &httperror.HandlerError{http.StatusInternalServerError, "An error occured while trying to create the endpoint", err}
}
return endpoint, nil
}
func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) {
endpointType := portainer.EdgeAgentEnvironment
endpointID := handler.EndpointService.GetNextIdentifier()

View File

@@ -12,10 +12,6 @@ import (
// DELETE request on /api/endpoints/:id
func (handler *Handler) endpointDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
if !handler.authorizeEndpointManagement {
return &httperror.HandlerError{http.StatusServiceUnavailable, "Endpoint management is disabled", ErrEndpointManagementDisabled}
}
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}

View File

@@ -23,10 +23,6 @@ func (handler *Handler) endpointSnapshot(w http.ResponseWriter, r *http.Request)
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
}
if endpoint.Type == portainer.AzureEnvironment {
return &httperror.HandlerError{http.StatusBadRequest, "Snapshots not supported for Azure endpoints", err}
}
snapshot, snapshotError := handler.Snapshotter.CreateSnapshot(endpoint)
latestEndpointReference, err := handler.EndpointService.Endpoint(endpoint.ID)

View File

@@ -17,10 +17,6 @@ func (handler *Handler) endpointSnapshots(w http.ResponseWriter, r *http.Request
}
for _, endpoint := range endpoints {
if endpoint.Type == portainer.AzureEnvironment {
continue
}
snapshot, snapshotError := handler.Snapshotter.CreateSnapshot(&endpoint)
latestEndpointReference, err := handler.EndpointService.Endpoint(endpoint.ID)

View File

@@ -9,24 +9,20 @@ import (
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/client"
)
type endpointUpdatePayload struct {
Name *string
URL *string
PublicURL *string
GroupID *int
TLS *bool
TLSSkipVerify *bool
TLSSkipClientVerify *bool
Status *int
AzureApplicationID *string
AzureTenantID *string
AzureAuthenticationKey *string
TagIDs []portainer.TagID
UserAccessPolicies portainer.UserAccessPolicies
TeamAccessPolicies portainer.TeamAccessPolicies
Name *string
URL *string
PublicURL *string
GroupID *int
TLS *bool
TLSSkipVerify *bool
TLSSkipClientVerify *bool
Status *int
TagIDs []portainer.TagID
UserAccessPolicies portainer.UserAccessPolicies
TeamAccessPolicies portainer.TeamAccessPolicies
}
func (payload *endpointUpdatePayload) Validate(r *http.Request) error {
@@ -35,10 +31,6 @@ func (payload *endpointUpdatePayload) Validate(r *http.Request) error {
// PUT request on /api/endpoints/:id
func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
if !handler.authorizeEndpointManagement {
return &httperror.HandlerError{http.StatusServiceUnavailable, "Endpoint management is disabled", ErrEndpointManagementDisabled}
}
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
@@ -141,26 +133,6 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
}
}
if endpoint.Type == portainer.AzureEnvironment {
credentials := endpoint.AzureCredentials
if payload.AzureApplicationID != nil {
credentials.ApplicationID = *payload.AzureApplicationID
}
if payload.AzureTenantID != nil {
credentials.TenantID = *payload.AzureTenantID
}
if payload.AzureAuthenticationKey != nil {
credentials.AuthenticationKey = *payload.AzureAuthenticationKey
}
httpClient := client.NewHTTPClient()
_, authErr := httpClient.ExecuteAzureAuthenticationRequest(&credentials)
if authErr != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to authenticate against Azure", authErr}
}
endpoint.AzureCredentials = credentials
}
if payload.TLS != nil {
folder := strconv.Itoa(endpointID)
@@ -205,7 +177,7 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
}
}
if payload.URL != nil || payload.TLS != nil || endpoint.Type == portainer.AzureEnvironment {
if payload.URL != nil || payload.TLS != nil {
_, err = handler.ProxyManager.CreateAndRegisterEndpointProxy(endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to register HTTP proxy for the endpoint", err}

View File

@@ -11,14 +11,7 @@ import (
"github.com/gorilla/mux"
)
const (
// ErrEndpointManagementDisabled is an error raised when trying to access the endpoints management endpoints
// when the server has been started with the --external-endpoints flag
ErrEndpointManagementDisabled = portainer.Error("Endpoint management is disabled")
)
func hideFields(endpoint *portainer.Endpoint) {
endpoint.AzureCredentials = portainer.AzureCredentials{}
if len(endpoint.Snapshots) > 0 {
endpoint.Snapshots[0].SnapshotRaw = portainer.SnapshotRaw{}
}
@@ -27,7 +20,6 @@ func hideFields(endpoint *portainer.Endpoint) {
// Handler is the HTTP handler used to handle endpoint operations.
type Handler struct {
*mux.Router
authorizeEndpointManagement bool
requestBouncer *security.RequestBouncer
AuthorizationService *portainer.AuthorizationService
EdgeGroupService portainer.EdgeGroupService
@@ -45,10 +37,9 @@ type Handler struct {
}
// NewHandler creates a handler to manage endpoint operations.
func NewHandler(bouncer *security.RequestBouncer, authorizeEndpointManagement bool) *Handler {
func NewHandler(bouncer *security.RequestBouncer) *Handler {
h := &Handler{
Router: mux.NewRouter(),
authorizeEndpointManagement: authorizeEndpointManagement,
requestBouncer: bouncer,
}

View File

@@ -90,8 +90,6 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.StripPrefix("/api/endpoints", h.EndpointProxyHandler).ServeHTTP(w, r)
case strings.Contains(r.URL.Path, "/storidge/"):
http.StripPrefix("/api/endpoints", h.EndpointProxyHandler).ServeHTTP(w, r)
case strings.Contains(r.URL.Path, "/azure/"):
http.StripPrefix("/api/endpoints", h.EndpointProxyHandler).ServeHTTP(w, r)
case strings.Contains(r.URL.Path, "/edge/"):
http.StripPrefix("/api/endpoints", h.EndpointEdgeHandler).ServeHTTP(w, r)
default:

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

@@ -1,20 +0,0 @@
package factory
import (
"net/http"
"net/url"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/proxy/factory/azure"
)
func newAzureProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
remoteURL, err := url.Parse(azureAPIBaseURL)
if err != nil {
return nil, err
}
proxy := newSingleHostReverseProxyWithHostHeader(remoteURL)
proxy.Transport = azure.NewTransport(&endpoint.AzureCredentials)
return proxy, nil
}

View File

@@ -1,80 +0,0 @@
package azure
import (
"net/http"
"strconv"
"sync"
"time"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/client"
)
type (
azureAPIToken struct {
value string
expirationTime time.Time
}
Transport struct {
credentials *portainer.AzureCredentials
client *client.HTTPClient
token *azureAPIToken
mutex sync.Mutex
}
)
// NewTransport returns a pointer to a new instance of Transport that implements the HTTP Transport
// interface for proxying requests to the Azure API.
func NewTransport(credentials *portainer.AzureCredentials) *Transport {
return &Transport{
credentials: credentials,
client: client.NewHTTPClient(),
}
}
// RoundTrip is the implementation of the the http.RoundTripper interface
func (transport *Transport) RoundTrip(request *http.Request) (*http.Response, error) {
err := transport.retrieveAuthenticationToken()
if err != nil {
return nil, err
}
request.Header.Set("Authorization", "Bearer "+transport.token.value)
return http.DefaultTransport.RoundTrip(request)
}
func (transport *Transport) authenticate() error {
token, err := transport.client.ExecuteAzureAuthenticationRequest(transport.credentials)
if err != nil {
return err
}
expiresOn, err := strconv.ParseInt(token.ExpiresOn, 10, 64)
if err != nil {
return err
}
transport.token = &azureAPIToken{
value: token.AccessToken,
expirationTime: time.Unix(expiresOn, 0),
}
return nil
}
func (transport *Transport) retrieveAuthenticationToken() error {
transport.mutex.Lock()
defer transport.mutex.Unlock()
if transport.token == nil {
return transport.authenticate()
}
timeLimit := time.Now().Add(-5 * time.Minute)
if timeLimit.After(transport.token.expirationTime) {
return transport.authenticate()
}
return nil
}

View File

@@ -10,8 +10,6 @@ import (
"github.com/portainer/portainer/api/docker"
)
const azureAPIBaseURL = "https://management.azure.com"
var extensionPorts = map[portainer.ExtensionID]string{
portainer.RegistryManagementExtension: "7001",
portainer.OAuthAuthenticationExtension: "7002",
@@ -100,11 +98,6 @@ func (factory *ProxyFactory) NewLegacyExtensionProxy(extensionAPIURL string) (ht
// NewEndpointProxy returns a new reverse proxy (filesystem based or HTTP) to an endpoint API server
func (factory *ProxyFactory) NewEndpointProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
switch endpoint.Type {
case portainer.AzureEnvironment:
return newAzureProxy(endpoint)
}
return factory.newDockerProxy(endpoint)
}

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

@@ -48,7 +48,6 @@ type Server struct {
BindAddress string
AssetsPath string
AuthDisabled bool
EndpointManagement bool
Status *portainer.Status
ReverseTunnelService portainer.ReverseTunnelService
ExtensionManager portainer.ExtensionManager
@@ -78,7 +77,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
@@ -171,7 +169,7 @@ func (server *Server) Start() error {
var edgeTemplatesHandler = edgetemplates.NewHandler(requestBouncer)
edgeTemplatesHandler.SettingsService = server.SettingsService
var endpointHandler = endpoints.NewHandler(requestBouncer, server.EndpointManagement)
var endpointHandler = endpoints.NewHandler(requestBouncer)
endpointHandler.AuthorizationService = authorizationService
endpointHandler.EdgeGroupService = server.EdgeGroupService
endpointHandler.EdgeStackService = server.EdgeStackService
@@ -282,7 +280,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

@@ -4,10 +4,9 @@ import (
"fmt"
"strings"
ldap "github.com/go-ldap/ldap/v3"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/crypto"
"gopkg.in/ldap.v2"
)
const (

View File

@@ -25,7 +25,7 @@ type (
Authorizations map[Authorization]bool
// AzureCredentials represents the credentials used to connect to an Azure
// environment.
// environment (deprecated).
AzureCredentials struct {
ApplicationID string `json:"ApplicationID"`
TenantID string `json:"TenantID"`
@@ -42,13 +42,11 @@ type (
Assets *string
Data *string
EndpointURL *string
ExternalEndpoints *string
Labels *[]Pair
Logo *string
NoAuth *bool
NoAnalytics *bool
Templates *string
TemplateFile *string
TLS *bool
TLSSkipVerify *bool
TLSCacert *string
@@ -57,8 +55,6 @@ type (
SSL *bool
SSLCert *string
SSLKey *string
SyncInterval *string
Snapshot *bool
SnapshotInterval *string
}
@@ -73,6 +69,7 @@ type (
Open() error
Init() error
Close() error
IsNew() bool
MigrateData() error
}
@@ -143,7 +140,6 @@ type (
PublicURL string `json:"PublicURL"`
TLSConfig TLSConfiguration `json:"TLSConfig"`
Extensions []EndpointExtension `json:"Extensions"`
AzureCredentials AzureCredentials `json:"AzureCredentials,omitempty"`
TagIDs []TagID `json:"TagIds"`
Status EndpointStatus `json:"Status"`
Snapshots []Snapshot `json:"Snapshots"`
@@ -164,6 +160,9 @@ type (
// Deprecated in DBVersion == 22
Tags []string `json:"Tags"`
// Deprecated in DBVersion == 24
AzureCredentials AzureCredentials `json:"AzureCredentials,omitempty"`
}
// EndpointAuthorizations represents the authorizations associated to a set of endpoints
@@ -210,6 +209,8 @@ type (
EndpointStatus int
// EndpointSyncJob represents a scheduled job that synchronize endpoints based on an external file
//
// Deprecated
EndpointSyncJob struct{}
// EndpointType represents the type of an endpoint
@@ -403,7 +404,9 @@ type (
EdgeSchedule *EdgeSchedule
ScriptExecutionJob *ScriptExecutionJob
SnapshotJob *SnapshotJob
EndpointSyncJob *EndpointSyncJob
// Deprecated fields
EndpointSyncJob *EndpointSyncJob
}
// ScheduleID represents a schedule identifier.
@@ -491,11 +494,9 @@ type (
// Status represents the application status
Status struct {
Authentication bool `json:"Authentication"`
EndpointManagement bool `json:"EndpointManagement"`
Snapshot bool `json:"Snapshot"`
Analytics bool `json:"Analytics"`
Version string `json:"Version"`
Authentication bool `json:"Authentication"`
Analytics bool `json:"Analytics"`
Version string `json:"Version"`
}
// Tag represents a tag that can be associated to a resource
@@ -538,7 +539,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 +555,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 +945,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)
@@ -1007,9 +1000,9 @@ type (
const (
// APIVersion is the version number of the Portainer API
APIVersion = "1.24.0-dev"
APIVersion = "2.0.0-dev"
// DBVersion is the version number of the Portainer database
DBVersion = 23
DBVersion = 24
// AssetsServerURL represents the URL of the Portainer asset server
AssetsServerURL = "https://portainer-io-assets.sfo2.digitaloceanspaces.com"
// MessageOfTheDayURL represents the URL where Portainer MOTD message can be retrieved
@@ -1039,8 +1032,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 (
@@ -1083,7 +1076,7 @@ const (
DockerEnvironment
// AgentOnDockerEnvironment represents an endpoint connected to a Portainer agent deployed on a Docker environment
AgentOnDockerEnvironment
// AzureEnvironment represents an endpoint connected to an Azure environment
// AzureEnvironment represents an endpoint connected to an Azure environment (deprecated)
AzureEnvironment
// EdgeAgentEnvironment represents an endpoint connected to an Edge agent
EdgeAgentEnvironment
@@ -1107,7 +1100,7 @@ const (
// SnapshotJobType is a system job used to create endpoint snapshots
SnapshotJobType
// EndpointSyncJobType is a system job used to synchronize endpoints from
// an external definition store
// an external definition store (Deprecated)
EndpointSyncJobType
)

View File

@@ -54,7 +54,7 @@ info:
**NOTE**: You can find more information on how to query the Docker API in the [Docker official documentation](https://docs.docker.com/engine/api/v1.30/) as well as in [this Portainer example](https://gist.github.com/deviantony/77026d402366b4b43fa5918d41bc42f8).
version: "1.24.0-dev"
version: "2.0.0-dev"
title: "Portainer API"
contact:
email: "info@portainer.io"
@@ -254,7 +254,7 @@ paths:
- name: "EndpointType"
in: "formData"
type: "integer"
description: "Environment type. Value must be one of: 1 (Docker environment), 2 (Agent environment), 3 (Azure environment) or 4 (Edge agent environment)"
description: "Environment type. Value must be one of: 1 (Docker environment), 2 (Agent environment) or 4 (Edge agent environment)"
required: true
- name: "URL"
in: "formData"
@@ -294,18 +294,6 @@ paths:
in: "formData"
type: "file"
description: "TLS client key file"
- name: "AzureApplicationID"
in: "formData"
type: "string"
description: "Azure application ID. Required if endpoint type is set to 3"
- name: "AzureTenantID"
in: "formData"
type: "string"
description: "Azure tenant ID. Required if endpoint type is set to 3"
- name: "AzureAuthenticationKey"
in: "formData"
type: "string"
description: "Azure authentication key. Required if endpoint type is set to 3"
responses:
200:
description: "Success"
@@ -3164,17 +3152,13 @@ definitions:
type: "boolean"
example: true
description: "Is authentication enabled"
EndpointManagement:
type: "boolean"
example: true
description: "Is endpoint management enabled"
Analytics:
type: "boolean"
example: true
description: "Is analytics enabled"
Version:
type: "string"
example: "1.24.0-dev"
example: "2.0.0-dev"
description: "Portainer API version"
PublicSettingsInspectResponse:
type: "object"
@@ -3225,21 +3209,6 @@ definitions:
type: "string"
example: "/data/tls/key.pem"
description: "Path to the TLS client key file"
AzureCredentials:
type: "object"
properties:
ApplicationID:
type: "string"
example: "eag7cdo9-o09l-9i83-9dO9-f0b23oe78db4"
description: "Azure application ID"
TenantID:
type: "string"
example: "34ddc78d-4fel-2358-8cc1-df84c8o839f5"
description: "Azure tenant ID"
AuthenticationKey:
type: "string"
example: "cOrXoK/1D35w8YQ8nH1/8ZGwzz45JIYD5jxHKXEQknk="
description: "Azure authentication key"
LDAPSearchSettings:
type: "object"
properties:
@@ -3511,7 +3480,7 @@ definitions:
Type:
type: "integer"
example: 1
description: "Endpoint environment type. 1 for a Docker environment, 2 for an agent on Docker environment or 3 for an Azure environment."
description: "Endpoint environment type. 1 for a Docker environment or 2 for an agent on Docker environment"
URL:
type: "string"
example: "docker.mydomain.tld:2375"
@@ -3540,8 +3509,6 @@ definitions:
description: "Team identifier"
TLSConfig:
$ref: "#/definitions/TLSConfiguration"
AzureCredentials:
$ref: "#/definitions/AzureCredentials"
EndpointSubset:
type: "object"
properties:
@@ -3556,7 +3523,7 @@ definitions:
Type:
type: "integer"
example: 1
description: "Endpoint environment type. 1 for a Docker environment, 2 for an agent on Docker environment, 3 for an Azure environment."
description: "Endpoint environment type. 1 for a Docker environment or 2 for an agent on Docker environment"
URL:
type: "string"
example: "docker.mydomain.tld:2375"
@@ -3736,18 +3703,6 @@ definitions:
type: "boolean"
example: false
description: "Skip client verification when using TLS"
ApplicationID:
type: "string"
example: "eag7cdo9-o09l-9i83-9dO9-f0b23oe78db4"
description: "Azure application ID"
TenantID:
type: "string"
example: "34ddc78d-4fel-2358-8cc1-df84c8o839f5"
description: "Azure tenant ID"
AuthenticationKey:
type: "string"
example: "cOrXoK/1D35w8YQ8nH1/8ZGwzz45JIYD5jxHKXEQknk="
description: "Azure authentication key"
UserAccessPolicies:
$ref: "#/definitions/UserAccessPolicies"
TeamAccessPolicies:

View File

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

View File

@@ -2,7 +2,6 @@ import '../assets/css/app.css';
import angular from 'angular';
import './agent/_module';
import './azure/_module';
import './docker/__module';
import './edge/__module';
import './portainer/__module';
@@ -28,7 +27,6 @@ angular.module('portainer', [
'luegg.directives',
'portainer.app',
'portainer.agent',
'portainer.azure',
'portainer.docker',
'portainer.edge',
'portainer.extensions',

View File

@@ -1,51 +0,0 @@
angular.module('portainer.azure', ['portainer.app']).config([
'$stateRegistryProvider',
function ($stateRegistryProvider) {
'use strict';
var azure = {
name: 'azure',
url: '/azure',
parent: 'root',
abstract: true,
};
var containerInstances = {
name: 'azure.containerinstances',
url: '/containerinstances',
views: {
'content@': {
templateUrl: './views/containerinstances/containerinstances.html',
controller: 'AzureContainerInstancesController',
},
},
};
var containerInstanceCreation = {
name: 'azure.containerinstances.new',
url: '/new/',
views: {
'content@': {
templateUrl: './views/containerinstances/create/createcontainerinstance.html',
controller: 'AzureCreateContainerInstanceController',
},
},
};
var dashboard = {
name: 'azure.dashboard',
url: '/dashboard',
views: {
'content@': {
templateUrl: './views/dashboard/dashboard.html',
controller: 'AzureDashboardController',
},
},
};
$stateRegistryProvider.register(azure);
$stateRegistryProvider.register(containerInstances);
$stateRegistryProvider.register(containerInstanceCreation);
$stateRegistryProvider.register(dashboard);
},
]);

View File

@@ -1,8 +0,0 @@
angular.module('portainer.azure').component('azureEndpointConfig', {
bindings: {
applicationId: '=',
tenantId: '=',
authenticationKey: '=',
},
templateUrl: './azureEndpointConfig.html',
});

View File

@@ -1,36 +0,0 @@
<div>
<div class="col-sm-12 form-section-title">
Azure configuration
</div>
<!-- applicationId-input -->
<div class="form-group">
<label for="azure_credential_appid" class="col-sm-3 col-lg-2 control-label text-left">Application ID</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" name="azure_credential_appid" ng-model="$ctrl.applicationId" placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" required />
</div>
</div>
<!-- !applicationId-input -->
<!-- tenantId-input -->
<div class="form-group">
<label for="azure_credential_tenantid" class="col-sm-3 col-lg-2 control-label text-left">Tenant ID</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" name="azure_credential_tenantid" ng-model="$ctrl.tenantId" placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" required />
</div>
</div>
<!-- !tenantId-input -->
<!-- authenticationkey-input -->
<div class="form-group">
<label for="azure_credential_authkey" class="col-sm-3 col-lg-2 control-label text-left">Authentication key</label>
<div class="col-sm-9 col-lg-10">
<input
type="text"
class="form-control"
name="azure_credential_authkey"
ng-model="$ctrl.authenticationKey"
placeholder="cOrXoK/1D35w8YQ8nH1/8ZGwzz45JIYD5jxHKXEQknk="
required
/>
</div>
</div>
<!-- !authenticationkey-input -->
</div>

View File

@@ -1,3 +0,0 @@
angular.module('portainer.azure').component('azureSidebarContent', {
templateUrl: './azureSidebarContent.html',
});

View File

@@ -1,6 +0,0 @@
<li class="sidebar-list">
<a ui-sref="azure.dashboard" ui-sref-active="active">Dashboard <span class="menu-icon fa fa-tachometer-alt fa-fw"></span></a>
</li>
<li class="sidebar-list">
<a ui-sref="azure.containerinstances" ui-sref-active="active">Container instances <span class="menu-icon fa fa-server fa-fw"></span></a>
</li>

View File

@@ -1,105 +0,0 @@
<div class="datatable">
<rd-widget>
<rd-widget-body classes="no-padding">
<div class="toolBar">
<div class="toolBarTitle"> <i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }} </div>
</div>
<div class="actionBar">
<button type="button" class="btn btn-sm btn-danger" ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
</button>
<button type="button" class="btn btn-sm btn-primary" ui-sref="azure.containerinstances.new">
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add container
</button>
</div>
<div class="searchBar">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input
type="text"
class="searchInput"
ng-model="$ctrl.state.textFilter"
ng-change="$ctrl.onTextFilterChange()"
placeholder="Search..."
auto-focus
ng-model-options="{ debounce: 300 }"
/>
</div>
<div class="table-responsive">
<table class="table table-hover table-filters nowrap-cells">
<thead>
<tr>
<th>
<span class="md-checkbox">
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
<label for="select_all"></label>
</span>
<a ng-click="$ctrl.changeOrderBy('Name')">
Name
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Location')">
Location
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Location' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Location' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
Published Ports
</th>
</tr>
</thead>
<tbody>
<tr
dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))"
ng-class="{ active: item.Checked }"
>
<td>
<span class="md-checkbox">
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-click="$ctrl.selectItem(item, $event)" />
<label for="select_{{ $index }}"></label>
</span>
<a ui-sref="azure.containerinstances.container({ id: item.Id })">{{ item.Name | truncate: 50 }}</a>
</td>
<td>{{ item.Location }}</td>
<td>
<a ng-if="item.Ports.length > 0" ng-repeat="p in item.Ports" class="image-tag" ng-href="http://{{ item.IPAddress }}:{{ p.port }}" target="_blank">
<i class="fa fa-external-link-alt" aria-hidden="true"></i> :{{ p.port }}
</a>
<span ng-if="item.Ports.length == 0">-</span>
</td>
</tr>
<tr ng-if="!$ctrl.dataset">
<td colspan="3" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
<td colspan="3" class="text-center text-muted">No container available.</td>
</tr>
</tbody>
</table>
</div>
<div class="footer" ng-if="$ctrl.dataset">
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0"> {{ $ctrl.state.selectedItemCount }} item(s) selected </div>
<div class="paginationControls">
<form class="form-inline">
<span class="limitSelector">
<span style="margin-right: 5px;">
Items per page
</span>
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</span>
<dir-pagination-controls max-size="5"></dir-pagination-controls>
</form>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>

View File

@@ -1,13 +0,0 @@
angular.module('portainer.azure').component('containergroupsDatatable', {
templateUrl: './containerGroupsDatatable.html',
controller: 'GenericDatatableController',
bindings: {
title: '@',
titleIcon: '@',
dataset: '<',
tableKey: '@',
orderBy: '@',
reverseOrder: '<',
removeAction: '<',
},
});

View File

@@ -1,66 +0,0 @@
export function ContainerGroupDefaultModel() {
this.Location = '';
this.OSType = 'Linux';
this.Name = '';
this.Image = '';
this.AllocatePublicIP = true;
this.Ports = [
{
container: 80,
host: 80,
protocol: 'TCP',
},
];
this.CPU = 1;
this.Memory = 1;
}
export function ContainerGroupViewModel(data) {
this.Id = data.id;
this.Name = data.name;
this.Location = data.location;
this.IPAddress = data.properties.ipAddress.ip;
this.Ports = data.properties.ipAddress.ports;
}
export function CreateContainerGroupRequest(model) {
this.location = model.Location;
var containerPorts = [];
var addressPorts = [];
for (var i = 0; i < model.Ports.length; i++) {
var binding = model.Ports[i];
containerPorts.push({
port: binding.container,
});
addressPorts.push({
port: binding.host,
protocol: binding.protocol,
});
}
this.properties = {
osType: model.OSType,
containers: [
{
name: model.Name,
properties: {
image: model.Image,
ports: containerPorts,
resources: {
requests: {
cpu: model.CPU,
memoryInGB: model.Memory,
},
},
},
},
],
ipAddress: {
type: model.AllocatePublicIP ? 'Public' : 'Private',
ports: addressPorts,
},
};
}

View File

@@ -1,6 +0,0 @@
export function LocationViewModel(data) {
this.Id = data.id;
this.SubscriptionId = data.subscriptionId;
this.DisplayName = data.displayName;
this.Name = data.name;
}

View File

@@ -1,9 +0,0 @@
import _ from 'lodash-es';
export function ContainerInstanceProviderViewModel(data) {
this.Id = data.id;
this.Namespace = data.namespace;
var containerGroupType = _.find(data.resourceTypes, { resourceType: 'containerGroups' });
this.Locations = containerGroupType.locations;
}

View File

@@ -1,6 +0,0 @@
export function ResourceGroupViewModel(data, subscriptionId) {
this.Id = data.id;
this.SubscriptionId = subscriptionId;
this.Name = data.name;
this.Location = data.location;
}

View File

@@ -1,4 +0,0 @@
export function SubscriptionViewModel(data) {
this.Id = data.subscriptionId;
this.Name = data.displayName;
}

View File

@@ -1,20 +0,0 @@
angular.module('portainer.azure').factory('Azure', [
'$http',
'API_ENDPOINT_ENDPOINTS',
'EndpointProvider',
function AzureFactory($http, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
'use strict';
var service = {};
service.delete = function (id, apiVersion) {
var url = API_ENDPOINT_ENDPOINTS + '/' + EndpointProvider.endpointID() + '/azure' + id + '?api-version=' + apiVersion;
return $http({
method: 'DELETE',
url: url,
});
};
return service;
},
]);

View File

@@ -1,45 +0,0 @@
angular.module('portainer.azure').factory('ContainerGroup', [
'$resource',
'API_ENDPOINT_ENDPOINTS',
'EndpointProvider',
function ContainerGroupFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
'use strict';
var resource = {};
var base = $resource(
API_ENDPOINT_ENDPOINTS + '/:endpointId/azure/subscriptions/:subscriptionId/providers/Microsoft.ContainerInstance/containerGroups',
{
endpointId: EndpointProvider.endpointID,
'api-version': '2018-04-01',
},
{
query: { method: 'GET', params: { subscriptionId: '@subscriptionId' } },
}
);
var withResourceGroup = $resource(
API_ENDPOINT_ENDPOINTS +
'/:endpointId/azure/subscriptions/:subscriptionId/resourceGroups/:resourceGroupName/providers/Microsoft.ContainerInstance/containerGroups/:containerGroupName',
{
endpointId: EndpointProvider.endpointID,
'api-version': '2018-04-01',
},
{
create: {
method: 'PUT',
params: {
subscriptionId: '@subscriptionId',
resourceGroupName: '@resourceGroupName',
containerGroupName: '@containerGroupName',
},
},
}
);
resource.query = base.query;
resource.create = withResourceGroup.create;
return resource;
},
]);

View File

@@ -1,18 +0,0 @@
angular.module('portainer.azure').factory('Location', [
'$resource',
'API_ENDPOINT_ENDPOINTS',
'EndpointProvider',
function LocationFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
'use strict';
return $resource(
API_ENDPOINT_ENDPOINTS + '/:endpointId/azure/subscriptions/:subscriptionId/locations',
{
endpointId: EndpointProvider.endpointID,
'api-version': '2016-06-01',
},
{
query: { method: 'GET', params: { subscriptionId: '@subscriptionId' } },
}
);
},
]);

View File

@@ -1,18 +0,0 @@
angular.module('portainer.azure').factory('Provider', [
'$resource',
'API_ENDPOINT_ENDPOINTS',
'EndpointProvider',
function ProviderFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
'use strict';
return $resource(
API_ENDPOINT_ENDPOINTS + '/:endpointId/azure/subscriptions/:subscriptionId/providers/:providerNamespace',
{
endpointId: EndpointProvider.endpointID,
'api-version': '2018-02-01',
},
{
get: { method: 'GET', params: { subscriptionId: '@subscriptionId', providerNamespace: '@providerNamespace' } },
}
);
},
]);

View File

@@ -1,18 +0,0 @@
angular.module('portainer.azure').factory('ResourceGroup', [
'$resource',
'API_ENDPOINT_ENDPOINTS',
'EndpointProvider',
function ResourceGroupFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
'use strict';
return $resource(
API_ENDPOINT_ENDPOINTS + '/:endpointId/azure/subscriptions/:subscriptionId/resourcegroups',
{
endpointId: EndpointProvider.endpointID,
'api-version': '2018-02-01',
},
{
query: { method: 'GET', params: { subscriptionId: '@subscriptionId' } },
}
);
},
]);

View File

@@ -1,18 +0,0 @@
angular.module('portainer.azure').factory('Subscription', [
'$resource',
'API_ENDPOINT_ENDPOINTS',
'EndpointProvider',
function SubscriptionFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
'use strict';
return $resource(
API_ENDPOINT_ENDPOINTS + '/:endpointId/azure/subscriptions',
{
endpointId: EndpointProvider.endpointID,
'api-version': '2016-06-01',
},
{
query: { method: 'GET' },
}
);
},
]);

View File

@@ -1,72 +0,0 @@
angular.module('portainer.azure').factory('AzureService', [
'$q',
'Azure',
'SubscriptionService',
'ResourceGroupService',
'ContainerGroupService',
'ProviderService',
function AzureServiceFactory($q, Azure, SubscriptionService, ResourceGroupService, ContainerGroupService, ProviderService) {
'use strict';
var service = {};
service.deleteContainerGroup = function (id) {
return Azure.delete(id, '2018-04-01');
};
service.createContainerGroup = function (model, subscriptionId, resourceGroupName) {
return ContainerGroupService.create(model, subscriptionId, resourceGroupName);
};
service.subscriptions = function () {
return SubscriptionService.subscriptions();
};
service.containerInstanceProvider = function (subscriptions) {
return retrieveResourcesForEachSubscription(subscriptions, ProviderService.containerInstanceProvider);
};
service.resourceGroups = function (subscriptions) {
return retrieveResourcesForEachSubscription(subscriptions, ResourceGroupService.resourceGroups);
};
service.containerGroups = function (subscriptions) {
return retrieveResourcesForEachSubscription(subscriptions, ContainerGroupService.containerGroups);
};
service.aggregate = function (resourcesBySubcription) {
var aggregatedResources = [];
Object.keys(resourcesBySubcription).forEach(function (key) {
aggregatedResources = aggregatedResources.concat(resourcesBySubcription[key]);
});
return aggregatedResources;
};
function retrieveResourcesForEachSubscription(subscriptions, resourceQuery) {
var deferred = $q.defer();
var resources = {};
var resourceQueries = [];
for (var i = 0; i < subscriptions.length; i++) {
var subscription = subscriptions[i];
resourceQueries.push(resourceQuery(subscription.Id));
}
$q.all(resourceQueries)
.then(function success(data) {
for (var i = 0; i < data.length; i++) {
var result = data[i];
resources[subscriptions[i].Id] = result;
}
deferred.resolve(resources);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve resources', err: err });
});
return deferred.promise;
}
return service;
},
]);

View File

@@ -1,41 +0,0 @@
import { ContainerGroupViewModel, CreateContainerGroupRequest } from '../models/container_group';
angular.module('portainer.azure').factory('ContainerGroupService', [
'$q',
'ContainerGroup',
function ContainerGroupServiceFactory($q, ContainerGroup) {
'use strict';
var service = {};
service.containerGroups = function (subscriptionId) {
var deferred = $q.defer();
ContainerGroup.query({ subscriptionId: subscriptionId })
.$promise.then(function success(data) {
var containerGroups = data.value.map(function (item) {
return new ContainerGroupViewModel(item);
});
deferred.resolve(containerGroups);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve container groups', err: err });
});
return deferred.promise;
};
service.create = function (model, subscriptionId, resourceGroupName) {
var payload = new CreateContainerGroupRequest(model);
return ContainerGroup.create(
{
subscriptionId: subscriptionId,
resourceGroupName: resourceGroupName,
containerGroupName: model.Name,
},
payload
).$promise;
};
return service;
},
]);

View File

@@ -1,29 +0,0 @@
import { LocationViewModel } from '../models/location';
angular.module('portainer.azure').factory('LocationService', [
'$q',
'Location',
function LocationServiceFactory($q, Location) {
'use strict';
var service = {};
service.locations = function (subscriptionId) {
var deferred = $q.defer();
Location.query({ subscriptionId: subscriptionId })
.$promise.then(function success(data) {
var locations = data.value.map(function (item) {
return new LocationViewModel(item);
});
deferred.resolve(locations);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve locations', err: err });
});
return deferred.promise;
};
return service;
},
]);

View File

@@ -1,27 +0,0 @@
import { ContainerInstanceProviderViewModel } from '../models/provider';
angular.module('portainer.azure').factory('ProviderService', [
'$q',
'Provider',
function ProviderServiceFactory($q, Provider) {
'use strict';
var service = {};
service.containerInstanceProvider = function (subscriptionId) {
var deferred = $q.defer();
Provider.get({ subscriptionId: subscriptionId, providerNamespace: 'Microsoft.ContainerInstance' })
.$promise.then(function success(data) {
var provider = new ContainerInstanceProviderViewModel(data);
deferred.resolve(provider);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve provider', err: err });
});
return deferred.promise;
};
return service;
},
]);

View File

@@ -1,29 +0,0 @@
import { ResourceGroupViewModel } from '../models/resource_group';
angular.module('portainer.azure').factory('ResourceGroupService', [
'$q',
'ResourceGroup',
function ResourceGroupServiceFactory($q, ResourceGroup) {
'use strict';
var service = {};
service.resourceGroups = function (subscriptionId) {
var deferred = $q.defer();
ResourceGroup.query({ subscriptionId: subscriptionId })
.$promise.then(function success(data) {
var resourceGroups = data.value.map(function (item) {
return new ResourceGroupViewModel(item, subscriptionId);
});
deferred.resolve(resourceGroups);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve resource groups', err: err });
});
return deferred.promise;
};
return service;
},
]);

View File

@@ -1,29 +0,0 @@
import { SubscriptionViewModel } from '../models/subscription';
angular.module('portainer.azure').factory('SubscriptionService', [
'$q',
'Subscription',
function SubscriptionServiceFactory($q, Subscription) {
'use strict';
var service = {};
service.subscriptions = function () {
var deferred = $q.defer();
Subscription.query({})
.$promise.then(function success(data) {
var subscriptions = data.value.map(function (item) {
return new SubscriptionViewModel(item);
});
deferred.resolve(subscriptions);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve subscriptions', err: err });
});
return deferred.promise;
};
return service;
},
]);

View File

@@ -1,44 +0,0 @@
angular.module('portainer.azure').controller('AzureContainerInstancesController', [
'$scope',
'$state',
'AzureService',
'Notifications',
function ($scope, $state, AzureService, Notifications) {
function initView() {
AzureService.subscriptions()
.then(function success(data) {
var subscriptions = data;
return AzureService.containerGroups(subscriptions);
})
.then(function success(data) {
$scope.containerGroups = AzureService.aggregate(data);
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to load container groups');
});
}
$scope.deleteAction = function (selectedItems) {
var actionCount = selectedItems.length;
angular.forEach(selectedItems, function (item) {
AzureService.deleteContainerGroup(item.Id)
.then(function success() {
Notifications.success('Container group successfully removed', item.Name);
var index = $scope.containerGroups.indexOf(item);
$scope.containerGroups.splice(index, 1);
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to remove container group');
})
.finally(function final() {
--actionCount;
if (actionCount === 0) {
$state.reload();
}
});
});
};
initView();
},
]);

View File

@@ -1,21 +0,0 @@
<rd-header>
<rd-header-title title-text="Container list">
<a data-toggle="tooltip" title="Refresh" ui-sref="azure.containerinstances" ui-sref-opts="{reload: true}">
<i class="fa fa-sync" aria-hidden="true"></i>
</a>
</rd-header-title>
<rd-header-content>Container instances</rd-header-content>
</rd-header>
<div class="row">
<div class="col-sm-12">
<containergroups-datatable
title-text="Containers"
title-icon="fa-server"
dataset="containerGroups"
table-key="containergroups"
order-by="Name"
remove-action="deleteAction"
></containergroups-datatable>
</div>
</div>

View File

@@ -1,93 +0,0 @@
import { ContainerGroupDefaultModel } from '../../../models/container_group';
angular.module('portainer.azure').controller('AzureCreateContainerInstanceController', [
'$q',
'$scope',
'$state',
'AzureService',
'Notifications',
function ($q, $scope, $state, AzureService, Notifications) {
var allResourceGroups = [];
var allProviders = [];
$scope.state = {
actionInProgress: false,
selectedSubscription: null,
selectedResourceGroup: null,
};
$scope.changeSubscription = function () {
var selectedSubscription = $scope.state.selectedSubscription;
updateResourceGroupsAndLocations(selectedSubscription, allResourceGroups, allProviders);
};
$scope.addPortBinding = function () {
$scope.model.Ports.push({ host: '', container: '', protocol: 'TCP' });
};
$scope.removePortBinding = function (index) {
$scope.model.Ports.splice(index, 1);
};
$scope.create = function () {
var model = $scope.model;
var subscriptionId = $scope.state.selectedSubscription.Id;
var resourceGroupName = $scope.state.selectedResourceGroup.Name;
$scope.state.actionInProgress = true;
AzureService.createContainerGroup(model, subscriptionId, resourceGroupName)
.then(function success() {
Notifications.success('Container successfully created', model.Name);
$state.go('azure.containerinstances');
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to create container');
})
.finally(function final() {
$scope.state.actionInProgress = false;
});
};
function updateResourceGroupsAndLocations(subscription, resourceGroups, providers) {
$scope.state.selectedResourceGroup = resourceGroups[subscription.Id][0];
$scope.resourceGroups = resourceGroups[subscription.Id];
var currentSubLocations = providers[subscription.Id].Locations;
$scope.model.Location = currentSubLocations[0];
$scope.locations = currentSubLocations;
}
function initView() {
var model = new ContainerGroupDefaultModel();
AzureService.subscriptions()
.then(function success(data) {
var subscriptions = data;
$scope.state.selectedSubscription = subscriptions[0];
$scope.subscriptions = subscriptions;
return $q.all({
resourceGroups: AzureService.resourceGroups(subscriptions),
containerInstancesProviders: AzureService.containerInstanceProvider(subscriptions),
});
})
.then(function success(data) {
var resourceGroups = data.resourceGroups;
allResourceGroups = resourceGroups;
var containerInstancesProviders = data.containerInstancesProviders;
allProviders = containerInstancesProviders;
$scope.model = model;
var selectedSubscription = $scope.state.selectedSubscription;
updateResourceGroupsAndLocations(selectedSubscription, resourceGroups, containerInstancesProviders);
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve Azure resources');
});
}
initView();
},
]);

View File

@@ -1,167 +0,0 @@
<rd-header>
<rd-header-title title-text="Create container instance"></rd-header-title>
<rd-header-content> <a ui-sref="azure.containerinstances">Container instances</a> &gt; Add container </rd-header-content>
</rd-header>
<div class="row">
<div class="col-sm-12">
<rd-widget>
<rd-widget-body>
<form class="form-horizontal" autocomplete="off">
<div class="col-sm-12 form-section-title">
Azure settings
</div>
<!-- subscription-input -->
<div class="form-group">
<label for="azure_subscription" class="col-sm-1 control-label text-left">Subscription</label>
<div class="col-sm-11">
<select
class="form-control"
name="azure_subscription"
ng-model="state.selectedSubscription"
ng-options="subscription.Name for subscription in subscriptions"
ng-change="changeSubscription()"
></select>
</div>
</div>
<!-- !subscription-input -->
<!-- resourcegroup-input -->
<div class="form-group">
<label for="azure_resourcegroup" class="col-sm-1 control-label text-left">Resource group</label>
<div class="col-sm-11">
<select
class="form-control"
name="azure_resourcegroup"
ng-model="state.selectedResourceGroup"
ng-options="resourceGroup.Name for resourceGroup in resourceGroups"
></select>
</div>
</div>
<!-- !resourcegroup-input -->
<!-- location-input -->
<div class="form-group">
<label for="azure_location" class="col-sm-1 control-label text-left">Location</label>
<div class="col-sm-11">
<select class="form-control" name="azure_location" ng-model="model.Location" ng-options="location for location in locations"></select>
</div>
</div>
<!-- !location-input -->
<div class="col-sm-12 form-section-title">
Container configuration
</div>
<!-- name-input -->
<div class="form-group">
<label for="container_name" class="col-sm-1 control-label text-left">Name</label>
<div class="col-sm-11">
<input type="text" class="form-control" ng-model="model.Name" name="container_name" placeholder="e.g. myContainer" />
</div>
</div>
<!-- !name-input -->
<!-- image-input -->
<div class="form-group">
<label for="image_name" class="col-sm-1 control-label text-left">Image</label>
<div class="col-sm-11">
<input type="text" class="form-control" ng-model="model.Image" name="image_name" placeholder="e.g. nginx:alpine" />
</div>
</div>
<!-- !image-input -->
<!-- os-input -->
<div class="form-group">
<label for="container_os" class="col-sm-1 control-label text-left">OS</label>
<div class="col-sm-11">
<select class="form-control" ng-model="model.OSType" name="container_os">
<option value="Linux">Linux</option>
<option value="Windows">Windows</option>
</select>
</div>
</div>
<!-- !os-input -->
<!-- port-mapping -->
<div class="form-group">
<div class="col-sm-12">
<label class="control-label text-left">Port mapping</label>
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addPortBinding()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> map additional port
</span>
</div>
<!-- port-mapping-input-list -->
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
<div ng-repeat="binding in model.Ports" style="margin-top: 2px;">
<!-- host-port -->
<div class="input-group col-sm-4 input-group-sm">
<span class="input-group-addon">host</span>
<input type="text" class="form-control" ng-model="binding.host" placeholder="e.g. 80" />
</div>
<!-- !host-port -->
<span style="margin: 0 10px 0 10px;">
<i class="fa fa-long-arrow-alt-right" aria-hidden="true"></i>
</span>
<!-- container-port -->
<div class="input-group col-sm-4 input-group-sm">
<span class="input-group-addon">container</span>
<input type="text" class="form-control" ng-model="binding.container" placeholder="e.g. 80" />
</div>
<!-- !container-port -->
<!-- protocol-actions -->
<div class="input-group col-sm-3 input-group-sm">
<div class="btn-group btn-group-sm">
<label class="btn btn-primary" ng-model="binding.protocol" uib-btn-radio="'TCP'">TCP</label>
<label class="btn btn-primary" ng-model="binding.protocol" uib-btn-radio="'UDP'">UDP</label>
</div>
<button class="btn btn-sm btn-danger" type="button" ng-click="removePortBinding($index)">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</div>
<!-- !protocol-actions -->
</div>
</div>
<!-- !port-mapping-input-list -->
</div>
<!-- !port-mapping -->
<!-- public-ip -->
<div class="form-group">
<div class="col-sm-12">
<label for="public_ip" class="control-label text-left">
Allocate public IP address
</label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" name="public_ip" ng-model="model.AllocatePublicIP" /><i></i> </label>
</div>
</div>
<!-- public-ip -->
<div class="col-sm-12 form-section-title">
Container resources
</div>
<!-- cpu-input -->
<div class="form-group">
<label for="container_cpu" class="col-sm-1 control-label text-left">CPU</label>
<div class="col-sm-11">
<input type="number" class="form-control" ng-model="model.CPU" name="container_cpu" placeholder="1" />
</div>
</div>
<!-- !cpu-input -->
<!-- memory-input -->
<div class="form-group">
<label for="container_memory" class="col-sm-1 control-label text-left">Memory</label>
<div class="col-sm-11">
<input type="number" class="form-control" ng-model="model.Memory" name="container_memory" placeholder="1" />
</div>
</div>
<!-- !memory-input -->
<!-- actions -->
<div class="col-sm-12 form-section-title">
Actions
</div>
<div class="form-group">
<div class="col-sm-12">
<button type="button" class="btn btn-primary btn-sm" ng-disabled="state.actionInProgress" ng-click="create()" button-spinner="state.actionInProgress">
<span ng-hide="state.actionInProgress">Deploy the container</span>
<span ng-show="state.actionInProgress">Deployment in progress...</span>
</button>
</div>
</div>
<!-- !actions -->
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>

View File

@@ -1,33 +0,0 @@
<rd-header>
<rd-header-title title-text="Home"></rd-header-title>
<rd-header-content>Dashboard</rd-header-content>
</rd-header>
<div class="row" ng-if="subscriptions">
<div class="col-sm-12 col-md-6">
<a ui-sref="azure.subscriptions">
<rd-widget>
<rd-widget-body>
<div class="widget-icon blue pull-left">
<i class="fa fa-th-list"></i>
</div>
<div class="title">{{ subscriptions.length }}</div>
<div class="comment">Subscriptions</div>
</rd-widget-body>
</rd-widget>
</a>
</div>
<div class="col-sm-12 col-md-6" ng-if="resourceGroups">
<a ui-sref="azure.resourceGroups">
<rd-widget>
<rd-widget-body>
<div class="widget-icon blue pull-left">
<i class="fa fa-th-list"></i>
</div>
<div class="title">{{ resourceGroups.length }}</div>
<div class="comment">Resource groups</div>
</rd-widget-body>
</rd-widget>
</a>
</div>
</div>

View File

@@ -1,23 +0,0 @@
angular.module('portainer.azure').controller('AzureDashboardController', [
'$scope',
'AzureService',
'Notifications',
function ($scope, AzureService, Notifications) {
function initView() {
AzureService.subscriptions()
.then(function success(data) {
var subscriptions = data;
$scope.subscriptions = subscriptions;
return AzureService.resourceGroups(subscriptions);
})
.then(function success(data) {
$scope.resourceGroups = AzureService.aggregate(data);
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to load dashboard data');
});
}
initView();
},
]);

View File

@@ -22,7 +22,6 @@ angular
.constant('API_ENDPOINT_TEAM_MEMBERSHIPS', 'api/team_memberships')
.constant('API_ENDPOINT_TEMPLATES', 'api/templates')
.constant('API_ENDPOINT_WEBHOOKS', 'api/webhooks')
.constant('DEFAULT_TEMPLATES_URL', 'https://raw.githubusercontent.com/portainer/templates/master/templates.json')
.constant('PAGINATION_MAX_ITEMS', 10)
.constant('APPLICATION_CACHE_VALIDITY', 3600)
.constant('CONSOLE_COMMANDS_LABEL_PREFIX', 'io.portainer.commands.')

View File

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

@@ -4,7 +4,7 @@
<div class="toolBar">
<div class="toolBarTitle"> <i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }} </div>
</div>
<div class="actionBar" ng-if="$ctrl.endpointManagement">
<div class="actionBar">
<button type="button" class="btn btn-sm btn-danger" ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
</button>
@@ -27,7 +27,7 @@
<thead>
<tr>
<th>
<span class="md-checkbox" ng-if="$ctrl.endpointManagement">
<span class="md-checkbox">
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
<label for="select_all"></label>
</span>
@@ -68,12 +68,11 @@
ng-class="{ active: item.Checked }"
>
<td>
<span class="md-checkbox" ng-if="$ctrl.endpointManagement">
<span class="md-checkbox">
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-click="$ctrl.selectItem(item, $event)" />
<label for="select_{{ $index }}"></label>
</span>
<a ui-sref="portainer.endpoints.endpoint({id: item.Id})" ng-if="$ctrl.endpointManagement">{{ item.Name }}</a>
<span ng-if="!$ctrl.endpointManagement">{{ item.Name }}</span>
<a ui-sref="portainer.endpoints.endpoint({id: item.Id})">{{ item.Name }}</a>
</td>
<td>
<span>

View File

@@ -7,7 +7,6 @@ angular.module('portainer.app').component('endpointsDatatable', {
tableKey: '@',
orderBy: '@',
reverseOrder: '<',
endpointManagement: '<',
accessManagement: '<',
removeAction: '<',
retrievePage: '<',

View File

@@ -1,6 +1,6 @@
<div class="blocklist-item" ng-click="$ctrl.onSelect($ctrl.model)">
<div class="blocklist-item-box">
<span ng-class="['blocklist-item-logo', 'endpoint-item', { azure: $ctrl.model.Type === 3 }]">
<span class="blocklist-item-logo endpoint-item">
<i ng-if="$ctrl.model.Type !== 4" ng-class="$ctrl.model.Type | endpointtypeicon" class="fa-4x blue-icon" aria-hidden="true"></i>
<img ng-if="$ctrl.model.Type === 4" src="../../../../../assets/images/edge_endpoint.png" />
</span>

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

@@ -128,8 +128,6 @@ angular
return 'Docker';
} else if (type === 2) {
return 'Agent';
} else if (type === 3) {
return 'Azure ACI';
} else if (type === 4) {
return 'Edge Agent';
}

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,7 +1,6 @@
export function StatusViewModel(data) {
this.Authentication = data.Authentication;
this.Snapshot = data.Snapshot;
this.EndpointManagement = data.EndpointManagement;
this.Analytics = data.Analytics;
this.Version = data.Version;
}

View File

@@ -1,84 +1,44 @@
import _ from 'lodash-es';
import { PorImageRegistryModel } from 'Docker/models/porImageRegistry';
export function TemplateDefaultModel() {
this.Type = 1;
this.AdministratorOnly = false;
this.Title = '';
this.Description = '';
this.Volumes = [];
this.Ports = [];
this.Env = [];
this.Labels = [];
this.RestartPolicy = 'always';
this.RegistryModel = new PorImageRegistryModel();
}
export function TemplateCreateRequest(model) {
this.Type = model.Type;
this.Name = model.Name;
this.Hostname = model.Hostname;
this.Title = model.Title;
this.Description = model.Description;
this.Note = model.Note;
this.Categories = model.Categories;
this.Platform = model.Platform;
this.Logo = model.Logo;
this.Image = model.RegistryModel.Image;
this.Registry = model.RegistryModel.Registry.URL;
this.Command = model.Command;
this.Network = model.Network && model.Network.Name;
this.Privileged = model.Privileged;
this.Interactive = model.Interactive;
this.RestartPolicy = model.RestartPolicy;
this.Labels = model.Labels;
this.Repository = model.Repository;
this.Env = model.Env;
this.AdministratorOnly = model.AdministratorOnly;
this.Ports = [];
for (var i = 0; i < model.Ports.length; i++) {
var binding = model.Ports[i];
if (binding.containerPort && binding.protocol) {
var port = binding.hostPort ? binding.hostPort + ':' + binding.containerPort + '/' + binding.protocol : binding.containerPort + '/' + binding.protocol;
this.Ports.push(port);
export class TemplateViewModel {
constructor(data, version) {
switch (version) {
case '2':
this.setTemplatesV2(data);
break;
default:
throw new Error('Unsupported template version');
}
}
this.Volumes = model.Volumes;
}
export function TemplateUpdateRequest(model) {
TemplateCreateRequest.call(this, model);
this.id = model.Id;
}
export function TemplateViewModel(data) {
this.Id = data.Id;
this.Title = data.title;
this.Type = data.type;
this.Description = data.description;
this.AdministratorOnly = data.AdministratorOnly;
this.Name = data.name;
this.Note = data.note;
this.Categories = data.categories ? data.categories : [];
this.Platform = data.platform ? data.platform : '';
this.Logo = data.logo;
this.Repository = data.repository;
this.Hostname = data.hostname;
this.RegistryModel = new PorImageRegistryModel();
this.RegistryModel.Image = data.image;
this.RegistryModel.Registry.URL = data.registry || '';
this.Command = data.command ? data.command : '';
this.Network = data.network ? data.network : '';
this.Privileged = data.privileged ? data.privileged : false;
this.Interactive = data.interactive ? data.interactive : false;
this.RestartPolicy = data.restart_policy ? data.restart_policy : 'always';
this.Labels = data.labels ? data.labels : [];
this.Hosts = data.hosts ? data.hosts : [];
this.Env = templateEnv(data);
this.Volumes = templateVolumes(data);
this.Ports = templatePorts(data);
setTemplatesV2(data) {
this.Id = data.Id;
this.Title = data.title;
this.Type = data.type;
this.Description = data.description;
this.AdministratorOnly = data.AdministratorOnly;
this.Name = data.name;
this.Note = data.note;
this.Categories = data.categories ? data.categories : [];
this.Platform = data.platform ? data.platform : '';
this.Logo = data.logo;
this.Repository = data.repository;
this.Hostname = data.hostname;
this.RegistryModel = new PorImageRegistryModel();
this.RegistryModel.Image = data.image;
this.RegistryModel.Registry.URL = data.registry || '';
this.Command = data.command ? data.command : '';
this.Network = data.network ? data.network : '';
this.Privileged = data.privileged ? data.privileged : false;
this.Interactive = data.interactive ? data.interactive : false;
this.RestartPolicy = data.restart_policy ? data.restart_policy : 'always';
this.Labels = data.labels ? data.labels : [];
this.Hosts = data.hosts ? data.hosts : [];
this.Env = templateEnv(data);
this.Volumes = templateVolumes(data);
this.Ports = templatePorts(data);
}
}
function templatePorts(data) {

View File

@@ -6,11 +6,7 @@ angular.module('portainer.app').factory('Templates', [
API_ENDPOINT_TEMPLATES + '/:id',
{},
{
create: { method: 'POST' },
query: { method: 'GET', isArray: true },
get: { method: 'GET', params: { id: '@id' } },
update: { method: 'PUT', params: { id: '@id' } },
remove: { method: 'DELETE', params: { id: '@id' } },
query: { method: 'GET' },
}
);
},

View File

@@ -84,20 +84,6 @@ angular.module('portainer.app').factory('EndpointService', [
return deferred.promise;
};
service.createAzureEndpoint = function (name, applicationId, tenantId, authenticationKey, groupId, tagIds) {
var deferred = $q.defer();
FileUploadService.createAzureEndpoint(name, applicationId, tenantId, authenticationKey, groupId, tagIds)
.then(function success(response) {
deferred.resolve(response.data);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to connect to Azure', err: err });
});
return deferred.promise;
};
service.executeJobFromFileUpload = function (image, jobFile, endpointId, nodeName) {
return FileUploadService.executeEndpointJob(image, jobFile, endpointId, nodeName);
};

View File

@@ -1,4 +1,5 @@
import { TemplateViewModel, TemplateCreateRequest, TemplateUpdateRequest } from '../../models/template';
import _ from 'lodash-es';
import { TemplateViewModel } from '../../models/template';
angular.module('portainer.app').factory('TemplateService', [
'$q',
@@ -13,7 +14,7 @@ angular.module('portainer.app').factory('TemplateService', [
var service = {};
service.templates = function () {
var deferred = $q.defer();
const deferred = $q.defer();
$q.all({
templates: Templates.query().$promise,
@@ -21,12 +22,17 @@ angular.module('portainer.app').factory('TemplateService', [
dockerhub: DockerHubService.dockerhub(),
})
.then(function success(data) {
const templates = data.templates.map(function (item) {
const res = new TemplateViewModel(item);
const registry = RegistryService.retrievePorRegistryModelFromRepositoryWithRegistries(res.RegistryModel.Registry.URL, data.registries, data.dockerhub);
registry.Image = res.RegistryModel.Image;
res.RegistryModel = registry;
return res;
const version = data.templates.version;
const templates = _.map(data.templates.templates, (item) => {
try {
const template = new TemplateViewModel(item, version);
const registry = RegistryService.retrievePorRegistryModelFromRepositoryWithRegistries(template.RegistryModel.Registry.URL, data.registries, data.dockerhub);
registry.Image = template.RegistryModel.Image;
template.RegistryModel = registry;
return template;
} catch (err) {
deferred.reject({ msg: 'Unable to retrieve templates', err: err });
}
});
deferred.resolve(templates);
})
@@ -37,40 +43,6 @@ angular.module('portainer.app').factory('TemplateService', [
return deferred.promise;
};
service.template = function (id) {
var deferred = $q.defer();
let template;
Templates.get({ id: id })
.$promise.then(function success(data) {
template = new TemplateViewModel(data);
return RegistryService.retrievePorRegistryModelFromRepository(template.RegistryModel.Registry.URL);
})
.then((registry) => {
registry.Image = template.RegistryModel.Image;
template.RegistryModel = registry;
deferred.resolve(template);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve template details', err: err });
});
return deferred.promise;
};
service.delete = function (id) {
return Templates.remove({ id: id }).$promise;
};
service.create = function (model) {
var payload = new TemplateCreateRequest(model);
return Templates.create(payload).$promise;
};
service.update = function (model) {
var payload = new TemplateUpdateRequest(model);
return Templates.update(payload).$promise;
};
service.createTemplateConfiguration = function (template, containerName, network) {
var imageConfiguration = ImageHelper.createImageConfigForContainer(template.RegistryModel);
var containerConfiguration = createContainerConfiguration(template, containerName, network);

View File

@@ -137,22 +137,6 @@ angular.module('portainer.app').factory('FileUploadService', [
});
};
service.createAzureEndpoint = function (name, applicationId, tenantId, authenticationKey, groupId, tagIds) {
return Upload.upload({
url: 'api/endpoints',
data: {
Name: name,
EndpointType: 3,
GroupID: groupId,
TagIds: Upload.json(tagIds),
AzureApplicationID: applicationId,
AzureTenantID: tenantId,
AzureAuthenticationKey: authenticationKey,
},
ignoreLoadingBar: true,
});
};
service.uploadLDAPTLSFiles = function (TLSCAFile, TLSCertFile, TLSKeyFile) {
var queue = [];

View File

@@ -79,8 +79,6 @@ angular.module('portainer.app').factory('StateManager', [
function assignStateFromStatusAndSettings(status, settings) {
state.application.authentication = status.Authentication;
state.application.analytics = status.Analytics;
state.application.endpointManagement = status.EndpointManagement;
state.application.snapshot = status.Snapshot;
state.application.version = status.Version;
state.application.logo = settings.LogoURL;
state.application.snapshotInterval = settings.SnapshotInterval;
@@ -170,14 +168,6 @@ angular.module('portainer.app').factory('StateManager', [
manager.updateEndpointState = function (endpoint, extensions) {
var deferred = $q.defer();
if (endpoint.Type === 3) {
state.endpoint.name = endpoint.Name;
state.endpoint.mode = { provider: 'AZURE' };
LocalStorage.storeEndpointState(state.endpoint);
deferred.resolve();
return deferred.promise;
}
$q.all({
version: endpoint.Status === 1 ? SystemService.version() : $q.when(endpoint.Snapshots[0].SnapshotRaw.Version),
info: endpoint.Status === 1 ? SystemService.info() : $q.when(endpoint.Snapshots[0].SnapshotRaw.Info),

View File

@@ -27,9 +27,6 @@ angular
PublicURL: '',
GroupId: 1,
SecurityFormData: new EndpointSecurityFormData(),
AzureApplicationId: '',
AzureTenantId: '',
AzureAuthenticationKey: '',
TagIds: [],
};
@@ -85,17 +82,6 @@ angular
addEndpoint(name, 4, URL, '', groupId, tagIds, false, false, false, null, null, null);
};
$scope.addAzureEndpoint = function () {
var name = $scope.formValues.Name;
var applicationId = $scope.formValues.AzureApplicationId;
var tenantId = $scope.formValues.AzureTenantId;
var authenticationKey = $scope.formValues.AzureAuthenticationKey;
var groupId = $scope.formValues.GroupId;
var tagIds = $scope.formValues.TagIds;
createAzureEndpoint(name, applicationId, tenantId, authenticationKey, groupId, tagIds);
};
$scope.onCreateTag = function onCreateTag(tagName) {
return $async(onCreateTagAsync, tagName);
};
@@ -110,21 +96,6 @@ angular
}
}
function createAzureEndpoint(name, applicationId, tenantId, authenticationKey, groupId, tagIds) {
$scope.state.actionInProgress = true;
EndpointService.createAzureEndpoint(name, applicationId, tenantId, authenticationKey, groupId, tagIds)
.then(function success() {
Notifications.success('Endpoint created', name);
$state.go('portainer.endpoints', {}, { reload: true });
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to create endpoint');
})
.finally(function final() {
$scope.state.actionInProgress = false;
});
}
function addEndpoint(name, type, URL, PublicURL, groupId, tagIds, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) {
$scope.state.actionInProgress = true;
EndpointService.createRemoteEndpoint(name, type, URL, PublicURL, groupId, tagIds, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile)

View File

@@ -44,16 +44,6 @@
<p>Directly connect to the Docker API</p>
</label>
</div>
<div>
<input type="radio" id="azure_endpoint" ng-model="state.EnvironmentType" value="azure" />
<label for="azure_endpoint">
<div class="boxselector_header">
<i class="fab fa-microsoft" aria-hidden="true" style="margin-right: 2px;"></i>
Azure
</div>
<p>Connect to Microsoft Azure ACI</p>
</label>
</div>
</div>
</div>
<div ng-if="state.EnvironmentType === 'docker'">
@@ -97,29 +87,6 @@
</span>
</div>
</div>
<div ng-if="state.EnvironmentType === 'azure'">
<div class="col-sm-12 form-section-title">
Information
</div>
<div class="form-group">
<div class="col-sm-12">
<span class="small">
<p class="text-muted"> <i class="fa fa-flask orange-icon" aria-hidden="true" style="margin-right: 2px;"></i> This feature is experimental. </p>
<p class="text-primary">
Connect to Microsoft Azure to manage Azure Container Instances (ACI).
</p>
<p class="text-muted">
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
Have a look at
<a href="https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-group-create-service-principal-portal" target="_blank"
>the Azure documentation</a
>
to retrieve the credentials required below.
</p>
</span>
</div>
</div>
</div>
<div class="col-sm-12 form-section-title">
Environment details
</div>
@@ -214,76 +181,6 @@
</div>
</div>
<!-- !endpoint-public-url-input -->
<!-- azure-details -->
<div ng-if="state.EnvironmentType === 'azure'">
<!-- applicationId-input -->
<div class="form-group">
<label for="azure_credential_appid" class="col-sm-3 col-lg-2 control-label text-left">Application ID</label>
<div class="col-sm-9 col-lg-10">
<input
type="text"
class="form-control"
name="azure_credential_appid"
ng-model="formValues.AzureApplicationId"
placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
required
/>
</div>
</div>
<div class="form-group" ng-show="endpointCreationForm.azure_credential_appid.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="endpointCreationForm.azure_credential_appid.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
</div>
<!-- !applicationId-input -->
<!-- tenantId-input -->
<div class="form-group">
<label for="azure_credential_tenantid" class="col-sm-3 col-lg-2 control-label text-left">Tenant ID</label>
<div class="col-sm-9 col-lg-10">
<input
type="text"
class="form-control"
name="azure_credential_tenantid"
ng-model="formValues.AzureTenantId"
placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
required
/>
</div>
</div>
<div class="form-group" ng-show="endpointCreationForm.azure_credential_tenantid.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="endpointCreationForm.azure_credential_tenantid.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
</div>
<!-- !tenantId-input -->
<!-- authenticationkey-input -->
<div class="form-group">
<label for="azure_credential_authkey" class="col-sm-3 col-lg-2 control-label text-left">Authentication key</label>
<div class="col-sm-9 col-lg-10">
<input
type="text"
class="form-control"
name="azure_credential_authkey"
ng-model="formValues.AzureAuthenticationKey"
placeholder="cOrXoK/1D35w8YQ8nH1/8ZGwzz45JIYD5jxHKXEQknk="
required
/>
</div>
</div>
<div class="form-group" ng-show="endpointCreationForm.azure_credential_authkey.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="endpointCreationForm.azure_credential_authkey.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
</div>
<!-- !authenticationkey-input -->
</div>
<!-- !azure-details -->
<!-- endpoint-security -->
<por-endpoint-security ng-if="state.EnvironmentType === 'docker'" form-data="formValues.SecurityFormData"></por-endpoint-security>
<!-- !endpoint-security -->
@@ -350,17 +247,6 @@
<span ng-hide="state.actionInProgress"><i class="fa fa-plus" aria-hidden="true"></i> Add endpoint</span>
<span ng-show="state.actionInProgress">Creating endpoint...</span>
</button>
<button
ng-if="state.EnvironmentType === 'azure'"
type="submit"
class="btn btn-primary btn-sm"
ng-disabled="state.actionInProgress || !endpointCreationForm.$valid"
ng-click="addAzureEndpoint()"
button-spinner="state.actionInProgress"
>
<span ng-hide="state.actionInProgress"><i class="fa fa-plus" aria-hidden="true"></i> Add endpoint</span>
<span ng-show="state.actionInProgress">Creating endpoint...</span>
</button>
</div>
</div>
<!-- !actions -->

View File

@@ -118,12 +118,6 @@
<input type="text" class="form-control" id="endpoint_public_url" ng-model="endpoint.PublicURL" placeholder="e.g. 10.0.0.10 or mydocker.mydomain.com" />
</div>
</div>
<azure-endpoint-config
ng-if="endpoint.Type === 3"
application-id="endpoint.AzureCredentials.ApplicationID"
tenant-id="endpoint.AzureCredentials.TenantID"
authentication-key="endpoint.AzureCredentials.AuthenticationKey"
></azure-endpoint-config>
<!-- !endpoint-public-url-input -->
<div class="col-sm-12 form-section-title">
Metadata

View File

@@ -19,10 +19,6 @@ angular
Notifications,
Authentication
) {
if (!$scope.applicationState.application.endpointManagement) {
$state.go('portainer.endpoints');
}
$scope.state = {
uploadInProgress: false,
actionInProgress: false,
@@ -93,9 +89,6 @@ angular
TLSCACert: TLSSkipVerify || securityData.TLSCACert === endpoint.TLSConfig.TLSCACert ? null : securityData.TLSCACert,
TLSCert: TLSSkipClientVerify || securityData.TLSCert === endpoint.TLSConfig.TLSCert ? null : securityData.TLSCert,
TLSKey: TLSSkipClientVerify || securityData.TLSKey === endpoint.TLSConfig.TLSKey ? null : securityData.TLSKey,
AzureApplicationID: endpoint.AzureCredentials.ApplicationID,
AzureTenantID: endpoint.AzureCredentials.TenantID,
AzureAuthenticationKey: endpoint.AzureCredentials.AuthenticationKey,
};
if ($scope.endpointType !== 'local' && endpoint.Type !== 3) {

View File

@@ -7,20 +7,6 @@
<rd-header-content>Endpoint management</rd-header-content>
</rd-header>
<div class="row" ng-if="!applicationState.application.endpointManagement">
<div class="col-sm-12">
<rd-widget>
<rd-widget-header icon="fa-exclamation-triangle" title-text="Endpoint management is not available"> </rd-widget-header>
<rd-widget-body>
<span class="small text-muted"
>Portainer has been started using the <code>--external-endpoints</code> flag. Endpoint management via the UI is disabled.
<span ng-if="applicationState.application.authentication">You can still manage endpoint access.</span>
</span>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<endpoints-datatable
@@ -28,7 +14,6 @@
title-icon="fa-plug"
table-key="endpoints"
order-by="Name"
endpoint-management="applicationState.application.endpointManagement"
access-management="applicationState.application.authentication"
remove-action="removeAction"
retrieve-page="getPaginatedEndpoints"

View File

@@ -26,9 +26,7 @@ angular
};
$scope.goToDashboard = function (endpoint) {
if (endpoint.Type === 3) {
return switchToAzureEndpoint(endpoint);
} else if (endpoint.Type === 4) {
if (endpoint.Type === 4) {
return switchToEdgeEndpoint(endpoint);
}
@@ -89,19 +87,6 @@ angular
return deferred.promise;
}
function switchToAzureEndpoint(endpoint) {
EndpointProvider.setEndpointID(endpoint.Id);
EndpointProvider.setEndpointPublicURL(endpoint.PublicURL);
EndpointProvider.setOfflineModeFromStatus(endpoint.Status);
StateManager.updateEndpointState(endpoint, [])
.then(function success() {
$state.go('azure.dashboard');
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to connect to the Azure endpoint');
});
}
function switchToEdgeEndpoint(endpoint) {
if (!endpoint.EdgeID) {
$state.go('portainer.endpoints.endpoint', { id: endpoint.Id });

View File

@@ -55,16 +55,6 @@
<p>Connect to a Portainer agent</p>
</label>
</div>
<div>
<input type="radio" id="azure_endpoint" ng-model="formValues.EndpointType" value="azure" />
<label for="azure_endpoint">
<div class="boxselector_header">
<i class="fab fa-microsoft" aria-hidden="true" style="margin-right: 2px;"></i>
Azure
</div>
<p>Connect to Microsoft Azure ACI</p>
</label>
</div>
</div>
</div>
<!-- !endpoint-type -->
@@ -168,91 +158,6 @@
<!-- !actions -->
</div>
<!-- !agent-endpoint -->
<!-- azure-endpoint -->
<div ng-if="formValues.EndpointType === 'azure'">
<div class="col-sm-12 form-section-title">
Information
</div>
<div class="form-group">
<div class="col-sm-12">
<span class="small">
<p class="text-muted"> <i class="fa fa-flask orange-icon" aria-hidden="true" style="margin-right: 2px;"></i> This feature is experimental. </p>
<p class="text-primary">
Connect to Microsoft Azure to manage Azure Container Instances (ACI).
</p>
<p class="text-muted">
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
Have a look at
<a href="https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-group-create-service-principal-portal" target="_blank"
>the Azure documentation</a
>
to retrieve the credentials required below.
</p>
</span>
</div>
</div>
<div class="col-sm-12 form-section-title">
Environment
</div>
<!-- name-input -->
<div class="form-group">
<label for="endpoint_name" class="col-sm-4 col-lg-3 control-label text-left">Name</label>
<div class="col-sm-8 col-lg-9">
<input type="text" class="form-control" id="endpoint_name" ng-model="formValues.Name" placeholder="e.g. azure-01" />
</div>
</div>
<!-- !name-input -->
<div class="col-sm-12 form-section-title">
Azure credentials
</div>
<!-- applicationId-input -->
<div class="form-group">
<label for="azure_credential_appid" class="col-sm-4 col-lg-3 control-label text-left">Application ID</label>
<div class="col-sm-8 col-lg-9">
<input type="text" class="form-control" id="azure_credential_appid" ng-model="formValues.AzureApplicationId" placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" />
</div>
</div>
<!-- !applicationId-input -->
<!-- tenantId-input -->
<div class="form-group">
<label for="azure_credential_tenantid" class="col-sm-4 col-lg-3 control-label text-left">Tenant ID</label>
<div class="col-sm-8 col-lg-9">
<input type="text" class="form-control" id="azure_credential_tenantid" ng-model="formValues.AzureTenantId" placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" />
</div>
</div>
<!-- !tenantId-input -->
<!-- authenticationkey-input -->
<div class="form-group">
<label for="azure_credential_authkey" class="col-sm-4 col-lg-3 control-label text-left">Authentication key</label>
<div class="col-sm-8 col-lg-9">
<input
type="text"
class="form-control"
id="azure_credential_authkey"
ng-model="formValues.AzureAuthenticationKey"
placeholder="cOrXoK/1D35w8YQ8nH1/8ZGwzz45JIYD5jxHKXEQknk="
/>
</div>
</div>
<!-- !authenticationkey-input -->
<!-- actions -->
<div class="form-group">
<div class="col-sm-12">
<button
type="submit"
class="btn btn-primary btn-sm"
ng-disabled="state.actionInProgress || !formValues.Name || !formValues.AzureApplicationId || !formValues.AzureTenantId || !formValues.AzureAuthenticationKey"
ng-click="createAzureEndpoint()"
button-spinner="state.actionInProgress"
>
<span ng-hide="state.actionInProgress"><i class="fa fa-bolt" aria-hidden="true"></i> Connect</span>
<span ng-show="state.actionInProgress">Connecting...</span>
</button>
</div>
</div>
<!-- !actions -->
</div>
<!-- !azure-endpoint -->
<!-- remote-endpoint -->
<div ng-if="formValues.EndpointType === 'remote'">
<div class="col-sm-12 form-section-title">

View File

@@ -28,9 +28,6 @@ angular.module('portainer.app').controller('InitEndpointController', [
TLSCACert: null,
TLSCert: null,
TLSKey: null,
AzureApplicationId: '',
AzureTenantId: '',
AzureAuthenticationKey: '',
};
$scope.createLocalEndpoint = function () {
@@ -47,15 +44,6 @@ angular.module('portainer.app').controller('InitEndpointController', [
});
};
$scope.createAzureEndpoint = function () {
var name = $scope.formValues.Name;
var applicationId = $scope.formValues.AzureApplicationId;
var tenantId = $scope.formValues.AzureTenantId;
var authenticationKey = $scope.formValues.AzureAuthenticationKey;
createAzureEndpoint(name, applicationId, tenantId, authenticationKey);
};
$scope.createAgentEndpoint = function () {
var name = $scope.formValues.Name;
var URL = $scope.formValues.URL;
@@ -78,20 +66,6 @@ angular.module('portainer.app').controller('InitEndpointController', [
createRemoteEndpoint(name, 1, URL, PublicURL, TLS, TLSSkipVerify, TLSSKipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile);
};
function createAzureEndpoint(name, applicationId, tenantId, authenticationKey) {
$scope.state.actionInProgress = true;
EndpointService.createAzureEndpoint(name, applicationId, tenantId, authenticationKey, 1, [])
.then(function success() {
$state.go('portainer.home');
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to connect to the Azure environment');
})
.finally(function final() {
$scope.state.actionInProgress = false;
});
}
function createRemoteEndpoint(name, type, URL, PublicURL, TLS, TLSSkipVerify, TLSSKipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) {
$scope.state.actionInProgress = true;
EndpointService.createRemoteEndpoint(name, type, URL, PublicURL, 1, [], TLS, TLSSkipVerify, TLSSKipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile)

View File

@@ -46,16 +46,7 @@
<div class="col-sm-12 form-section-title">
App Templates
</div>
<div class="form-group">
<div class="col-sm-12">
<label for="toggle_templates" class="control-label text-left">
Use external templates
<portainer-tooltip position="bottom" message="When using external templates, in-app template management will be disabled."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" name="toggle_templates" ng-model="formValues.externalTemplates" /><i></i> </label>
</div>
</div>
<div ng-if="formValues.externalTemplates">
<div>
<div class="form-group">
<span class="col-sm-12 text-muted small">
You can specify the URL to your own template definitions file here. See
@@ -67,7 +58,7 @@
URL
</label>
<div class="col-sm-11">
<input type="text" class="form-control" ng-model="settings.TemplatesURL" id="templates_url" placeholder="https://myserver.mydomain/templates.json" />
<input type="text" class="form-control" ng-model="settings.TemplatesURL" id="templates_url" placeholder="https://myserver.mydomain/templates.json" required/>
</div>
</div>
</div>
@@ -157,7 +148,7 @@
type="button"
class="btn btn-primary btn-sm"
ng-click="saveApplicationSettings()"
ng-disabled="state.actionInProgress"
ng-disabled="state.actionInProgress || !settings.TemplatesURL"
button-spinner="state.actionInProgress"
>
<span ng-hide="state.actionInProgress">Save settings</span>

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