diff --git a/cmd/deploy.go b/cmd/deploy.go index db24a7a6f0..1ff4b0bc7a 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -147,9 +147,11 @@ func (dc *deployCmd) validate(cmd *cobra.Command, args []string) error { func autofillApimodel(dc *deployCmd) { var err error - if dc.containerService.Properties.LinuxProfile.AdminUsername == "" { - log.Warnf("apimodel: no linuxProfile.adminUsername was specified. Will use 'azureuser'.") - dc.containerService.Properties.LinuxProfile.AdminUsername = "azureuser" + if dc.containerService.Properties.LinuxProfile != nil { + if dc.containerService.Properties.LinuxProfile.AdminUsername == "" { + log.Warnf("apimodel: no linuxProfile.adminUsername was specified. Will use 'azureuser'.") + dc.containerService.Properties.LinuxProfile.AdminUsername = "azureuser" + } } if dc.dnsPrefix != "" && dc.containerService.Properties.MasterProfile.DNSPrefix != "" { @@ -181,15 +183,13 @@ func autofillApimodel(dc *deployCmd) { } } - if dc.containerService.Properties.LinuxProfile.SSH.PublicKeys == nil || + if dc.containerService.Properties.LinuxProfile != nil && (dc.containerService.Properties.LinuxProfile.SSH.PublicKeys == nil || len(dc.containerService.Properties.LinuxProfile.SSH.PublicKeys) == 0 || - dc.containerService.Properties.LinuxProfile.SSH.PublicKeys[0].KeyData == "" { - creator := &acsengine.SSHCreator{ - Translator: &i18n.Translator{ - Locale: dc.locale, - }, + dc.containerService.Properties.LinuxProfile.SSH.PublicKeys[0].KeyData == "") { + translator := &i18n.Translator{ + Locale: dc.locale, } - _, publicKey, err := creator.CreateSaveSSH(dc.containerService.Properties.LinuxProfile.AdminUsername, dc.outputDirectory) + _, publicKey, err := acsengine.CreateSaveSSH(dc.containerService.Properties.LinuxProfile.AdminUsername, dc.outputDirectory, translator) if err != nil { log.Fatal("Failed to generate SSH Key") } diff --git a/pkg/acsengine/ssh.go b/pkg/acsengine/ssh.go index 2a955311d9..bcee867057 100644 --- a/pkg/acsengine/ssh.go +++ b/pkg/acsengine/ssh.go @@ -4,26 +4,15 @@ import ( "crypto/rand" "crypto/rsa" "fmt" - "io" + "github.com/Azure/acs-engine/pkg/helpers" "github.com/Azure/acs-engine/pkg/i18n" - log "github.com/sirupsen/logrus" - "golang.org/x/crypto/ssh" -) - -// SSHCreator represents the object that creates SSH key pair -type SSHCreator struct { - Translator *i18n.Translator -} - -const ( - // SSHKeySize is the size (in bytes) of SSH key to create - SSHKeySize = 4096 ) // CreateSaveSSH generates and stashes an SSH key pair. -func (s *SSHCreator) CreateSaveSSH(username, outputDirectory string) (privateKey *rsa.PrivateKey, publicKeyString string, err error) { - privateKey, publicKeyString, err = s.CreateSSH(rand.Reader) +func CreateSaveSSH(username, outputDirectory string, s *i18n.Translator) (privateKey *rsa.PrivateKey, publicKeyString string, err error) { + + privateKey, publicKeyString, err = helpers.CreateSSH(rand.Reader, s) if err != nil { return nil, "", err } @@ -31,7 +20,7 @@ func (s *SSHCreator) CreateSaveSSH(username, outputDirectory string) (privateKey privateKeyPem := privateKeyToPem(privateKey) f := &FileSaver{ - Translator: s.Translator, + Translator: s, } err = f.SaveFile(outputDirectory, fmt.Sprintf("%s_rsa", username), privateKeyPem) @@ -41,22 +30,3 @@ func (s *SSHCreator) CreateSaveSSH(username, outputDirectory string) (privateKey return privateKey, publicKeyString, nil } - -// CreateSSH creates an SSH key pair. -func (s *SSHCreator) CreateSSH(rg io.Reader) (privateKey *rsa.PrivateKey, publicKeyString string, err error) { - log.Debugf("ssh: generating %dbit rsa key", SSHKeySize) - privateKey, err = rsa.GenerateKey(rg, SSHKeySize) - if err != nil { - return nil, "", s.Translator.Errorf("failed to generate private key for ssh: %q", err) - } - - publicKey := privateKey.PublicKey - sshPublicKey, err := ssh.NewPublicKey(&publicKey) - if err != nil { - return nil, "", s.Translator.Errorf("failed to create openssh public key string: %q", err) - } - authorizedKeyBytes := ssh.MarshalAuthorizedKey(sshPublicKey) - authorizedKey := string(authorizedKeyBytes) - - return privateKey, authorizedKey, nil -} diff --git a/pkg/acsengine/ssh_test.go b/pkg/acsengine/ssh_test.go index 4c6a1c740a..fe4f82f96a 100644 --- a/pkg/acsengine/ssh_test.go +++ b/pkg/acsengine/ssh_test.go @@ -3,6 +3,10 @@ package acsengine import ( "math/rand" "testing" + + "github.com/Azure/acs-engine/pkg/helpers" + + "github.com/Azure/acs-engine/pkg/i18n" ) func TestCreateSSH(t *testing.T) { @@ -63,11 +67,11 @@ EPDesL0rH+3s1CKpgkhYdbJ675GFoGoq+X21QaqsdvoXmmuJF9qq9Tq+JaWloUNq -----END RSA PRIVATE KEY----- ` - creator := &SSHCreator{ - Translator: nil, + translator := &i18n.Translator{ + Locale: nil, } - privateKey, publicKey, err := creator.CreateSSH(rg) + privateKey, publicKey, err := helpers.CreateSSH(rg, translator) if err != nil { t.Fatalf("failed to generate SSH: %s", err) } diff --git a/pkg/acsengine/testdata/agentPoolOnly/v20180331/agents.json b/pkg/acsengine/testdata/agentPoolOnly/v20180331/agents.json new file mode 100644 index 0000000000..bb4edfb639 --- /dev/null +++ b/pkg/acsengine/testdata/agentPoolOnly/v20180331/agents.json @@ -0,0 +1,19 @@ +{ + "apiVersion": "2018-03-31", + "properties": { + "dnsPrefix": "agents006", + "fqdn": "agents006.azmk8s.io", + "kubernetesVersion": "1.8.7", + "agentPoolProfiles": [ + { + "name": "agentpool1", + "count": 1, + "vmSize": "Standard_D2_v2" + } + ], + "servicePrincipalProfile": { + "clientID": "ServicePrincipalClientID", + "secret": "myServicePrincipalClientSecret" + } + } +} diff --git a/pkg/api/agentPoolOnlyApi/v20180331/validate.go b/pkg/api/agentPoolOnlyApi/v20180331/validate.go index 61d2ec171b..9176143639 100644 --- a/pkg/api/agentPoolOnlyApi/v20180331/validate.go +++ b/pkg/api/agentPoolOnlyApi/v20180331/validate.go @@ -84,9 +84,12 @@ func (a *Properties) Validate() error { } } - if e := a.LinuxProfile.Validate(); e != nil { - return e + if a.LinuxProfile != nil { + if e := a.LinuxProfile.Validate(); e != nil { + return e + } } + if e := validateVNET(a); e != nil { return e } diff --git a/pkg/api/apiloader.go b/pkg/api/apiloader.go index f381b0c9ae..02bade36e1 100644 --- a/pkg/api/apiloader.go +++ b/pkg/api/apiloader.go @@ -1,6 +1,7 @@ package api import ( + "crypto/rand" "encoding/json" "io/ioutil" "reflect" @@ -45,7 +46,7 @@ func (a *Apiloader) DeserializeContainerService(contents []byte, validate, isUpd if service == nil || err != nil { if isAgentPoolOnlyClusterJSON(contents) { log.Info("No masterProfile: interpreting API model as agent pool only") - service, err := a.LoadContainerServiceForAgentPoolOnlyCluster(contents, version, validate, isUpdate, "") + service, _, err := a.LoadContainerServiceForAgentPoolOnlyCluster(contents, version, validate, isUpdate, "") if service == nil || err != nil { log.Infof("Error returned by LoadContainerServiceForAgentPoolOnlyCluster: %+v", err) } @@ -185,59 +186,72 @@ func (a *Apiloader) LoadContainerService( } // LoadContainerServiceForAgentPoolOnlyCluster loads an ACS Cluster API Model, validates it, and returns the unversioned representation -func (a *Apiloader) LoadContainerServiceForAgentPoolOnlyCluster(contents []byte, version string, validate, isUpdate bool, defaultKubernetesVersion string) (*ContainerService, error) { +func (a *Apiloader) LoadContainerServiceForAgentPoolOnlyCluster(contents []byte, version string, validate, isUpdate bool, defaultKubernetesVersion string) (*ContainerService, bool, error) { + IsSSHAutoGenerated := false switch version { case v20170831.APIVersion: managedCluster := &v20170831.ManagedCluster{} if e := json.Unmarshal(contents, &managedCluster); e != nil { - return nil, e + return nil, IsSSHAutoGenerated, e } // verify orchestrator version if len(managedCluster.Properties.KubernetesVersion) > 0 && !common.AllKubernetesSupportedVersions[managedCluster.Properties.KubernetesVersion] { - return nil, a.Translator.Errorf("The selected orchestrator version '%s' is not supported", managedCluster.Properties.KubernetesVersion) + return nil, IsSSHAutoGenerated, a.Translator.Errorf("The selected orchestrator version '%s' is not supported", managedCluster.Properties.KubernetesVersion) } // use defaultKubernetesVersion arg if no version was supplied in the request contents if managedCluster.Properties.KubernetesVersion == "" && defaultKubernetesVersion != "" { if !common.AllKubernetesSupportedVersions[defaultKubernetesVersion] { - return nil, a.Translator.Errorf("The selected orchestrator version '%s' is not supported", defaultKubernetesVersion) + return nil, IsSSHAutoGenerated, a.Translator.Errorf("The selected orchestrator version '%s' is not supported", defaultKubernetesVersion) } managedCluster.Properties.KubernetesVersion = defaultKubernetesVersion } if e := managedCluster.Properties.Validate(); validate && e != nil { - return nil, e + return nil, IsSSHAutoGenerated, e } - return ConvertV20170831AgentPoolOnly(managedCluster), nil + return ConvertV20170831AgentPoolOnly(managedCluster), false, nil case v20180331.APIVersion: managedCluster := &v20180331.ManagedCluster{} if e := json.Unmarshal(contents, &managedCluster); e != nil { - return nil, e + return nil, IsSSHAutoGenerated, e } // verify orchestrator version if len(managedCluster.Properties.KubernetesVersion) > 0 && !common.AllKubernetesSupportedVersions[managedCluster.Properties.KubernetesVersion] { - return nil, a.Translator.Errorf("The selected orchestrator version '%s' is not supported", managedCluster.Properties.KubernetesVersion) + return nil, IsSSHAutoGenerated, a.Translator.Errorf("The selected orchestrator version '%s' is not supported", managedCluster.Properties.KubernetesVersion) } // use defaultKubernetesVersion arg if no version was supplied in the request contents if managedCluster.Properties.KubernetesVersion == "" && defaultKubernetesVersion != "" { if !common.AllKubernetesSupportedVersions[defaultKubernetesVersion] { - return nil, a.Translator.Errorf("The selected orchestrator version '%s' is not supported", defaultKubernetesVersion) + return nil, IsSSHAutoGenerated, a.Translator.Errorf("The selected orchestrator version '%s' is not supported", defaultKubernetesVersion) } managedCluster.Properties.KubernetesVersion = defaultKubernetesVersion } if e := managedCluster.Properties.Validate(); validate && e != nil { - return nil, e + return nil, IsSSHAutoGenerated, e } - return ConvertV20180331AgentPoolOnly(managedCluster), nil + + if managedCluster.Properties.LinuxProfile == nil { + linuxProfile := &v20180331.LinuxProfile{} + linuxProfile.AdminUsername = "azureuser" + _, publicKey, err := helpers.CreateSSH(rand.Reader, a.Translator) + if err != nil { + return nil, IsSSHAutoGenerated, err + } + linuxProfile.SSH.PublicKeys = []v20180331.PublicKey{{KeyData: publicKey}} + managedCluster.Properties.LinuxProfile = linuxProfile + IsSSHAutoGenerated = true + } + return ConvertV20180331AgentPoolOnly(managedCluster), IsSSHAutoGenerated, nil case apvlabs.APIVersion: managedCluster := &apvlabs.ManagedCluster{} if e := json.Unmarshal(contents, &managedCluster); e != nil { - return nil, e + return nil, IsSSHAutoGenerated, e } if e := managedCluster.Properties.Validate(); validate && e != nil { - return nil, e + return nil, IsSSHAutoGenerated, e } - return ConvertVLabsAgentPoolOnly(managedCluster), nil + return ConvertVLabsAgentPoolOnly(managedCluster), IsSSHAutoGenerated, nil default: - return nil, a.Translator.Errorf("unrecognized APIVersion in LoadContainerServiceForAgentPoolOnlyCluster '%s'", version) + return nil, IsSSHAutoGenerated, a.Translator.Errorf("unrecognized APIVersion in LoadContainerServiceForAgentPoolOnlyCluster '%s'", version) } } diff --git a/pkg/api/convertertoagentpoolonlyapi.go b/pkg/api/convertertoagentpoolonlyapi.go index ff0bd60670..65935f1b74 100644 --- a/pkg/api/convertertoagentpoolonlyapi.go +++ b/pkg/api/convertertoagentpoolonlyapi.go @@ -317,6 +317,7 @@ func convertV20180331AgentPoolOnlyProperties(obj *v20180331.Properties) *Propert if obj.LinuxProfile != nil { properties.LinuxProfile = convertV20180331AgentPoolOnlyLinuxProfile(obj.LinuxProfile) } + if obj.WindowsProfile != nil { properties.WindowsProfile = convertV20180331AgentPoolOnlyWindowsProfile(obj.WindowsProfile) } diff --git a/pkg/helpers/helpers.go b/pkg/helpers/helpers.go index 9affc4fac2..2cade4d925 100644 --- a/pkg/helpers/helpers.go +++ b/pkg/helpers/helpers.go @@ -3,7 +3,17 @@ package helpers import ( // "fmt" "bytes" + "crypto/rsa" "encoding/json" + "io" + + "github.com/Azure/acs-engine/pkg/i18n" + "golang.org/x/crypto/ssh" +) + +const ( + // SSHKeySize is the size (in bytes) of SSH key to create + SSHKeySize = 4096 ) // JSONMarshalIndent marshals formatted JSON w/ optional SetEscapeHTML @@ -46,3 +56,21 @@ func PointerToBool(b bool) *bool { p := b return &p } + +// CreateSSH creates an SSH key pair. +func CreateSSH(rg io.Reader, s *i18n.Translator) (privateKey *rsa.PrivateKey, publicKeyString string, err error) { + privateKey, err = rsa.GenerateKey(rg, SSHKeySize) + if err != nil { + return nil, "", s.Errorf("failed to generate private key for ssh: %q", err) + } + + publicKey := privateKey.PublicKey + sshPublicKey, err := ssh.NewPublicKey(&publicKey) + if err != nil { + return nil, "", s.Errorf("failed to create openssh public key string: %q", err) + } + authorizedKeyBytes := ssh.MarshalAuthorizedKey(sshPublicKey) + authorizedKey := string(authorizedKeyBytes) + + return privateKey, authorizedKey, nil +}