github.com/jenkins-x/test-infra@v0.0.7/kubetest/azure.go (about) 1 /* 2 Copyright 2018 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package main 18 19 import ( 20 "context" 21 "encoding/json" 22 "errors" 23 "flag" 24 "fmt" 25 "io/ioutil" 26 "log" 27 "net/url" 28 "os" 29 "os/exec" 30 "path" 31 "path/filepath" 32 "strings" 33 "time" 34 35 "github.com/pelletier/go-toml" 36 "k8s.io/test-infra/kubetest/util" 37 38 "github.com/Azure/azure-storage-blob-go/2016-05-31/azblob" 39 "github.com/Azure/go-autorest/autorest/azure" 40 "github.com/satori/go.uuid" 41 ) 42 43 var ( 44 // azure specific flags 45 acsResourceName = flag.String("acsengine-resource-name", "", "Azure Resource Name") 46 acsResourceGroupName = flag.String("acsengine-resourcegroup-name", "", "Azure Resource Group Name") 47 acsLocation = flag.String("acsengine-location", "westus2", "Azure ACS location") 48 acsMasterVmSize = flag.String("acsengine-mastervmsize", "Standard_D2s_v3", "Azure Master VM size") 49 acsAgentVmSize = flag.String("acsengine-agentvmsize", "Standard_D2s_v3", "Azure Agent VM size") 50 acsAdminUsername = flag.String("acsengine-admin-username", "", "Admin username") 51 acsAdminPassword = flag.String("acsengine-admin-password", "", "Admin password") 52 acsAgentPoolCount = flag.Int("acsengine-agentpoolcount", 2, "Azure Agent Pool Count") 53 acsAgentOSType = flag.String("acsengine-agentOSType", "Windows", "OS Type of Agent Nodes. Options: Windows|Linux") 54 acsTemplatePath = flag.String("acsengine-template", "", "Azure Template Name") 55 acsDnsPrefix = flag.String("acsengine-dnsprefix", "", "Azure K8s Master DNS Prefix") 56 acsEngineURL = flag.String("acsengine-download-url", "", "Download URL for ACS engine") 57 acsEngineMD5 = flag.String("acsengine-md5-sum", "", "Checksum for acs engine download") 58 acsSSHPublicKeyPath = flag.String("acsengine-public-key", "", "Path to SSH Public Key") 59 acsWinBinariesURL = flag.String("acsengine-win-binaries-url", "", "Path to get the zip file containing kubelet and kubeproxy binaries for Windows") 60 acsHyperKubeURL = flag.String("acsengine-hyperkube-url", "", "Path to get the kyberkube image for the deployment") 61 acsCredentialsFile = flag.String("acsengine-creds", "", "Path to credential file for Azure") 62 acsOrchestratorRelease = flag.String("acsengine-orchestratorRelease", "1.11", "Orchestrator Profile for acs-engine") 63 acsWinZipBuildScript = flag.String("acsengine-winZipBuildScript", "https://raw.githubusercontent.com/Azure/acs-engine/master/scripts/build-windows-k8s.sh", "Build script to create custom zip containing win binaries for acs-engine") 64 acsNetworkPlugin = flag.String("acsengine-networkPlugin", "azure", "Network pluging to use with acs-engine") 65 ) 66 67 type Creds struct { 68 ClientID string 69 ClientSecret string 70 TenantID string 71 SubscriptionID string 72 StorageAccountName string 73 StorageAccountKey string 74 } 75 76 type Config struct { 77 Creds Creds 78 } 79 80 type Cluster struct { 81 ctx context.Context 82 credentials *Creds 83 location string 84 resourceGroup string 85 name string 86 apiModelPath string 87 dnsPrefix string 88 templateJSON map[string]interface{} 89 parametersJSON map[string]interface{} 90 outputDir string 91 sshPublicKey string 92 adminUsername string 93 adminPassword string 94 masterVMSize string 95 agentVMSize string 96 acsCustomHyperKubeURL string 97 acsCustomWinBinariesURL string 98 acsEngineBinaryPath string 99 azureClient *AzureClient 100 } 101 102 func (c *Cluster) getAzCredentials() error { 103 content, err := ioutil.ReadFile(*acsCredentialsFile) 104 log.Printf("Reading credentials file %v", *acsCredentialsFile) 105 if err != nil { 106 return fmt.Errorf("error reading credentials file %v %v", *acsCredentialsFile, err) 107 } 108 config := Config{} 109 err = toml.Unmarshal(content, &config) 110 c.credentials = &config.Creds 111 if err != nil { 112 return fmt.Errorf("error parsing credentials file %v %v", *acsCredentialsFile, err) 113 } 114 return nil 115 } 116 117 func checkParams() error { 118 if *acsCredentialsFile == "" { 119 return fmt.Errorf("no credentials file path specified") 120 } 121 if *acsResourceName == "" { 122 *acsResourceName = "kubetest-" + uuid.NewV1().String() 123 } 124 if *acsResourceGroupName == "" { 125 *acsResourceGroupName = *acsResourceName + "-rg" 126 } 127 if *acsDnsPrefix == "" { 128 *acsDnsPrefix = *acsResourceName 129 } 130 if *acsSSHPublicKeyPath == "" { 131 *acsSSHPublicKeyPath = os.Getenv("HOME") + "/.ssh/id_rsa.pub" 132 } 133 if *acsAdminUsername == "" { 134 return fmt.Errorf("error parsing flags. No admin username specified") 135 } 136 if *acsAdminPassword == "" { 137 return fmt.Errorf("error parting flags. No admin password specified.") 138 } 139 return nil 140 } 141 142 func newAcsEngine() (*Cluster, error) { 143 if err := checkParams(); err != nil { 144 return nil, fmt.Errorf("error creating Azure K8S cluster: %v", err) 145 } 146 147 tempdir, _ := ioutil.TempDir(os.Getenv("HOME"), "acs") 148 sshKey, err := ioutil.ReadFile(*acsSSHPublicKeyPath) 149 if err != nil { 150 return nil, fmt.Errorf("error reading SSH Key %v %v", *acsSSHPublicKeyPath, err) 151 } 152 c := Cluster{ 153 ctx: context.Background(), 154 apiModelPath: *acsTemplatePath, 155 name: *acsResourceName, 156 dnsPrefix: *acsDnsPrefix, 157 location: *acsLocation, 158 resourceGroup: *acsResourceGroupName, 159 outputDir: tempdir, 160 sshPublicKey: fmt.Sprintf("%s", sshKey), 161 credentials: &Creds{}, 162 acsCustomHyperKubeURL: "", 163 acsCustomWinBinariesURL: "", 164 acsEngineBinaryPath: "acs-engine", // use the one in path by default 165 } 166 c.getAzCredentials() 167 err = c.getARMClient(c.ctx) 168 if err != nil { 169 return nil, fmt.Errorf("failed to generate ARM client: %v", err) 170 } 171 // like kops and gke set KUBERNETES_CONFORMANCE_TEST so the auth is picked up 172 // from kubectl instead of bash inference. 173 if err := os.Setenv("KUBERNETES_CONFORMANCE_TEST", "yes"); err != nil { 174 return nil, err 175 } 176 177 return &c, nil 178 } 179 180 func (c *Cluster) generateTemplate() error { 181 v := &AcsEngineAPIModel{ 182 APIVersion: "vlabs", 183 Location: c.location, 184 Name: c.name, 185 Tags: map[string]string{ 186 "date": time.Now().String(), 187 }, 188 Properties: &Properties{ 189 OrchestratorProfile: &OrchestratorProfile{ 190 OrchestratorType: "Kubernetes", 191 OrchestratorRelease: *acsOrchestratorRelease, 192 KubernetesConfig: &KubernetesConfig{ 193 NetworkPlugin: *acsNetworkPlugin, 194 }, 195 }, 196 MasterProfile: &MasterProfile{ 197 Count: 1, 198 DNSPrefix: c.dnsPrefix, 199 VMSize: *acsMasterVmSize, 200 IPAddressCount: 200, 201 Extensions: []map[string]string{ 202 { 203 "name": "win-e2e-master-extension", 204 }, 205 }, 206 }, 207 AgentPoolProfiles: []*AgentPoolProfile{ 208 { 209 Name: "agentpool0", 210 VMSize: *acsAgentVmSize, 211 Count: *acsAgentPoolCount, 212 OSType: *acsAgentOSType, 213 AvailabilityProfile: "AvailabilitySet", 214 IPAddressCount: 200, 215 PreProvisionExtension: map[string]string{ 216 "name": "node_setup", 217 "singleOrAll": "all", 218 }, 219 Extensions: []map[string]string{ 220 { 221 "name": "winrm", 222 }, 223 }, 224 }, 225 }, 226 LinuxProfile: &LinuxProfile{ 227 AdminUsername: *acsAdminUsername, 228 SSHKeys: &SSH{ 229 PublicKeys: []PublicKey{{ 230 KeyData: c.sshPublicKey, 231 }, 232 }, 233 }, 234 }, 235 WindowsProfile: &WindowsProfile{ 236 AdminUsername: *acsAdminUsername, 237 AdminPassword: *acsAdminPassword, 238 }, 239 ServicePrincipalProfile: &ServicePrincipalProfile{ 240 ClientID: c.credentials.ClientID, 241 Secret: c.credentials.ClientSecret, 242 }, 243 ExtensionProfiles: []map[string]string{ 244 { 245 /* Agent node preprovision template 246 Used to setup windows node for e2e tests: i.e creates c:\tmp folder that some 247 tests expect 248 249 Extension source: 250 https://github.com/e2e-win/e2e-win-prow-deployment/blob/master/extensions/agent_preprovision_extension/node_setup.ps1 251 */ 252 "name": "node_setup", 253 "version": "v1", 254 "rootURL": "https://k8swin.blob.core.windows.net/k8s-windows/preprovision_extensions/", 255 "script": "node_setup.ps1", 256 }, 257 { 258 /* 259 WinRM template used for accessing windows nodes for debugging and logs collection. 260 */ 261 "name": "winrm", 262 "version": "v1", 263 }, 264 { 265 /* 266 Master node custom script. Runs after provisioning. 267 268 Taints master node as not schedulable for tests. As this is the only 269 Linux node in the deployment, we need to wait until kube-system pods 270 start before tainting master 271 272 Extension source: 273 https://github.com/e2e-win/e2e-win-prow-deployment/blob/master/extensions/master_extension/win-e2e-master-extension.sh 274 */ 275 "name": "win-e2e-master-extension", 276 "version": "v1", 277 "extensionParameters": "parameters", 278 "rootURL": "https://k8swin.blob.core.windows.net/k8s-windows/extensions/", 279 "script": "win-e2e-master-extension.sh", 280 }, 281 }, 282 }, 283 } 284 if *acsHyperKubeURL != "" { 285 v.Properties.OrchestratorProfile.KubernetesConfig.CustomHyperkubeImage = *acsHyperKubeURL 286 } else if c.acsCustomHyperKubeURL != "" { 287 v.Properties.OrchestratorProfile.KubernetesConfig.CustomHyperkubeImage = c.acsCustomHyperKubeURL 288 } 289 290 if *acsWinBinariesURL != "" { 291 v.Properties.OrchestratorProfile.KubernetesConfig.CustomWindowsPackageURL = *acsWinBinariesURL 292 } else if c.acsCustomWinBinariesURL != "" { 293 v.Properties.OrchestratorProfile.KubernetesConfig.CustomWindowsPackageURL = c.acsCustomWinBinariesURL 294 } 295 apiModel, _ := json.Marshal(v) 296 c.apiModelPath = path.Join(c.outputDir, "kubernetes.json") 297 err := ioutil.WriteFile(c.apiModelPath, apiModel, 0644) 298 if err != nil { 299 return fmt.Errorf("cannot write apimodel to file: %v", err) 300 } 301 return nil 302 } 303 304 func (c *Cluster) getAcsEngine(retry int) error { 305 downloadPath := path.Join(os.Getenv("HOME"), "acs-engine.tar.gz") 306 f, err := os.Create(downloadPath) 307 if err != nil { 308 return err 309 } 310 defer f.Close() 311 312 for i := 0; i < retry; i++ { 313 log.Printf("downloading %v from %v.", downloadPath, *acsEngineURL) 314 if err := httpRead(*acsEngineURL, f); err == nil { 315 break 316 } 317 err = fmt.Errorf("url=%s failed get %v: %v.", *acsEngineURL, downloadPath, err) 318 if i == retry-1 { 319 return err 320 } 321 log.Println(err) 322 sleep(time.Duration(i) * time.Second) 323 } 324 325 f.Close() 326 if *acsEngineMD5 != "" { 327 o, err := control.Output(exec.Command("md5sum", f.Name())) 328 if err != nil { 329 return err 330 } 331 if strings.Split(string(o), " ")[0] != *acsEngineMD5 { 332 return fmt.Errorf("wrong md5 sum for acs-engine.") 333 } 334 } 335 336 cwd, err := os.Getwd() 337 if err != nil { 338 return fmt.Errorf("unable to get current directory: %v .", err) 339 } 340 log.Printf("Extracting tar file %v into directory %v .", f.Name(), cwd) 341 342 if err = control.FinishRunning(exec.Command("tar", "-xzf", f.Name(), "--strip", "1")); err != nil { 343 return err 344 } 345 c.acsEngineBinaryPath = path.Join(cwd, "acs-engine") 346 return nil 347 348 } 349 350 func (c Cluster) generateARMTemplates() error { 351 if err := control.FinishRunning(exec.Command(c.acsEngineBinaryPath, "generate", c.apiModelPath, "--output-directory", c.outputDir)); err != nil { 352 return fmt.Errorf("failed to generate ARM templates: %v.", err) 353 } 354 return nil 355 } 356 357 func (c *Cluster) loadARMTemplates() error { 358 var err error 359 template, err := ioutil.ReadFile(path.Join(c.outputDir, "azuredeploy.json")) 360 if err != nil { 361 return fmt.Errorf("error reading ARM template file: %v.", err) 362 } 363 c.templateJSON = make(map[string]interface{}) 364 err = json.Unmarshal(template, &c.templateJSON) 365 if err != nil { 366 return fmt.Errorf("error unmarshall template %v", err.Error()) 367 } 368 parameters, err := ioutil.ReadFile(path.Join(c.outputDir, "azuredeploy.parameters.json")) 369 if err != nil { 370 return fmt.Errorf("error reading ARM parameters file: %v", err) 371 } 372 c.parametersJSON = make(map[string]interface{}) 373 err = json.Unmarshal(parameters, &c.parametersJSON) 374 if err != nil { 375 return fmt.Errorf("error unmarshall parameters %v", err.Error()) 376 } 377 c.parametersJSON = c.parametersJSON["parameters"].(map[string]interface{}) 378 379 return nil 380 } 381 382 func (c *Cluster) getARMClient(ctx context.Context) error { 383 // instantiate Azure Resource Manager Client 384 env, err := azure.EnvironmentFromName("AzurePublicCloud") 385 var client *AzureClient 386 if client, err = getAzureClient(env, 387 c.credentials.SubscriptionID, 388 c.credentials.ClientID, 389 c.credentials.TenantID, 390 c.credentials.ClientSecret); err != nil { 391 return fmt.Errorf("error trying to get Azure Client: %v", err) 392 } 393 c.azureClient = client 394 return nil 395 } 396 397 func (c *Cluster) createCluster() error { 398 var err error 399 kubecfgDir, _ := ioutil.ReadDir(path.Join(c.outputDir, "kubeconfig")) 400 kubecfg := path.Join(c.outputDir, "kubeconfig", kubecfgDir[0].Name()) 401 log.Printf("Setting kubeconfig env variable: kubeconfig path: %v.", kubecfg) 402 os.Setenv("KUBECONFIG", kubecfg) 403 log.Printf("Creating resource group: %v.", c.resourceGroup) 404 405 log.Printf("Creating Azure resource group: %v for cluster deployment.", c.resourceGroup) 406 _, err = c.azureClient.EnsureResourceGroup(c.ctx, c.resourceGroup, c.location, nil) 407 if err != nil { 408 return fmt.Errorf("could not ensure resource group: %v", err) 409 } 410 log.Printf("Validating deployment ARM templates.") 411 if _, err := c.azureClient.ValidateDeployment( 412 c.ctx, c.resourceGroup, c.name, &c.templateJSON, &c.parametersJSON, 413 ); err != nil { 414 return fmt.Errorf("ARM template invalid: %v", err) 415 } 416 log.Printf("Deploying cluster %v in resource group %v.", c.name, c.resourceGroup) 417 if _, err := c.azureClient.DeployTemplate( 418 c.ctx, c.resourceGroup, c.name, &c.templateJSON, &c.parametersJSON, 419 ); err != nil { 420 return fmt.Errorf("cannot deploy: %v", err) 421 } 422 return nil 423 424 } 425 426 func (c *Cluster) buildHyperKube() error { 427 428 os.Setenv("VERSION", fmt.Sprintf("win-e2e-%v", os.Getenv("BUILD_ID"))) 429 430 cwd, _ := os.Getwd() 431 log.Printf("CWD %v", cwd) 432 log.Printf("Attempt docker gcloud login") 433 prepareDocker := util.K8s("gcloud", "auth", "configure-docker") 434 if err := control.FinishRunning(exec.Command(prepareDocker)); err != nil { 435 return err 436 } 437 pushHyperkube := util.K8s("kubernetes", "hack", "dev-push-hyperkube.sh") 438 if err1 := control.FinishRunning(exec.Command(pushHyperkube)); err1 != nil { 439 return err1 440 } 441 c.acsCustomHyperKubeURL = fmt.Sprintf("%s/hyperkube-amd64:%s", os.Getenv("REGISTRY"), os.Getenv("VERSION")) 442 443 log.Printf("Custom hyperkube url: %v", c.acsCustomHyperKubeURL) 444 return nil 445 } 446 447 func (c *Cluster) uploadZip(zipPath string) error { 448 449 credential := azblob.NewSharedKeyCredential(c.credentials.StorageAccountName, c.credentials.StorageAccountKey) 450 p := azblob.NewPipeline(credential, azblob.PipelineOptions{}) 451 452 var containerName string = os.Getenv("AZ_STORAGE_CONTAINER_NAME") 453 454 URL, _ := url.Parse( 455 fmt.Sprintf("https://%s.blob.core.windows.net/%s", c.credentials.StorageAccountName, containerName)) 456 457 containerURL := azblob.NewContainerURL(*URL, p) 458 file, err := os.Open(zipPath) 459 if err != nil { 460 return fmt.Errorf("failed to open file %v . Error %v", zipPath, err) 461 } 462 blobURL := containerURL.NewBlockBlobURL(filepath.Base(file.Name())) 463 _, err1 := azblob.UploadFileToBlockBlob(context.Background(), file, blobURL, azblob.UploadToBlockBlobOptions{}) 464 file.Close() 465 if err1 != nil { 466 return err1 467 } 468 blobURLString := blobURL.URL() 469 c.acsCustomWinBinariesURL = blobURLString.String() 470 log.Printf("Custom win binaries url: %v", c.acsCustomWinBinariesURL) 471 return nil 472 } 473 474 func getZipBuildScript(buildScriptURL string, retry int) (string, error) { 475 downloadPath := path.Join(os.Getenv("HOME"), "build-win-zip.sh") 476 f, err := os.Create(downloadPath) 477 if err != nil { 478 return "", err 479 } 480 defer f.Close() 481 482 for i := 0; i < retry; i++ { 483 log.Printf("downloading %v from %v.", downloadPath, buildScriptURL) 484 if err := httpRead(buildScriptURL, f); err == nil { 485 break 486 } 487 err = fmt.Errorf("url=%s failed get %v: %v.", buildScriptURL, downloadPath, err) 488 if i == retry-1 { 489 return "", err 490 } 491 log.Println(err) 492 sleep(time.Duration(i) * time.Second) 493 } 494 f.Chmod(0744) 495 return downloadPath, nil 496 } 497 498 func (c *Cluster) buildWinZip() error { 499 500 zipName := fmt.Sprintf("%s.zip", os.Getenv("BUILD_ID")) 501 buildFolder := path.Join(os.Getenv("HOME"), "winbuild") 502 zipPath := path.Join(os.Getenv("HOME"), zipName) 503 log.Printf("Building %s", zipName) 504 buildScriptPath, err := getZipBuildScript(*acsWinZipBuildScript, 2) 505 if err != nil { 506 return err 507 } 508 if err := control.FinishRunning(exec.Command(buildScriptPath, "-u", zipName, "-z", buildFolder)); err != nil { 509 return err 510 } 511 log.Printf("Uploading %s", zipPath) 512 if err := c.uploadZip(zipPath); err != nil { 513 return err 514 } 515 return nil 516 } 517 518 func (c Cluster) Up() error { 519 520 var err error 521 if *acsHyperKubeURL == "" { 522 err = c.buildHyperKube() 523 if err != nil { 524 return fmt.Errorf("error building hyperkube %v", err) 525 } 526 } 527 if *acsWinBinariesURL == "" { 528 err = c.buildWinZip() 529 if err != nil { 530 return fmt.Errorf("error building windowsZipFile %v", err) 531 } 532 } 533 if c.apiModelPath == "" { 534 err = c.generateTemplate() 535 if err != nil { 536 return fmt.Errorf("failed to generate acs-engine apimodel template: %v", err) 537 } 538 } 539 if *acsEngineURL != "" { 540 err = c.getAcsEngine(2) 541 if err != nil { 542 return fmt.Errorf("failed to get ACS Engine binary: %v", err) 543 } 544 } 545 err = c.generateARMTemplates() 546 if err != nil { 547 return fmt.Errorf("failed to generate ARM templates: %v", err) 548 } 549 err = c.loadARMTemplates() 550 if err != nil { 551 return fmt.Errorf("error loading ARM templates: %v", err) 552 } 553 err = c.createCluster() 554 if err != nil { 555 return fmt.Errorf("error creating cluster: %v", err) 556 } 557 return nil 558 } 559 560 func (c Cluster) Down() error { 561 log.Printf("Deleting resource group: %v.", c.resourceGroup) 562 return c.azureClient.DeleteResourceGroup(c.ctx, c.resourceGroup) 563 } 564 565 func (c Cluster) DumpClusterLogs(localPath, gcsPath string) error { 566 return nil 567 } 568 569 func (c Cluster) GetClusterCreated(clusterName string) (time.Time, error) { 570 return time.Time{}, errors.New("not implemented") 571 } 572 573 func (c Cluster) TestSetup() error { 574 575 // Download repo-list that defines repositories for Windows test images. 576 577 downloadUrl, ok := os.LookupEnv("KUBE_TEST_REPO_LIST_DOWNLOAD_LOCATION") 578 if !ok { 579 // Env value for downloadUrl is not set, nothing to do 580 log.Printf("KUBE_TEST_REPO_LIST_DOWNLOAD_LOCATION not set. Using default test image repos.") 581 return nil 582 } 583 584 downloadPath := path.Join(os.Getenv("HOME"), "repo-list") 585 f, err := os.Create(downloadPath) 586 if err != nil { 587 return err 588 } 589 defer f.Close() 590 591 log.Printf("downloading %v from %v.", downloadPath, downloadUrl) 592 err = httpRead(downloadUrl, f) 593 594 if err != nil { 595 return fmt.Errorf("url=%s failed get %v: %v.", downloadUrl, downloadPath, err) 596 } 597 f.Chmod(0744) 598 if err := os.Setenv("KUBE_TEST_REPO_LIST", downloadPath); err != nil { 599 return err 600 } 601 return nil 602 } 603 604 func (c Cluster) IsUp() error { 605 return isUp(c) 606 } 607 608 func (_ Cluster) KubectlCommand() (*exec.Cmd, error) { return nil, nil }