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
|
||||
}
|
||||
|
||||
func initSwarmStackManager(assetsPath string, configPath string, signatureService portainer.DigitalSignatureService, fileService portainer.FileService, reverseTunnelService portainer.ReverseTunnelService) (portainer.SwarmStackManager, error) {
|
||||
return exec.NewSwarmStackManager(assetsPath, configPath, signatureService, fileService, reverseTunnelService)
|
||||
func initSwarmStackManager(
|
||||
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 {
|
||||
|
@ -532,7 +539,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
|||
|
||||
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 {
|
||||
log.Fatalf("failed initializing swarm stack manager: %s", err)
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
"strings"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/internal/registryutils"
|
||||
"github.com/portainer/portainer/api/internal/stackutils"
|
||||
)
|
||||
|
||||
|
@ -22,17 +23,25 @@ type SwarmStackManager struct {
|
|||
signatureService portainer.DigitalSignatureService
|
||||
fileService portainer.FileService
|
||||
reverseTunnelService portainer.ReverseTunnelService
|
||||
dataStore portainer.DataStore
|
||||
}
|
||||
|
||||
// NewSwarmStackManager initializes a new SwarmStackManager service.
|
||||
// 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{
|
||||
binaryPath: binaryPath,
|
||||
configPath: configPath,
|
||||
signatureService: signatureService,
|
||||
fileService: fileService,
|
||||
reverseTunnelService: reverseTunnelService,
|
||||
dataStore: datastore,
|
||||
}
|
||||
|
||||
err := manager.updateDockerCLIConfiguration(manager.configPath)
|
||||
|
@ -51,7 +60,17 @@ func (manager *SwarmStackManager) Login(registries []portainer.Registry, endpoin
|
|||
}
|
||||
for _, registry := range registries {
|
||||
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, "")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,9 @@ require (
|
|||
github.com/Microsoft/go-winio v0.4.17
|
||||
github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 // indirect
|
||||
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/containerd/containerd v1.5.7 // indirect
|
||||
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/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-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-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
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.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.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.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g=
|
||||
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/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.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/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
|
|
|
@ -21,7 +21,8 @@ type registryConfigurePayload struct {
|
|||
Username string `example:"registry_user"`
|
||||
// Password used to authenticate against this registry. required when Authentication is true
|
||||
Password string `example:"registry_password"`
|
||||
|
||||
// ECR region
|
||||
Region string
|
||||
// Use TLS
|
||||
TLS bool `example:"true"`
|
||||
// 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)
|
||||
payload.Password = password
|
||||
|
||||
region, _ := request.RetrieveMultiPartFormValue(r, "Region", true)
|
||||
payload.Region = region
|
||||
}
|
||||
|
||||
useTLS, _ := request.RetrieveBooleanMultiPartFormValue(r, "TLS", true)
|
||||
|
@ -134,6 +138,10 @@ func (handler *Handler) registryConfigure(w http.ResponseWriter, r *http.Request
|
|||
} else {
|
||||
registry.ManagementConfiguration.Password = payload.Password
|
||||
}
|
||||
|
||||
if payload.Region != "" {
|
||||
registry.ManagementConfiguration.Ecr.Region = payload.Region
|
||||
}
|
||||
}
|
||||
|
||||
if payload.TLS {
|
||||
|
|
|
@ -17,8 +17,15 @@ import (
|
|||
type registryCreatePayload struct {
|
||||
// Name that will be used to identify this registry
|
||||
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)
|
||||
Type portainer.RegistryType `example:"1" validate:"required" enums:"1,2,3,4,5,6"`
|
||||
// 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)
|
||||
Type portainer.RegistryType `example:"1" validate:"required" enums:"1,2,3,4,5,6,7"`
|
||||
// URL or IP address of the Docker registry
|
||||
URL string `example:"registry.mydomain.tld:2375/feed" validate:"required"`
|
||||
// BaseURL required for ProGet registry
|
||||
|
@ -33,6 +40,8 @@ type registryCreatePayload struct {
|
|||
Gitlab portainer.GitlabRegistryData
|
||||
// Quay specific details, required when type = 1
|
||||
Quay portainer.QuayRegistryData
|
||||
// ECR specific details, required when type = 7
|
||||
Ecr portainer.EcrData
|
||||
}
|
||||
|
||||
func (payload *registryCreatePayload) Validate(_ *http.Request) error {
|
||||
|
@ -42,14 +51,22 @@ func (payload *registryCreatePayload) Validate(_ *http.Request) error {
|
|||
if govalidator.IsNull(payload.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 {
|
||||
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:
|
||||
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 == "" {
|
||||
|
@ -96,9 +113,10 @@ func (handler *Handler) registryCreate(w http.ResponseWriter, r *http.Request) *
|
|||
Authentication: payload.Authentication,
|
||||
Username: payload.Username,
|
||||
Password: payload.Password,
|
||||
RegistryAccesses: portainer.RegistryAccesses{},
|
||||
Gitlab: payload.Gitlab,
|
||||
Quay: payload.Quay,
|
||||
RegistryAccesses: portainer.RegistryAccesses{},
|
||||
Ecr: payload.Ecr,
|
||||
}
|
||||
|
||||
registries, err := handler.DataStore.Registry().Registries()
|
||||
|
|
|
@ -33,6 +33,20 @@ func Test_registryCreatePayload_Validate(t *testing.T) {
|
|||
err := payload.Validate(nil)
|
||||
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 {
|
||||
|
|
|
@ -2,6 +2,7 @@ package registries
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
|
@ -26,8 +27,12 @@ type registryUpdatePayload struct {
|
|||
Username *string `example:"registry_user"`
|
||||
// Password used to authenticate against this registry. required when Authentication is true
|
||||
Password *string `example:"registry_password"`
|
||||
RegistryAccesses *portainer.RegistryAccesses
|
||||
// Quay data
|
||||
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 {
|
||||
|
@ -56,6 +61,7 @@ func (handler *Handler) registryUpdate(w http.ResponseWriter, r *http.Request) *
|
|||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||
}
|
||||
|
||||
if !securityContext.IsAdmin {
|
||||
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
|
||||
}
|
||||
|
||||
shouldUpdateSecrets := false
|
||||
|
||||
if registry.Type == portainer.ProGetRegistry && payload.BaseURL != nil {
|
||||
registry.BaseURL = *payload.BaseURL
|
||||
}
|
||||
|
||||
shouldUpdateSecrets := false
|
||||
|
||||
if payload.Authentication != nil {
|
||||
shouldUpdateSecrets = shouldUpdateSecrets || (registry.Authentication != *payload.Authentication)
|
||||
|
||||
if *payload.Authentication {
|
||||
registry.Authentication = true
|
||||
shouldUpdateSecrets = shouldUpdateSecrets || (payload.Username != nil && *payload.Username != registry.Username) || (payload.Password != nil && *payload.Password != registry.Password)
|
||||
|
||||
if payload.Username != nil {
|
||||
shouldUpdateSecrets = shouldUpdateSecrets || (registry.Username != *payload.Username)
|
||||
registry.Username = *payload.Username
|
||||
}
|
||||
|
||||
if payload.Password != nil && *payload.Password != "" {
|
||||
shouldUpdateSecrets = shouldUpdateSecrets || (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 {
|
||||
registry.Authentication = false
|
||||
registry.Username = ""
|
||||
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 {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve registries from the database", err}
|
||||
}
|
||||
|
||||
for _, r := range registries {
|
||||
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")}
|
||||
|
@ -124,13 +143,16 @@ func (handler *Handler) registryUpdate(w http.ResponseWriter, r *http.Request) *
|
|||
}
|
||||
|
||||
if shouldUpdateSecrets {
|
||||
registry.AccessToken = ""
|
||||
registry.AccessTokenExpiry = 0
|
||||
|
||||
for endpointID, endpointAccess := range registry.RegistryAccesses {
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(endpointID)
|
||||
if err != nil {
|
||||
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)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update access to registry", err}
|
||||
|
|
|
@ -3,6 +3,7 @@ package docker
|
|||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/registryutils"
|
||||
)
|
||||
|
||||
type (
|
||||
|
@ -25,13 +26,13 @@ type (
|
|||
}
|
||||
)
|
||||
|
||||
func createRegistryAuthenticationHeader(registryId portainer.RegistryID, accessContext *registryAccessContext) *registryAuthenticationHeader {
|
||||
var authenticationHeader *registryAuthenticationHeader
|
||||
|
||||
func createRegistryAuthenticationHeader(
|
||||
dataStore portainer.DataStore,
|
||||
registryId portainer.RegistryID,
|
||||
accessContext *registryAccessContext,
|
||||
) (authenticationHeader registryAuthenticationHeader, err error) {
|
||||
if registryId == 0 { // dockerhub (anonymous)
|
||||
authenticationHeader = ®istryAuthenticationHeader{
|
||||
Serveraddress: "docker.io",
|
||||
}
|
||||
authenticationHeader.Serveraddress = "docker.io"
|
||||
} else { // any "custom" registry
|
||||
var matchingRegistry *portainer.Registry
|
||||
for _, registry := range accessContext.registries {
|
||||
|
@ -44,13 +45,14 @@ func createRegistryAuthenticationHeader(registryId portainer.RegistryID, accessC
|
|||
}
|
||||
|
||||
if matchingRegistry != nil {
|
||||
authenticationHeader = ®istryAuthenticationHeader{
|
||||
Username: matchingRegistry.Username,
|
||||
Password: matchingRegistry.Password,
|
||||
Serveraddress: matchingRegistry.URL,
|
||||
err = registryutils.EnsureRegTokenValid(dataStore, matchingRegistry)
|
||||
if (err != nil) {
|
||||
return
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
authenticationHeader := createRegistryAuthenticationHeader(originalHeaderData.RegistryId, accessContext)
|
||||
authenticationHeader, err := createRegistryAuthenticationHeader(transport.dataStore, originalHeaderData.RegistryId, accessContext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
headerData, err := json.Marshal(authenticationHeader)
|
||||
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
|
||||
// on the requested operation.
|
||||
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, "")
|
||||
|
||||
switch {
|
||||
|
@ -66,6 +69,10 @@ func (transport *baseTransport) proxyNamespacedRequest(request *http.Request, fu
|
|||
}
|
||||
|
||||
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":
|
||||
return transport.proxyNamespaceDeleteOperation(request, namespace)
|
||||
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"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/internal/registryutils"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
@ -15,6 +16,8 @@ import (
|
|||
|
||||
const (
|
||||
secretDockerConfigKey = ".dockerconfigjson"
|
||||
labelRegistryType = "io.portainer.kubernetes.registry.type"
|
||||
annotationRegistryID = "portainer.io/registry.id"
|
||||
)
|
||||
|
||||
type (
|
||||
|
@ -38,12 +41,17 @@ func (kcl *KubeClient) DeleteRegistrySecret(registry *portainer.Registry, namesp
|
|||
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{
|
||||
Auths: map[string]registryDockerConfig{
|
||||
registry.URL: {
|
||||
Username: registry.Username,
|
||||
Password: registry.Password,
|
||||
Username: username,
|
||||
Password: password,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -57,8 +65,11 @@ func (kcl *KubeClient) CreateRegistrySecret(registry *portainer.Registry, namesp
|
|||
TypeMeta: metav1.TypeMeta{},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: registrySecretName(registry),
|
||||
Labels: map[string]string{
|
||||
labelRegistryType: strconv.Itoa(int(registry.Type)),
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"portainer.io/registry.id": strconv.Itoa(int(registry.ID)),
|
||||
annotationRegistryID: strconv.Itoa(int(registry.ID)),
|
||||
},
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
|
|
|
@ -155,8 +155,8 @@ type (
|
|||
ImageCount int `json:"ImageCount"`
|
||||
ServiceCount int `json:"ServiceCount"`
|
||||
StackCount int `json:"StackCount"`
|
||||
NodeCount int `json:"NodeCount"`
|
||||
SnapshotRaw DockerSnapshotRaw `json:"DockerSnapshotRaw"`
|
||||
NodeCount int `json:"NodeCount"`
|
||||
}
|
||||
|
||||
// DockerSnapshotRaw represents all the information related to a snapshot as returned by the Docker API
|
||||
|
@ -444,6 +444,11 @@ type (
|
|||
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 int
|
||||
|
||||
|
@ -589,8 +594,8 @@ type (
|
|||
Registry struct {
|
||||
// Registry Identifier
|
||||
ID RegistryID `json:"Id" example:"1"`
|
||||
// Registry Type (1 - Quay, 2 - Azure, 3 - Custom, 4 - Gitlab, 5 - ProGet, 6 - DockerHub)
|
||||
Type RegistryType `json:"Type" enums:"1,2,3,4,5,6"`
|
||||
// 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,7"`
|
||||
// Registry Name
|
||||
Name string `json:"Name" example:"my-registry"`
|
||||
// URL or IP address of the Docker registry
|
||||
|
@ -599,13 +604,14 @@ type (
|
|||
BaseURL string `json:"BaseURL" example:"registry.mydomain.tld:2375"`
|
||||
// Is authentication against this registry enabled
|
||||
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"`
|
||||
// Password used to authenticate against this registry
|
||||
// Password or SecretAccessKey used to authenticate against this registry
|
||||
Password string `json:"Password,omitempty" example:"registry_password"`
|
||||
ManagementConfiguration *RegistryManagementConfiguration `json:"ManagementConfiguration"`
|
||||
Gitlab GitlabRegistryData `json:"Gitlab"`
|
||||
Quay QuayRegistryData `json:"Quay"`
|
||||
Ecr EcrData `json:"Ecr"`
|
||||
RegistryAccesses RegistryAccesses `json:"RegistryAccesses"`
|
||||
|
||||
// Deprecated fields
|
||||
|
@ -618,6 +624,10 @@ type (
|
|||
AuthorizedUsers []UserID `json:"AuthorizedUsers"`
|
||||
// Deprecated in DBVersion == 18
|
||||
AuthorizedTeams []TeamID `json:"AuthorizedTeams"`
|
||||
|
||||
// Stores temporary access token
|
||||
AccessToken string `json:"AccessToken,omitempty"`
|
||||
AccessTokenExpiry int64 `json:"AccessTokenExpiry,omitempty"`
|
||||
}
|
||||
|
||||
RegistryAccesses map[EndpointID]RegistryAccessPolicies
|
||||
|
@ -634,11 +644,14 @@ type (
|
|||
// RegistryManagementConfiguration represents a configuration that can be used to query
|
||||
// the registry API via the registry management extension.
|
||||
RegistryManagementConfiguration struct {
|
||||
Type RegistryType `json:"Type"`
|
||||
Authentication bool `json:"Authentication"`
|
||||
Username string `json:"Username"`
|
||||
Password string `json:"Password"`
|
||||
TLSConfig TLSConfiguration `json:"TLSConfig"`
|
||||
Type RegistryType `json:"Type"`
|
||||
Authentication bool `json:"Authentication"`
|
||||
Username string `json:"Username"`
|
||||
Password string `json:"Password"`
|
||||
TLSConfig TLSConfiguration `json:"TLSConfig"`
|
||||
Ecr EcrData `json:"Ecr"`
|
||||
AccessToken string `json:"AccessToken,omitempty"`
|
||||
AccessTokenExpiry int64 `json:"AccessTokenExpiry,omitempty"`
|
||||
}
|
||||
|
||||
// RegistryType represents a type of registry
|
||||
|
@ -1714,6 +1727,8 @@ const (
|
|||
ProGetRegistry
|
||||
// DockerHubRegistry represents a dockerhub registry
|
||||
DockerHubRegistry
|
||||
// EcrRegistry represents an ECR registry
|
||||
EcrRegistry
|
||||
)
|
||||
|
||||
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.Gitlab = data.Gitlab;
|
||||
this.Quay = data.Quay;
|
||||
this.Ecr = data.Ecr;
|
||||
}
|
||||
|
||||
export function RegistryManagementConfigurationDefaultModel(registry) {
|
||||
|
@ -25,7 +26,12 @@ export function RegistryManagementConfigurationDefaultModel(registry) {
|
|||
this.TLSCertFile = 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.Username = registry.Username;
|
||||
this.TLS = true;
|
||||
|
@ -63,6 +69,9 @@ export function RegistryCreateRequest(model) {
|
|||
ProjectPath: model.Gitlab.ProjectPath,
|
||||
};
|
||||
}
|
||||
if (model.Type === RegistryTypes.ECR) {
|
||||
this.Ecr = model.Ecr;
|
||||
}
|
||||
if (model.Type === RegistryTypes.QUAY) {
|
||||
this.Quay = {
|
||||
useOrganisation: model.Quay.useOrganisation,
|
||||
|
|
|
@ -6,4 +6,5 @@ export const RegistryTypes = Object.freeze({
|
|||
GITLAB: 4,
|
||||
PROGET: 5,
|
||||
DOCKERHUB: 6,
|
||||
ECR: 7,
|
||||
});
|
||||
|
|
|
@ -26,6 +26,16 @@
|
|||
<p>DockerHub authenticated account</p>
|
||||
</label>
|
||||
</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>
|
||||
<input type="radio" id="registry_quay" ng-model="$ctrl.model.Type" ng-value="$ctrl.RegistryTypes.QUAY" />
|
||||
<label for="registry_quay" ng-click="$ctrl.selectQuayRegistry()">
|
||||
|
@ -103,6 +113,14 @@
|
|||
action-in-progress="$ctrl.state.actionInProgress"
|
||||
></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
|
||||
ng-if="$ctrl.model.Type === $ctrl.RegistryTypes.PROGET"
|
||||
model="$ctrl.model"
|
||||
|
|
|
@ -74,6 +74,18 @@ class CreateRegistryController {
|
|||
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() {
|
||||
return this.$async(async () => {
|
||||
this.state.actionInProgress = true;
|
||||
|
|
|
@ -32,6 +32,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<!-- !registry-url-input -->
|
||||
|
||||
<!-- authentication-checkbox -->
|
||||
<div class="form-group" ng-if="registry.Type !== RegistryTypes.PROGET">
|
||||
<div class="col-sm-12">
|
||||
|
@ -43,11 +44,14 @@
|
|||
</div>
|
||||
</div>
|
||||
<!-- !authentication-checkbox -->
|
||||
|
||||
<!-- authentication-credentials -->
|
||||
<div ng-if="registry.Authentication">
|
||||
<!-- credentials-user -->
|
||||
<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">
|
||||
<input type="text" class="form-control" id="credentials_username" ng-model="registry.Username" />
|
||||
</div>
|
||||
|
@ -55,7 +59,9 @@
|
|||
<!-- !credentials-user -->
|
||||
<!-- credentials-password -->
|
||||
<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">
|
||||
<input type="password" class="form-control" id="credentials_password" ng-model="formValues.Password" placeholder="*******" />
|
||||
</div>
|
||||
|
@ -87,6 +93,24 @@
|
|||
</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="col-sm-12">
|
||||
<button
|
||||
|
|
|
@ -63,6 +63,7 @@
|
|||
"node": ">= 0.8.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-crypto/sha256-js": "^2.0.0",
|
||||
"@babel/polyfill": "^7.2.5",
|
||||
"@fortawesome/fontawesome-free": "^5.11.2",
|
||||
"@nxmix/tokenize-ansi": "^3.0.0",
|
||||
|
|
35
yarn.lock
35
yarn.lock
|
@ -44,6 +44,36 @@
|
|||
call-me-maybe "^1.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":
|
||||
version "7.10.4"
|
||||
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:
|
||||
angular-mocks "1.6.1"
|
||||
angular-sanitize "1.6.1"
|
||||
lodash-es "^4.17.15"
|
||||
|
||||
angular-resource@1.8.0:
|
||||
version "1.8.0"
|
||||
|
@ -12132,7 +12161,7 @@ locate-path@^6.0.0:
|
|||
dependencies:
|
||||
p-locate "^5.0.0"
|
||||
|
||||
lodash-es@^4.17.15, lodash-es@^4.17.21:
|
||||
lodash-es@^4.17.21:
|
||||
version "4.17.21"
|
||||
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
|
||||
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"
|
||||
strip-bom "^3.0.0"
|
||||
|
||||
tslib@^1.8.1:
|
||||
tslib@^1.11.1, tslib@^1.8.1:
|
||||
version "1.14.1"
|
||||
resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz"
|
||||
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
||||
|
|
Loading…
Reference in New Issue