mirror of
https://github.com/docker/compose.git
synced 2025-07-01 19:04:34 +02:00
Merge pull request #448 from docker/azure_sp_login
Add options to `docker login azure` to support Service Principal login. Use it in E2E tests
This commit is contained in:
commit
ba6e8045b2
@ -64,7 +64,19 @@ type ContextParams struct {
|
|||||||
|
|
||||||
// LoginParams azure login options
|
// LoginParams azure login options
|
||||||
type LoginParams struct {
|
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() {
|
func init() {
|
||||||
@ -377,12 +389,18 @@ func (cs *aciComposeService) Logs(ctx context.Context, opts cli.ProjectOptions)
|
|||||||
}
|
}
|
||||||
|
|
||||||
type aciCloudService struct {
|
type aciCloudService struct {
|
||||||
loginService *login.AzureLoginService
|
loginService login.AzureLoginServiceAPI
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cs *aciCloudService) Login(ctx context.Context, params interface{}) error {
|
func (cs *aciCloudService) Login(ctx context.Context, params interface{}) error {
|
||||||
createOpts := params.(LoginParams)
|
opts, ok := params.(LoginParams)
|
||||||
return cs.loginService.Login(ctx, createOpts.TenantID)
|
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 {
|
func (cs *aciCloudService) Logout(ctx context.Context) error {
|
||||||
|
@ -20,9 +20,10 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/docker/api/containers"
|
"github.com/stretchr/testify/mock"
|
||||||
|
|
||||||
"gotest.tools/v3/assert"
|
"gotest.tools/v3/assert"
|
||||||
|
|
||||||
|
"github.com/docker/api/containers"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGetContainerName(t *testing.T) {
|
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. "+
|
assert.Error(t, err, "ACI exec command does not accept arguments to the command. "+
|
||||||
"Only the binary should be specified")
|
"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)
|
||||||
|
}
|
||||||
|
@ -72,6 +72,13 @@ type AzureLoginService struct {
|
|||||||
apiHelper apiHelper
|
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"
|
const tokenStoreFilename = "dockerAccessToken.json"
|
||||||
|
|
||||||
// NewAzureLoginService creates a NewAzureLoginService
|
// NewAzureLoginService creates a NewAzureLoginService
|
||||||
@ -90,9 +97,9 @@ func newAzureLoginServiceFromPath(tokenStorePath string, helper apiHelper) (*Azu
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestLoginFromServicePrincipal login with clientId / clientSecret from a previously created service principal.
|
// LoginServicePrincipal login with clientId / clientSecret from a service principal.
|
||||||
// The resulting token does not include a refresh token, used for tests only
|
// The resulting token does not include a refresh token
|
||||||
func (login *AzureLoginService) TestLoginFromServicePrincipal(clientID string, clientSecret string, tenantID string) error {
|
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
|
// 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)
|
creds := auth2.NewClientCredentialsConfig(clientID, clientSecret, tenantID)
|
||||||
|
|
||||||
|
@ -14,11 +14,16 @@ func AzureLoginCommand() *cobra.Command {
|
|||||||
Short: "Log in to azure",
|
Short: "Log in to azure",
|
||||||
Args: cobra.MaximumNArgs(0),
|
Args: cobra.MaximumNArgs(0),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
if err := opts.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return cloudLogin(cmd, "aci", opts)
|
return cloudLogin(cmd, "aci", opts)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
flags := cmd.Flags()
|
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
|
return cmd
|
||||||
}
|
}
|
||||||
|
@ -76,7 +76,7 @@ func TestLoginLogout(t *testing.T) {
|
|||||||
rg := "E2E-" + startTime
|
rg := "E2E-" + startTime
|
||||||
|
|
||||||
t.Run("login", func(t *testing.T) {
|
t.Run("login", func(t *testing.T) {
|
||||||
azureLogin(t)
|
azureLogin(t, c)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("create context", func(t *testing.T) {
|
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) {
|
func setupTestResourceGroup(t *testing.T, c *E2eCLI, tName string) (string, string) {
|
||||||
startTime := strconv.Itoa(int(time.Now().Unix()))
|
startTime := strconv.Itoa(int(time.Now().Unix()))
|
||||||
rg := "E2E-" + tName + "-" + startTime
|
rg := "E2E-" + tName + "-" + startTime
|
||||||
azureLogin(t)
|
azureLogin(t, c)
|
||||||
sID := getSubscriptionID(t)
|
sID := getSubscriptionID(t)
|
||||||
t.Logf("Create resource group %q", rg)
|
t.Logf("Create resource group %q", rg)
|
||||||
err := createResourceGroup(sID, rg)
|
err := createResourceGroup(sID, rg)
|
||||||
@ -537,17 +537,14 @@ func deleteResourceGroup(rgName string) error {
|
|||||||
return helper.DeleteAsync(ctx, *models[0].SubscriptionID, rgName)
|
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")
|
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`
|
// 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")
|
clientID := os.Getenv("AZURE_CLIENT_ID")
|
||||||
clientSecret := os.Getenv("AZURE_CLIENT_SECRET")
|
clientSecret := os.Getenv("AZURE_CLIENT_SECRET")
|
||||||
tenantID := os.Getenv("AZURE_TENANT_ID")
|
tenantID := os.Getenv("AZURE_TENANT_ID")
|
||||||
err = login.TestLoginFromServicePrincipal(clientID, clientSecret, tenantID)
|
res := c.RunDockerCmd("login", "azure", "--client-id", clientID, "--client-secret", clientSecret, "--tenant-id", tenantID)
|
||||||
assert.NilError(t, err)
|
res.Assert(t, icmd.Success)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSubscriptionID(t *testing.T) string {
|
func getSubscriptionID(t *testing.T) string {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user