Merge branch 'release/1.17.1'

pull/1924/head 1.17.1
Anthony Lapenna 2018-05-21 11:03:55 +02:00
commit f42733b74c
32 changed files with 297 additions and 210 deletions

View File

@ -44,7 +44,3 @@ steps:
candidate: '${{build_image}}'
tag: '${{CF_BRANCH}}'
registry: dockerhub
when:
branch:
only:
- develop

View File

@ -0,0 +1,28 @@
package bolt
import "github.com/portainer/portainer"
func (m *Migrator) updateEndpointsToVersion11() error {
legacyEndpoints, err := m.EndpointService.Endpoints()
if err != nil {
return err
}
for _, endpoint := range legacyEndpoints {
if endpoint.Type == portainer.AgentOnDockerEnvironment {
endpoint.TLSConfig.TLS = true
endpoint.TLSConfig.TLSSkipVerify = true
} else {
if endpoint.TLSConfig.TLSSkipVerify && !endpoint.TLSConfig.TLS {
endpoint.TLSConfig.TLSSkipVerify = false
}
}
err = m.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return err
}
}
return nil
}

View File

@ -112,6 +112,14 @@ func (m *Migrator) Migrate() error {
}
}
// https://github.com/portainer/portainer/issues/1906
if m.CurrentDBVersion < 11 {
err := m.updateEndpointsToVersion11()
if err != nil {
return err
}
}
err := m.VersionService.StoreDBVersion(portainer.DBVersion)
if err != nil {
return err

View File

@ -33,11 +33,11 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
Addr: kingpin.Flag("bind", "Address and port to serve Portainer").Default(defaultBindAddress).Short('p').String(),
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(),
Endpoint: kingpin.Flag("host", "Dockerd endpoint").Short('H').String(),
EndpointURL: kingpin.Flag("host", "Endpoint URL").Short('H').String(),
ExternalEndpoints: kingpin.Flag("external-endpoints", "Path to a file defining available endpoints").String(),
NoAuth: kingpin.Flag("no-auth", "Disable authentication").Default(defaultNoAuth).Bool(),
NoAnalytics: kingpin.Flag("no-analytics", "Disable Analytics in app").Default(defaultNoAnalytics).Bool(),
TLSVerify: kingpin.Flag("tlsverify", "TLS support").Default(defaultTLSVerify).Bool(),
TLS: kingpin.Flag("tlsverify", "TLS support").Default(defaultTLS).Bool(),
TLSSkipVerify: kingpin.Flag("tlsskipverify", "Disable TLS server verification").Default(defaultTLSSkipVerify).Bool(),
TLSCacert: kingpin.Flag("tlscacert", "Path to the CA").Default(defaultTLSCACertPath).String(),
TLSCert: kingpin.Flag("tlscert", "Path to the TLS certificate file").Default(defaultTLSCertPath).String(),
@ -69,11 +69,11 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
// ValidateFlags validates the values of the flags.
func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
if *flags.Endpoint != "" && *flags.ExternalEndpoints != "" {
if *flags.EndpointURL != "" && *flags.ExternalEndpoints != "" {
return errEndpointExcludeExternal
}
err := validateEndpoint(*flags.Endpoint)
err := validateEndpointURL(*flags.EndpointURL)
if err != nil {
return err
}
@ -99,14 +99,14 @@ func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
return nil
}
func validateEndpoint(endpoint string) error {
if endpoint != "" {
if !strings.HasPrefix(endpoint, "unix://") && !strings.HasPrefix(endpoint, "tcp://") {
func validateEndpointURL(endpointURL string) error {
if endpointURL != "" {
if !strings.HasPrefix(endpointURL, "unix://") && !strings.HasPrefix(endpointURL, "tcp://") {
return errInvalidEndpointProtocol
}
if strings.HasPrefix(endpoint, "unix://") {
socketPath := strings.TrimPrefix(endpoint, "unix://")
if strings.HasPrefix(endpointURL, "unix://") {
socketPath := strings.TrimPrefix(endpointURL, "unix://")
if _, err := os.Stat(socketPath); err != nil {
if os.IsNotExist(err) {
return errSocketNotFound

View File

@ -8,7 +8,7 @@ const (
defaultAssetsDirectory = "./"
defaultNoAuth = "false"
defaultNoAnalytics = "false"
defaultTLSVerify = "false"
defaultTLS = "false"
defaultTLSSkipVerify = "false"
defaultTLSCACertPath = "/certs/ca.pem"
defaultTLSCertPath = "/certs/cert.pem"

View File

@ -6,7 +6,7 @@ const (
defaultAssetsDirectory = "./"
defaultNoAuth = "false"
defaultNoAnalytics = "false"
defaultTLSVerify = "false"
defaultTLS = "false"
defaultTLSSkipVerify = "false"
defaultTLSCACertPath = "C:\\certs\\ca.pem"
defaultTLSCertPath = "C:\\certs\\cert.pem"

View File

@ -1,6 +1,8 @@
package main // import "github.com/portainer/portainer"
import (
"strings"
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt"
"github.com/portainer/portainer/cli"
@ -62,8 +64,8 @@ func initStore(dataStorePath string) *bolt.Store {
return store
}
func initStackManager(assetsPath string, signatureService portainer.DigitalSignatureService, fileService portainer.FileService) (portainer.StackManager, error) {
return exec.NewStackManager(assetsPath, signatureService, fileService)
func initStackManager(assetsPath string, dataStorePath string, signatureService portainer.DigitalSignatureService, fileService portainer.FileService) (portainer.StackManager, error) {
return exec.NewStackManager(assetsPath, dataStorePath, signatureService, fileService)
}
func initJWTService(authenticationEnabled bool) portainer.JWTService {
@ -207,6 +209,93 @@ func initKeyPair(fileService portainer.FileService, signatureService portainer.D
return generateAndStoreKeyPair(fileService, signatureService)
}
func createTLSSecuredEndpoint(flags *portainer.CLIFlags, endpointService portainer.EndpointService) error {
tlsConfiguration := portainer.TLSConfiguration{
TLS: *flags.TLS,
TLSSkipVerify: *flags.TLSSkipVerify,
}
if *flags.TLS {
tlsConfiguration.TLSCACertPath = *flags.TLSCacert
tlsConfiguration.TLSCertPath = *flags.TLSCert
tlsConfiguration.TLSKeyPath = *flags.TLSKey
} else if !*flags.TLS && *flags.TLSSkipVerify {
tlsConfiguration.TLS = true
}
endpoint := &portainer.Endpoint{
Name: "primary",
URL: *flags.EndpointURL,
GroupID: portainer.EndpointGroupID(1),
Type: portainer.DockerEnvironment,
TLSConfig: tlsConfiguration,
AuthorizedUsers: []portainer.UserID{},
AuthorizedTeams: []portainer.TeamID{},
Extensions: []portainer.EndpointExtension{},
}
if strings.HasPrefix(endpoint.URL, "tcp://") {
tlsConfig, err := crypto.CreateTLSConfigurationFromDisk(tlsConfiguration.TLSCACertPath, tlsConfiguration.TLSCertPath, tlsConfiguration.TLSKeyPath, tlsConfiguration.TLSSkipVerify)
if err != nil {
return err
}
agentOnDockerEnvironment, err := client.ExecutePingOperation(endpoint.URL, tlsConfig)
if err != nil {
return err
}
if agentOnDockerEnvironment {
endpoint.Type = portainer.AgentOnDockerEnvironment
}
}
return endpointService.CreateEndpoint(endpoint)
}
func createUnsecuredEndpoint(endpointURL string, endpointService portainer.EndpointService) error {
if strings.HasPrefix(endpointURL, "tcp://") {
_, err := client.ExecutePingOperation(endpointURL, nil)
if err != nil {
return err
}
}
endpoint := &portainer.Endpoint{
Name: "primary",
URL: endpointURL,
GroupID: portainer.EndpointGroupID(1),
Type: portainer.DockerEnvironment,
TLSConfig: portainer.TLSConfiguration{},
AuthorizedUsers: []portainer.UserID{},
AuthorizedTeams: []portainer.TeamID{},
Extensions: []portainer.EndpointExtension{},
}
return endpointService.CreateEndpoint(endpoint)
}
func initEndpoint(flags *portainer.CLIFlags, endpointService portainer.EndpointService) error {
if *flags.EndpointURL == "" {
return nil
}
endpoints, err := endpointService.Endpoints()
if err != nil {
return err
}
if len(endpoints) > 0 {
log.Println("Instance already has defined endpoints. Skipping the endpoint defined via CLI.")
return nil
}
if *flags.TLS || *flags.TLSSkipVerify {
return createTLSSecuredEndpoint(flags, endpointService)
}
return createUnsecuredEndpoint(*flags.EndpointURL, endpointService)
}
func main() {
flags := initCLI()
@ -232,7 +321,7 @@ func main() {
log.Fatal(err)
}
stackManager, err := initStackManager(*flags.Assets, digitalSignatureService, fileService)
stackManager, err := initStackManager(*flags.Assets, *flags.Data, digitalSignatureService, fileService)
if err != nil {
log.Fatal(err)
}
@ -249,44 +338,9 @@ func main() {
applicationStatus := initStatus(authorizeEndpointMgmt, flags)
if *flags.Endpoint != "" {
endpoints, err := store.EndpointService.Endpoints()
if err != nil {
log.Fatal(err)
}
if len(endpoints) == 0 {
endpoint := &portainer.Endpoint{
Name: "primary",
URL: *flags.Endpoint,
Type: portainer.DockerEnvironment,
TLSConfig: portainer.TLSConfiguration{
TLS: *flags.TLSVerify,
TLSSkipVerify: *flags.TLSSkipVerify,
TLSCACertPath: *flags.TLSCacert,
TLSCertPath: *flags.TLSCert,
TLSKeyPath: *flags.TLSKey,
},
AuthorizedUsers: []portainer.UserID{},
AuthorizedTeams: []portainer.TeamID{},
Extensions: []portainer.EndpointExtension{},
}
agentOnDockerEnvironment, err := client.ExecutePingOperationFromEndpoint(endpoint)
if err != nil {
log.Fatal(err)
}
if agentOnDockerEnvironment {
endpoint.Type = portainer.AgentOnDockerEnvironment
}
err = store.EndpointService.CreateEndpoint(endpoint)
if err != nil {
log.Fatal(err)
}
} else {
log.Println("Instance already has defined endpoints. Skipping the endpoint defined via CLI.")
}
err = initEndpoint(flags, store.EndpointService)
if err != nil {
log.Fatal(err)
}
adminPasswordHash := ""

View File

@ -4,11 +4,11 @@ import (
"crypto/tls"
"crypto/x509"
"io/ioutil"
"github.com/portainer/portainer"
)
func CreateTLSConfig(caCert, cert, key []byte, skipClientVerification, skipServerVerification bool) (*tls.Config, error) {
// CreateTLSConfigurationFromBytes initializes a tls.Config using a CA certificate, a certificate and a key
// loaded from memory.
func CreateTLSConfigurationFromBytes(caCert, cert, key []byte, skipClientVerification, skipServerVerification bool) (*tls.Config, error) {
config := &tls.Config{}
config.InsecureSkipVerify = skipServerVerification
@ -29,32 +29,31 @@ func CreateTLSConfig(caCert, cert, key []byte, skipClientVerification, skipServe
return config, nil
}
// CreateTLSConfiguration initializes a tls.Config using a CA certificate, a certificate and a key
func CreateTLSConfiguration(config *portainer.TLSConfiguration) (*tls.Config, error) {
TLSConfig := &tls.Config{}
// CreateTLSConfigurationFromDisk initializes a tls.Config using a CA certificate, a certificate and a key
// loaded from disk.
func CreateTLSConfigurationFromDisk(caCertPath, certPath, keyPath string, skipServerVerification bool) (*tls.Config, error) {
config := &tls.Config{}
config.InsecureSkipVerify = skipServerVerification
if config.TLS && config.TLSCertPath != "" && config.TLSKeyPath != "" {
cert, err := tls.LoadX509KeyPair(config.TLSCertPath, config.TLSKeyPath)
if certPath != "" && keyPath != "" {
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
if err != nil {
return nil, err
}
TLSConfig.Certificates = []tls.Certificate{cert}
config.Certificates = []tls.Certificate{cert}
}
if config.TLS && !config.TLSSkipVerify {
caCert, err := ioutil.ReadFile(config.TLSCACertPath)
if !skipServerVerification && caCertPath != "" {
caCert, err := ioutil.ReadFile(caCertPath)
if err != nil {
return nil, err
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
TLSConfig.RootCAs = caCertPool
config.RootCAs = caCertPool
}
TLSConfig.InsecureSkipVerify = config.TLSSkipVerify
return TLSConfig, nil
return config, nil
}

View File

@ -2,6 +2,7 @@ package exec
import (
"bytes"
"encoding/json"
"os"
"os/exec"
"path"
@ -13,28 +14,22 @@ import (
// StackManager represents a service for managing stacks.
type StackManager struct {
binaryPath string
dataPath string
signatureService portainer.DigitalSignatureService
fileService portainer.FileService
}
type dockerCLIConfiguration struct {
HTTPHeaders struct {
ManagerOperationHeader string `json:"X-PortainerAgent-ManagerOperation"`
SignatureHeader string `json:"X-PortainerAgent-Signature"`
PublicKey string `json:"X-PortainerAgent-PublicKey"`
} `json:"HttpHeaders"`
}
// NewStackManager initializes a new StackManager service.
// It also updates the configuration of the Docker CLI binary.
func NewStackManager(binaryPath string, signatureService portainer.DigitalSignatureService, fileService portainer.FileService) (*StackManager, error) {
func NewStackManager(binaryPath, dataPath string, signatureService portainer.DigitalSignatureService, fileService portainer.FileService) (*StackManager, error) {
manager := &StackManager{
binaryPath: binaryPath,
dataPath: dataPath,
signatureService: signatureService,
fileService: fileService,
}
err := manager.updateDockerCLIConfiguration(binaryPath)
err := manager.updateDockerCLIConfiguration(dataPath)
if err != nil {
return nil, err
}
@ -44,7 +39,7 @@ func NewStackManager(binaryPath string, signatureService portainer.DigitalSignat
// Login executes the docker login command against a list of registries (including DockerHub).
func (manager *StackManager) Login(dockerhub *portainer.DockerHub, registries []portainer.Registry, endpoint *portainer.Endpoint) {
command, args := prepareDockerCommandAndArgs(manager.binaryPath, endpoint)
command, args := prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint)
for _, registry := range registries {
if registry.Authentication {
registryArgs := append(args, "login", "--username", registry.Username, "--password", registry.Password, registry.URL)
@ -60,7 +55,7 @@ func (manager *StackManager) Login(dockerhub *portainer.DockerHub, registries []
// Logout executes the docker logout command.
func (manager *StackManager) Logout(endpoint *portainer.Endpoint) error {
command, args := prepareDockerCommandAndArgs(manager.binaryPath, endpoint)
command, args := prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint)
args = append(args, "logout")
return runCommandAndCaptureStdErr(command, args, nil, "")
}
@ -68,7 +63,7 @@ func (manager *StackManager) Logout(endpoint *portainer.Endpoint) error {
// Deploy executes the docker stack deploy command.
func (manager *StackManager) Deploy(stack *portainer.Stack, prune bool, endpoint *portainer.Endpoint) error {
stackFilePath := path.Join(stack.ProjectPath, stack.EntryPoint)
command, args := prepareDockerCommandAndArgs(manager.binaryPath, endpoint)
command, args := prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint)
if prune {
args = append(args, "stack", "deploy", "--prune", "--with-registry-auth", "--compose-file", stackFilePath, stack.Name)
@ -87,7 +82,7 @@ func (manager *StackManager) Deploy(stack *portainer.Stack, prune bool, endpoint
// Remove executes the docker stack rm command.
func (manager *StackManager) Remove(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
command, args := prepareDockerCommandAndArgs(manager.binaryPath, endpoint)
command, args := prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint)
args = append(args, "stack", "rm", stack.Name)
return runCommandAndCaptureStdErr(command, args, nil, "")
}
@ -111,7 +106,7 @@ func runCommandAndCaptureStdErr(command string, args []string, env []string, wor
return nil
}
func prepareDockerCommandAndArgs(binaryPath string, endpoint *portainer.Endpoint) (string, []string) {
func prepareDockerCommandAndArgs(binaryPath, dataPath string, endpoint *portainer.Endpoint) (string, []string) {
// Assume Linux as a default
command := path.Join(binaryPath, "docker")
@ -120,12 +115,10 @@ func prepareDockerCommandAndArgs(binaryPath string, endpoint *portainer.Endpoint
}
args := make([]string, 0)
args = append(args, "--config", binaryPath)
args = append(args, "--config", dataPath)
args = append(args, "-H", endpoint.URL)
if !endpoint.TLSConfig.TLS && endpoint.TLSConfig.TLSSkipVerify {
args = append(args, "--tls")
} else if endpoint.TLSConfig.TLS {
if endpoint.TLSConfig.TLS {
args = append(args, "--tls")
if !endpoint.TLSConfig.TLSSkipVerify {
@ -140,21 +133,46 @@ func prepareDockerCommandAndArgs(binaryPath string, endpoint *portainer.Endpoint
return command, args
}
func (manager *StackManager) updateDockerCLIConfiguration(binaryPath string) error {
config := dockerCLIConfiguration{}
config.HTTPHeaders.ManagerOperationHeader = "1"
func (manager *StackManager) updateDockerCLIConfiguration(dataPath string) error {
configFilePath := path.Join(dataPath, "config.json")
config, err := manager.retrieveConfigurationFromDisk(configFilePath)
if err != nil {
return err
}
signature, err := manager.signatureService.Sign(portainer.PortainerAgentSignatureMessage)
if err != nil {
return err
}
config.HTTPHeaders.SignatureHeader = signature
config.HTTPHeaders.PublicKey = manager.signatureService.EncodedPublicKey()
err = manager.fileService.WriteJSONToFile(path.Join(binaryPath, "config.json"), config)
if config["HttpHeaders"] == nil {
config["HttpHeaders"] = make(map[string]interface{})
}
headersObject := config["HttpHeaders"].(map[string]interface{})
headersObject["X-PortainerAgent-ManagerOperation"] = "1"
headersObject["X-PortainerAgent-Signature"] = signature
headersObject["X-PortainerAgent-PublicKey"] = manager.signatureService.EncodedPublicKey()
err = manager.fileService.WriteJSONToFile(configFilePath, config)
if err != nil {
return err
}
return nil
}
func (manager *StackManager) retrieveConfigurationFromDisk(path string) (map[string]interface{}, error) {
var config map[string]interface{}
raw, err := manager.fileService.GetFileContent(path)
if err != nil {
return make(map[string]interface{}), nil
}
err = json.Unmarshal([]byte(raw), &config)
if err != nil {
return nil, err
}
return config, nil
}

View File

@ -7,39 +7,8 @@ import (
"time"
"github.com/portainer/portainer"
"github.com/portainer/portainer/crypto"
)
// ExecutePingOperationFromEndpoint will send a SystemPing operation HTTP request to a Docker environment
// using the specified endpoint configuration. It is used exclusively when
// specifying an endpoint from the CLI via the -H flag.
func ExecutePingOperationFromEndpoint(endpoint *portainer.Endpoint) (bool, error) {
if strings.HasPrefix(endpoint.URL, "unix://") {
return false, nil
}
transport := &http.Transport{}
scheme := "http"
if endpoint.TLSConfig.TLS || endpoint.TLSConfig.TLSSkipVerify {
tlsConfig, err := crypto.CreateTLSConfiguration(&endpoint.TLSConfig)
if err != nil {
return false, err
}
scheme = "https"
transport.TLSClientConfig = tlsConfig
}
client := &http.Client{
Timeout: time.Second * 3,
Transport: transport,
}
target := strings.Replace(endpoint.URL, "tcp://", scheme+"://", 1)
return pingOperation(client, target)
}
// ExecutePingOperation will send a SystemPing operation HTTP request to a Docker environment
// using the specified host and optional TLS configuration.
func ExecutePingOperation(host string, tlsConfig *tls.Config) (bool, error) {

View File

@ -121,7 +121,7 @@ func (handler *EndpointHandler) handleGetEndpoints(w http.ResponseWriter, r *htt
}
func (handler *EndpointHandler) createTLSSecuredEndpoint(payload *postEndpointPayload) (*portainer.Endpoint, error) {
tlsConfig, err := crypto.CreateTLSConfig(payload.caCert, payload.cert, payload.key, payload.skipTLSClientVerification, payload.skipTLSServerVerification)
tlsConfig, err := crypto.CreateTLSConfigurationFromBytes(payload.caCert, payload.cert, payload.key, payload.skipTLSClientVerification, payload.skipTLSServerVerification)
if err != nil {
return nil, err
}
@ -472,7 +472,7 @@ func (handler *EndpointHandler) handlePutEndpoint(w http.ResponseWriter, r *http
}
} else {
endpoint.TLSConfig.TLS = false
endpoint.TLSConfig.TLSSkipVerify = true
endpoint.TLSConfig.TLSSkipVerify = false
endpoint.TLSConfig.TLSCACertPath = ""
endpoint.TLSConfig.TLSCertPath = ""
endpoint.TLSConfig.TLSKeyPath = ""

View File

@ -94,6 +94,8 @@ func (handler *WebSocketHandler) handleWebsocketExec(w http.ResponseWriter, r *h
}
func (handler *WebSocketHandler) handleRequest(w http.ResponseWriter, r *http.Request, params *webSocketExecRequestParams) error {
r.Header.Del("Origin")
if params.nodeName != "" {
return handler.proxyWebsocketRequest(w, r, params)
}
@ -135,7 +137,6 @@ func (handler *WebSocketHandler) proxyWebsocketRequest(w http.ResponseWriter, r
out.Set(portainer.PortainerAgentTargetHeader, params.nodeName)
}
r.Header.Del("Origin")
proxy.ServeHTTP(w, r)
return nil
@ -187,7 +188,7 @@ func createDial(endpoint *portainer.Endpoint) (net.Conn, error) {
}
if endpoint.TLSConfig.TLS {
tlsConfig, err := crypto.CreateTLSConfiguration(&endpoint.TLSConfig)
tlsConfig, err := crypto.CreateTLSConfigurationFromDisk(endpoint.TLSConfig.TLSCACertPath, endpoint.TLSConfig.TLSCertPath, endpoint.TLSConfig.TLSKeyPath, endpoint.TLSConfig.TLSSkipVerify)
if err != nil {
return nil, err
}

View File

@ -29,7 +29,7 @@ func (factory *proxyFactory) newDockerHTTPSProxy(u *url.URL, tlsConfig *portaine
u.Scheme = "https"
proxy := factory.createDockerReverseProxy(u, enableSignature)
config, err := crypto.CreateTLSConfiguration(tlsConfig)
config, err := crypto.CreateTLSConfigurationFromDisk(tlsConfig.TLSCACertPath, tlsConfig.TLSCertPath, tlsConfig.TLSKeyPath, tlsConfig.TLSSkipVerify)
if err != nil {
return nil, err
}

View File

@ -60,7 +60,7 @@ func (manager *Manager) CreateAndRegisterProxy(endpoint *portainer.Endpoint) (ht
}
if endpointURL.Scheme == "tcp" {
if endpoint.TLSConfig.TLS || endpoint.TLSConfig.TLSSkipVerify {
if endpoint.TLSConfig.TLS {
proxy, err = manager.proxyFactory.newDockerHTTPSProxy(endpointURL, &endpoint.TLSConfig, enableSignature)
if err != nil {
return nil, err

View File

@ -4,6 +4,7 @@ import (
"bytes"
"encoding/json"
"io/ioutil"
"log"
"net/http"
"strconv"
@ -48,8 +49,10 @@ func getResponseAsJSONArray(response *http.Response) ([]interface{}, error) {
if responseObject["message"] != nil {
return nil, portainer.Error(responseObject["message"].(string))
}
log.Printf("Response: %+v\n", responseObject)
return nil, ErrInvalidResponseContent
default:
log.Printf("Response: %+v\n", responseObject)
return nil, ErrInvalidResponseContent
}
}

View File

@ -55,7 +55,7 @@ func searchUser(username string, conn *ldap.Conn, settings []portainer.LDAPSearc
func createConnection(settings *portainer.LDAPSettings) (*ldap.Conn, error) {
if settings.TLSConfig.TLS || settings.StartTLS {
config, err := crypto.CreateTLSConfiguration(&settings.TLSConfig)
config, err := crypto.CreateTLSConfigurationFromDisk(settings.TLSConfig.TLSCACertPath, settings.TLSConfig.TLSCertPath, settings.TLSConfig.TLSKeyPath, settings.TLSConfig.TLSSkipVerify)
if err != nil {
return nil, err
}

View File

@ -16,14 +16,14 @@ type (
AdminPasswordFile *string
Assets *string
Data *string
Endpoint *string
EndpointURL *string
ExternalEndpoints *string
Labels *[]Pair
Logo *string
NoAuth *bool
NoAnalytics *bool
Templates *string
TLSVerify *bool
TLS *bool
TLSSkipVerify *bool
TLSCacert *string
TLSCert *string
@ -443,9 +443,9 @@ type (
const (
// APIVersion is the version number of the Portainer API.
APIVersion = "1.17.0"
APIVersion = "1.17.1"
// DBVersion is the version number of the Portainer database.
DBVersion = 10
DBVersion = 11
// DefaultTemplatesURL represents the default URL for the templates definitions.
DefaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates.json"
// PortainerAgentHeader represents the name of the header available in any agent response

View File

@ -56,7 +56,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.17.0"
version: "1.17.1"
title: "Portainer API"
contact:
email: "info@portainer.io"
@ -228,12 +228,45 @@ paths:
produces:
- "application/json"
parameters:
- in: "body"
name: "body"
description: "Endpoint details"
- name: "Name"
in: "formData"
type: "string"
description: "Name that will be used to identify this endpoint (example: my-endpoint)"
required: true
schema:
$ref: "#/definitions/EndpointCreateRequest"
- name: "URL"
in: "formData"
type: "string"
description: "URL or IP address of a Docker host (example: docker.mydomain.tld:2375)"
required: true
- name: "PublicURL"
in: "formData"
type: "string"
description: "URL or IP address where exposed containers will be reachable.\
\ Defaults to URL if not specified (example: docker.mydomain.tld:2375)"
- name: "GroupID"
in: "formData"
type: "string"
description: "Endpoint group identifier. If not specified will default to 1 (unassigned)."
- name: "TLS"
in: "formData"
type: "string"
description: "Require TLS to connect against this endpoint (example: true)"
- name: "TLSSkipVerify"
in: "formData"
type: "string"
description: "Skip server verification when using TLS" (example: false)
- name: "TLSCACertFile"
in: "formData"
type: "file"
description: "TLS CA certificate file"
- name: "TLSCertFile"
in: "formData"
type: "file"
description: "TLS client certificate file"
- name: "TLSKeyFile"
in: "formData"
type: "file"
description: "TLS client key file"
responses:
200:
description: "Success"
@ -2143,7 +2176,7 @@ definitions:
description: "Is analytics enabled"
Version:
type: "string"
example: "1.17.0"
example: "1.17.1"
description: "Portainer API version"
PublicSettingsInspectResponse:
type: "object"
@ -2344,10 +2377,22 @@ definitions:
type: "string"
example: "my-endpoint"
description: "Endpoint name"
Type:
type: "integer"
example: 1
description: "Endpoint environment type. 1 for a Docker environment, 2 for an agent on Docker environment."
URL:
type: "string"
example: "docker.mydomain.tld:2375"
description: "URL or IP address of the Docker host associated to this endpoint"
PublicURL:
type: "string"
example: "docker.mydomain.tld:2375"
description: "URL or IP address where exposed containers will be reachable"
GroupID:
type: "integer"
example: 1
description: "Endpoint group identifier"
AuthorizedUsers:
type: "array"
description: "List of user identifiers authorized to connect to this endpoint"
@ -2424,53 +2469,6 @@ definitions:
type: "string"
example: "hub_password"
description: "Password used to authenticate against the DockerHub"
EndpointCreateRequest:
type: "object"
required:
- "Name"
- "URL"
properties:
Name:
type: "string"
example: "my-endpoint"
description: "Name that will be used to identify this endpoint"
URL:
type: "string"
example: "docker.mydomain.tld:2375"
description: "URL or IP address of a Docker host"
PublicURL:
type: "string"
example: "docker.mydomain.tld:2375"
description: "URL or IP address where exposed containers will be reachable.\
\ Defaults to URL if not specified"
TLS:
type: "boolean"
example: true
description: "Require TLS to connect against this endpoint"
TLSSkipVerify:
type: "boolean"
example: false
description: "Skip server verification when using TLS"
TLSSkipClientVerify:
type: "boolean"
example: false
description: "Skip client verification when using TLS"
TLSCACertFile:
type: "file"
description: "TLS CA certificate file"
TLSCertFile:
type: "file"
description: "TLS client certificate file"
TLSKeyFile:
type: "file"
description: "TLS client key file"
EndpointCreateResponse:
type: "object"
properties:
Id:
type: "integer"
example: 1
description: "Id of the endpoint"
EndpointListResponse:
type: "array"
items:

View File

@ -108,7 +108,7 @@
</a>
</span>
<span ng-if="item.Mode === 'replicated' && item.Scale">
<input class="input-sm" type="number" ng-model="item.Replicas" on-enter-key="$ctrl.scaleAction(item)" />
<input class="input-sm" type="number" ng-model="item.Replicas" on-enter-key="$ctrl.scaleAction(item)" auto-focus/>
<a class="interactive" ng-click="item.Scale = false;"><i class="fa fa-times"></i></a>
<a class="interactive" ng-click="$ctrl.scaleAction(item)"><i class="fa fa-check-square"></i></a>
</span>

View File

@ -60,8 +60,8 @@
<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>
<a ng-if="!$ctrl.agentProxy || !item.Container" ui-sref="docker.tasks.task({id: item.Id})" class="monospaced">{{ item.Service.Name }}{{ item.Slot ? '.' + item.Slot : '' }}{{ '.' + item.Id }}</a>
<a ng-if="$ctrl.agentProxy && item.Container" ui-sref="docker.containers.container({ id: item.Container.Id, nodeName: item.Container.NodeName })" class="monospaced">{{ item.Service.Name }}{{ item.Slot ? '.' + item.Slot : '' }}{{ '.' + item.Id }}</a>
<a ng-if="!$ctrl.agentProxy || !item.Container" ui-sref="docker.tasks.task({id: item.Id})" class="monospaced">{{ item.ServiceName }}{{ item.Slot ? '.' + item.Slot : '' }}{{ '.' + item.Id }}</a>
<a ng-if="$ctrl.agentProxy && item.Container" ui-sref="docker.containers.container({ id: item.Container.Id, nodeName: item.Container.NodeName })" class="monospaced">{{ item.ServiceName }}{{ item.Slot ? '.' + item.Slot : '' }}{{ '.' + item.Id }}</a>
</td>
<td><span class="label label-{{ item.Status.State | taskstatusbadge }}">{{ item.Status.State }}</span></td>
<td ng-if="$ctrl.showSlotColumn">{{ item.Slot ? item.Slot : '-' }}</td>
@ -77,10 +77,10 @@
</td>
</tr>
<tr ng-if="!$ctrl.dataset">
<td colspan="5" class="text-center text-muted">Loading...</td>
<td colspan="6" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
<td colspan="5" class="text-center text-muted">No task available.</td>
<td colspan="6" class="text-center text-muted">No task available.</td>
</tr>
</tbody>
</table>

View File

@ -4,6 +4,7 @@ angular.module('portainer.docker').component('logViewer', {
bindings: {
data: '=',
displayTimestamps: '=',
logCollectionChange: '<'
logCollectionChange: '<',
lineCount: '='
}
});

View File

@ -33,6 +33,14 @@
<input class="form-control" type="text" name="logs_search" ng-model="$ctrl.state.search" ng-change="$ctrl.state.selectedLines.length = 0;" placeholder="Filter...">
</div>
</div>
<div class="form-group">
<label for="lines_count" class="col-sm-1 control-label text-left">
Lines
</label>
<div class="col-sm-11">
<input class="form-control" type="number" name="lines_count" ng-model="$ctrl.lineCount" placeholder="Enter no of lines...">
</div>
</div>
<div class="form-group" ng-if="$ctrl.state.copySupported">
<label class="col-sm-1 control-label text-left">
Actions

View File

@ -11,7 +11,7 @@ angular.module('portainer.docker')
var task = tasks[i];
if (task.ServiceId === service.Id) {
service.Tasks.push(task);
task.Service = service;
task.ServiceName = service.Name;
} else {
otherServicesTasks.push(task);
}

View File

@ -6,5 +6,5 @@
</rd-header>
<log-viewer
data="logs" ng-if="logs" log-collection-change="changeLogCollection" display-timestamps="state.displayTimestamps"
data="logs" ng-if="logs" log-collection-change="changeLogCollection" display-timestamps="state.displayTimestamps" line-count="state.lineCount"
></log-viewer>

View File

@ -6,5 +6,5 @@
</rd-header>
<log-viewer
data="logs" ng-if="logs" log-collection-change="changeLogCollection" display-timestamps="state.displayTimestamps"
data="logs" ng-if="logs" log-collection-change="changeLogCollection" display-timestamps="state.displayTimestamps" line-count="state.lineCount"
></log-viewer>

View File

@ -6,5 +6,5 @@
</rd-header>
<log-viewer
data="logs" ng-if="logs" log-collection-change="changeLogCollection" display-timestamps="state.displayTimestamps"
data="logs" ng-if="logs" log-collection-change="changeLogCollection" display-timestamps="state.displayTimestamps" line-count="state.lineCount"
></log-viewer>

View File

@ -69,10 +69,10 @@
</a>
</span>
<span ng-if="item.Mode === 'replicated' && item.Scale">
<input class="input-sm" type="number" ng-model="item.Replicas" on-enter-key="$ctrl.scaleAction(item)" />
<input class="input-sm" type="number" ng-model="item.Replicas" on-enter-key="$ctrl.scaleAction(item)" auto-focus/>
<a class="interactive" ng-click="item.Scale = false;"><i class="fa fa-times"></i></a>
<a class="interactive" ng-click="$ctrl.scaleAction(item)"><i class="fa fa-check-square"></i></a>
</span>
</span>
</td>
<td>
<a ng-if="item.Ports && item.Ports.length > 0 && p.PublishedPort" ng-repeat="p in item.Ports" class="image-tag" ng-href="http://{{ $ctrl.publicUrl }}:{{ p.PublishedPort }}" target="_blank">

View File

@ -14,7 +14,7 @@
groups="groups"
select-endpoint="switchEndpoint"
></endpoint-selector>
<li class="sidebar-title"><span>{{ activeEndpoint.Name }}</span></li>
<li class="sidebar-title"><span class="endpoint-name">{{ activeEndpoint.Name }}</span></li>
<docker-sidebar-content
endpoint-api-version="applicationState.endpoint.apiVersion"
swarm-management="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER'"

View File

@ -58,8 +58,8 @@ function ($q, $scope, $state, EndpointService, GroupService, StateManager, Endpo
})
.then(function success(data) {
var endpoints = data.endpoints;
$scope.groups = data.groups;
$scope.endpoints = endpoints;
$scope.groups = _.sortBy(data.groups, ['Name']);
$scope.endpoints = _.sortBy(endpoints, ['Name']);
setActiveEndpoint(endpoints);

View File

@ -304,6 +304,10 @@ ul.sidebar .sidebar-title {
height: auto;
}
ul.sidebar .sidebar-title .endpoint-name {
color: #fff;
}
ul.sidebar .sidebar-list a {
font-size: 14px;
}

View File

@ -1,5 +1,5 @@
Name: portainer
Version: 1.17.0
Version: 1.17.1
Release: 0
License: Zlib
Summary: A lightweight docker management UI

View File

@ -2,7 +2,7 @@
"author": "Portainer.io",
"name": "portainer",
"homepage": "http://portainer.io",
"version": "1.17.0",
"version": "1.17.1",
"repository": {
"type": "git",
"url": "git@github.com:portainer/portainer.git"