feat(motd): add the ability to display motd and dimiss information panels (#2191)

* feat(api): add motd handler

* feat(app): add the motd api layer

* feat(motd): display motd and add the ability to dismiss information messages

* style(home): relocate important message before info01

* feat(api): silently fail when an error occurs during motd retrieval
pull/2161/head
Anthony Lapenna 2018-08-21 20:40:42 +02:00 committed by GitHub
parent 74ca908759
commit 6ab6cfafb7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 269 additions and 108 deletions

View File

@ -3,7 +3,6 @@ package crypto
import ( import (
"crypto/ecdsa" "crypto/ecdsa"
"crypto/elliptic" "crypto/elliptic"
"crypto/md5"
"crypto/rand" "crypto/rand"
"crypto/x509" "crypto/x509"
"encoding/base64" "encoding/base64"
@ -97,9 +96,7 @@ func (service *ECDSAService) GenerateKeyPair() ([]byte, []byte, error) {
// that hash. // that hash.
// It then encodes the generated signature in base64. // It then encodes the generated signature in base64.
func (service *ECDSAService) Sign(message string) (string, error) { func (service *ECDSAService) Sign(message string) (string, error) {
digest := md5.New() hash := HashFromBytes([]byte(message))
digest.Write([]byte(message))
hash := digest.Sum(nil)
r := big.NewInt(0) r := big.NewInt(0)
s := big.NewInt(0) s := big.NewInt(0)

10
api/crypto/md5.go Normal file
View File

@ -0,0 +1,10 @@
package crypto
import "crypto/md5"
// HashFromBytes returns the hash of the specified data
func HashFromBytes(data []byte) []byte {
digest := md5.New()
digest.Write(data)
return digest.Sum(nil)
}

View File

@ -13,6 +13,10 @@ import (
"github.com/portainer/portainer" "github.com/portainer/portainer"
) )
const (
errInvalidResponseStatus = portainer.Error("Invalid response status (expecting 200)")
)
// HTTPClient represents a client to send HTTP requests. // HTTPClient represents a client to send HTTP requests.
type HTTPClient struct { type HTTPClient struct {
*http.Client *http.Client
@ -75,6 +79,10 @@ func Get(url string) ([]byte, error) {
} }
defer response.Body.Close() defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return nil, errInvalidResponseStatus
}
body, err := ioutil.ReadAll(response.Body) body, err := ioutil.ReadAll(response.Body)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -10,6 +10,7 @@ import (
"github.com/portainer/portainer/http/handler/endpointproxy" "github.com/portainer/portainer/http/handler/endpointproxy"
"github.com/portainer/portainer/http/handler/endpoints" "github.com/portainer/portainer/http/handler/endpoints"
"github.com/portainer/portainer/http/handler/file" "github.com/portainer/portainer/http/handler/file"
"github.com/portainer/portainer/http/handler/motd"
"github.com/portainer/portainer/http/handler/registries" "github.com/portainer/portainer/http/handler/registries"
"github.com/portainer/portainer/http/handler/resourcecontrols" "github.com/portainer/portainer/http/handler/resourcecontrols"
"github.com/portainer/portainer/http/handler/settings" "github.com/portainer/portainer/http/handler/settings"
@ -33,6 +34,7 @@ type Handler struct {
EndpointHandler *endpoints.Handler EndpointHandler *endpoints.Handler
EndpointProxyHandler *endpointproxy.Handler EndpointProxyHandler *endpointproxy.Handler
FileHandler *file.Handler FileHandler *file.Handler
MOTDHandler *motd.Handler
RegistryHandler *registries.Handler RegistryHandler *registries.Handler
ResourceControlHandler *resourcecontrols.Handler ResourceControlHandler *resourcecontrols.Handler
SettingsHandler *settings.Handler SettingsHandler *settings.Handler
@ -67,6 +69,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
default: default:
http.StripPrefix("/api", h.EndpointHandler).ServeHTTP(w, r) http.StripPrefix("/api", h.EndpointHandler).ServeHTTP(w, r)
} }
case strings.HasPrefix(r.URL.Path, "/api/motd"):
http.StripPrefix("/api", h.MOTDHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/registries"): case strings.HasPrefix(r.URL.Path, "/api/registries"):
http.StripPrefix("/api", h.RegistryHandler).ServeHTTP(w, r) http.StripPrefix("/api", h.RegistryHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/resource_controls"): case strings.HasPrefix(r.URL.Path, "/api/resource_controls"):

View File

@ -0,0 +1,24 @@
package motd
import (
"net/http"
"github.com/gorilla/mux"
"github.com/portainer/portainer/http/security"
)
// Handler is the HTTP handler used to handle MOTD operations.
type Handler struct {
*mux.Router
}
// NewHandler returns a new Handler
func NewHandler(bouncer *security.RequestBouncer) *Handler {
h := &Handler{
Router: mux.NewRouter(),
}
h.Handle("/motd",
bouncer.AuthenticatedAccess(http.HandlerFunc(h.motd))).Methods(http.MethodGet)
return h
}

View File

@ -0,0 +1,27 @@
package motd
import (
"net/http"
"github.com/portainer/portainer"
"github.com/portainer/portainer/crypto"
"github.com/portainer/portainer/http/client"
"github.com/portainer/portainer/http/response"
)
type motdResponse struct {
Message string `json:"Message"`
Hash []byte `json:"Hash"`
}
func (handler *Handler) motd(w http.ResponseWriter, r *http.Request) {
motd, err := client.Get(portainer.MessageOfTheDayURL)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
hash := crypto.HashFromBytes(motd)
response.JSON(w, &motdResponse{Message: string(motd), Hash: hash})
}

View File

@ -11,6 +11,7 @@ import (
"github.com/portainer/portainer/http/handler/endpointproxy" "github.com/portainer/portainer/http/handler/endpointproxy"
"github.com/portainer/portainer/http/handler/endpoints" "github.com/portainer/portainer/http/handler/endpoints"
"github.com/portainer/portainer/http/handler/file" "github.com/portainer/portainer/http/handler/file"
"github.com/portainer/portainer/http/handler/motd"
"github.com/portainer/portainer/http/handler/registries" "github.com/portainer/portainer/http/handler/registries"
"github.com/portainer/portainer/http/handler/resourcecontrols" "github.com/portainer/portainer/http/handler/resourcecontrols"
"github.com/portainer/portainer/http/handler/settings" "github.com/portainer/portainer/http/handler/settings"
@ -115,6 +116,8 @@ func (server *Server) Start() error {
var fileHandler = file.NewHandler(filepath.Join(server.AssetsPath, "public")) var fileHandler = file.NewHandler(filepath.Join(server.AssetsPath, "public"))
var motdHandler = motd.NewHandler(requestBouncer)
var registryHandler = registries.NewHandler(requestBouncer) var registryHandler = registries.NewHandler(requestBouncer)
registryHandler.RegistryService = server.RegistryService registryHandler.RegistryService = server.RegistryService
@ -175,6 +178,7 @@ func (server *Server) Start() error {
EndpointHandler: endpointHandler, EndpointHandler: endpointHandler,
EndpointProxyHandler: endpointProxyHandler, EndpointProxyHandler: endpointProxyHandler,
FileHandler: fileHandler, FileHandler: fileHandler,
MOTDHandler: motdHandler,
RegistryHandler: registryHandler, RegistryHandler: registryHandler,
ResourceControlHandler: resourceControlHandler, ResourceControlHandler: resourceControlHandler,
SettingsHandler: settingsHandler, SettingsHandler: settingsHandler,

View File

@ -7,7 +7,7 @@ type (
Value string `json:"value"` Value string `json:"value"`
} }
// CLIFlags represents the available flags on the CLI. // CLIFlags represents the available flags on the CLI
CLIFlags struct { CLIFlags struct {
Addr *string Addr *string
AdminPassword *string AdminPassword *string
@ -35,7 +35,7 @@ type (
SnapshotInterval *string SnapshotInterval *string
} }
// Status represents the application status. // Status represents the application status
Status struct { Status struct {
Authentication bool `json:"Authentication"` Authentication bool `json:"Authentication"`
EndpointManagement bool `json:"EndpointManagement"` EndpointManagement bool `json:"EndpointManagement"`
@ -44,7 +44,7 @@ type (
Version string `json:"Version"` Version string `json:"Version"`
} }
// LDAPSettings represents the settings used to connect to a LDAP server. // LDAPSettings represents the settings used to connect to a LDAP server
LDAPSettings struct { LDAPSettings struct {
ReaderDN string `json:"ReaderDN"` ReaderDN string `json:"ReaderDN"`
Password string `json:"Password"` Password string `json:"Password"`
@ -56,7 +56,7 @@ type (
AutoCreateUsers bool `json:"AutoCreateUsers"` AutoCreateUsers bool `json:"AutoCreateUsers"`
} }
// TLSConfiguration represents a TLS configuration. // TLSConfiguration represents a TLS configuration
TLSConfiguration struct { TLSConfiguration struct {
TLS bool `json:"TLS"` TLS bool `json:"TLS"`
TLSSkipVerify bool `json:"TLSSkipVerify"` TLSSkipVerify bool `json:"TLSSkipVerify"`
@ -65,21 +65,21 @@ type (
TLSKeyPath string `json:"TLSKey,omitempty"` TLSKeyPath string `json:"TLSKey,omitempty"`
} }
// LDAPSearchSettings represents settings used to search for users in a LDAP server. // LDAPSearchSettings represents settings used to search for users in a LDAP server
LDAPSearchSettings struct { LDAPSearchSettings struct {
BaseDN string `json:"BaseDN"` BaseDN string `json:"BaseDN"`
Filter string `json:"Filter"` Filter string `json:"Filter"`
UserNameAttribute string `json:"UserNameAttribute"` UserNameAttribute string `json:"UserNameAttribute"`
} }
// LDAPGroupSearchSettings represents settings used to search for groups in a LDAP server. // LDAPGroupSearchSettings represents settings used to search for groups in a LDAP server
LDAPGroupSearchSettings struct { LDAPGroupSearchSettings struct {
GroupBaseDN string `json:"GroupBaseDN"` GroupBaseDN string `json:"GroupBaseDN"`
GroupFilter string `json:"GroupFilter"` GroupFilter string `json:"GroupFilter"`
GroupAttribute string `json:"GroupAttribute"` GroupAttribute string `json:"GroupAttribute"`
} }
// Settings represents the application settings. // Settings represents the application settings
Settings struct { Settings struct {
LogoURL string `json:"LogoURL"` LogoURL string `json:"LogoURL"`
BlackListedLabels []Pair `json:"BlackListedLabels"` BlackListedLabels []Pair `json:"BlackListedLabels"`
@ -95,7 +95,7 @@ type (
DisplayExternalContributors bool DisplayExternalContributors bool
} }
// User represents a user account. // User represents a user account
User struct { User struct {
ID UserID `json:"Id"` ID UserID `json:"Id"`
Username string `json:"Username"` Username string `json:"Username"`
@ -110,10 +110,10 @@ type (
// or a regular user // or a regular user
UserRole int UserRole int
// AuthenticationMethod represents the authentication method used to authenticate a user. // AuthenticationMethod represents the authentication method used to authenticate a user
AuthenticationMethod int AuthenticationMethod int
// Team represents a list of user accounts. // Team represents a list of user accounts
Team struct { Team struct {
ID TeamID `json:"Id"` ID TeamID `json:"Id"`
Name string `json:"Name"` Name string `json:"Name"`
@ -136,20 +136,20 @@ type (
// MembershipRole represents the role of a user within a team // MembershipRole represents the role of a user within a team
MembershipRole int MembershipRole int
// TokenData represents the data embedded in a JWT token. // TokenData represents the data embedded in a JWT token
TokenData struct { TokenData struct {
ID UserID ID UserID
Username string Username string
Role UserRole Role UserRole
} }
// StackID represents a stack identifier (it must be composed of Name + "_" + SwarmID to create a unique identifier). // StackID represents a stack identifier (it must be composed of Name + "_" + SwarmID to create a unique identifier)
StackID int StackID int
// StackType represents the type of the stack (compose v2, stack deploy v3). // StackType represents the type of the stack (compose v2, stack deploy v3)
StackType int StackType int
// Stack represents a Docker stack created via docker stack deploy. // Stack represents a Docker stack created via docker stack deploy
Stack struct { Stack struct {
ID StackID `json:"Id"` ID StackID `json:"Id"`
Name string `json:"Name"` Name string `json:"Name"`
@ -161,11 +161,11 @@ type (
ProjectPath string ProjectPath string
} }
// RegistryID represents a registry identifier. // RegistryID represents a registry identifier
RegistryID int RegistryID int
// Registry represents a Docker registry with all the info required // Registry represents a Docker registry with all the info required
// to connect to it. // to connect to it
Registry struct { Registry struct {
ID RegistryID `json:"Id"` ID RegistryID `json:"Id"`
Name string `json:"Name"` Name string `json:"Name"`
@ -178,24 +178,24 @@ type (
} }
// DockerHub represents all the required information to connect and use the // DockerHub represents all the required information to connect and use the
// Docker Hub. // Docker Hub
DockerHub struct { DockerHub struct {
Authentication bool `json:"Authentication"` Authentication bool `json:"Authentication"`
Username string `json:"Username"` Username string `json:"Username"`
Password string `json:"Password,omitempty"` Password string `json:"Password,omitempty"`
} }
// EndpointID represents an endpoint identifier. // EndpointID represents an endpoint identifier
EndpointID int EndpointID int
// EndpointType represents the type of an endpoint. // EndpointType represents the type of an endpoint
EndpointType int EndpointType int
// EndpointStatus represents the status of an endpoint // EndpointStatus represents the status of an endpoint
EndpointStatus int EndpointStatus int
// Endpoint represents a Docker endpoint with all the info required // Endpoint represents a Docker endpoint with all the info required
// to connect to it. // to connect to it
Endpoint struct { Endpoint struct {
ID EndpointID `json:"Id"` ID EndpointID `json:"Id"`
Name string `json:"Name"` Name string `json:"Name"`
@ -243,10 +243,10 @@ type (
StackCount int `json:"StackCount"` StackCount int `json:"StackCount"`
} }
// EndpointGroupID represents an endpoint group identifier. // EndpointGroupID represents an endpoint group identifier
EndpointGroupID int EndpointGroupID int
// EndpointGroup represents a group of endpoints. // EndpointGroup represents a group of endpoints
EndpointGroup struct { EndpointGroup struct {
ID EndpointGroupID `json:"Id"` ID EndpointGroupID `json:"Id"`
Name string `json:"Name"` Name string `json:"Name"`
@ -259,17 +259,17 @@ type (
Labels []Pair `json:"Labels"` Labels []Pair `json:"Labels"`
} }
// EndpointExtension represents a extension associated to an endpoint. // EndpointExtension represents a extension associated to an endpoint
EndpointExtension struct { EndpointExtension struct {
Type EndpointExtensionType `json:"Type"` Type EndpointExtensionType `json:"Type"`
URL string `json:"URL"` URL string `json:"URL"`
} }
// EndpointExtensionType represents the type of an endpoint extension. Only // EndpointExtensionType represents the type of an endpoint extension. Only
// one extension of each type can be associated to an endpoint. // one extension of each type can be associated to an endpoint
EndpointExtensionType int EndpointExtensionType int
// ResourceControlID represents a resource control identifier. // ResourceControlID represents a resource control identifier
ResourceControlID int ResourceControlID int
// ResourceControl represent a reference to a Docker resource with specific access controls // ResourceControl represent a reference to a Docker resource with specific access controls
@ -291,37 +291,37 @@ type (
AdministratorsOnly bool `json:"AdministratorsOnly,omitempty"` AdministratorsOnly bool `json:"AdministratorsOnly,omitempty"`
} }
// ResourceControlType represents the type of resource associated to the resource control (volume, container, service...). // ResourceControlType represents the type of resource associated to the resource control (volume, container, service...)
ResourceControlType int ResourceControlType int
// UserResourceAccess represents the level of control on a resource for a specific user. // UserResourceAccess represents the level of control on a resource for a specific user
UserResourceAccess struct { UserResourceAccess struct {
UserID UserID `json:"UserId"` UserID UserID `json:"UserId"`
AccessLevel ResourceAccessLevel `json:"AccessLevel"` AccessLevel ResourceAccessLevel `json:"AccessLevel"`
} }
// TeamResourceAccess represents the level of control on a resource for a specific team. // TeamResourceAccess represents the level of control on a resource for a specific team
TeamResourceAccess struct { TeamResourceAccess struct {
TeamID TeamID `json:"TeamId"` TeamID TeamID `json:"TeamId"`
AccessLevel ResourceAccessLevel `json:"AccessLevel"` AccessLevel ResourceAccessLevel `json:"AccessLevel"`
} }
// TagID represents a tag identifier. // TagID represents a tag identifier
TagID int TagID int
// Tag represents a tag that can be associated to a resource. // Tag represents a tag that can be associated to a resource
Tag struct { Tag struct {
ID TagID ID TagID
Name string `json:"Name"` Name string `json:"Name"`
} }
// TemplateID represents a template identifier. // TemplateID represents a template identifier
TemplateID int TemplateID int
// TemplateType represents the type of a template. // TemplateType represents the type of a template
TemplateType int TemplateType int
// Template represents an application template. // Template represents an application template
Template struct { Template struct {
// Mandatory container/stack fields // Mandatory container/stack fields
ID TemplateID `json:"Id"` ID TemplateID `json:"Id"`
@ -357,7 +357,7 @@ type (
Hostname string `json:"hostname,omitempty"` Hostname string `json:"hostname,omitempty"`
} }
// TemplateEnv represents a template environment variable configuration. // TemplateEnv represents a template environment variable configuration
TemplateEnv struct { TemplateEnv struct {
Name string `json:"name"` Name string `json:"name"`
Label string `json:"label,omitempty"` Label string `json:"label,omitempty"`
@ -367,41 +367,41 @@ type (
Select []TemplateEnvSelect `json:"select,omitempty"` Select []TemplateEnvSelect `json:"select,omitempty"`
} }
// TemplateVolume represents a template volume configuration. // TemplateVolume represents a template volume configuration
TemplateVolume struct { TemplateVolume struct {
Container string `json:"container"` Container string `json:"container"`
Bind string `json:"bind,omitempty"` Bind string `json:"bind,omitempty"`
ReadOnly bool `json:"readonly,omitempty"` ReadOnly bool `json:"readonly,omitempty"`
} }
// TemplateRepository represents the git repository configuration for a template. // TemplateRepository represents the git repository configuration for a template
TemplateRepository struct { TemplateRepository struct {
URL string `json:"url"` URL string `json:"url"`
StackFile string `json:"stackfile"` StackFile string `json:"stackfile"`
} }
// TemplateEnvSelect represents text/value pair that will be displayed as a choice for the // TemplateEnvSelect represents text/value pair that will be displayed as a choice for the
// template user. // template user
TemplateEnvSelect struct { TemplateEnvSelect struct {
Text string `json:"text"` Text string `json:"text"`
Value string `json:"value"` Value string `json:"value"`
Default bool `json:"default"` Default bool `json:"default"`
} }
// ResourceAccessLevel represents the level of control associated to a resource. // ResourceAccessLevel represents the level of control associated to a resource
ResourceAccessLevel int ResourceAccessLevel int
// TLSFileType represents a type of TLS file required to connect to a Docker endpoint. // TLSFileType represents a type of TLS file required to connect to a Docker endpoint.
// It can be either a TLS CA file, a TLS certificate file or a TLS key file. // It can be either a TLS CA file, a TLS certificate file or a TLS key file
TLSFileType int TLSFileType int
// CLIService represents a service for managing CLI. // CLIService represents a service for managing CLI
CLIService interface { CLIService interface {
ParseFlags(version string) (*CLIFlags, error) ParseFlags(version string) (*CLIFlags, error)
ValidateFlags(flags *CLIFlags) error ValidateFlags(flags *CLIFlags) error
} }
// DataStore defines the interface to manage the data. // DataStore defines the interface to manage the data
DataStore interface { DataStore interface {
Open() error Open() error
Init() error Init() error
@ -409,12 +409,12 @@ type (
MigrateData() error MigrateData() error
} }
// Server defines the interface to serve the API. // Server defines the interface to serve the API
Server interface { Server interface {
Start() error Start() error
} }
// UserService represents a service for managing user data. // UserService represents a service for managing user data
UserService interface { UserService interface {
User(ID UserID) (*User, error) User(ID UserID) (*User, error)
UserByUsername(username string) (*User, error) UserByUsername(username string) (*User, error)
@ -425,7 +425,7 @@ type (
DeleteUser(ID UserID) error DeleteUser(ID UserID) error
} }
// TeamService represents a service for managing user data. // TeamService represents a service for managing user data
TeamService interface { TeamService interface {
Team(ID TeamID) (*Team, error) Team(ID TeamID) (*Team, error)
TeamByName(name string) (*Team, error) TeamByName(name string) (*Team, error)
@ -435,7 +435,7 @@ type (
DeleteTeam(ID TeamID) error DeleteTeam(ID TeamID) error
} }
// TeamMembershipService represents a service for managing team membership data. // TeamMembershipService represents a service for managing team membership data
TeamMembershipService interface { TeamMembershipService interface {
TeamMembership(ID TeamMembershipID) (*TeamMembership, error) TeamMembership(ID TeamMembershipID) (*TeamMembership, error)
TeamMemberships() ([]TeamMembership, error) TeamMemberships() ([]TeamMembership, error)
@ -448,7 +448,7 @@ type (
DeleteTeamMembershipByTeamID(teamID TeamID) error DeleteTeamMembershipByTeamID(teamID TeamID) error
} }
// EndpointService represents a service for managing endpoint data. // EndpointService represents a service for managing endpoint data
EndpointService interface { EndpointService interface {
Endpoint(ID EndpointID) (*Endpoint, error) Endpoint(ID EndpointID) (*Endpoint, error)
Endpoints() ([]Endpoint, error) Endpoints() ([]Endpoint, error)
@ -459,7 +459,7 @@ type (
GetNextIdentifier() int GetNextIdentifier() int
} }
// EndpointGroupService represents a service for managing endpoint group data. // EndpointGroupService represents a service for managing endpoint group data
EndpointGroupService interface { EndpointGroupService interface {
EndpointGroup(ID EndpointGroupID) (*EndpointGroup, error) EndpointGroup(ID EndpointGroupID) (*EndpointGroup, error)
EndpointGroups() ([]EndpointGroup, error) EndpointGroups() ([]EndpointGroup, error)
@ -468,7 +468,7 @@ type (
DeleteEndpointGroup(ID EndpointGroupID) error DeleteEndpointGroup(ID EndpointGroupID) error
} }
// RegistryService represents a service for managing registry data. // RegistryService represents a service for managing registry data
RegistryService interface { RegistryService interface {
Registry(ID RegistryID) (*Registry, error) Registry(ID RegistryID) (*Registry, error)
Registries() ([]Registry, error) Registries() ([]Registry, error)
@ -477,7 +477,7 @@ type (
DeleteRegistry(ID RegistryID) error DeleteRegistry(ID RegistryID) error
} }
// StackService represents a service for managing stack data. // StackService represents a service for managing stack data
StackService interface { StackService interface {
Stack(ID StackID) (*Stack, error) Stack(ID StackID) (*Stack, error)
StackByName(name string) (*Stack, error) StackByName(name string) (*Stack, error)
@ -488,25 +488,25 @@ type (
GetNextIdentifier() int GetNextIdentifier() int
} }
// DockerHubService represents a service for managing the DockerHub object. // DockerHubService represents a service for managing the DockerHub object
DockerHubService interface { DockerHubService interface {
DockerHub() (*DockerHub, error) DockerHub() (*DockerHub, error)
UpdateDockerHub(registry *DockerHub) error UpdateDockerHub(registry *DockerHub) error
} }
// SettingsService represents a service for managing application settings. // SettingsService represents a service for managing application settings
SettingsService interface { SettingsService interface {
Settings() (*Settings, error) Settings() (*Settings, error)
UpdateSettings(settings *Settings) error UpdateSettings(settings *Settings) error
} }
// VersionService represents a service for managing version data. // VersionService represents a service for managing version data
VersionService interface { VersionService interface {
DBVersion() (int, error) DBVersion() (int, error)
StoreDBVersion(version int) error StoreDBVersion(version int) error
} }
// ResourceControlService represents a service for managing resource control data. // ResourceControlService represents a service for managing resource control data
ResourceControlService interface { ResourceControlService interface {
ResourceControl(ID ResourceControlID) (*ResourceControl, error) ResourceControl(ID ResourceControlID) (*ResourceControl, error)
ResourceControlByResourceID(resourceID string) (*ResourceControl, error) ResourceControlByResourceID(resourceID string) (*ResourceControl, error)
@ -516,14 +516,14 @@ type (
DeleteResourceControl(ID ResourceControlID) error DeleteResourceControl(ID ResourceControlID) error
} }
// TagService represents a service for managing tag data. // TagService represents a service for managing tag data
TagService interface { TagService interface {
Tags() ([]Tag, error) Tags() ([]Tag, error)
CreateTag(tag *Tag) error CreateTag(tag *Tag) error
DeleteTag(ID TagID) error DeleteTag(ID TagID) error
} }
// TemplateService represents a service for managing template data. // TemplateService represents a service for managing template data
TemplateService interface { TemplateService interface {
Templates() ([]Template, error) Templates() ([]Template, error)
Template(ID TemplateID) (*Template, error) Template(ID TemplateID) (*Template, error)
@ -532,13 +532,13 @@ type (
DeleteTemplate(ID TemplateID) error DeleteTemplate(ID TemplateID) error
} }
// CryptoService represents a service for encrypting/hashing data. // CryptoService represents a service for encrypting/hashing data
CryptoService interface { CryptoService interface {
Hash(data string) (string, error) Hash(data string) (string, error)
CompareHashAndData(hash string, data string) error CompareHashAndData(hash string, data string) error
} }
// DigitalSignatureService represents a service to manage digital signatures. // DigitalSignatureService represents a service to manage digital signatures
DigitalSignatureService interface { DigitalSignatureService interface {
ParseKeyPair(private, public []byte) error ParseKeyPair(private, public []byte) error
GenerateKeyPair() ([]byte, []byte, error) GenerateKeyPair() ([]byte, []byte, error)
@ -547,13 +547,13 @@ type (
Sign(message string) (string, error) Sign(message string) (string, error)
} }
// JWTService represents a service for managing JWT tokens. // JWTService represents a service for managing JWT tokens
JWTService interface { JWTService interface {
GenerateToken(data *TokenData) (string, error) GenerateToken(data *TokenData) (string, error)
ParseAndVerifyToken(token string) (*TokenData, error) ParseAndVerifyToken(token string) (*TokenData, error)
} }
// FileService represents a service for managing files. // FileService represents a service for managing files
FileService interface { FileService interface {
GetFileContent(filePath string) ([]byte, error) GetFileContent(filePath string) ([]byte, error)
Rename(oldPath, newPath string) error Rename(oldPath, newPath string) error
@ -571,13 +571,13 @@ type (
FileExists(path string) (bool, error) FileExists(path string) (bool, error)
} }
// GitService represents a service for managing Git. // GitService represents a service for managing Git
GitService interface { GitService interface {
ClonePublicRepository(repositoryURL, referenceName string, destination string) error ClonePublicRepository(repositoryURL, referenceName string, destination string) error
ClonePrivateRepositoryWithBasicAuth(repositoryURL, referenceName string, destination, username, password string) error ClonePrivateRepositoryWithBasicAuth(repositoryURL, referenceName string, destination, username, password string) error
} }
// JobScheduler represents a service to run jobs on a periodic basis. // JobScheduler represents a service to run jobs on a periodic basis
JobScheduler interface { JobScheduler interface {
ScheduleEndpointSyncJob(endpointFilePath, interval string) error ScheduleEndpointSyncJob(endpointFilePath, interval string) error
ScheduleSnapshotJob(interval string) error ScheduleSnapshotJob(interval string) error
@ -585,19 +585,19 @@ type (
Start() Start()
} }
// Snapshotter represents a service used to create endpoint snapshots. // Snapshotter represents a service used to create endpoint snapshots
Snapshotter interface { Snapshotter interface {
CreateSnapshot(endpoint *Endpoint) (*Snapshot, error) CreateSnapshot(endpoint *Endpoint) (*Snapshot, error)
} }
// LDAPService represents a service used to authenticate users against a LDAP/AD. // LDAPService represents a service used to authenticate users against a LDAP/AD
LDAPService interface { LDAPService interface {
AuthenticateUser(username, password string, settings *LDAPSettings) error AuthenticateUser(username, password string, settings *LDAPSettings) error
TestConnectivity(settings *LDAPSettings) error TestConnectivity(settings *LDAPSettings) error
GetUserGroups(username string, settings *LDAPSettings) ([]string, error) GetUserGroups(username string, settings *LDAPSettings) ([]string, error)
} }
// SwarmStackManager represents a service to manage Swarm stacks. // SwarmStackManager represents a service to manage Swarm stacks
SwarmStackManager interface { SwarmStackManager interface {
Login(dockerhub *DockerHub, registries []Registry, endpoint *Endpoint) Login(dockerhub *DockerHub, registries []Registry, endpoint *Endpoint)
Logout(endpoint *Endpoint) error Logout(endpoint *Endpoint) error
@ -605,7 +605,7 @@ type (
Remove(stack *Stack, endpoint *Endpoint) error Remove(stack *Stack, endpoint *Endpoint) error
} }
// ComposeStackManager represents a service to manage Compose stacks. // ComposeStackManager represents a service to manage Compose stacks
ComposeStackManager interface { ComposeStackManager interface {
Up(stack *Stack, endpoint *Endpoint) error Up(stack *Stack, endpoint *Endpoint) error
Down(stack *Stack, endpoint *Endpoint) error Down(stack *Stack, endpoint *Endpoint) error
@ -613,13 +613,15 @@ type (
) )
const ( const (
// APIVersion is the version number of the Portainer API. // APIVersion is the version number of the Portainer API
APIVersion = "1.19.2-dev" APIVersion = "1.19.2-dev"
// DBVersion is the version number of the Portainer database. // DBVersion is the version number of the Portainer database
DBVersion = 14 DBVersion = 14
// MessageOfTheDayURL represents the URL where Portainer MOTD message can be retrieved
MessageOfTheDayURL = "https://raw.githubusercontent.com/portainer/motd/master/message.html"
// PortainerAgentHeader represents the name of the header available in any agent response // PortainerAgentHeader represents the name of the header available in any agent response
PortainerAgentHeader = "Portainer-Agent" PortainerAgentHeader = "Portainer-Agent"
// PortainerAgentTargetHeader represent the name of the header containing the target node name. // PortainerAgentTargetHeader represent the name of the header containing the target node name
PortainerAgentTargetHeader = "X-PortainerAgent-Target" PortainerAgentTargetHeader = "X-PortainerAgent-Target"
// PortainerAgentSignatureHeader represent the name of the header containing the digital signature // PortainerAgentSignatureHeader represent the name of the header containing the digital signature
PortainerAgentSignatureHeader = "X-PortainerAgent-Signature" PortainerAgentSignatureHeader = "X-PortainerAgent-Signature"
@ -628,16 +630,16 @@ const (
// PortainerAgentSignatureMessage represents the message used to create a digital signature // PortainerAgentSignatureMessage represents the message used to create a digital signature
// to be used when communicating with an agent // to be used when communicating with an agent
PortainerAgentSignatureMessage = "Portainer-App" PortainerAgentSignatureMessage = "Portainer-App"
// SupportedDockerAPIVersion is the minimum Docker API version supported by Portainer. // SupportedDockerAPIVersion is the minimum Docker API version supported by Portainer
SupportedDockerAPIVersion = "1.24" SupportedDockerAPIVersion = "1.24"
) )
const ( const (
// TLSFileCA represents a TLS CA certificate file. // TLSFileCA represents a TLS CA certificate file
TLSFileCA TLSFileType = iota TLSFileCA TLSFileType = iota
// TLSFileCert represents a TLS certificate file. // TLSFileCert represents a TLS certificate file
TLSFileCert TLSFileCert
// TLSFileKey represents a TLS key file. // TLSFileKey represents a TLS key file
TLSFileKey TLSFileKey
) )

View File

@ -3,6 +3,7 @@ angular.module('portainer')
.constant('API_ENDPOINT_DOCKERHUB', 'api/dockerhub') .constant('API_ENDPOINT_DOCKERHUB', 'api/dockerhub')
.constant('API_ENDPOINT_ENDPOINTS', 'api/endpoints') .constant('API_ENDPOINT_ENDPOINTS', 'api/endpoints')
.constant('API_ENDPOINT_ENDPOINT_GROUPS', 'api/endpoint_groups') .constant('API_ENDPOINT_ENDPOINT_GROUPS', 'api/endpoint_groups')
.constant('API_ENDPOINT_MOTD', 'api/motd')
.constant('API_ENDPOINT_REGISTRIES', 'api/registries') .constant('API_ENDPOINT_REGISTRIES', 'api/registries')
.constant('API_ENDPOINT_RESOURCE_CONTROLS', 'api/resource_controls') .constant('API_ENDPOINT_RESOURCE_CONTROLS', 'api/resource_controls')
.constant('API_ENDPOINT_SETTINGS', 'api/settings') .constant('API_ENDPOINT_SETTINGS', 'api/settings')

View File

@ -9,14 +9,10 @@
</div> </div>
</div> </div>
<div class="row" ng-if="!applicationState.endpoint.mode.agentProxy && applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'"> <information-panel
<div class="col-sm-12"> ng-if="!applicationState.UI.dismissedInfoPanels['docker-dashboard-info-01'] && !applicationState.endpoint.mode.agentProxy && applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'"
<rd-widget> title-text="Information"
<rd-widget-body> dismiss-action="dismissInformationPanel('docker-dashboard-info-01')">
<div class="col-sm-12 form-section-title">
Information
</div>
<div class="form-group">
<span class="small"> <span class="small">
<p class="text-muted" ng-if="applicationState.endpoint.mode.role === 'MANAGER'"> <p class="text-muted" ng-if="applicationState.endpoint.mode.role === 'MANAGER'">
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i> <i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
@ -28,11 +24,7 @@
Portainer is connected to a worker node. Swarm management features will not be available. Portainer is connected to a worker node. Swarm management features will not be available.
</p> </p>
</span> </span>
</div> </information-panel>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row" ng-if="(!applicationState.endpoint.mode.agentProxy || applicationState.endpoint.mode.provider !== 'DOCKER_SWARM_MODE') && info && endpoint"> <div class="row" ng-if="(!applicationState.endpoint.mode.agentProxy || applicationState.endpoint.mode.provider !== 'DOCKER_SWARM_MODE') && info && endpoint">
<div class="col-sm-12"> <div class="col-sm-12">

View File

@ -1,6 +1,10 @@
angular.module('portainer.docker') angular.module('portainer.docker')
.controller('DashboardController', ['$scope', '$q', 'ContainerService', 'ImageService', 'NetworkService', 'VolumeService', 'SystemService', 'ServiceService', 'StackService', 'EndpointService', 'Notifications', 'EndpointProvider', .controller('DashboardController', ['$scope', '$q', 'ContainerService', 'ImageService', 'NetworkService', 'VolumeService', 'SystemService', 'ServiceService', 'StackService', 'EndpointService', 'Notifications', 'EndpointProvider', 'StateManager',
function ($scope, $q, ContainerService, ImageService, NetworkService, VolumeService, SystemService, ServiceService, StackService, EndpointService, Notifications, EndpointProvider) { function ($scope, $q, ContainerService, ImageService, NetworkService, VolumeService, SystemService, ServiceService, StackService, EndpointService, Notifications, EndpointProvider, StateManager) {
$scope.dismissInformationPanel = function(id) {
StateManager.dismissInformationPanel(id);
};
function initView() { function initView() {
var endpointMode = $scope.applicationState.endpoint.mode; var endpointMode = $scope.applicationState.endpoint.mode;

View File

@ -1,7 +1,8 @@
angular.module('portainer.app').component('informationPanel', { angular.module('portainer.app').component('informationPanel', {
templateUrl: 'app/portainer/components/information-panel/informationPanel.html', templateUrl: 'app/portainer/components/information-panel/informationPanel.html',
bindings: { bindings: {
titleText: '@' titleText: '@',
dismissAction: '&'
}, },
transclude: true transclude: true
}); });

View File

@ -3,7 +3,12 @@
<rd-widget> <rd-widget>
<rd-widget-body> <rd-widget-body>
<div class="col-sm-12 form-section-title"> <div class="col-sm-12 form-section-title">
<span style="float: left;">
{{ $ctrl.titleText }} {{ $ctrl.titleText }}
</span>
<span class="small" style="float: right;">
<a ng-click="$ctrl.dismissAction()"><i class="fa fa-times"></i> dismiss</a>
</span>
</div> </div>
<div class="form-group"> <div class="form-group">
<ng-transclude></ng-transclude> <ng-transclude></ng-transclude>

View File

@ -0,0 +1,4 @@
function MotdViewModel(data) {
this.Message = data.Message;
this.Hash = data.Hash;
}

View File

@ -0,0 +1,7 @@
angular.module('portainer.app')
.factory('Motd', ['$resource', 'API_ENDPOINT_MOTD', function MotdFactory($resource, API_ENDPOINT_MOTD) {
'use strict';
return $resource(API_ENDPOINT_MOTD, {}, {
get: { method: 'GET' }
});
}]);

View File

@ -0,0 +1,22 @@
angular.module('portainer.app')
.factory('MotdService', ['$q', 'Motd', function MotdServiceFactory($q, Motd) {
'use strict';
var service = {};
service.motd = function() {
var deferred = $q.defer();
Motd.get().$promise
.then(function success(data) {
var motd = new MotdViewModel(data);
deferred.resolve(motd);
})
.catch(function error(err) {
deferred.reject({msg: 'Unable to retrieve information message', err: err});
});
return deferred.promise;
};
return service;
}]);

View File

@ -26,6 +26,12 @@ angular.module('portainer.app')
getApplicationState: function() { getApplicationState: function() {
return localStorageService.get('APPLICATION_STATE'); return localStorageService.get('APPLICATION_STATE');
}, },
storeUIState: function(state) {
localStorageService.cookie.set('UI_STATE', state);
},
getUIState: function() {
return localStorageService.cookie.get('UI_STATE');
},
storeJWT: function(jwt) { storeJWT: function(jwt) {
localStorageService.set('JWT', jwt); localStorageService.set('JWT', jwt);
}, },

View File

@ -9,7 +9,20 @@ function StateManagerFactory($q, SystemService, InfoHelper, LocalStorage, Settin
loading: true, loading: true,
application: {}, application: {},
endpoint: {}, endpoint: {},
UI: {} UI: {
dismissedInfoPanels: {},
dismissedInfoHash: ''
}
};
manager.dismissInformationPanel = function(id) {
state.UI.dismissedInfoPanels[id] = true;
LocalStorage.storeUIState(state.UI);
};
manager.dismissImportantInformation = function(hash) {
state.UI.dismissedInfoHash = hash;
LocalStorage.storeUIState(state.UI);
}; };
manager.getState = function() { manager.getState = function() {
@ -68,6 +81,11 @@ function StateManagerFactory($q, SystemService, InfoHelper, LocalStorage, Settin
manager.initialize = function () { manager.initialize = function () {
var deferred = $q.defer(); var deferred = $q.defer();
var UIState = LocalStorage.getUIState();
if (UIState) {
state.UI = UIState;
}
var endpointState = LocalStorage.getEndpointState(); var endpointState = LocalStorage.getEndpointState();
if (endpointState) { if (endpointState) {
state.endpoint = endpointState; state.endpoint = endpointState;

View File

@ -7,7 +7,19 @@
<rd-header-content>Endpoints</rd-header-content> <rd-header-content>Endpoints</rd-header-content>
</rd-header> </rd-header>
<information-panel title-text="Information"> <information-panel
ng-if="motd && applicationState.UI.dismissedInfoHash !== motd.Hash"
title-text="Important message"
dismiss-action="dismissImportantInformation(motd.Hash)">
<span class="text-muted">
<p ng-bind-html="motd.Message"></p>
</span>
</information-panel>
<information-panel
ng-if="!applicationState.UI.dismissedInfoPanels['home-info-01']"
title-text="Information"
dismiss-action="dismissInformationPanel('home-info-01')">
<span class="small text-muted"> <span class="small text-muted">
<p ng-if="endpoints.length > 0"> <p ng-if="endpoints.length > 0">
Welcome to Portainer ! Click on any endpoint in the list below to access management features. Welcome to Portainer ! Click on any endpoint in the list below to access management features.

View File

@ -1,6 +1,6 @@
angular.module('portainer.app') angular.module('portainer.app')
.controller('HomeController', ['$q', '$scope', '$state', 'Authentication', 'EndpointService', 'EndpointHelper', 'GroupService', 'Notifications', 'EndpointProvider', 'StateManager', 'ExtensionManager', 'ModalService', .controller('HomeController', ['$q', '$scope', '$state', 'Authentication', 'EndpointService', 'EndpointHelper', 'GroupService', 'Notifications', 'EndpointProvider', 'StateManager', 'ExtensionManager', 'ModalService', 'MotdService',
function ($q, $scope, $state, Authentication, EndpointService, EndpointHelper, GroupService, Notifications, EndpointProvider, StateManager, ExtensionManager, ModalService) { function ($q, $scope, $state, Authentication, EndpointService, EndpointHelper, GroupService, Notifications, EndpointProvider, StateManager, ExtensionManager, ModalService, MotdService) {
$scope.goToDashboard = function(endpoint) { $scope.goToDashboard = function(endpoint) {
EndpointProvider.setEndpointID(endpoint.Id); EndpointProvider.setEndpointID(endpoint.Id);
@ -12,6 +12,14 @@ function ($q, $scope, $state, Authentication, EndpointService, EndpointHelper, G
} }
}; };
$scope.dismissImportantInformation = function(hash) {
StateManager.dismissImportantInformation(hash);
};
$scope.dismissInformationPanel = function(id) {
StateManager.dismissInformationPanel(id);
};
function triggerSnapshot() { function triggerSnapshot() {
EndpointService.snapshot() EndpointService.snapshot()
.then(function success(data) { .then(function success(data) {
@ -57,6 +65,11 @@ function ($q, $scope, $state, Authentication, EndpointService, EndpointHelper, G
function initView() { function initView() {
$scope.isAdmin = Authentication.getUserDetails().role === 1; $scope.isAdmin = Authentication.getUserDetails().role === 1;
MotdService.motd()
.then(function success(data) {
$scope.motd = data;
});
$q.all({ $q.all({
endpoints: EndpointService.endpoints(), endpoints: EndpointService.endpoints(),
groups: GroupService.groups() groups: GroupService.groups()