Add options to `docker login azure` to support Service Principal login. Use it in E2E tests

This commit is contained in:
Guillaume Tardif 2020-08-10 16:38:59 +02:00
parent c049f3c7af
commit ec5f4e9202
5 changed files with 130 additions and 18 deletions

View File

@ -64,7 +64,19 @@ type ContextParams struct {
// LoginParams azure login options
type LoginParams struct {
TenantID string
TenantID string
ClientID string
ClientSecret string
}
// Validate returns an error if options are not used properly
func (opts LoginParams) Validate() error {
if opts.ClientID != "" || opts.ClientSecret != "" {
if opts.ClientID == "" || opts.ClientSecret == "" || opts.TenantID == "" {
return errors.New("for Service Principal login, 3 options must be specified: --client-id, --client-secret and --tenant-id")
}
}
return nil
}
func init() {
@ -377,12 +389,18 @@ func (cs *aciComposeService) Logs(ctx context.Context, opts cli.ProjectOptions)
}
type aciCloudService struct {
loginService *login.AzureLoginService
loginService login.AzureLoginServiceAPI
}
func (cs *aciCloudService) Login(ctx context.Context, params interface{}) error {
createOpts := params.(LoginParams)
return cs.loginService.Login(ctx, createOpts.TenantID)
opts, ok := params.(LoginParams)
if !ok {
return errors.New("Could not read azure LoginParams struct from generic parameter")
}
if opts.ClientID != "" {
return cs.loginService.LoginServicePrincipal(opts.ClientID, opts.ClientSecret, opts.TenantID)
}
return cs.loginService.Login(ctx, opts.TenantID)
}
func (cs *aciCloudService) Logout(ctx context.Context) error {

View File

@ -20,9 +20,10 @@ import (
"context"
"testing"
"github.com/docker/api/containers"
"github.com/stretchr/testify/mock"
"gotest.tools/v3/assert"
"github.com/docker/api/containers"
)
func TestGetContainerName(t *testing.T) {
@ -58,3 +59,86 @@ func TestVerifyCommand(t *testing.T) {
assert.Error(t, err, "ACI exec command does not accept arguments to the command. "+
"Only the binary should be specified")
}
func TestLoginParamsValidate(t *testing.T) {
err := LoginParams{
ClientID: "someID",
}.Validate()
assert.Error(t, err, "for Service Principal login, 3 options must be specified: --client-id, --client-secret and --tenant-id")
err = LoginParams{
ClientSecret: "someSecret",
}.Validate()
assert.Error(t, err, "for Service Principal login, 3 options must be specified: --client-id, --client-secret and --tenant-id")
err = LoginParams{}.Validate()
assert.NilError(t, err)
err = LoginParams{
TenantID: "tenant",
}.Validate()
assert.NilError(t, err)
}
func TestLoginServicePrincipal(t *testing.T) {
loginService := mockLoginService{}
loginService.On("LoginServicePrincipal", "someID", "secret", "tenant").Return(nil)
loginBackend := aciCloudService{
loginService: &loginService,
}
err := loginBackend.Login(context.Background(), LoginParams{
ClientID: "someID",
ClientSecret: "secret",
TenantID: "tenant",
})
assert.NilError(t, err)
}
func TestLoginWithTenant(t *testing.T) {
loginService := mockLoginService{}
ctx := context.Background()
loginService.On("Login", ctx, "tenant").Return(nil)
loginBackend := aciCloudService{
loginService: &loginService,
}
err := loginBackend.Login(ctx, LoginParams{
TenantID: "tenant",
})
assert.NilError(t, err)
}
func TestLoginWithoutTenant(t *testing.T) {
loginService := mockLoginService{}
ctx := context.Background()
loginService.On("Login", ctx, "").Return(nil)
loginBackend := aciCloudService{
loginService: &loginService,
}
err := loginBackend.Login(ctx, LoginParams{})
assert.NilError(t, err)
}
type mockLoginService struct {
mock.Mock
}
func (s *mockLoginService) Login(ctx context.Context, requestedTenantID string) error {
args := s.Called(ctx, requestedTenantID)
return args.Error(0)
}
func (s *mockLoginService) LoginServicePrincipal(clientID string, clientSecret string, tenantID string) error {
args := s.Called(clientID, clientSecret, tenantID)
return args.Error(0)
}
func (s *mockLoginService) Logout(ctx context.Context) error {
args := s.Called(ctx)
return args.Error(0)
}

View File

@ -72,6 +72,13 @@ type AzureLoginService struct {
apiHelper apiHelper
}
// AzureLoginServiceAPI interface for Azure login service
type AzureLoginServiceAPI interface {
LoginServicePrincipal(clientID string, clientSecret string, tenantID string) error
Login(ctx context.Context, requestedTenantID string) error
Logout(ctx context.Context) error
}
const tokenStoreFilename = "dockerAccessToken.json"
// NewAzureLoginService creates a NewAzureLoginService
@ -90,9 +97,9 @@ func newAzureLoginServiceFromPath(tokenStorePath string, helper apiHelper) (*Azu
}, nil
}
// TestLoginFromServicePrincipal login with clientId / clientSecret from a previously created service principal.
// The resulting token does not include a refresh token, used for tests only
func (login *AzureLoginService) TestLoginFromServicePrincipal(clientID string, clientSecret string, tenantID string) error {
// LoginServicePrincipal login with clientId / clientSecret from a service principal.
// The resulting token does not include a refresh token
func (login *AzureLoginService) LoginServicePrincipal(clientID string, clientSecret string, tenantID string) error {
// Tried with auth2.NewUsernamePasswordConfig() but could not make this work with username / password, setting this for CI with clientID / clientSecret
creds := auth2.NewClientCredentialsConfig(clientID, clientSecret, tenantID)

View File

@ -14,11 +14,17 @@ func AzureLoginCommand() *cobra.Command {
Short: "Log in to azure",
Args: cobra.MaximumNArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
err := opts.Validate()
if err != nil {
return err
}
return cloudLogin(cmd, "aci", opts)
},
}
flags := cmd.Flags()
flags.StringVar(&opts.TenantID, "tenant-id", "", "Specify tenant ID to use from your azure account")
flags.StringVar(&opts.TenantID, "tenant-id", "", "Specify tenant ID to use")
flags.StringVar(&opts.ClientID, "client-id", "", "Client ID for Service principal login")
flags.StringVar(&opts.ClientSecret, "client-secret", "", "Client secret for Service principal login")
return cmd
}

View File

@ -76,7 +76,7 @@ func TestLoginLogout(t *testing.T) {
rg := "E2E-" + startTime
t.Run("login", func(t *testing.T) {
azureLogin(t)
azureLogin(t, c)
})
t.Run("create context", func(t *testing.T) {
@ -506,7 +506,7 @@ func TestRunEnvVars(t *testing.T) {
func setupTestResourceGroup(t *testing.T, c *E2eCLI, tName string) (string, string) {
startTime := strconv.Itoa(int(time.Now().Unix()))
rg := "E2E-" + tName + "-" + startTime
azureLogin(t)
azureLogin(t, c)
sID := getSubscriptionID(t)
t.Logf("Create resource group %q", rg)
err := createResourceGroup(sID, rg)
@ -537,17 +537,14 @@ func deleteResourceGroup(rgName string) error {
return helper.DeleteAsync(ctx, *models[0].SubscriptionID, rgName)
}
func azureLogin(t *testing.T) {
func azureLogin(t *testing.T, c *E2eCLI) {
t.Log("Log in to Azure")
login, err := login.NewAzureLoginService()
assert.NilError(t, err)
// in order to create new service principal and get these 3 values : `az ad sp create-for-rbac --name 'TestServicePrincipal' --sdk-auth`
clientID := os.Getenv("AZURE_CLIENT_ID")
clientSecret := os.Getenv("AZURE_CLIENT_SECRET")
tenantID := os.Getenv("AZURE_TENANT_ID")
err = login.TestLoginFromServicePrincipal(clientID, clientSecret, tenantID)
assert.NilError(t, err)
res := c.RunDockerCmd("login", "azure", "--client-id", clientID, "--client-secret", clientSecret, "--tenant-id", tenantID)
res.Assert(t, icmd.Success)
}
func getSubscriptionID(t *testing.T) string {