mirror of https://github.com/portainer/portainer
feat(registry) EE-806 add support for AWS ECR (#6165)
* feat(ecr) EE-806 add support for aws ecr * feat(ecr) EE-806 fix wrong doc for Ecr Region Co-authored-by: Simon Meng <simon.meng@portainer.io>pull/6195/head
parent
ff6185cc81
commit
a86c7046df
|
@ -0,0 +1,61 @@
|
||||||
|
package ecr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Service) GetEncodedAuthorizationToken() (token *string, expiry *time.Time, err error) {
|
||||||
|
getAuthorizationTokenOutput, err := s.client.GetAuthorizationToken(context.TODO(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(getAuthorizationTokenOutput.AuthorizationData) == 0 {
|
||||||
|
err = fmt.Errorf("AuthorizationData is empty")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
authData := getAuthorizationTokenOutput.AuthorizationData[0]
|
||||||
|
|
||||||
|
token = authData.AuthorizationToken
|
||||||
|
expiry = authData.ExpiresAt
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetAuthorizationToken() (token *string, expiry *time.Time, err error) {
|
||||||
|
tokenEncodedStr, expiry, err := s.GetEncodedAuthorizationToken()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenByte, err := base64.StdEncoding.DecodeString(*tokenEncodedStr)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tokenStr := string(tokenByte)
|
||||||
|
token = &tokenStr
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ParseAuthorizationToken(token string) (username string, password string, err error) {
|
||||||
|
if len(token) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
splitToken := strings.Split(token, ":")
|
||||||
|
if len(splitToken) < 2 {
|
||||||
|
err = fmt.Errorf("invalid ECR authorization token")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
username = splitToken[0]
|
||||||
|
password = splitToken[1]
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
package ecr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/aws/aws-sdk-go-v2/aws"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/credentials"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/service/ecr"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Service struct {
|
||||||
|
accessKey string
|
||||||
|
secretKey string
|
||||||
|
region string
|
||||||
|
client *ecr.Client
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewService(accessKey, secretKey, region string) *Service {
|
||||||
|
options := ecr.Options{
|
||||||
|
Region: region,
|
||||||
|
Credentials: aws.NewCredentialsCache(credentials.NewStaticCredentialsProvider(accessKey, secretKey, "")),
|
||||||
|
}
|
||||||
|
|
||||||
|
client := ecr.New(options)
|
||||||
|
|
||||||
|
return &Service{
|
||||||
|
accessKey: accessKey,
|
||||||
|
secretKey: secretKey,
|
||||||
|
region: region,
|
||||||
|
client: client,
|
||||||
|
}
|
||||||
|
}
|
|
@ -105,8 +105,15 @@ func initComposeStackManager(assetsPath string, configPath string, reverseTunnel
|
||||||
return composeWrapper
|
return composeWrapper
|
||||||
}
|
}
|
||||||
|
|
||||||
func initSwarmStackManager(assetsPath string, configPath string, signatureService portainer.DigitalSignatureService, fileService portainer.FileService, reverseTunnelService portainer.ReverseTunnelService) (portainer.SwarmStackManager, error) {
|
func initSwarmStackManager(
|
||||||
return exec.NewSwarmStackManager(assetsPath, configPath, signatureService, fileService, reverseTunnelService)
|
assetsPath string,
|
||||||
|
configPath string,
|
||||||
|
signatureService portainer.DigitalSignatureService,
|
||||||
|
fileService portainer.FileService,
|
||||||
|
reverseTunnelService portainer.ReverseTunnelService,
|
||||||
|
dataStore portainer.DataStore,
|
||||||
|
) (portainer.SwarmStackManager, error) {
|
||||||
|
return exec.NewSwarmStackManager(assetsPath, configPath, signatureService, fileService, reverseTunnelService, dataStore)
|
||||||
}
|
}
|
||||||
|
|
||||||
func initKubernetesDeployer(kubernetesTokenCacheManager *kubeproxy.TokenCacheManager, kubernetesClientFactory *kubecli.ClientFactory, dataStore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, proxyManager *proxy.Manager, assetsPath string) portainer.KubernetesDeployer {
|
func initKubernetesDeployer(kubernetesTokenCacheManager *kubeproxy.TokenCacheManager, kubernetesClientFactory *kubecli.ClientFactory, dataStore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, proxyManager *proxy.Manager, assetsPath string) portainer.KubernetesDeployer {
|
||||||
|
@ -532,7 +539,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||||
|
|
||||||
composeStackManager := initComposeStackManager(*flags.Assets, dockerConfigPath, reverseTunnelService, proxyManager)
|
composeStackManager := initComposeStackManager(*flags.Assets, dockerConfigPath, reverseTunnelService, proxyManager)
|
||||||
|
|
||||||
swarmStackManager, err := initSwarmStackManager(*flags.Assets, dockerConfigPath, digitalSignatureService, fileService, reverseTunnelService)
|
swarmStackManager, err := initSwarmStackManager(*flags.Assets, dockerConfigPath, digitalSignatureService, fileService, reverseTunnelService, dataStore)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("failed initializing swarm stack manager: %s", err)
|
log.Fatalf("failed initializing swarm stack manager: %s", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/internal/registryutils"
|
||||||
"github.com/portainer/portainer/api/internal/stackutils"
|
"github.com/portainer/portainer/api/internal/stackutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -22,17 +23,25 @@ type SwarmStackManager struct {
|
||||||
signatureService portainer.DigitalSignatureService
|
signatureService portainer.DigitalSignatureService
|
||||||
fileService portainer.FileService
|
fileService portainer.FileService
|
||||||
reverseTunnelService portainer.ReverseTunnelService
|
reverseTunnelService portainer.ReverseTunnelService
|
||||||
|
dataStore portainer.DataStore
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSwarmStackManager initializes a new SwarmStackManager service.
|
// NewSwarmStackManager initializes a new SwarmStackManager service.
|
||||||
// It also updates the configuration of the Docker CLI binary.
|
// It also updates the configuration of the Docker CLI binary.
|
||||||
func NewSwarmStackManager(binaryPath, configPath string, signatureService portainer.DigitalSignatureService, fileService portainer.FileService, reverseTunnelService portainer.ReverseTunnelService) (*SwarmStackManager, error) {
|
func NewSwarmStackManager(
|
||||||
|
binaryPath, configPath string,
|
||||||
|
signatureService portainer.DigitalSignatureService,
|
||||||
|
fileService portainer.FileService,
|
||||||
|
reverseTunnelService portainer.ReverseTunnelService,
|
||||||
|
datastore portainer.DataStore,
|
||||||
|
) (*SwarmStackManager, error) {
|
||||||
manager := &SwarmStackManager{
|
manager := &SwarmStackManager{
|
||||||
binaryPath: binaryPath,
|
binaryPath: binaryPath,
|
||||||
configPath: configPath,
|
configPath: configPath,
|
||||||
signatureService: signatureService,
|
signatureService: signatureService,
|
||||||
fileService: fileService,
|
fileService: fileService,
|
||||||
reverseTunnelService: reverseTunnelService,
|
reverseTunnelService: reverseTunnelService,
|
||||||
|
dataStore: datastore,
|
||||||
}
|
}
|
||||||
|
|
||||||
err := manager.updateDockerCLIConfiguration(manager.configPath)
|
err := manager.updateDockerCLIConfiguration(manager.configPath)
|
||||||
|
@ -51,7 +60,17 @@ func (manager *SwarmStackManager) Login(registries []portainer.Registry, endpoin
|
||||||
}
|
}
|
||||||
for _, registry := range registries {
|
for _, registry := range registries {
|
||||||
if registry.Authentication {
|
if registry.Authentication {
|
||||||
registryArgs := append(args, "login", "--username", registry.Username, "--password", registry.Password, registry.URL)
|
err = registryutils.EnsureRegTokenValid(manager.dataStore, ®istry)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
username, password, err := registryutils.GetRegEffectiveCredential(®istry)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
registryArgs := append(args, "login", "--username", username, "--password", password, registry.URL)
|
||||||
runCommandAndCaptureStdErr(command, registryArgs, nil, "")
|
runCommandAndCaptureStdErr(command, registryArgs, nil, "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,9 @@ require (
|
||||||
github.com/Microsoft/go-winio v0.4.17
|
github.com/Microsoft/go-winio v0.4.17
|
||||||
github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 // indirect
|
github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 // indirect
|
||||||
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535
|
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535
|
||||||
|
github.com/aws/aws-sdk-go-v2 v1.11.1
|
||||||
|
github.com/aws/aws-sdk-go-v2/credentials v1.6.2
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/ecr v1.10.1
|
||||||
github.com/boltdb/bolt v1.3.1
|
github.com/boltdb/bolt v1.3.1
|
||||||
github.com/containerd/containerd v1.5.7 // indirect
|
github.com/containerd/containerd v1.5.7 // indirect
|
||||||
github.com/coreos/go-semver v0.3.0
|
github.com/coreos/go-semver v0.3.0
|
||||||
|
|
21
api/go.sum
21
api/go.sum
|
@ -86,6 +86,22 @@ github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:l
|
||||||
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 h1:4daAzAu0S6Vi7/lbWECcX0j45yZReDZ56BQsrVBOEEY=
|
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 h1:4daAzAu0S6Vi7/lbWECcX0j45yZReDZ56BQsrVBOEEY=
|
||||||
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
|
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
|
||||||
github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0=
|
github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0=
|
||||||
|
github.com/aws/aws-sdk-go-v2 v1.11.1 h1:GzvOVAdTbWxhEMRK4FfiblkGverOkAT0UodDxC1jHQM=
|
||||||
|
github.com/aws/aws-sdk-go-v2 v1.11.1/go.mod h1:SQfA+m2ltnu1cA0soUkj4dRSsmITiVQUJvBIZjzfPyQ=
|
||||||
|
github.com/aws/aws-sdk-go-v2/credentials v1.6.2 h1:2faRNX8JgZVy7dDxERkaGBqb/xo5Rgmc8JMPL5j1o58=
|
||||||
|
github.com/aws/aws-sdk-go-v2/credentials v1.6.2/go.mod h1:8kRH9fthlxHEeNJ3g1N3NTSUMBba+KtTM8hp6SvUWn8=
|
||||||
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.8.1/go.mod h1:MYiG3oeEcmrdBOV7JOIWhionzyRZJWCnByS5FmvhAoU=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.1 h1:LZwqhOyqQ2w64PZk04V0Om9AEExtW8WMkCRoE1h9/94=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.1/go.mod h1:22SEiBSQm5AyKEjoPcG1hzpeTI+m9CXfE6yt1h49wBE=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.0.1 h1:ObMfGNk0xjOWduPxsrRWVwZZia3e9fOcO6zlKCkt38s=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.0.1/go.mod h1:1xvCD+I5BcDuQUc+psZr7LI1a9pclAWZs3S3Gce5+lg=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/ecr v1.10.1 h1:onTF83DG9dsRv6UzuhYb7phiktjwQ++s/n+ZtNlTQnM=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/ecr v1.10.1/go.mod h1:9RH1zeu1Ls3x2EQew/eCDuq2AlC0M8RzYfYy5+5gSLc=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.5.1/go.mod h1:fEaHB2bi+wVZw4uKMHEXTL9LwtT4EL//DOhTeflqIVo=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sso v1.6.1/go.mod h1:/73aFBwUl60wKBKhdth2pEOkut5ZNjVHGF9hjXz0bM0=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sts v1.10.1/go.mod h1:+BmlPeQ1Y+PuIho93MMKDby12PoUnt1SZXQdEHCzSlw=
|
||||||
|
github.com/aws/smithy-go v1.9.0 h1:c7FUdEqrQA1/UVKKCNDFQPNKGp4FQg3YW4Ck5SLTG58=
|
||||||
|
github.com/aws/smithy-go v1.9.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
|
||||||
github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||||
|
@ -380,6 +396,8 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
|
||||||
|
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g=
|
github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g=
|
||||||
github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
@ -443,6 +461,9 @@ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i
|
||||||
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
|
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
|
||||||
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||||
github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||||
|
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||||
|
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||||
|
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||||
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
|
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
|
||||||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||||
|
|
|
@ -21,7 +21,8 @@ type registryConfigurePayload struct {
|
||||||
Username string `example:"registry_user"`
|
Username string `example:"registry_user"`
|
||||||
// Password used to authenticate against this registry. required when Authentication is true
|
// Password used to authenticate against this registry. required when Authentication is true
|
||||||
Password string `example:"registry_password"`
|
Password string `example:"registry_password"`
|
||||||
|
// ECR region
|
||||||
|
Region string
|
||||||
// Use TLS
|
// Use TLS
|
||||||
TLS bool `example:"true"`
|
TLS bool `example:"true"`
|
||||||
// Skip the verification of the server TLS certificate
|
// Skip the verification of the server TLS certificate
|
||||||
|
@ -47,6 +48,9 @@ func (payload *registryConfigurePayload) Validate(r *http.Request) error {
|
||||||
|
|
||||||
password, _ := request.RetrieveMultiPartFormValue(r, "Password", true)
|
password, _ := request.RetrieveMultiPartFormValue(r, "Password", true)
|
||||||
payload.Password = password
|
payload.Password = password
|
||||||
|
|
||||||
|
region, _ := request.RetrieveMultiPartFormValue(r, "Region", true)
|
||||||
|
payload.Region = region
|
||||||
}
|
}
|
||||||
|
|
||||||
useTLS, _ := request.RetrieveBooleanMultiPartFormValue(r, "TLS", true)
|
useTLS, _ := request.RetrieveBooleanMultiPartFormValue(r, "TLS", true)
|
||||||
|
@ -134,6 +138,10 @@ func (handler *Handler) registryConfigure(w http.ResponseWriter, r *http.Request
|
||||||
} else {
|
} else {
|
||||||
registry.ManagementConfiguration.Password = payload.Password
|
registry.ManagementConfiguration.Password = payload.Password
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if payload.Region != "" {
|
||||||
|
registry.ManagementConfiguration.Ecr.Region = payload.Region
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if payload.TLS {
|
if payload.TLS {
|
||||||
|
|
|
@ -17,8 +17,15 @@ import (
|
||||||
type registryCreatePayload struct {
|
type registryCreatePayload struct {
|
||||||
// Name that will be used to identify this registry
|
// Name that will be used to identify this registry
|
||||||
Name string `example:"my-registry" validate:"required"`
|
Name string `example:"my-registry" validate:"required"`
|
||||||
// Registry Type. Valid values are: 1 (Quay.io), 2 (Azure container registry), 3 (custom registry), 4 (Gitlab registry), 5 (ProGet registry) or 6 (DockerHub)
|
// Registry Type. Valid values are:
|
||||||
Type portainer.RegistryType `example:"1" validate:"required" enums:"1,2,3,4,5,6"`
|
// 1 (Quay.io),
|
||||||
|
// 2 (Azure container registry),
|
||||||
|
// 3 (custom registry),
|
||||||
|
// 4 (Gitlab registry),
|
||||||
|
// 5 (ProGet registry),
|
||||||
|
// 6 (DockerHub)
|
||||||
|
// 7 (ECR)
|
||||||
|
Type portainer.RegistryType `example:"1" validate:"required" enums:"1,2,3,4,5,6,7"`
|
||||||
// URL or IP address of the Docker registry
|
// URL or IP address of the Docker registry
|
||||||
URL string `example:"registry.mydomain.tld:2375/feed" validate:"required"`
|
URL string `example:"registry.mydomain.tld:2375/feed" validate:"required"`
|
||||||
// BaseURL required for ProGet registry
|
// BaseURL required for ProGet registry
|
||||||
|
@ -33,6 +40,8 @@ type registryCreatePayload struct {
|
||||||
Gitlab portainer.GitlabRegistryData
|
Gitlab portainer.GitlabRegistryData
|
||||||
// Quay specific details, required when type = 1
|
// Quay specific details, required when type = 1
|
||||||
Quay portainer.QuayRegistryData
|
Quay portainer.QuayRegistryData
|
||||||
|
// ECR specific details, required when type = 7
|
||||||
|
Ecr portainer.EcrData
|
||||||
}
|
}
|
||||||
|
|
||||||
func (payload *registryCreatePayload) Validate(_ *http.Request) error {
|
func (payload *registryCreatePayload) Validate(_ *http.Request) error {
|
||||||
|
@ -42,14 +51,22 @@ func (payload *registryCreatePayload) Validate(_ *http.Request) error {
|
||||||
if govalidator.IsNull(payload.URL) {
|
if govalidator.IsNull(payload.URL) {
|
||||||
return errors.New("Invalid registry URL")
|
return errors.New("Invalid registry URL")
|
||||||
}
|
}
|
||||||
if payload.Authentication && (govalidator.IsNull(payload.Username) || govalidator.IsNull(payload.Password)) {
|
|
||||||
return errors.New("Invalid credentials. Username and password must be specified when authentication is enabled")
|
if payload.Authentication {
|
||||||
|
if govalidator.IsNull(payload.Username) || govalidator.IsNull(payload.Password) {
|
||||||
|
return errors.New("Invalid credentials. Username and password must be specified when authentication is enabled")
|
||||||
|
}
|
||||||
|
if payload.Type == portainer.EcrRegistry {
|
||||||
|
if govalidator.IsNull(payload.Ecr.Region) {
|
||||||
|
return errors.New("invalid credentials: access key ID, secret access key and region must be specified when authentication is enabled")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch payload.Type {
|
switch payload.Type {
|
||||||
case portainer.QuayRegistry, portainer.AzureRegistry, portainer.CustomRegistry, portainer.GitlabRegistry, portainer.ProGetRegistry, portainer.DockerHubRegistry:
|
case portainer.QuayRegistry, portainer.AzureRegistry, portainer.CustomRegistry, portainer.GitlabRegistry, portainer.ProGetRegistry, portainer.DockerHubRegistry, portainer.EcrRegistry:
|
||||||
default:
|
default:
|
||||||
return errors.New("invalid registry type. Valid values are: 1 (Quay.io), 2 (Azure container registry), 3 (custom registry), 4 (Gitlab registry), 5 (ProGet registry) or 6 (DockerHub)")
|
return errors.New("invalid registry type. Valid values are: 1 (Quay.io), 2 (Azure container registry), 3 (custom registry), 4 (Gitlab registry), 5 (ProGet registry), 6 (DockerHub), 7 (ECR)")
|
||||||
}
|
}
|
||||||
|
|
||||||
if payload.Type == portainer.ProGetRegistry && payload.BaseURL == "" {
|
if payload.Type == portainer.ProGetRegistry && payload.BaseURL == "" {
|
||||||
|
@ -96,9 +113,10 @@ func (handler *Handler) registryCreate(w http.ResponseWriter, r *http.Request) *
|
||||||
Authentication: payload.Authentication,
|
Authentication: payload.Authentication,
|
||||||
Username: payload.Username,
|
Username: payload.Username,
|
||||||
Password: payload.Password,
|
Password: payload.Password,
|
||||||
RegistryAccesses: portainer.RegistryAccesses{},
|
|
||||||
Gitlab: payload.Gitlab,
|
Gitlab: payload.Gitlab,
|
||||||
Quay: payload.Quay,
|
Quay: payload.Quay,
|
||||||
|
RegistryAccesses: portainer.RegistryAccesses{},
|
||||||
|
Ecr: payload.Ecr,
|
||||||
}
|
}
|
||||||
|
|
||||||
registries, err := handler.DataStore.Registry().Registries()
|
registries, err := handler.DataStore.Registry().Registries()
|
||||||
|
|
|
@ -33,6 +33,20 @@ func Test_registryCreatePayload_Validate(t *testing.T) {
|
||||||
err := payload.Validate(nil)
|
err := payload.Validate(nil)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
})
|
})
|
||||||
|
t.Run("Can't create a AWS ECR registry if authentication required, but access key ID, secret access key or region is empty", func(t *testing.T) {
|
||||||
|
payload := basePayload
|
||||||
|
payload.Type = portainer.EcrRegistry
|
||||||
|
payload.Authentication = true
|
||||||
|
err := payload.Validate(nil)
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
t.Run("Do not require access key ID, secret access key, region for public AWS ECR registry", func(t *testing.T) {
|
||||||
|
payload := basePayload
|
||||||
|
payload.Type = portainer.EcrRegistry
|
||||||
|
payload.Authentication = false
|
||||||
|
err := payload.Validate(nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
type testRegistryService struct {
|
type testRegistryService struct {
|
||||||
|
|
|
@ -2,6 +2,7 @@ package registries
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
|
@ -26,8 +27,12 @@ type registryUpdatePayload struct {
|
||||||
Username *string `example:"registry_user"`
|
Username *string `example:"registry_user"`
|
||||||
// Password used to authenticate against this registry. required when Authentication is true
|
// Password used to authenticate against this registry. required when Authentication is true
|
||||||
Password *string `example:"registry_password"`
|
Password *string `example:"registry_password"`
|
||||||
RegistryAccesses *portainer.RegistryAccesses
|
// Quay data
|
||||||
Quay *portainer.QuayRegistryData
|
Quay *portainer.QuayRegistryData
|
||||||
|
// Registry access control
|
||||||
|
RegistryAccesses *portainer.RegistryAccesses `json:",omitempty"`
|
||||||
|
// ECR data
|
||||||
|
Ecr *portainer.EcrData `json:",omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (payload *registryUpdatePayload) Validate(r *http.Request) error {
|
func (payload *registryUpdatePayload) Validate(r *http.Request) error {
|
||||||
|
@ -56,6 +61,7 @@ func (handler *Handler) registryUpdate(w http.ResponseWriter, r *http.Request) *
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !securityContext.IsAdmin {
|
if !securityContext.IsAdmin {
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to update registry", httperrors.ErrResourceAccessDenied}
|
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to update registry", httperrors.ErrResourceAccessDenied}
|
||||||
}
|
}
|
||||||
|
@ -82,29 +88,41 @@ func (handler *Handler) registryUpdate(w http.ResponseWriter, r *http.Request) *
|
||||||
registry.Name = *payload.Name
|
registry.Name = *payload.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldUpdateSecrets := false
|
|
||||||
|
|
||||||
if registry.Type == portainer.ProGetRegistry && payload.BaseURL != nil {
|
if registry.Type == portainer.ProGetRegistry && payload.BaseURL != nil {
|
||||||
registry.BaseURL = *payload.BaseURL
|
registry.BaseURL = *payload.BaseURL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
shouldUpdateSecrets := false
|
||||||
|
|
||||||
if payload.Authentication != nil {
|
if payload.Authentication != nil {
|
||||||
|
shouldUpdateSecrets = shouldUpdateSecrets || (registry.Authentication != *payload.Authentication)
|
||||||
|
|
||||||
if *payload.Authentication {
|
if *payload.Authentication {
|
||||||
registry.Authentication = true
|
registry.Authentication = true
|
||||||
shouldUpdateSecrets = shouldUpdateSecrets || (payload.Username != nil && *payload.Username != registry.Username) || (payload.Password != nil && *payload.Password != registry.Password)
|
|
||||||
|
|
||||||
if payload.Username != nil {
|
if payload.Username != nil {
|
||||||
|
shouldUpdateSecrets = shouldUpdateSecrets || (registry.Username != *payload.Username)
|
||||||
registry.Username = *payload.Username
|
registry.Username = *payload.Username
|
||||||
}
|
}
|
||||||
|
|
||||||
if payload.Password != nil && *payload.Password != "" {
|
if payload.Password != nil && *payload.Password != "" {
|
||||||
|
shouldUpdateSecrets = shouldUpdateSecrets || (registry.Password != *payload.Password)
|
||||||
registry.Password = *payload.Password
|
registry.Password = *payload.Password
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if registry.Type == portainer.EcrRegistry && payload.Ecr != nil && payload.Ecr.Region != "" {
|
||||||
|
shouldUpdateSecrets = shouldUpdateSecrets || (registry.Ecr.Region != payload.Ecr.Region)
|
||||||
|
registry.Ecr.Region = payload.Ecr.Region
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
registry.Authentication = false
|
registry.Authentication = false
|
||||||
registry.Username = ""
|
registry.Username = ""
|
||||||
registry.Password = ""
|
registry.Password = ""
|
||||||
|
|
||||||
|
registry.Ecr.Region = ""
|
||||||
|
|
||||||
|
registry.AccessToken = ""
|
||||||
|
registry.AccessTokenExpiry = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,6 +134,7 @@ func (handler *Handler) registryUpdate(w http.ResponseWriter, r *http.Request) *
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve registries from the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve registries from the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, r := range registries {
|
for _, r := range registries {
|
||||||
if r.ID != registry.ID && handler.registriesHaveSameURLAndCredentials(&r, registry) {
|
if r.ID != registry.ID && handler.registriesHaveSameURLAndCredentials(&r, registry) {
|
||||||
return &httperror.HandlerError{http.StatusConflict, "Another registry with the same URL and credentials already exists", errors.New("A registry is already defined for this URL and credentials")}
|
return &httperror.HandlerError{http.StatusConflict, "Another registry with the same URL and credentials already exists", errors.New("A registry is already defined for this URL and credentials")}
|
||||||
|
@ -124,13 +143,16 @@ func (handler *Handler) registryUpdate(w http.ResponseWriter, r *http.Request) *
|
||||||
}
|
}
|
||||||
|
|
||||||
if shouldUpdateSecrets {
|
if shouldUpdateSecrets {
|
||||||
|
registry.AccessToken = ""
|
||||||
|
registry.AccessTokenExpiry = 0
|
||||||
|
|
||||||
for endpointID, endpointAccess := range registry.RegistryAccesses {
|
for endpointID, endpointAccess := range registry.RegistryAccesses {
|
||||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(endpointID)
|
endpoint, err := handler.DataStore.Endpoint().Endpoint(endpointID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update access to registry", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update access to registry", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
if endpoint.Type == portainer.KubernetesLocalEnvironment || endpoint.Type == portainer.AgentOnKubernetesEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment {
|
if endpointutils.IsKubernetesEndpoint(endpoint) {
|
||||||
err = handler.updateEndpointRegistryAccess(endpoint, registry, endpointAccess)
|
err = handler.updateEndpointRegistryAccess(endpoint, registry, endpointAccess)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update access to registry", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update access to registry", err}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package docker
|
||||||
import (
|
import (
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
|
"github.com/portainer/portainer/api/internal/registryutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
@ -25,13 +26,13 @@ type (
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func createRegistryAuthenticationHeader(registryId portainer.RegistryID, accessContext *registryAccessContext) *registryAuthenticationHeader {
|
func createRegistryAuthenticationHeader(
|
||||||
var authenticationHeader *registryAuthenticationHeader
|
dataStore portainer.DataStore,
|
||||||
|
registryId portainer.RegistryID,
|
||||||
|
accessContext *registryAccessContext,
|
||||||
|
) (authenticationHeader registryAuthenticationHeader, err error) {
|
||||||
if registryId == 0 { // dockerhub (anonymous)
|
if registryId == 0 { // dockerhub (anonymous)
|
||||||
authenticationHeader = ®istryAuthenticationHeader{
|
authenticationHeader.Serveraddress = "docker.io"
|
||||||
Serveraddress: "docker.io",
|
|
||||||
}
|
|
||||||
} else { // any "custom" registry
|
} else { // any "custom" registry
|
||||||
var matchingRegistry *portainer.Registry
|
var matchingRegistry *portainer.Registry
|
||||||
for _, registry := range accessContext.registries {
|
for _, registry := range accessContext.registries {
|
||||||
|
@ -44,13 +45,14 @@ func createRegistryAuthenticationHeader(registryId portainer.RegistryID, accessC
|
||||||
}
|
}
|
||||||
|
|
||||||
if matchingRegistry != nil {
|
if matchingRegistry != nil {
|
||||||
authenticationHeader = ®istryAuthenticationHeader{
|
err = registryutils.EnsureRegTokenValid(dataStore, matchingRegistry)
|
||||||
Username: matchingRegistry.Username,
|
if (err != nil) {
|
||||||
Password: matchingRegistry.Password,
|
return
|
||||||
Serveraddress: matchingRegistry.URL,
|
|
||||||
}
|
}
|
||||||
|
authenticationHeader.Serveraddress = matchingRegistry.URL
|
||||||
|
authenticationHeader.Username, authenticationHeader.Password, err = registryutils.GetRegEffectiveCredential(matchingRegistry)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return authenticationHeader
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -414,7 +414,10 @@ func (transport *Transport) replaceRegistryAuthenticationHeader(request *http.Re
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
authenticationHeader := createRegistryAuthenticationHeader(originalHeaderData.RegistryId, accessContext)
|
authenticationHeader, err := createRegistryAuthenticationHeader(transport.dataStore, originalHeaderData.RegistryId, accessContext)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
headerData, err := json.Marshal(authenticationHeader)
|
headerData, err := json.Marshal(authenticationHeader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
package kubernetes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (transport *baseTransport) proxyDeploymentsRequest(request *http.Request, namespace, requestPath string) (*http.Response, error) {
|
||||||
|
switch request.Method {
|
||||||
|
case http.MethodPost, http.MethodPatch:
|
||||||
|
transport.refreshRegistry(request, namespace)
|
||||||
|
return transport.executeKubernetesRequest(request)
|
||||||
|
default:
|
||||||
|
return transport.executeKubernetesRequest(request)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
package kubernetes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (transport *baseTransport) proxyPodsRequest(request *http.Request, namespace, requestPath string) (*http.Response, error) {
|
||||||
|
switch request.Method {
|
||||||
|
case "DELETE":
|
||||||
|
transport.refreshRegistry(request, namespace)
|
||||||
|
return transport.executeKubernetesRequest(request)
|
||||||
|
default:
|
||||||
|
return transport.executeKubernetesRequest(request)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
package kubernetes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/portainer/portainer/api/internal/registryutils"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (transport *baseTransport) refreshRegistry(request *http.Request, namespace string) (err error) {
|
||||||
|
cli, err := transport.k8sClientFactory.GetKubeClient(transport.endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = registryutils.RefreshEcrSecret(cli, transport.endpoint, transport.dataStore, namespace)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
|
@ -42,7 +42,10 @@ func newBaseTransport(httpTransport *http.Transport, tokenManager *tokenManager,
|
||||||
// proxyKubernetesRequest intercepts a Kubernetes API request and apply logic based
|
// proxyKubernetesRequest intercepts a Kubernetes API request and apply logic based
|
||||||
// on the requested operation.
|
// on the requested operation.
|
||||||
func (transport *baseTransport) proxyKubernetesRequest(request *http.Request) (*http.Response, error) {
|
func (transport *baseTransport) proxyKubernetesRequest(request *http.Request) (*http.Response, error) {
|
||||||
apiVersionRe := regexp.MustCompile(`^(/kubernetes)?/api/v[0-9](\.[0-9])?`)
|
// URL path examples:
|
||||||
|
// http://localhost:9000/api/endpoints/3/kubernetes/api/v1/namespaces
|
||||||
|
// http://localhost:9000/api/endpoints/3/kubernetes/apis/apps/v1/namespaces/default/deployments
|
||||||
|
apiVersionRe := regexp.MustCompile(`^(/kubernetes)?/(api|apis/apps)/v[0-9](\.[0-9])?`)
|
||||||
requestPath := apiVersionRe.ReplaceAllString(request.URL.Path, "")
|
requestPath := apiVersionRe.ReplaceAllString(request.URL.Path, "")
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
|
@ -66,6 +69,10 @@ func (transport *baseTransport) proxyNamespacedRequest(request *http.Request, fu
|
||||||
}
|
}
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
|
case strings.HasPrefix(requestPath, "pods"):
|
||||||
|
return transport.proxyPodsRequest(request, namespace, requestPath)
|
||||||
|
case strings.HasPrefix(requestPath, "deployments"):
|
||||||
|
return transport.proxyDeploymentsRequest(request, namespace, requestPath)
|
||||||
case requestPath == "" && request.Method == "DELETE":
|
case requestPath == "" && request.Method == "DELETE":
|
||||||
return transport.proxyNamespaceDeleteOperation(request, namespace)
|
return transport.proxyNamespaceDeleteOperation(request, namespace)
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
package registryutils
|
||||||
|
|
||||||
|
import (
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
func isRegistryAssignedToNamespace(registry portainer.Registry, endpointID portainer.EndpointID, namespace string) (in bool){
|
||||||
|
for _, ns := range registry.RegistryAccesses[endpointID].Namespaces {
|
||||||
|
if ns == namespace {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func RefreshEcrSecret(cli portainer.KubeClient, endpoint *portainer.Endpoint, dataStore portainer.DataStore, namespace string) (err error) {
|
||||||
|
registries, err := dataStore.Registry().Registries()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, registry := range registries {
|
||||||
|
if registry.Type != portainer.EcrRegistry {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isRegistryAssignedToNamespace(registry, endpoint.ID, namespace) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
err = EnsureRegTokenValid(dataStore, ®istry)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = cli.DeleteRegistrySecret(®istry, namespace)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = cli.CreateRegistrySecret(®istry, namespace)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
package registryutils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/aws/ecr"
|
||||||
|
)
|
||||||
|
|
||||||
|
func isRegTokenValid(registry *portainer.Registry) (valid bool) {
|
||||||
|
return registry.AccessToken != "" && registry.AccessTokenExpiry > time.Now().Unix();
|
||||||
|
}
|
||||||
|
|
||||||
|
func doGetRegToken(dataStore portainer.DataStore, registry *portainer.Registry) (err error) {
|
||||||
|
ecrClient := ecr.NewService(registry.Username, registry.Password, registry.Ecr.Region)
|
||||||
|
accessToken, expiryAt, err := ecrClient.GetAuthorizationToken()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
registry.AccessToken = *accessToken
|
||||||
|
registry.AccessTokenExpiry = expiryAt.Unix()
|
||||||
|
|
||||||
|
err = dataStore.Registry().UpdateRegistry(registry.ID, registry)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseRegToken(registry *portainer.Registry) (username, password string, err error) {
|
||||||
|
ecrClient := ecr.NewService(registry.Username, registry.Password, registry.Ecr.Region)
|
||||||
|
return ecrClient.ParseAuthorizationToken(registry.AccessToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
func EnsureRegTokenValid(dataStore portainer.DataStore, registry *portainer.Registry) (err error) {
|
||||||
|
if registry.Type == portainer.EcrRegistry {
|
||||||
|
if isRegTokenValid(registry) {
|
||||||
|
log.Println("[DEBUG] [registry, GetEcrAccessToken] [message: curretn ECR token is still valid]")
|
||||||
|
} else {
|
||||||
|
err = doGetRegToken(dataStore, registry)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("[DEBUG] [registry, GetEcrAccessToken] [message: refresh ECR token]")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetRegEffectiveCredential(registry *portainer.Registry) (username, password string, err error) {
|
||||||
|
if registry.Type == portainer.EcrRegistry {
|
||||||
|
username, password, err = parseRegToken(registry)
|
||||||
|
} else {
|
||||||
|
username = registry.Username
|
||||||
|
password = registry.Password
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
|
@ -8,6 +8,7 @@ import (
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/internal/registryutils"
|
||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
@ -15,6 +16,8 @@ import (
|
||||||
|
|
||||||
const (
|
const (
|
||||||
secretDockerConfigKey = ".dockerconfigjson"
|
secretDockerConfigKey = ".dockerconfigjson"
|
||||||
|
labelRegistryType = "io.portainer.kubernetes.registry.type"
|
||||||
|
annotationRegistryID = "portainer.io/registry.id"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
@ -38,12 +41,17 @@ func (kcl *KubeClient) DeleteRegistrySecret(registry *portainer.Registry, namesp
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (kcl *KubeClient) CreateRegistrySecret(registry *portainer.Registry, namespace string) error {
|
func (kcl *KubeClient) CreateRegistrySecret(registry *portainer.Registry, namespace string) (err error) {
|
||||||
|
username, password, err := registryutils.GetRegEffectiveCredential(registry)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
config := dockerConfig{
|
config := dockerConfig{
|
||||||
Auths: map[string]registryDockerConfig{
|
Auths: map[string]registryDockerConfig{
|
||||||
registry.URL: {
|
registry.URL: {
|
||||||
Username: registry.Username,
|
Username: username,
|
||||||
Password: registry.Password,
|
Password: password,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -57,8 +65,11 @@ func (kcl *KubeClient) CreateRegistrySecret(registry *portainer.Registry, namesp
|
||||||
TypeMeta: metav1.TypeMeta{},
|
TypeMeta: metav1.TypeMeta{},
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: registrySecretName(registry),
|
Name: registrySecretName(registry),
|
||||||
|
Labels: map[string]string{
|
||||||
|
labelRegistryType: strconv.Itoa(int(registry.Type)),
|
||||||
|
},
|
||||||
Annotations: map[string]string{
|
Annotations: map[string]string{
|
||||||
"portainer.io/registry.id": strconv.Itoa(int(registry.ID)),
|
annotationRegistryID: strconv.Itoa(int(registry.ID)),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Data: map[string][]byte{
|
Data: map[string][]byte{
|
||||||
|
|
|
@ -155,8 +155,8 @@ type (
|
||||||
ImageCount int `json:"ImageCount"`
|
ImageCount int `json:"ImageCount"`
|
||||||
ServiceCount int `json:"ServiceCount"`
|
ServiceCount int `json:"ServiceCount"`
|
||||||
StackCount int `json:"StackCount"`
|
StackCount int `json:"StackCount"`
|
||||||
NodeCount int `json:"NodeCount"`
|
|
||||||
SnapshotRaw DockerSnapshotRaw `json:"DockerSnapshotRaw"`
|
SnapshotRaw DockerSnapshotRaw `json:"DockerSnapshotRaw"`
|
||||||
|
NodeCount int `json:"NodeCount"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DockerSnapshotRaw represents all the information related to a snapshot as returned by the Docker API
|
// DockerSnapshotRaw represents all the information related to a snapshot as returned by the Docker API
|
||||||
|
@ -444,6 +444,11 @@ type (
|
||||||
OrganisationName string `json:"OrganisationName"`
|
OrganisationName string `json:"OrganisationName"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EcrData represents data required for ECR registry
|
||||||
|
EcrData struct {
|
||||||
|
Region string `json:"Region" example:"ap-southeast-2"`
|
||||||
|
}
|
||||||
|
|
||||||
// JobType represents a job type
|
// JobType represents a job type
|
||||||
JobType int
|
JobType int
|
||||||
|
|
||||||
|
@ -589,8 +594,8 @@ type (
|
||||||
Registry struct {
|
Registry struct {
|
||||||
// Registry Identifier
|
// Registry Identifier
|
||||||
ID RegistryID `json:"Id" example:"1"`
|
ID RegistryID `json:"Id" example:"1"`
|
||||||
// Registry Type (1 - Quay, 2 - Azure, 3 - Custom, 4 - Gitlab, 5 - ProGet, 6 - DockerHub)
|
// Registry Type (1 - Quay, 2 - Azure, 3 - Custom, 4 - Gitlab, 5 - ProGet, 6 - DockerHub, 7 - ECR)
|
||||||
Type RegistryType `json:"Type" enums:"1,2,3,4,5,6"`
|
Type RegistryType `json:"Type" enums:"1,2,3,4,5,6,7"`
|
||||||
// Registry Name
|
// Registry Name
|
||||||
Name string `json:"Name" example:"my-registry"`
|
Name string `json:"Name" example:"my-registry"`
|
||||||
// URL or IP address of the Docker registry
|
// URL or IP address of the Docker registry
|
||||||
|
@ -599,13 +604,14 @@ type (
|
||||||
BaseURL string `json:"BaseURL" example:"registry.mydomain.tld:2375"`
|
BaseURL string `json:"BaseURL" example:"registry.mydomain.tld:2375"`
|
||||||
// Is authentication against this registry enabled
|
// Is authentication against this registry enabled
|
||||||
Authentication bool `json:"Authentication" example:"true"`
|
Authentication bool `json:"Authentication" example:"true"`
|
||||||
// Username used to authenticate against this registry
|
// Username or AccessKeyID used to authenticate against this registry
|
||||||
Username string `json:"Username" example:"registry user"`
|
Username string `json:"Username" example:"registry user"`
|
||||||
// Password used to authenticate against this registry
|
// Password or SecretAccessKey used to authenticate against this registry
|
||||||
Password string `json:"Password,omitempty" example:"registry_password"`
|
Password string `json:"Password,omitempty" example:"registry_password"`
|
||||||
ManagementConfiguration *RegistryManagementConfiguration `json:"ManagementConfiguration"`
|
ManagementConfiguration *RegistryManagementConfiguration `json:"ManagementConfiguration"`
|
||||||
Gitlab GitlabRegistryData `json:"Gitlab"`
|
Gitlab GitlabRegistryData `json:"Gitlab"`
|
||||||
Quay QuayRegistryData `json:"Quay"`
|
Quay QuayRegistryData `json:"Quay"`
|
||||||
|
Ecr EcrData `json:"Ecr"`
|
||||||
RegistryAccesses RegistryAccesses `json:"RegistryAccesses"`
|
RegistryAccesses RegistryAccesses `json:"RegistryAccesses"`
|
||||||
|
|
||||||
// Deprecated fields
|
// Deprecated fields
|
||||||
|
@ -618,6 +624,10 @@ type (
|
||||||
AuthorizedUsers []UserID `json:"AuthorizedUsers"`
|
AuthorizedUsers []UserID `json:"AuthorizedUsers"`
|
||||||
// Deprecated in DBVersion == 18
|
// Deprecated in DBVersion == 18
|
||||||
AuthorizedTeams []TeamID `json:"AuthorizedTeams"`
|
AuthorizedTeams []TeamID `json:"AuthorizedTeams"`
|
||||||
|
|
||||||
|
// Stores temporary access token
|
||||||
|
AccessToken string `json:"AccessToken,omitempty"`
|
||||||
|
AccessTokenExpiry int64 `json:"AccessTokenExpiry,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
RegistryAccesses map[EndpointID]RegistryAccessPolicies
|
RegistryAccesses map[EndpointID]RegistryAccessPolicies
|
||||||
|
@ -634,11 +644,14 @@ type (
|
||||||
// RegistryManagementConfiguration represents a configuration that can be used to query
|
// RegistryManagementConfiguration represents a configuration that can be used to query
|
||||||
// the registry API via the registry management extension.
|
// the registry API via the registry management extension.
|
||||||
RegistryManagementConfiguration struct {
|
RegistryManagementConfiguration struct {
|
||||||
Type RegistryType `json:"Type"`
|
Type RegistryType `json:"Type"`
|
||||||
Authentication bool `json:"Authentication"`
|
Authentication bool `json:"Authentication"`
|
||||||
Username string `json:"Username"`
|
Username string `json:"Username"`
|
||||||
Password string `json:"Password"`
|
Password string `json:"Password"`
|
||||||
TLSConfig TLSConfiguration `json:"TLSConfig"`
|
TLSConfig TLSConfiguration `json:"TLSConfig"`
|
||||||
|
Ecr EcrData `json:"Ecr"`
|
||||||
|
AccessToken string `json:"AccessToken,omitempty"`
|
||||||
|
AccessTokenExpiry int64 `json:"AccessTokenExpiry,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegistryType represents a type of registry
|
// RegistryType represents a type of registry
|
||||||
|
@ -1714,6 +1727,8 @@ const (
|
||||||
ProGetRegistry
|
ProGetRegistry
|
||||||
// DockerHubRegistry represents a dockerhub registry
|
// DockerHubRegistry represents a dockerhub registry
|
||||||
DockerHubRegistry
|
DockerHubRegistry
|
||||||
|
// EcrRegistry represents an ECR registry
|
||||||
|
EcrRegistry
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
|
@ -0,0 +1,143 @@
|
||||||
|
<form class="form-horizontal" name="registryFormEcr" ng-submit="$ctrl.formAction()">
|
||||||
|
<div class="col-sm-12 form-section-title">
|
||||||
|
Important notice
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<span class="col-sm-12 text-muted small">
|
||||||
|
For information on how to generate an Access Key, follow the
|
||||||
|
<a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/id_users_create.html#id_users_create_console" target="_blank">AWS guide</a>.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-sm-12 form-section-title">
|
||||||
|
ECR connection details
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- name-input -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="registry_name" class="col-sm-3 col-lg-2 control-label text-left">Name</label>
|
||||||
|
<div class="col-sm-9 col-lg-10">
|
||||||
|
<input type="text" class="form-control" id="registry_name" name="registry_name" ng-model="$ctrl.model.Name" placeholder="my-ecr-registry" required auto-focus />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" ng-show="registryFormEcr.registry_name.$invalid">
|
||||||
|
<div class="col-sm-12 small text-warning">
|
||||||
|
<div ng-messages="registryFormEcr.registry_name.$error">
|
||||||
|
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !name-input -->
|
||||||
|
|
||||||
|
<!-- url-input -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="registry_url" class="col-sm-3 col-lg-2 control-label text-left">
|
||||||
|
Registry URL
|
||||||
|
<portainer-tooltip position="bottom" message="URL of an Amazon Elastic Container Registry, which contains an account id and region."></portainer-tooltip>
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-9 col-lg-10">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="registry_url"
|
||||||
|
name="registry_url"
|
||||||
|
ng-model="$ctrl.model.URL"
|
||||||
|
placeholder="aws-account-id.dkr.ecr.us-east-1.amazonaws.com/"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" ng-show="registryFormEcr.registry_url.$invalid">
|
||||||
|
<div class="col-sm-12 small text-warning">
|
||||||
|
<div ng-messages="registryFormEcr.registry_url.$error">
|
||||||
|
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- url-input -->
|
||||||
|
|
||||||
|
<!-- authentication-checkbox -->
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<label class="control-label text-left">
|
||||||
|
Authentication
|
||||||
|
<portainer-tooltip position="bottom" message="Enable this option if you need to specify credentials to connect to a private registry."></portainer-tooltip>
|
||||||
|
</label>
|
||||||
|
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="$ctrl.model.Authentication" /><i></i> </label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !authentication-checkbox -->
|
||||||
|
|
||||||
|
<div ng-if="$ctrl.model.Authentication">
|
||||||
|
<!-- aws-access-key -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="registry_access_key" class="col-sm-3 col-lg-2 control-label text-left">AWS Access Key</label>
|
||||||
|
<div class="col-sm-9 col-lg-10">
|
||||||
|
<input type="text" class="form-control" id="registry_access_key" name="registry_access_key" ng-model="$ctrl.model.Username" required />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" ng-show="registryFormEcr.registry_access_key.$invalid">
|
||||||
|
<div class="col-sm-12 small text-warning">
|
||||||
|
<div ng-messages="registryFormEcr.registry_access_key.$error">
|
||||||
|
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !aws-access-key -->
|
||||||
|
|
||||||
|
<!-- aws-secret-access-key -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="registry_secret_access_key" class="col-sm-3 col-lg-2 control-label text-left">AWS Secret Access Key</label>
|
||||||
|
<div class="col-sm-9 col-lg-10">
|
||||||
|
<input type="password" class="form-control" id="registry_secret_access_key" name="registry_secret_access_key" ng-model="$ctrl.model.Password" required />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" ng-show="registryFormEcr.registry_secret_access_key.$invalid">
|
||||||
|
<div class="col-sm-12 small text-warning">
|
||||||
|
<div ng-messages="registryFormEcr.registry_secret_access_key.$error">
|
||||||
|
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !aws-secret-access-key -->
|
||||||
|
|
||||||
|
<!-- region -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="registry_region" class="col-sm-3 col-lg-2 control-label text-left">Region</label>
|
||||||
|
<div class="col-sm-9 col-lg-10">
|
||||||
|
<input type="text" class="form-control" id="registry_region" name="registry_region" placeholder="us-west-1" ng-model="$ctrl.model.Ecr.Region" required />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" ng-show="registryFormEcr.registry_region.$invalid">
|
||||||
|
<div class="col-sm-12 small text-warning">
|
||||||
|
<div ng-messages="registryFormEcr.registry_region.$error">
|
||||||
|
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !region -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- actions -->
|
||||||
|
<div class="col-sm-12 form-section-title">
|
||||||
|
Actions
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-primary btn-sm"
|
||||||
|
ng-disabled="$ctrl.actionInProgress || !registryFormEcr.$valid"
|
||||||
|
button-spinner="$ctrl.actionInProgress"
|
||||||
|
analytics-on
|
||||||
|
analytics-category="portainer"
|
||||||
|
analytics-event="portainer-registry-creation"
|
||||||
|
analytics-properties="{ metadata: { type: 'ecr' } }"
|
||||||
|
>
|
||||||
|
<span ng-hide="$ctrl.actionInProgress">{{ $ctrl.formActionLabel }}</span>
|
||||||
|
<span ng-show="$ctrl.actionInProgress">In progress...</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !actions -->
|
||||||
|
</form>
|
|
@ -0,0 +1,9 @@
|
||||||
|
angular.module('portainer.app').component('registryFormEcr', {
|
||||||
|
templateUrl: './registry-form-ecr.html',
|
||||||
|
bindings: {
|
||||||
|
model: '=',
|
||||||
|
formAction: '<',
|
||||||
|
formActionLabel: '@',
|
||||||
|
actionInProgress: '<',
|
||||||
|
},
|
||||||
|
});
|
|
@ -14,6 +14,7 @@ export function RegistryViewModel(data) {
|
||||||
this.Checked = false;
|
this.Checked = false;
|
||||||
this.Gitlab = data.Gitlab;
|
this.Gitlab = data.Gitlab;
|
||||||
this.Quay = data.Quay;
|
this.Quay = data.Quay;
|
||||||
|
this.Ecr = data.Ecr;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RegistryManagementConfigurationDefaultModel(registry) {
|
export function RegistryManagementConfigurationDefaultModel(registry) {
|
||||||
|
@ -25,7 +26,12 @@ export function RegistryManagementConfigurationDefaultModel(registry) {
|
||||||
this.TLSCertFile = null;
|
this.TLSCertFile = null;
|
||||||
this.TLSKeyFile = null;
|
this.TLSKeyFile = null;
|
||||||
|
|
||||||
if (registry.Type === RegistryTypes.QUAY || registry.Type === RegistryTypes.AZURE) {
|
if (registry.Type === RegistryTypes.ECR) {
|
||||||
|
this.Region = registry.Ecr.Region;
|
||||||
|
this.TLSSkipVerify = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (registry.Type === RegistryTypes.QUAY || registry.Type === RegistryTypes.AZURE || registry.Type === RegistryTypes.ECR) {
|
||||||
this.Authentication = true;
|
this.Authentication = true;
|
||||||
this.Username = registry.Username;
|
this.Username = registry.Username;
|
||||||
this.TLS = true;
|
this.TLS = true;
|
||||||
|
@ -63,6 +69,9 @@ export function RegistryCreateRequest(model) {
|
||||||
ProjectPath: model.Gitlab.ProjectPath,
|
ProjectPath: model.Gitlab.ProjectPath,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (model.Type === RegistryTypes.ECR) {
|
||||||
|
this.Ecr = model.Ecr;
|
||||||
|
}
|
||||||
if (model.Type === RegistryTypes.QUAY) {
|
if (model.Type === RegistryTypes.QUAY) {
|
||||||
this.Quay = {
|
this.Quay = {
|
||||||
useOrganisation: model.Quay.useOrganisation,
|
useOrganisation: model.Quay.useOrganisation,
|
||||||
|
|
|
@ -6,4 +6,5 @@ export const RegistryTypes = Object.freeze({
|
||||||
GITLAB: 4,
|
GITLAB: 4,
|
||||||
PROGET: 5,
|
PROGET: 5,
|
||||||
DOCKERHUB: 6,
|
DOCKERHUB: 6,
|
||||||
|
ECR: 7,
|
||||||
});
|
});
|
||||||
|
|
|
@ -26,6 +26,16 @@
|
||||||
<p>DockerHub authenticated account</p>
|
<p>DockerHub authenticated account</p>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<input type="radio" id="registry_aws_ecr" ng-model="$ctrl.model.Type" ng-value="$ctrl.RegistryTypes.ECR" />
|
||||||
|
<label for="registry_aws_ecr" ng-click="$ctrl.selectEcr()">
|
||||||
|
<div class="boxselector_header">
|
||||||
|
<i class="fab fa-aws" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
|
AWS ECR
|
||||||
|
</div>
|
||||||
|
<p>Amazon elastic container registry</p>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<input type="radio" id="registry_quay" ng-model="$ctrl.model.Type" ng-value="$ctrl.RegistryTypes.QUAY" />
|
<input type="radio" id="registry_quay" ng-model="$ctrl.model.Type" ng-value="$ctrl.RegistryTypes.QUAY" />
|
||||||
<label for="registry_quay" ng-click="$ctrl.selectQuayRegistry()">
|
<label for="registry_quay" ng-click="$ctrl.selectQuayRegistry()">
|
||||||
|
@ -103,6 +113,14 @@
|
||||||
action-in-progress="$ctrl.state.actionInProgress"
|
action-in-progress="$ctrl.state.actionInProgress"
|
||||||
></registry-form-custom>
|
></registry-form-custom>
|
||||||
|
|
||||||
|
<registry-form-ecr
|
||||||
|
ng-if="$ctrl.model.Type === $ctrl.RegistryTypes.ECR"
|
||||||
|
model="$ctrl.model"
|
||||||
|
form-action="$ctrl.createRegistry"
|
||||||
|
form-action-label="Add registry"
|
||||||
|
action-in-progress="$ctrl.state.actionInProgress"
|
||||||
|
></registry-form-ecr>
|
||||||
|
|
||||||
<registry-form-proget
|
<registry-form-proget
|
||||||
ng-if="$ctrl.model.Type === $ctrl.RegistryTypes.PROGET"
|
ng-if="$ctrl.model.Type === $ctrl.RegistryTypes.PROGET"
|
||||||
model="$ctrl.model"
|
model="$ctrl.model"
|
||||||
|
|
|
@ -74,6 +74,18 @@ class CreateRegistryController {
|
||||||
this.model.Authentication = true;
|
this.model.Authentication = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useDefaultEcrConfiguration() {
|
||||||
|
this.model.Ecr.Region = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
selectEcr() {
|
||||||
|
this.model.Name = '';
|
||||||
|
this.model.URL = '';
|
||||||
|
this.model.Authentication = false;
|
||||||
|
this.model.Ecr = {};
|
||||||
|
this.useDefaultEcrConfiguration();
|
||||||
|
}
|
||||||
|
|
||||||
retrieveGitlabRegistries() {
|
retrieveGitlabRegistries() {
|
||||||
return this.$async(async () => {
|
return this.$async(async () => {
|
||||||
this.state.actionInProgress = true;
|
this.state.actionInProgress = true;
|
||||||
|
|
|
@ -32,6 +32,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- !registry-url-input -->
|
<!-- !registry-url-input -->
|
||||||
|
|
||||||
<!-- authentication-checkbox -->
|
<!-- authentication-checkbox -->
|
||||||
<div class="form-group" ng-if="registry.Type !== RegistryTypes.PROGET">
|
<div class="form-group" ng-if="registry.Type !== RegistryTypes.PROGET">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
|
@ -43,11 +44,14 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- !authentication-checkbox -->
|
<!-- !authentication-checkbox -->
|
||||||
|
|
||||||
<!-- authentication-credentials -->
|
<!-- authentication-credentials -->
|
||||||
<div ng-if="registry.Authentication">
|
<div ng-if="registry.Authentication">
|
||||||
<!-- credentials-user -->
|
<!-- credentials-user -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="credentials_username" class="col-sm-3 col-lg-2 control-label text-left">Username</label>
|
<label for="credentials_username" class="col-sm-3 col-lg-2 control-label text-left">
|
||||||
|
{{ registry.Type === RegistryTypes.ECR ? 'AWS Access Key' : 'Username' }}
|
||||||
|
</label>
|
||||||
<div class="col-sm-9 col-lg-10">
|
<div class="col-sm-9 col-lg-10">
|
||||||
<input type="text" class="form-control" id="credentials_username" ng-model="registry.Username" />
|
<input type="text" class="form-control" id="credentials_username" ng-model="registry.Username" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -55,7 +59,9 @@
|
||||||
<!-- !credentials-user -->
|
<!-- !credentials-user -->
|
||||||
<!-- credentials-password -->
|
<!-- credentials-password -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="credentials_password" class="col-sm-3 col-lg-2 control-label text-left">Password</label>
|
<label for="credentials_password" class="col-sm-3 col-lg-2 control-label text-left">
|
||||||
|
{{ registry.Type === RegistryTypes.ECR ? 'AWS Secret Access Key' : 'Password' }}
|
||||||
|
</label>
|
||||||
<div class="col-sm-9 col-lg-10">
|
<div class="col-sm-9 col-lg-10">
|
||||||
<input type="password" class="form-control" id="credentials_password" ng-model="formValues.Password" placeholder="*******" />
|
<input type="password" class="form-control" id="credentials_password" ng-model="formValues.Password" placeholder="*******" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -87,6 +93,24 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div ng-if="registry.Type == RegistryTypes.ECR">
|
||||||
|
<!-- region -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="registry_region" class="col-sm-3 col-lg-2 control-label text-left">Region</label>
|
||||||
|
<div class="col-sm-9 col-lg-10">
|
||||||
|
<input type="text" class="form-control" id="registry_region" name="registry_region" placeholder="us-west-1" ng-model="registry.Ecr.Region" required />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" ng-show="registryFormEcr.registry_region.$invalid">
|
||||||
|
<div class="col-sm-12 small text-warning">
|
||||||
|
<div ng-messages="registryFormEcr.registry_region.$error">
|
||||||
|
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !region -->
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<button
|
<button
|
||||||
|
|
|
@ -63,6 +63,7 @@
|
||||||
"node": ">= 0.8.4"
|
"node": ">= 0.8.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@aws-crypto/sha256-js": "^2.0.0",
|
||||||
"@babel/polyfill": "^7.2.5",
|
"@babel/polyfill": "^7.2.5",
|
||||||
"@fortawesome/fontawesome-free": "^5.11.2",
|
"@fortawesome/fontawesome-free": "^5.11.2",
|
||||||
"@nxmix/tokenize-ansi": "^3.0.0",
|
"@nxmix/tokenize-ansi": "^3.0.0",
|
||||||
|
|
35
yarn.lock
35
yarn.lock
|
@ -44,6 +44,36 @@
|
||||||
call-me-maybe "^1.0.1"
|
call-me-maybe "^1.0.1"
|
||||||
z-schema "^5.0.1"
|
z-schema "^5.0.1"
|
||||||
|
|
||||||
|
"@aws-crypto/sha256-js@^2.0.0":
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@aws-crypto/sha256-js/-/sha256-js-2.0.0.tgz#f1f936039bdebd0b9e2dd834d65afdc2aac4efcb"
|
||||||
|
integrity sha512-VZY+mCY4Nmrs5WGfitmNqXzaE873fcIZDu54cbaDaaamsaTOP1DBImV9F4pICc3EHjQXujyE8jig+PFCaew9ig==
|
||||||
|
dependencies:
|
||||||
|
"@aws-crypto/util" "^2.0.0"
|
||||||
|
"@aws-sdk/types" "^3.1.0"
|
||||||
|
tslib "^1.11.1"
|
||||||
|
|
||||||
|
"@aws-crypto/util@^2.0.0":
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@aws-crypto/util/-/util-2.0.0.tgz#17ba6f83c7e447b70fc24b84c5f6714d1e329f4a"
|
||||||
|
integrity sha512-YDooyH83m2P5A3h6lNH7hm6mIP93sU/dtzRmXIgtO4BCB7SvtX8ysVKQAE8tVky2DQ3HHxPCjNTuUe7YoAMrNQ==
|
||||||
|
dependencies:
|
||||||
|
"@aws-sdk/types" "^3.1.0"
|
||||||
|
"@aws-sdk/util-utf8-browser" "^3.0.0"
|
||||||
|
tslib "^1.11.1"
|
||||||
|
|
||||||
|
"@aws-sdk/types@^3.1.0":
|
||||||
|
version "3.40.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.40.0.tgz#a9d7926fcb9b699bc46be975033559d2293e60d1"
|
||||||
|
integrity sha512-KpILcfvRaL88TLvo3SY4OuCCg90SvcNLPyjDwUuBqiOyWODjrKShHtAPJzej4CLp92lofh+ul0UnBfV9Jb/5PA==
|
||||||
|
|
||||||
|
"@aws-sdk/util-utf8-browser@^3.0.0":
|
||||||
|
version "3.37.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.37.0.tgz#d896899f4c475ceeaf8b77c5d7cdc453e5fe6b83"
|
||||||
|
integrity sha512-tuiOxzfqet1kKGYzlgpMGfhr64AHJnYsFx2jZiH/O6Yq8XQg43ryjQlbJlim/K/XHGNzY0R+nabeJg34q3Ua1g==
|
||||||
|
dependencies:
|
||||||
|
tslib "^2.3.0"
|
||||||
|
|
||||||
"@babel/code-frame@7.10.4":
|
"@babel/code-frame@7.10.4":
|
||||||
version "7.10.4"
|
version "7.10.4"
|
||||||
resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz"
|
resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz"
|
||||||
|
@ -4016,7 +4046,6 @@ angular-moment-picker@^0.10.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
angular-mocks "1.6.1"
|
angular-mocks "1.6.1"
|
||||||
angular-sanitize "1.6.1"
|
angular-sanitize "1.6.1"
|
||||||
lodash-es "^4.17.15"
|
|
||||||
|
|
||||||
angular-resource@1.8.0:
|
angular-resource@1.8.0:
|
||||||
version "1.8.0"
|
version "1.8.0"
|
||||||
|
@ -12132,7 +12161,7 @@ locate-path@^6.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
p-locate "^5.0.0"
|
p-locate "^5.0.0"
|
||||||
|
|
||||||
lodash-es@^4.17.15, lodash-es@^4.17.21:
|
lodash-es@^4.17.21:
|
||||||
version "4.17.21"
|
version "4.17.21"
|
||||||
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
|
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
|
||||||
integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
|
integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
|
||||||
|
@ -17371,7 +17400,7 @@ tsconfig-paths@^3.11.0, tsconfig-paths@^3.9.0:
|
||||||
minimist "^1.2.0"
|
minimist "^1.2.0"
|
||||||
strip-bom "^3.0.0"
|
strip-bom "^3.0.0"
|
||||||
|
|
||||||
tslib@^1.8.1:
|
tslib@^1.11.1, tslib@^1.8.1:
|
||||||
version "1.14.1"
|
version "1.14.1"
|
||||||
resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz"
|
resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz"
|
||||||
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
||||||
|
|
Loading…
Reference in New Issue