380 строки
13 KiB
Go
380 строки
13 KiB
Go
// Copyright (c) Microsoft Corporation. All rights reserved.
|
|
// Licensed under the MIT license.
|
|
|
|
package cmd
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/Azure/aks-engine-azurestack/pkg/api"
|
|
"github.com/Azure/aks-engine-azurestack/pkg/api/vlabs"
|
|
"github.com/Azure/aks-engine-azurestack/pkg/armhelpers"
|
|
"github.com/Azure/aks-engine-azurestack/pkg/engine"
|
|
"github.com/Azure/aks-engine-azurestack/pkg/engine/transform"
|
|
"github.com/Azure/aks-engine-azurestack/pkg/helpers"
|
|
"github.com/Azure/aks-engine-azurestack/pkg/i18n"
|
|
"github.com/Azure/aks-engine-azurestack/pkg/kubernetes"
|
|
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
|
|
"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
|
|
"github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
|
|
"github.com/google/uuid"
|
|
"github.com/pkg/errors"
|
|
log "github.com/sirupsen/logrus"
|
|
"github.com/spf13/cobra"
|
|
flag "github.com/spf13/pflag"
|
|
ini "gopkg.in/ini.v1"
|
|
)
|
|
|
|
const (
|
|
rootName = "aks-engine-azurestack"
|
|
rootShortDescription = "AKS Engine deploys and manages Kubernetes clusters in Azure Stack Hub"
|
|
rootLongDescription = "AKS Engine deploys and manages Kubernetes clusters in Azure Stack Hub"
|
|
)
|
|
|
|
var (
|
|
debug bool
|
|
dumpDefaultModel bool
|
|
)
|
|
|
|
// NewRootCmd returns the root command for AKS Engine.
|
|
func NewRootCmd() *cobra.Command {
|
|
rootCmd := &cobra.Command{
|
|
Use: rootName,
|
|
Short: rootShortDescription,
|
|
Long: rootLongDescription,
|
|
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
|
if debug {
|
|
log.SetLevel(log.DebugLevel)
|
|
}
|
|
},
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
if dumpDefaultModel {
|
|
return writeDefaultModel(cmd.OutOrStdout())
|
|
}
|
|
return cmd.Usage()
|
|
},
|
|
}
|
|
|
|
p := rootCmd.PersistentFlags()
|
|
p.BoolVar(&debug, "debug", false, "enable verbose debug logs")
|
|
|
|
f := rootCmd.Flags()
|
|
f.BoolVar(&dumpDefaultModel, "show-default-model", false, "Dump the default API model to stdout")
|
|
|
|
rootCmd.AddCommand(newVersionCmd())
|
|
rootCmd.AddCommand(newGenerateCmd())
|
|
rootCmd.AddCommand(newDeployCmd())
|
|
rootCmd.AddCommand(newGetLogsCmd())
|
|
rootCmd.AddCommand(newGetVersionsCmd())
|
|
rootCmd.AddCommand(newOrchestratorsCmd())
|
|
rootCmd.AddCommand(newUpgradeCmd())
|
|
rootCmd.AddCommand(newScaleCmd())
|
|
rootCmd.AddCommand(newRotateCertsCmd())
|
|
rootCmd.AddCommand(newAddPoolCmd())
|
|
rootCmd.AddCommand(getCompletionCmd(rootCmd))
|
|
|
|
return rootCmd
|
|
}
|
|
|
|
func writeDefaultModel(out io.Writer) error {
|
|
meta, p := api.LoadDefaultContainerServiceProperties()
|
|
type withMeta struct {
|
|
APIVersion string `json:"apiVersion"`
|
|
Properties *vlabs.Properties `json:"properties"`
|
|
}
|
|
|
|
b, err := json.MarshalIndent(withMeta{APIVersion: meta.APIVersion, Properties: p}, "", "\t")
|
|
if err != nil {
|
|
return errors.Wrap(err, "error encoding model to json")
|
|
}
|
|
b = append(b, '\n')
|
|
if _, err := out.Write(b); err != nil {
|
|
return errors.Wrap(err, "error writing output")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type authProvider interface {
|
|
getAuthArgs() *authArgs
|
|
getClient(env *api.Environment) (armhelpers.AKSEngineClient, error)
|
|
}
|
|
|
|
type authArgs struct {
|
|
RawAzureEnvironment string
|
|
rawSubscriptionID string
|
|
SubscriptionID uuid.UUID
|
|
AuthMethod string
|
|
rawClientID string
|
|
|
|
ClientID uuid.UUID
|
|
ClientSecret string
|
|
CertificatePath string
|
|
PrivateKeyPath string
|
|
IdentitySystem string
|
|
language string
|
|
}
|
|
|
|
func addAuthFlags(authArgs *authArgs, f *flag.FlagSet) {
|
|
f.StringVar(&authArgs.RawAzureEnvironment, "azure-env", "AzurePublicCloud", "the target Azure cloud")
|
|
f.StringVarP(&authArgs.rawSubscriptionID, "subscription-id", "s", "", "azure subscription id (required)")
|
|
f.StringVar(&authArgs.AuthMethod, "auth-method", "client_secret", "auth method (default:`client_secret`, `client_certificate`)")
|
|
f.StringVar(&authArgs.rawClientID, "client-id", "", "client id (used with --auth-method=[client_secret|client_certificate])")
|
|
f.StringVar(&authArgs.ClientSecret, "client-secret", "", "client secret (used with --auth-method=client_secret)")
|
|
f.StringVar(&authArgs.CertificatePath, "certificate-path", "", "path to client certificate (used with --auth-method=client_certificate)")
|
|
f.StringVar(&authArgs.PrivateKeyPath, "private-key-path", "", "path to private key (used with --auth-method=client_certificate)")
|
|
f.StringVar(&authArgs.IdentitySystem, "identity-system", "azure_ad", "identity system (default:`azure_ad`, `adfs`)")
|
|
f.StringVar(&authArgs.language, "language", "en-us", "language to return error messages in")
|
|
}
|
|
|
|
func (authArgs *authArgs) getAuthArgs() *authArgs {
|
|
return authArgs
|
|
}
|
|
|
|
func (authArgs *authArgs) isAzureStackCloud() bool {
|
|
return strings.EqualFold(authArgs.RawAzureEnvironment, api.AzureStackCloud)
|
|
}
|
|
|
|
func (authArgs *authArgs) validateAuthArgs() error {
|
|
var err error
|
|
|
|
if authArgs.AuthMethod == "" {
|
|
return errors.New("--auth-method is a required parameter")
|
|
}
|
|
|
|
if authArgs.AuthMethod == "client_secret" || authArgs.AuthMethod == "client_certificate" {
|
|
authArgs.ClientID, err = uuid.Parse(authArgs.rawClientID)
|
|
if err != nil {
|
|
return errors.Wrap(err, "parsing --client-id")
|
|
}
|
|
if authArgs.AuthMethod == "client_secret" {
|
|
if authArgs.ClientSecret == "" {
|
|
return errors.New(`--client-secret must be specified when --auth-method="client_secret"`)
|
|
}
|
|
} else if authArgs.AuthMethod == "client_certificate" {
|
|
if authArgs.CertificatePath == "" || authArgs.PrivateKeyPath == "" {
|
|
return errors.New(`--certificate-path and --private-key-path must be specified when --auth-method="client_certificate"`)
|
|
}
|
|
}
|
|
}
|
|
|
|
authArgs.SubscriptionID, _ = uuid.Parse(authArgs.rawSubscriptionID)
|
|
if authArgs.SubscriptionID.String() == "00000000-0000-0000-0000-000000000000" {
|
|
var subID uuid.UUID
|
|
subID, err = getSubFromAzDir(filepath.Join(helpers.GetHomeDir(), ".azure"))
|
|
if err != nil || subID.String() == "00000000-0000-0000-0000-000000000000" {
|
|
return errors.New("--subscription-id is required (and must be a valid UUID)")
|
|
}
|
|
log.Infoln("No subscription provided, using selected subscription from azure CLI:", subID.String())
|
|
authArgs.SubscriptionID = subID
|
|
}
|
|
|
|
switch strings.ToUpper(authArgs.RawAzureEnvironment) {
|
|
case "AZURESTACKCLOUD":
|
|
// Azure stack cloud environment, verify file path can be read
|
|
fileName := os.Getenv("AZURE_ENVIRONMENT_FILEPATH")
|
|
if fileContents, err := os.ReadFile(fileName); err != nil ||
|
|
json.Unmarshal(fileContents, &api.Environment{}) != nil {
|
|
return fmt.Errorf("failed to read file or unmarshal JSON from file %s: %v", fileName, err)
|
|
}
|
|
case "AZURECHINACLOUD", "AZUREGERMANCLOUD", "AZUREPUBLICCLOUD", "AZUREUSGOVERNMENTCLOUD":
|
|
// Known environment, no action needed
|
|
default:
|
|
return errors.New("failed to parse --azure-env as a valid target Azure cloud environment")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func getSubFromAzDir(root string) (uuid.UUID, error) {
|
|
subConfig, err := ini.Load(filepath.Join(root, "clouds.config"))
|
|
if err != nil {
|
|
return uuid.UUID{}, errors.Wrap(err, "error decoding cloud subscription config")
|
|
}
|
|
|
|
cloudConfig, err := ini.Load(filepath.Join(root, "config"))
|
|
if err != nil {
|
|
return uuid.UUID{}, errors.Wrap(err, "error decoding cloud config")
|
|
}
|
|
|
|
cloud := getSelectedCloudFromAzConfig(cloudConfig)
|
|
return getCloudSubFromAzConfig(cloud, subConfig)
|
|
}
|
|
|
|
func getSelectedCloudFromAzConfig(f *ini.File) string {
|
|
selectedCloud := "AzureCloud"
|
|
if cloud, err := f.GetSection("cloud"); err == nil {
|
|
if name, err := cloud.GetKey("name"); err == nil {
|
|
if s := name.String(); s != "" {
|
|
selectedCloud = s
|
|
}
|
|
}
|
|
}
|
|
return selectedCloud
|
|
}
|
|
|
|
func getCloudSubFromAzConfig(cloud string, f *ini.File) (uuid.UUID, error) {
|
|
cfg, err := f.GetSection(cloud)
|
|
if err != nil {
|
|
return uuid.UUID{}, errors.New("could not find user defined subscription id")
|
|
}
|
|
sub, err := cfg.GetKey("subscription")
|
|
if err != nil {
|
|
return uuid.UUID{}, errors.Wrap(err, "error reading subscription id from cloud config")
|
|
}
|
|
return uuid.Parse(sub.String())
|
|
}
|
|
|
|
func (authArgs *authArgs) getClient(env *api.Environment) (armhelpers.AKSEngineClient, error) {
|
|
var cc cloud.Configuration
|
|
switch authArgs.RawAzureEnvironment {
|
|
case api.AzureUSGovernmentCloud:
|
|
cc = cloud.AzureGovernment
|
|
case api.AzureChinaCloud:
|
|
cc = cloud.AzureChina
|
|
default:
|
|
cc = cloud.AzurePublic
|
|
}
|
|
if authArgs.isAzureStackCloud() {
|
|
if env == nil {
|
|
return nil, errors.New("failed to get azure stack cloud client, API model Properties.CustomCloudProfile.Environment cannot be nil")
|
|
}
|
|
cc = cloud.Configuration{
|
|
ActiveDirectoryAuthorityHost: env.ActiveDirectoryEndpoint,
|
|
Services: map[cloud.ServiceName]cloud.ServiceConfiguration{
|
|
cloud.ResourceManager: {
|
|
Audience: env.ServiceManagementEndpoint,
|
|
Endpoint: env.ResourceManagerEndpoint,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
credential, err := authArgs.getCredentials(cc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return authArgs.getAzureClient(credential, cc)
|
|
}
|
|
|
|
func (authArgs *authArgs) getCredentials(env cloud.Configuration) (azcore.TokenCredential, error) {
|
|
if !authArgs.isAzureStackCloud() {
|
|
return armhelpers.NewDefaultCredential(env, authArgs.SubscriptionID.String())
|
|
}
|
|
switch authArgs.AuthMethod {
|
|
case "client_secret":
|
|
if authArgs.IdentitySystem == "azure_ad" {
|
|
return armhelpers.NewClientSecretCredential(env, authArgs.SubscriptionID.String(), authArgs.ClientID.String(), authArgs.ClientSecret)
|
|
} else if authArgs.IdentitySystem == "adfs" {
|
|
return armhelpers.NewClientSecretCredentialExternalTenant(env, authArgs.SubscriptionID.String(), authArgs.ClientID.String(), authArgs.ClientSecret)
|
|
} else {
|
|
return nil, errors.Errorf("--auth-method: ERROR: method unsupported. method=%q identitysystem=%q", authArgs.AuthMethod, authArgs.IdentitySystem)
|
|
}
|
|
case "client_certificate":
|
|
if authArgs.IdentitySystem == "azure_ad" {
|
|
return armhelpers.NewClientCertificateCredential(env, authArgs.SubscriptionID.String(), authArgs.ClientID.String(), authArgs.CertificatePath, authArgs.PrivateKeyPath)
|
|
} else if authArgs.IdentitySystem == "adfs" {
|
|
return armhelpers.NewClientCertificateCredentialExternalTenant(env, authArgs.SubscriptionID.String(), authArgs.ClientID.String(), authArgs.CertificatePath, authArgs.PrivateKeyPath)
|
|
}
|
|
fallthrough
|
|
default:
|
|
return nil, errors.Errorf("--auth-method: ERROR: method unsupported. method=%q identitysystem=%q", authArgs.AuthMethod, authArgs.IdentitySystem)
|
|
}
|
|
}
|
|
|
|
func (authArgs *authArgs) getAzureClient(credential azcore.TokenCredential, env cloud.Configuration) (armhelpers.AKSEngineClient, error) {
|
|
client, err := armhelpers.NewAzureClient(authArgs.SubscriptionID.String(), credential, &arm.ClientOptions{
|
|
ClientOptions: azcore.ClientOptions{
|
|
Cloud: env,
|
|
},
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = client.EnsureProvidersRegistered(authArgs.SubscriptionID.String())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
client.AddAcceptLanguages([]string{authArgs.language})
|
|
return client, nil
|
|
}
|
|
|
|
func getCompletionCmd(root *cobra.Command) *cobra.Command {
|
|
var completionCmd = &cobra.Command{
|
|
Use: "completion",
|
|
Short: "Generates bash completion scripts",
|
|
Long: `To load completion run
|
|
|
|
source <(aks-engine-azurestack completion)
|
|
|
|
To configure your bash shell to load completions for each session, add this to your bashrc
|
|
|
|
# ~/.bashrc or ~/.profile
|
|
source <(aks-engine-azurestack completion)
|
|
`,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
return root.GenBashCompletion(os.Stdout)
|
|
},
|
|
}
|
|
return completionCmd
|
|
}
|
|
|
|
func writeCustomCloudProfile(cs *api.ContainerService) error {
|
|
|
|
tmpFile, err := os.CreateTemp("", "azurestackcloud.json")
|
|
tmpFileName := tmpFile.Name()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
log.Infoln(fmt.Sprintf("Writing cloud profile to: %s", tmpFileName))
|
|
|
|
// Build content for the file
|
|
content, err := cs.Properties.GetCustomEnvironmentJSON(false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err = os.WriteFile(tmpFileName, []byte(content), os.ModeAppend); err != nil {
|
|
return err
|
|
}
|
|
|
|
os.Setenv("AZURE_ENVIRONMENT_FILEPATH", tmpFileName)
|
|
|
|
return nil
|
|
}
|
|
|
|
func getKubeClient(cs *api.ContainerService, interval, timeout time.Duration) (kubernetes.Client, error) {
|
|
kubeconfig, err := engine.GenerateKubeConfig(cs.Properties, cs.Location)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "generating kubeconfig")
|
|
}
|
|
var az *armhelpers.AzureClient
|
|
client, err := az.GetKubernetesClient("", kubeconfig, interval, timeout)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return client, nil
|
|
}
|
|
|
|
func writeArtifacts(outputDirectory string, cs *api.ContainerService, apiVersion string, translator *i18n.Translator) error {
|
|
ctx := engine.Context{Translator: translator}
|
|
tplgen, err := engine.InitializeTemplateGenerator(ctx)
|
|
if err != nil {
|
|
return errors.Wrap(err, "initializing template generator")
|
|
}
|
|
tpl, params, err := tplgen.GenerateTemplateV2(cs, engine.DefaultGeneratorCode, BuildTag)
|
|
if err != nil {
|
|
return errors.Wrap(err, "generating template")
|
|
}
|
|
if tpl, err = transform.PrettyPrintArmTemplate(tpl); err != nil {
|
|
return errors.Wrap(err, "pretty-printing template")
|
|
}
|
|
if params, err = transform.BuildAzureParametersFile(params); err != nil {
|
|
return errors.Wrap(err, "pretty-printing template parameters")
|
|
}
|
|
w := &engine.ArtifactWriter{Translator: translator}
|
|
return w.WriteTLSArtifacts(cs, apiVersion, tpl, params, outputDirectory, true, false)
|
|
}
|