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
cong meng 2021-12-01 13:18:57 +13:00 committed by GitHub
parent ff6185cc81
commit a86c7046df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 694 additions and 51 deletions

View File

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

32
api/aws/ecr/ecr.go Normal file
View File

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

View File

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

View File

@ -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, &registry)
if err != nil {
return err
}
username, password, err := registryutils.GetRegEffectiveCredential(&registry)
if err != nil {
return err
}
registryArgs := append(args, "login", "--username", username, "--password", password, registry.URL)
runCommandAndCaptureStdErr(command, registryArgs, nil, "")
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 = &registryAuthenticationHeader{
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 = &registryAuthenticationHeader{
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
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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, &registry)
if err != nil {
return
}
err = cli.DeleteRegistrySecret(&registry, namespace)
if err != nil {
return
}
err = cli.CreateRegistrySecret(&registry, namespace)
if err != nil {
return
}
}
return
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,9 @@
angular.module('portainer.app').component('registryFormEcr', {
templateUrl: './registry-form-ecr.html',
bindings: {
model: '=',
formAction: '<',
formActionLabel: '@',
actionInProgress: '<',
},
});

View File

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

View File

@ -6,4 +6,5 @@ export const RegistryTypes = Object.freeze({
GITLAB: 4,
PROGET: 5,
DOCKERHUB: 6,
ECR: 7,
});

View File

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

View File

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

View File

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

View File

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

View File

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