From 49269d0a72169ac104be981a6733985cabd65621 Mon Sep 17 00:00:00 2001 From: Weinong Wang Date: Mon, 12 Mar 2018 20:22:31 +0000 Subject: [PATCH 1/3] added hcp 20180331 api model to support addon --- pkg/api/agentPoolOnlyApi/v20180331/const.go | 31 +++ pkg/api/agentPoolOnlyApi/v20180331/doc.go | 2 + pkg/api/agentPoolOnlyApi/v20180331/types.go | 243 ++++++++++++++++++ .../agentPoolOnlyApi/v20180331/types_test.go | 27 ++ .../agentPoolOnlyApi/v20180331/validate.go | 184 +++++++++++++ pkg/api/apiloader.go | 31 +++ pkg/api/converterfromagentpoolonlyapi.go | 109 ++++++++ pkg/api/converterfromagentpoolonlyapi_test.go | 38 +++ pkg/api/convertertoagentpoolonlyapi.go | 118 +++++++++ pkg/api/convertertoagentpoolonlyapi_test.go | 31 +++ pkg/api/types.go | 16 ++ 11 files changed, 830 insertions(+) create mode 100644 pkg/api/agentPoolOnlyApi/v20180331/const.go create mode 100644 pkg/api/agentPoolOnlyApi/v20180331/doc.go create mode 100644 pkg/api/agentPoolOnlyApi/v20180331/types.go create mode 100644 pkg/api/agentPoolOnlyApi/v20180331/types_test.go create mode 100644 pkg/api/agentPoolOnlyApi/v20180331/validate.go create mode 100644 pkg/api/converterfromagentpoolonlyapi_test.go diff --git a/pkg/api/agentPoolOnlyApi/v20180331/const.go b/pkg/api/agentPoolOnlyApi/v20180331/const.go new file mode 100644 index 0000000000..7ac69c8f06 --- /dev/null +++ b/pkg/api/agentPoolOnlyApi/v20180331/const.go @@ -0,0 +1,31 @@ +package v20180331 + +const ( + // APIVersion is the version of this API + APIVersion = "2018-03-31" +) + +const ( + // Windows OSType + Windows OSType = "Windows" + // Linux OSType + Linux OSType = "Linux" +) + +// validation values +const ( + // MinAgentCount are the minimum number of agents + MinAgentCount = 1 + // MaxAgentCount are the maximum number of agents + MaxAgentCount = 100 + // MinDiskSizeGB specifies the minimum attached disk size + MinDiskSizeGB = 1 + // MaxDiskSizeGB specifies the maximum attached disk size + MaxDiskSizeGB = 1023 +) + +// storage profiles +const ( + // ManagedDisks means that the nodes use managed disks for their os and attached volumes + ManagedDisks = "ManagedDisks" +) diff --git a/pkg/api/agentPoolOnlyApi/v20180331/doc.go b/pkg/api/agentPoolOnlyApi/v20180331/doc.go new file mode 100644 index 0000000000..50fc1ab8da --- /dev/null +++ b/pkg/api/agentPoolOnlyApi/v20180331/doc.go @@ -0,0 +1,2 @@ +// Package v20180331 stores api model for version "2018-03-31" +package v20180331 diff --git a/pkg/api/agentPoolOnlyApi/v20180331/types.go b/pkg/api/agentPoolOnlyApi/v20180331/types.go new file mode 100644 index 0000000000..4da8e82ee5 --- /dev/null +++ b/pkg/api/agentPoolOnlyApi/v20180331/types.go @@ -0,0 +1,243 @@ +package v20180331 + +import "encoding/json" + +// The validate tag is used for validation +// Reference to gopkg.in/go-playground/validator.v9 + +// ResourcePurchasePlan defines resource plan as required by ARM +// for billing purposes. +type ResourcePurchasePlan struct { + Name string `json:"name,omitempty"` + Product string `json:"product,omitempty"` + PromotionCode string `json:"promotionCode,omitempty"` + Publisher string `json:"publisher,omitempty"` +} + +// ManagedCluster complies with the ARM model of +// resource definition in a JSON template. +type ManagedCluster struct { + ID string `json:"id,omitempty"` + Location string `json:"location,omitempty" validate:"required"` + Name string `json:"name,omitempty"` + Plan *ResourcePurchasePlan `json:"plan,omitempty"` + Tags map[string]string `json:"tags,omitempty"` + Type string `json:"type,omitempty"` + + Properties *Properties `json:"properties"` +} + +// Properties represents the ACS cluster definition +type Properties struct { + ProvisioningState ProvisioningState `json:"provisioningState,omitempty"` + KubernetesVersion string `json:"kubernetesVersion"` + DNSPrefix string `json:"dnsPrefix" validate:"required"` + FQDN string `json:"fqdn,omitempty"` + AgentPoolProfiles []*AgentPoolProfile `json:"agentPoolProfiles,omitempty" validate:"dive,required"` + LinuxProfile *LinuxProfile `json:"linuxProfile,omitempty" validate:"required"` + WindowsProfile *WindowsProfile `json:"windowsProfile,omitempty"` + ServicePrincipalProfile *ServicePrincipalProfile `json:"servicePrincipalProfile,omitempty"` + AccessProfiles map[string]AccessProfile `json:"accessProfiles,omitempty"` + AddonProfiles map[string]AddonProfile `json:"addonProfiles,omitempty"` +} + +// AddonProfile represents the collection of addons for managed cluster +type AddonProfile struct { + Enabled bool `json:"enabled"` + Config map[string]string `json:"config"` +} + +// ManagedClusterAccessProfile represents the access profile definition for managed cluster +// The Id captures the Role Name e.g. clusterAdmin, clusterUser +type ManagedClusterAccessProfile struct { + ID string `json:"id,omitempty"` + Location string `json:"location,omitempty" validate:"required"` + Name string `json:"name,omitempty"` + Type string `json:"type,omitempty"` + + Properties *AccessProfile `json:"properties"` +} + +// ServicePrincipalProfile contains the client and secret used by the cluster for Azure Resource CRUD +// The 'Secret' parameter could be either a plain text, or referenced to a secret in a keyvault. +// In the latter case, the format of the parameter's value should be +// "/subscriptions//resourceGroups//providers/Microsoft.KeyVault/vaults//secrets/[/]" +// where: +// is the subscription ID of the keyvault +// is the resource group of the keyvault +// is the name of the keyvault +// is the name of the secret. +// (optional) is the version of the secret (default: the latest version) +type ServicePrincipalProfile struct { + ClientID string `json:"clientId,omitempty" validate:"required"` + Secret string `json:"secret,omitempty"` +} + +// LinuxProfile represents the Linux configuration passed to the cluster +type LinuxProfile struct { + AdminUsername string `json:"adminUsername" validate:"required"` + + SSH struct { + PublicKeys []PublicKey `json:"publicKeys" validate:"required,len=1"` + } `json:"ssh" validate:"required"` +} + +// PublicKey represents an SSH key for LinuxProfile +type PublicKey struct { + KeyData string `json:"keyData"` +} + +// WindowsProfile represents the Windows configuration passed to the cluster +type WindowsProfile struct { + AdminUsername string `json:"adminUsername,omitempty" validate:"required"` + AdminPassword string `json:"adminPassword,omitempty"` +} + +// ProvisioningState represents the current state of container service resource. +type ProvisioningState string + +const ( + // Creating means ContainerService resource is being created. + Creating ProvisioningState = "Creating" + // Updating means an existing ContainerService resource is being updated + Updating ProvisioningState = "Updating" + // Failed means resource is in failed state + Failed ProvisioningState = "Failed" + // Succeeded means resource created succeeded during last create/update + Succeeded ProvisioningState = "Succeeded" + // Deleting means resource is in the process of being deleted + Deleting ProvisioningState = "Deleting" + // Migrating means resource is being migrated from one subscription or + // resource group to another + Migrating ProvisioningState = "Migrating" + // Upgrading means an existing resource is being upgraded + Upgrading ProvisioningState = "Upgrading" +) + +// PoolUpgradeProfile contains pool properties: +// - kubernetes version +// - pool name (for agent pool) +// - OS type of the VMs in the pool +// - list of applicable upgrades +type PoolUpgradeProfile struct { + KubernetesVersion string `json:"kubernetesVersion"` + Name string `json:"name,omitempty"` + OSType string `json:"osType,omitempty"` + Upgrades []string `json:"upgrades,omitempty"` +} + +// UpgradeProfileProperties contains properties of UpgradeProfile +type UpgradeProfileProperties struct { + ControlPlaneProfile *PoolUpgradeProfile `json:"controlPlaneProfile"` + AgentPoolProfiles []*PoolUpgradeProfile `json:"agentPoolProfiles"` +} + +// UpgradeProfile contains controlPlane and agent pools upgrade profiles +type UpgradeProfile struct { + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Type string `json:"type,omitempty"` + Properties UpgradeProfileProperties `json:"properties"` +} + +// AgentPoolProfile represents configuration of VMs running agent +// daemons that register with the master and offer resources to +// host applications in containers. +type AgentPoolProfile struct { + Name string `json:"name" validate:"required"` + Count int `json:"count" validate:"required,min=1,max=100"` + VMSize string `json:"vmSize" validate:"required"` + OSDiskSizeGB int `json:"osDiskSizeGB,omitempty" validate:"min=0,max=1023"` + StorageProfile string `json:"storageProfile" validate:"eq=ManagedDisks|len=0"` + VnetSubnetID string `json:"vnetSubnetID,omitempty"` + + // OSType is the operating system type for agents + // Set as nullable to support backward compat because + // this property was added later. + // If the value is null or not set, it defaulted to Linux. + OSType OSType `json:"osType,omitempty"` + + // subnet is internal + subnet string +} + +// AccessProfile represents role name and kubeconfig +type AccessProfile struct { + KubeConfig string `json:"kubeConfig"` +} + +// UnmarshalJSON unmarshal json using the default behavior +// And do fields manipulation, such as populating default value +func (a *AgentPoolProfile) UnmarshalJSON(b []byte) error { + // Need to have a alias type to avoid circular unmarshal + type aliasAgentPoolProfile AgentPoolProfile + aa := aliasAgentPoolProfile{} + if e := json.Unmarshal(b, &aa); e != nil { + return e + } + *a = AgentPoolProfile(aa) + if a.Count == 0 { + // if AgentPoolProfile.Count is missing or 0, set it to default 1 + a.Count = 1 + } + + if a.StorageProfile == "" { + // if StorageProfile is missing, set to default ManagedDisks + a.StorageProfile = ManagedDisks + } + + if string(a.OSType) == "" { + // OSType is the operating system type for agents + // Set as nullable to support backward compat because + // this property was added later. + // If the value is null or not set, it defaulted to Linux. + a.OSType = Linux + } + + // OSDiskSizeGB is an override value. vm sizes have default OS disk sizes. + // If it is not set. The user should get the default for the vm size + return nil +} + +// OSType represents OS types of agents +type OSType string + +// HasWindows returns true if the cluster contains windows +func (a *Properties) HasWindows() bool { + for _, agentPoolProfile := range a.AgentPoolProfiles { + if agentPoolProfile.OSType == Windows { + return true + } + } + return false +} + +// IsCustomVNET returns true if the customer brought their own VNET +func (a *AgentPoolProfile) IsCustomVNET() bool { + return len(a.VnetSubnetID) > 0 +} + +// IsWindows returns true if the agent pool is windows +func (a *AgentPoolProfile) IsWindows() bool { + return a.OSType == Windows +} + +// IsLinux returns true if the agent pool is linux +func (a *AgentPoolProfile) IsLinux() bool { + return a.OSType == Linux +} + +// GetSubnet returns the read-only subnet for the agent pool +func (a *AgentPoolProfile) GetSubnet() string { + return a.subnet +} + +// SetSubnet sets the read-only subnet for the agent pool +func (a *AgentPoolProfile) SetSubnet(subnet string) { + a.subnet = subnet +} + +// IsManagedDisks returns true if the customer specified managed disks +func (a *AgentPoolProfile) IsManagedDisks() bool { + return a.StorageProfile == ManagedDisks +} diff --git a/pkg/api/agentPoolOnlyApi/v20180331/types_test.go b/pkg/api/agentPoolOnlyApi/v20180331/types_test.go new file mode 100644 index 0000000000..95d415e6b1 --- /dev/null +++ b/pkg/api/agentPoolOnlyApi/v20180331/types_test.go @@ -0,0 +1,27 @@ +package v20180331 + +import ( + "encoding/json" + "testing" +) + +func TestAgentPoolProfile(t *testing.T) { + // With osType not specified + AgentPoolProfileText := "{\"count\" : 0}" + ap := &AgentPoolProfile{} + if e := json.Unmarshal([]byte(AgentPoolProfileText), ap); e != nil { + t.Fatalf("unexpectedly detected unmarshal failure for AgentPoolProfile, %+v", e) + } + + if ap.Count != 1 { + t.Fatalf("unexpectedly detected AgentPoolProfile.Count != 1 after unmarshal") + } + + if ap.OSType != Linux { + t.Fatalf("unexpectedly detected AgentPoolProfile.OSType != Linux after unmarshal") + } + + if !ap.IsManagedDisks() { + t.Fatalf("unexpectedly detected AgentPoolProfile.StorageProfile != ManagedDisks after unmarshal") + } +} diff --git a/pkg/api/agentPoolOnlyApi/v20180331/validate.go b/pkg/api/agentPoolOnlyApi/v20180331/validate.go new file mode 100644 index 0000000000..8798d77fb0 --- /dev/null +++ b/pkg/api/agentPoolOnlyApi/v20180331/validate.go @@ -0,0 +1,184 @@ +package v20180331 + +import ( + "fmt" + "regexp" + "strings" + + validator "gopkg.in/go-playground/validator.v9" +) + +var validate *validator.Validate + +func init() { + validate = validator.New() +} + +// Validate implements APIObject +func (a *AgentPoolProfile) Validate() error { + // Don't need to call validate.Struct(a) + // It is handled by Properties.Validate() + if e := validatePoolName(a.Name); e != nil { + return e + } + return nil +} + +// Validate implements APIObject +func (l *LinuxProfile) Validate() error { + // Don't need to call validate.Struct(l) + // It is handled by Properties.Validate() + if e := validate.Var(l.SSH.PublicKeys[0].KeyData, "required"); e != nil { + return fmt.Errorf("KeyData in LinuxProfile.SSH.PublicKeys cannot be empty string") + } + return nil +} + +func handleValidationErrors(e validator.ValidationErrors) error { + err := e[0] + ns := err.Namespace() + switch ns { + // TODO: Add more validation here + case "Properties.LinuxProfile", "Properties.ServicePrincipalProfile.ClientID", + "Properties.ServicePrincipalProfile.Secret", "Properties.WindowsProfile.AdminUsername", + "Properties.WindowsProfile.AdminPassword": + return fmt.Errorf("missing %s", ns) + default: + if strings.HasPrefix(ns, "Properties.AgentPoolProfiles") { + switch { + case strings.HasSuffix(ns, ".Name") || strings.HasSuffix(ns, "VMSize"): + return fmt.Errorf("missing %s", ns) + case strings.HasSuffix(ns, ".Count"): + return fmt.Errorf("AgentPoolProfile count needs to be in the range [%d,%d]", MinAgentCount, MaxAgentCount) + case strings.HasSuffix(ns, ".OSDiskSizeGB"): + return fmt.Errorf("Invalid os disk size of %d specified. The range of valid values are [%d, %d]", err.Value().(int), MinDiskSizeGB, MaxDiskSizeGB) + case strings.HasSuffix(ns, ".StorageProfile"): + return fmt.Errorf("Unknown storageProfile '%s'. Must specify %s", err.Value().(string), ManagedDisks) + default: + break + } + } + } + return fmt.Errorf("Namespace %s is not caught, %+v", ns, e) +} + +// Validate implements APIObject +func (a *Properties) Validate() error { + if e := validate.Struct(a); e != nil { + return handleValidationErrors(e.(validator.ValidationErrors)) + } + + // Don't need to call validate.Struct(m) + // It is handled by Properties.Validate() + if e := validateDNSName(a.DNSPrefix); e != nil { + return e + } + + if e := validateUniqueProfileNames(a.AgentPoolProfiles); e != nil { + return e + } + + for _, agentPoolProfile := range a.AgentPoolProfiles { + if e := agentPoolProfile.Validate(); e != nil { + return e + } + } + + if e := a.LinuxProfile.Validate(); e != nil { + return e + } + if e := validateVNET(a); e != nil { + return e + } + return nil +} + +func validatePoolName(poolName string) error { + // we will cap at length of 12 and all lowercase letters since this makes up the VMName + poolNameRegex := `^([a-z][a-z0-9]{0,11})$` + re, err := regexp.Compile(poolNameRegex) + if err != nil { + return err + } + submatches := re.FindStringSubmatch(poolName) + if len(submatches) != 2 { + return fmt.Errorf("pool name '%s' is invalid. A pool name must start with a lowercase letter, have max length of 12, and only have characters a-z0-9", poolName) + } + return nil +} + +func validateDNSName(dnsName string) error { + dnsNameRegex := `^([A-Za-z][A-Za-z0-9-]{1,43}[A-Za-z0-9])$` + re, err := regexp.Compile(dnsNameRegex) + if err != nil { + return err + } + if !re.MatchString(dnsName) { + return fmt.Errorf("DNS name '%s' is invalid. The DNS name must contain between 3 and 45 characters. The name can contain only letters, numbers, and hyphens. The name must start with a letter and must end with a letter or a number. (length was %d)", dnsName, len(dnsName)) + } + return nil +} + +func validateUniqueProfileNames(profiles []*AgentPoolProfile) error { + profileNames := make(map[string]bool) + for _, profile := range profiles { + if _, ok := profileNames[profile.Name]; ok { + return fmt.Errorf("profile name '%s' already exists, profile names must be unique across pools", profile.Name) + } + profileNames[profile.Name] = true + } + return nil +} + +func validateVNET(a *Properties) error { + var customVNETCount int + var isCustomVNET bool + for _, agentPool := range a.AgentPoolProfiles { + if agentPool.IsCustomVNET() { + customVNETCount++ + isCustomVNET = agentPool.IsCustomVNET() + } + } + + if !(customVNETCount == 0 || customVNETCount == len(a.AgentPoolProfiles)) { + return fmt.Errorf("Multiple VNET Subnet configurations specified. Each agent pool profile must all specify a custom VNET Subnet, or none at all") + } + + subIDMap := make(map[string]int) + resourceGroupMap := make(map[string]int) + agentVNETMap := make(map[string]int) + if isCustomVNET { + for _, agentPool := range a.AgentPoolProfiles { + agentSubID, agentRG, agentVNET, _, err := GetVNETSubnetIDComponents(agentPool.VnetSubnetID) + if err != nil { + return err + } + + subIDMap[agentSubID] = subIDMap[agentSubID] + 1 + resourceGroupMap[agentRG] = resourceGroupMap[agentRG] + 1 + agentVNETMap[agentVNET] = agentVNETMap[agentVNET] + 1 + } + + // TODO: Add more validation to ensure all agent pools belong to the same VNET, subscription, and resource group + // if(len(subIDMap) != len(a.AgentPoolProfiles)) + + // return errors.New("Multiple VNETS specified. Each agent pool must reference the same VNET (but it is ok to reference different subnets on that VNET)") + // } + } + + return nil +} + +// GetVNETSubnetIDComponents extract subscription, resourcegroup, vnetname, subnetname from the vnetSubnetID +func GetVNETSubnetIDComponents(vnetSubnetID string) (string, string, string, string, error) { + vnetSubnetIDRegex := `^\/subscriptions\/([^\/]*)\/resourceGroups\/([^\/]*)\/providers\/Microsoft.Network\/virtualNetworks\/([^\/]*)\/subnets\/([^\/]*)$` + re, err := regexp.Compile(vnetSubnetIDRegex) + if err != nil { + return "", "", "", "", err + } + submatches := re.FindStringSubmatch(vnetSubnetID) + if len(submatches) != 4 { + return "", "", "", "", err + } + return submatches[1], submatches[2], submatches[3], submatches[4], nil +} diff --git a/pkg/api/apiloader.go b/pkg/api/apiloader.go index 1e3d8e3712..f381b0c9ae 100644 --- a/pkg/api/apiloader.go +++ b/pkg/api/apiloader.go @@ -6,6 +6,7 @@ import ( "reflect" "github.com/Azure/acs-engine/pkg/api/agentPoolOnlyApi/v20170831" + "github.com/Azure/acs-engine/pkg/api/agentPoolOnlyApi/v20180331" apvlabs "github.com/Azure/acs-engine/pkg/api/agentPoolOnlyApi/vlabs" "github.com/Azure/acs-engine/pkg/api/common" "github.com/Azure/acs-engine/pkg/api/v20160330" @@ -206,6 +207,26 @@ func (a *Apiloader) LoadContainerServiceForAgentPoolOnlyCluster(contents []byte, return nil, e } return ConvertV20170831AgentPoolOnly(managedCluster), nil + case v20180331.APIVersion: + managedCluster := &v20180331.ManagedCluster{} + if e := json.Unmarshal(contents, &managedCluster); e != nil { + return nil, 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) + } + // 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) + } + managedCluster.Properties.KubernetesVersion = defaultKubernetesVersion + } + if e := managedCluster.Properties.Validate(); validate && e != nil { + return nil, e + } + return ConvertV20180331AgentPoolOnly(managedCluster), nil case apvlabs.APIVersion: managedCluster := &apvlabs.ManagedCluster{} if e := json.Unmarshal(contents, &managedCluster); e != nil { @@ -302,6 +323,16 @@ func (a *Apiloader) serializeHostedContainerService(containerService *ContainerS return nil, err } return b, nil + case v20180331.APIVersion: + v20180331ContainerService := ConvertContainerServiceToV20180331AgentPoolOnly(containerService) + armContainerService := &V20180331ARMManagedContainerService{} + armContainerService.ManagedCluster = v20180331ContainerService + armContainerService.APIVersion = version + b, err := helpers.JSONMarshalIndent(armContainerService, "", " ", false) + if err != nil { + return nil, err + } + return b, nil default: return nil, a.Translator.Errorf("invalid version %s for conversion back from unversioned object", version) } diff --git a/pkg/api/converterfromagentpoolonlyapi.go b/pkg/api/converterfromagentpoolonlyapi.go index 55d6244f1d..00548552b1 100644 --- a/pkg/api/converterfromagentpoolonlyapi.go +++ b/pkg/api/converterfromagentpoolonlyapi.go @@ -1,6 +1,7 @@ package api import "github.com/Azure/acs-engine/pkg/api/agentPoolOnlyApi/v20170831" +import "github.com/Azure/acs-engine/pkg/api/agentPoolOnlyApi/v20180331" /////////////////////////////////////////////////////////// // The converter exposes functions to convert the top level @@ -30,6 +31,26 @@ func ConvertContainerServiceToV20170831AgentPoolOnly(api *ContainerService) *v20 return v20170831HCP } +// ConvertContainerServiceToV20180331AgentPoolOnly converts an unversioned ContainerService to a v20180331 ContainerService +func ConvertContainerServiceToV20180331AgentPoolOnly(api *ContainerService) *v20180331.ManagedCluster { + v20180331HCP := &v20180331.ManagedCluster{} + v20180331HCP.ID = api.ID + v20180331HCP.Location = api.Location + v20180331HCP.Name = api.Name + if api.Plan != nil { + v20180331HCP.Plan = &v20180331.ResourcePurchasePlan{} + convertResourcePurchasePlanToV20180331AgentPoolOnly(api.Plan, v20180331HCP.Plan) + } + v20180331HCP.Tags = map[string]string{} + for k, v := range api.Tags { + v20180331HCP.Tags[k] = v + } + v20180331HCP.Type = api.Type + v20180331HCP.Properties = &v20180331.Properties{} + convertPropertiesToV20180331AgentPoolOnly(api.Properties, v20180331HCP.Properties) + return v20180331HCP +} + // convertResourcePurchasePlanToV20170831 converts a v20170831 ResourcePurchasePlan to an unversioned ResourcePurchasePlan func convertResourcePurchasePlanToV20170831AgentPoolOnly(api *ResourcePurchasePlan, v20170831 *v20170831.ResourcePurchasePlan) { v20170831.Name = api.Name @@ -100,3 +121,91 @@ func convertServicePrincipalProfileToV20170831AgentPoolOnly(api *ServicePrincipa v20170831.Secret = api.Secret // v20170831.KeyvaultSecretRef = api.KeyvaultSecretRef } + +// convertResourcePurchasePlanToV20180331 converts a v20180331 ResourcePurchasePlan to an unversioned ResourcePurchasePlan +func convertResourcePurchasePlanToV20180331AgentPoolOnly(api *ResourcePurchasePlan, v20180331 *v20180331.ResourcePurchasePlan) { + v20180331.Name = api.Name + v20180331.Product = api.Product + v20180331.PromotionCode = api.PromotionCode + v20180331.Publisher = api.Publisher +} + +func convertPropertiesToV20180331AgentPoolOnly(api *Properties, p *v20180331.Properties) { + p.ProvisioningState = v20180331.ProvisioningState(api.ProvisioningState) + if api.OrchestratorProfile != nil { + if api.OrchestratorProfile.OrchestratorVersion != "" { + p.KubernetesVersion = api.OrchestratorProfile.OrchestratorVersion + } + } + if api.HostedMasterProfile != nil { + p.DNSPrefix = api.HostedMasterProfile.DNSPrefix + p.FQDN = api.HostedMasterProfile.FQDN + } + p.AgentPoolProfiles = []*v20180331.AgentPoolProfile{} + for _, apiProfile := range api.AgentPoolProfiles { + v20180331Profile := &v20180331.AgentPoolProfile{} + convertAgentPoolProfileToV20180331AgentPoolOnly(apiProfile, v20180331Profile) + p.AgentPoolProfiles = append(p.AgentPoolProfiles, v20180331Profile) + } + if api.LinuxProfile != nil { + p.LinuxProfile = &v20180331.LinuxProfile{} + convertLinuxProfileToV20180331AgentPoolOnly(api.LinuxProfile, p.LinuxProfile) + } + if api.WindowsProfile != nil { + p.WindowsProfile = &v20180331.WindowsProfile{} + convertWindowsProfileToV20180331AgentPoolOnly(api.WindowsProfile, p.WindowsProfile) + } + if api.ServicePrincipalProfile != nil { + p.ServicePrincipalProfile = &v20180331.ServicePrincipalProfile{} + convertServicePrincipalProfileToV20180331AgentPoolOnly(api.ServicePrincipalProfile, p.ServicePrincipalProfile) + } + if api.AddonProfiles != nil { + p.AddonProfiles = make(map[string]v20180331.AddonProfile) + convertAddonsProfileToV20180331AgentPoolOnly(api.AddonProfiles, p.AddonProfiles) + } +} + +func convertLinuxProfileToV20180331AgentPoolOnly(api *LinuxProfile, obj *v20180331.LinuxProfile) { + obj.AdminUsername = api.AdminUsername + obj.SSH.PublicKeys = []v20180331.PublicKey{} + for _, d := range api.SSH.PublicKeys { + obj.SSH.PublicKeys = append(obj.SSH.PublicKeys, v20180331.PublicKey{ + KeyData: d.KeyData, + }) + } +} + +func convertWindowsProfileToV20180331AgentPoolOnly(api *WindowsProfile, v20180331Profile *v20180331.WindowsProfile) { + v20180331Profile.AdminUsername = api.AdminUsername + v20180331Profile.AdminPassword = api.AdminPassword +} + +func convertAgentPoolProfileToV20180331AgentPoolOnly(api *AgentPoolProfile, p *v20180331.AgentPoolProfile) { + p.Name = api.Name + p.Count = api.Count + p.VMSize = api.VMSize + p.OSType = v20180331.OSType(api.OSType) + p.SetSubnet(api.Subnet) + p.OSDiskSizeGB = api.OSDiskSizeGB + p.StorageProfile = api.StorageProfile + p.VnetSubnetID = api.VnetSubnetID +} + +func convertServicePrincipalProfileToV20180331AgentPoolOnly(api *ServicePrincipalProfile, v20180331 *v20180331.ServicePrincipalProfile) { + v20180331.ClientID = api.ClientID + v20180331.Secret = api.Secret + // v20180331.KeyvaultSecretRef = api.KeyvaultSecretRef +} + +func convertAddonsProfileToV20180331AgentPoolOnly(api map[string]AddonProfile, p map[string]v20180331.AddonProfile) { + if api == nil { + return + } + + for k, v := range api { + p[k] = v20180331.AddonProfile{ + Enabled: v.Enabled, + Config: v.Config, + } + } +} diff --git a/pkg/api/converterfromagentpoolonlyapi_test.go b/pkg/api/converterfromagentpoolonlyapi_test.go new file mode 100644 index 0000000000..15cc4575fa --- /dev/null +++ b/pkg/api/converterfromagentpoolonlyapi_test.go @@ -0,0 +1,38 @@ +package api + +import ( + "github.com/Azure/acs-engine/pkg/api/agentPoolOnlyApi/v20180331" + "testing" +) + +func TestConvertToV20180331AddonProfile(t *testing.T) { + addonName := "AddonFoo" + api := map[string]AddonProfile{ + addonName: AddonProfile{ + Enabled: true, + Config: map[string]string{ + "opt1": "value1", + }, + }, + } + + p := make(map[string]v20180331.AddonProfile) + convertAddonsProfileToV20180331AgentPoolOnly(api, p) + + if len(p) != 1 { + t.Error("there has to be one addon") + } + if _, ok := p[addonName]; !ok { + t.Error("addon is not found") + } + if p[addonName].Enabled != true { + t.Error("addon should be enabled") + } + v, ok := p[addonName].Config["opt1"] + if !ok { + t.Error("Addon config opt1 is not found") + } + if v != "value1" { + t.Error("addon config value does not match") + } +} diff --git a/pkg/api/convertertoagentpoolonlyapi.go b/pkg/api/convertertoagentpoolonlyapi.go index e768f42635..ff0bd60670 100644 --- a/pkg/api/convertertoagentpoolonlyapi.go +++ b/pkg/api/convertertoagentpoolonlyapi.go @@ -4,6 +4,7 @@ import ( "encoding/json" "github.com/Azure/acs-engine/pkg/api/agentPoolOnlyApi/v20170831" + "github.com/Azure/acs-engine/pkg/api/agentPoolOnlyApi/v20180331" "github.com/Azure/acs-engine/pkg/api/agentPoolOnlyApi/vlabs" "github.com/Azure/acs-engine/pkg/api/common" ) @@ -34,6 +35,24 @@ func ConvertV20170831AgentPoolOnly(v20170831 *v20170831.ManagedCluster) *Contain return c } +// ConvertV20180331AgentPoolOnly converts an AgentPoolOnly object into an in-memory container service +func ConvertV20180331AgentPoolOnly(v20180331 *v20180331.ManagedCluster) *ContainerService { + c := &ContainerService{} + c.ID = v20180331.ID + c.Location = NormalizeAzureRegion(v20180331.Location) + c.Name = v20180331.Name + if v20180331.Plan != nil { + c.Plan = convertv20180331AgentPoolOnlyResourcePurchasePlan(v20180331.Plan) + } + c.Tags = map[string]string{} + for k, v := range v20180331.Tags { + c.Tags[k] = v + } + c.Type = v20180331.Type + c.Properties = convertV20180331AgentPoolOnlyProperties(v20180331.Properties) + return c +} + func convertv20170831AgentPoolOnlyResourcePurchasePlan(v20170831 *v20170831.ResourcePurchasePlan) *ResourcePurchasePlan { return &ResourcePurchasePlan{ Name: v20170831.Name, @@ -269,3 +288,102 @@ func propertiesAsMap(contents []byte) (map[string]interface{}, bool) { } return properties.(map[string]interface{}), true } + +func convertv20180331AgentPoolOnlyResourcePurchasePlan(v20180331 *v20180331.ResourcePurchasePlan) *ResourcePurchasePlan { + return &ResourcePurchasePlan{ + Name: v20180331.Name, + Product: v20180331.Product, + PromotionCode: v20180331.PromotionCode, + Publisher: v20180331.Publisher, + } +} + +func convertV20180331AgentPoolOnlyProperties(obj *v20180331.Properties) *Properties { + properties := &Properties{ + ProvisioningState: ProvisioningState(obj.ProvisioningState), + MasterProfile: nil, + } + + properties.HostedMasterProfile = &HostedMasterProfile{} + properties.HostedMasterProfile.DNSPrefix = obj.DNSPrefix + properties.HostedMasterProfile.FQDN = obj.FQDN + + properties.OrchestratorProfile = convertV20180331AgentPoolOnlyOrchestratorProfile(obj.KubernetesVersion) + + properties.AgentPoolProfiles = make([]*AgentPoolProfile, len(obj.AgentPoolProfiles)) + for i := range obj.AgentPoolProfiles { + properties.AgentPoolProfiles[i] = convertV20180331AgentPoolOnlyAgentPoolProfile(obj.AgentPoolProfiles[i], AvailabilitySet) + } + if obj.LinuxProfile != nil { + properties.LinuxProfile = convertV20180331AgentPoolOnlyLinuxProfile(obj.LinuxProfile) + } + if obj.WindowsProfile != nil { + properties.WindowsProfile = convertV20180331AgentPoolOnlyWindowsProfile(obj.WindowsProfile) + } + + if obj.ServicePrincipalProfile != nil { + properties.ServicePrincipalProfile = convertV20180331AgentPoolOnlyServicePrincipalProfile(obj.ServicePrincipalProfile) + } + if obj.AddonProfiles != nil { + properties.AddonProfiles = convertV20180331AgentPoolOnlyAddonProfiles(obj.AddonProfiles) + } + + return properties +} + +func convertV20180331AgentPoolOnlyLinuxProfile(obj *v20180331.LinuxProfile) *LinuxProfile { + api := &LinuxProfile{ + AdminUsername: obj.AdminUsername, + } + api.SSH.PublicKeys = []PublicKey{} + for _, d := range obj.SSH.PublicKeys { + api.SSH.PublicKeys = append(api.SSH.PublicKeys, PublicKey{KeyData: d.KeyData}) + } + return api +} + +func convertV20180331AgentPoolOnlyWindowsProfile(obj *v20180331.WindowsProfile) *WindowsProfile { + return &WindowsProfile{ + AdminUsername: obj.AdminUsername, + AdminPassword: obj.AdminPassword, + } +} + +func convertV20180331AgentPoolOnlyOrchestratorProfile(kubernetesVersion string) *OrchestratorProfile { + return &OrchestratorProfile{ + OrchestratorType: Kubernetes, + OrchestratorVersion: common.GetSupportedKubernetesVersion(kubernetesVersion), + } +} + +func convertV20180331AgentPoolOnlyAgentPoolProfile(v20180331 *v20180331.AgentPoolProfile, availabilityProfile string) *AgentPoolProfile { + api := &AgentPoolProfile{} + api.Name = v20180331.Name + api.Count = v20180331.Count + api.VMSize = v20180331.VMSize + api.OSDiskSizeGB = v20180331.OSDiskSizeGB + api.OSType = OSType(v20180331.OSType) + api.StorageProfile = v20180331.StorageProfile + api.VnetSubnetID = v20180331.VnetSubnetID + api.Subnet = v20180331.GetSubnet() + api.AvailabilityProfile = availabilityProfile + return api +} + +func convertV20180331AgentPoolOnlyServicePrincipalProfile(obj *v20180331.ServicePrincipalProfile) *ServicePrincipalProfile { + return &ServicePrincipalProfile{ + ClientID: obj.ClientID, + Secret: obj.Secret, + } +} + +func convertV20180331AgentPoolOnlyAddonProfiles(obj map[string]v20180331.AddonProfile) map[string]AddonProfile { + api := make(map[string]AddonProfile) + for k, v := range obj { + api[k] = AddonProfile{ + Enabled: v.Enabled, + Config: v.Config, + } + } + return api +} diff --git a/pkg/api/convertertoagentpoolonlyapi_test.go b/pkg/api/convertertoagentpoolonlyapi_test.go index 4939417eaa..3c17b1988b 100644 --- a/pkg/api/convertertoagentpoolonlyapi_test.go +++ b/pkg/api/convertertoagentpoolonlyapi_test.go @@ -1,9 +1,40 @@ package api import ( + "github.com/Azure/acs-engine/pkg/api/agentPoolOnlyApi/v20180331" "testing" ) +func TestConvertFromV20180331AddonProfile(t *testing.T) { + addonName := "AddonFoo" + p := map[string]v20180331.AddonProfile{ + addonName: v20180331.AddonProfile{ + Enabled: true, + Config: map[string]string{ + "opt1": "value1", + }, + }, + } + api := convertV20180331AgentPoolOnlyAddonProfiles(p) + + if len(api) != 1 { + t.Error("there has to be one addon") + } + if _, ok := api[addonName]; !ok { + t.Error("addon is not found") + } + if api[addonName].Enabled != true { + t.Error("addon should be enabled") + } + v, ok := api[addonName].Config["opt1"] + if !ok { + t.Error("Addon config opt1 is not found") + } + if v != "value1" { + t.Error("addon config value does not match") + } +} + func TestIfMasterProfileIsMissingThenApiModelIsAgentPoolOnly(t *testing.T) { json := ` { diff --git a/pkg/api/types.go b/pkg/api/types.go index 26c3af7400..5faf3bb180 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -4,6 +4,7 @@ import ( neturl "net/url" "github.com/Azure/acs-engine/pkg/api/agentPoolOnlyApi/v20170831" + "github.com/Azure/acs-engine/pkg/api/agentPoolOnlyApi/v20180331" "github.com/Azure/acs-engine/pkg/api/common" "github.com/Azure/acs-engine/pkg/api/v20160330" "github.com/Azure/acs-engine/pkg/api/v20160930" @@ -57,6 +58,13 @@ type Properties struct { AADProfile *AADProfile `json:"aadProfile,omitempty"` CustomProfile *CustomProfile `json:"customProfile,omitempty"` HostedMasterProfile *HostedMasterProfile `json:"hostedMasterProfile,omitempty"` + AddonProfiles map[string]AddonProfile `json:"addonProfiles,omitempty"` +} + +// AddonProfile represents the collection of addons for managed cluster +type AddonProfile struct { + Enabled bool `json:"enabled"` + Config map[string]string `json:"config"` } // ServicePrincipalProfile contains the client and secret used by the cluster for Azure Resource CRUD @@ -502,6 +510,14 @@ type V20170831ARMManagedContainerService struct { *v20170831.ManagedCluster } +// V20180331ARMManagedContainerService is the type we read and write from file +// needed because the json that is sent to ARM and acs-engine +// is different from the json that the ACS RP Api gets from ARM +type V20180331ARMManagedContainerService struct { + TypeMeta + *v20180331.ManagedCluster +} + // HasWindows returns true if the cluster contains windows func (p *Properties) HasWindows() bool { for _, agentPoolProfile := range p.AgentPoolProfiles { From 7ae337ab0a5398501b404963a39474aca918fb17 Mon Sep 17 00:00:00 2001 From: Weinong Wang Date: Mon, 12 Mar 2018 20:37:23 +0000 Subject: [PATCH 2/3] updated struct comment --- pkg/api/agentPoolOnlyApi/v20180331/types.go | 2 +- pkg/api/types.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/api/agentPoolOnlyApi/v20180331/types.go b/pkg/api/agentPoolOnlyApi/v20180331/types.go index 4da8e82ee5..462791298a 100644 --- a/pkg/api/agentPoolOnlyApi/v20180331/types.go +++ b/pkg/api/agentPoolOnlyApi/v20180331/types.go @@ -41,7 +41,7 @@ type Properties struct { AddonProfiles map[string]AddonProfile `json:"addonProfiles,omitempty"` } -// AddonProfile represents the collection of addons for managed cluster +// AddonProfile represents an addon for managed cluster type AddonProfile struct { Enabled bool `json:"enabled"` Config map[string]string `json:"config"` diff --git a/pkg/api/types.go b/pkg/api/types.go index 5faf3bb180..dcd4505b68 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -61,7 +61,7 @@ type Properties struct { AddonProfiles map[string]AddonProfile `json:"addonProfiles,omitempty"` } -// AddonProfile represents the collection of addons for managed cluster +// AddonProfile represents an addon for managed cluster type AddonProfile struct { Enabled bool `json:"enabled"` Config map[string]string `json:"config"` From 8ce66b3de8f388c89839005a8e5badfe8a4364af Mon Sep 17 00:00:00 2001 From: Weinong Wang Date: Mon, 12 Mar 2018 21:39:49 +0000 Subject: [PATCH 3/3] fix style --- pkg/api/converterfromagentpoolonlyapi_test.go | 2 +- pkg/api/convertertoagentpoolonlyapi_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/api/converterfromagentpoolonlyapi_test.go b/pkg/api/converterfromagentpoolonlyapi_test.go index 15cc4575fa..4b63380edd 100644 --- a/pkg/api/converterfromagentpoolonlyapi_test.go +++ b/pkg/api/converterfromagentpoolonlyapi_test.go @@ -8,7 +8,7 @@ import ( func TestConvertToV20180331AddonProfile(t *testing.T) { addonName := "AddonFoo" api := map[string]AddonProfile{ - addonName: AddonProfile{ + addonName: { Enabled: true, Config: map[string]string{ "opt1": "value1", diff --git a/pkg/api/convertertoagentpoolonlyapi_test.go b/pkg/api/convertertoagentpoolonlyapi_test.go index 3c17b1988b..8965ddf3f7 100644 --- a/pkg/api/convertertoagentpoolonlyapi_test.go +++ b/pkg/api/convertertoagentpoolonlyapi_test.go @@ -8,7 +8,7 @@ import ( func TestConvertFromV20180331AddonProfile(t *testing.T) { addonName := "AddonFoo" p := map[string]v20180331.AddonProfile{ - addonName: v20180331.AddonProfile{ + addonName: { Enabled: true, Config: map[string]string{ "opt1": "value1",