diff --git a/apis/compute/v1alpha3/types.go b/apis/compute/v1alpha3/types.go index e566dc4a..ac57ea2e 100644 --- a/apis/compute/v1alpha3/types.go +++ b/apis/compute/v1alpha3/types.go @@ -81,6 +81,24 @@ type AKSClusterParameters struct { // cluster. // +optional DisableRBAC bool `json:"disableRBAC,omitempty"` + + // Identity is the managed identity configuration for the control-plane. + // +kubebuilder:validation:Required + Identity Identity `json:"identity"` +} + +type Identity struct { + // Type specifies the type of the managed identity to be used by + // the control-plane. Allowed values are: `SystemAssigned` or + // `UserAssigned`. + // +required + // +kubebuilder:validation:Enum=SystemAssigned;UserAssigned + Type string `json:"type"` + // IdentityNames are the names of the user-assigned managed identity + // resources to be used by the control-plane. + // Required if Type is `UserAssigned`. + // +optional + IdentityNames []string `json:"identityNames,omitempty"` } // An AKSClusterSpec defines the desired state of a AKSCluster. diff --git a/apis/compute/v1alpha3/zz_generated.deepcopy.go b/apis/compute/v1alpha3/zz_generated.deepcopy.go index 8384721e..235988ef 100644 --- a/apis/compute/v1alpha3/zz_generated.deepcopy.go +++ b/apis/compute/v1alpha3/zz_generated.deepcopy.go @@ -113,6 +113,7 @@ func (in *AKSClusterParameters) DeepCopyInto(out *AKSClusterParameters) { *out = new(int) **out = **in } + in.Identity.DeepCopyInto(&out.Identity) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AKSClusterParameters. @@ -157,3 +158,23 @@ func (in *AKSClusterStatus) DeepCopy() *AKSClusterStatus { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Identity) DeepCopyInto(out *Identity) { + *out = *in + if in.IdentityNames != nil { + in, out := &in.IdentityNames, &out.IdentityNames + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Identity. +func (in *Identity) DeepCopy() *Identity { + if in == nil { + return nil + } + out := new(Identity) + in.DeepCopyInto(out) + return out +} diff --git a/apis/v1beta1/zz_generated.deepcopy.go b/apis/v1beta1/zz_generated.deepcopy.go index 0d7cb6a0..fdd658e6 100644 --- a/apis/v1beta1/zz_generated.deepcopy.go +++ b/apis/v1beta1/zz_generated.deepcopy.go @@ -93,6 +93,16 @@ func (in *ProviderConfigSpec) DeepCopyInto(out *ProviderConfigSpec) { *out = new(string) **out = **in } + if in.ARMEndpoint != nil { + in, out := &in.ARMEndpoint, &out.ARMEndpoint + *out = new(string) + **out = **in + } + if in.SubscriptionID != nil { + in, out := &in.SubscriptionID, &out.SubscriptionID + *out = new(string) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderConfigSpec. diff --git a/go.mod b/go.mod index 7c476814..a1155c59 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.16 require ( github.com/Azure/azure-pipeline-go v0.2.2 // indirect - github.com/Azure/azure-sdk-for-go v61.4.0+incompatible + github.com/Azure/azure-sdk-for-go v62.3.0+incompatible github.com/Azure/azure-sdk-for-go/sdk/azcore v0.22.0 github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.13.2 github.com/Azure/azure-storage-blob-go v0.7.0 diff --git a/go.sum b/go.sum index 22a3fdb1..8bc28666 100644 --- a/go.sum +++ b/go.sum @@ -25,8 +25,8 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7 github.com/Azure/azure-pipeline-go v0.2.1/go.mod h1:UGSo8XybXnIGZ3epmeBw7Jdz+HiUVpqIlpz/HKHylF4= github.com/Azure/azure-pipeline-go v0.2.2 h1:6oiIS9yaG6XCCzhgAgKFfIWyo4LLCiDhZot6ltoThhY= github.com/Azure/azure-pipeline-go v0.2.2/go.mod h1:4rQ/NZncSvGqNkkOsNpOU1tgoNuIlp9AfUH5G1tvCHc= -github.com/Azure/azure-sdk-for-go v61.4.0+incompatible h1:BF2Pm3aQWIa6q9KmxyF1JYKYXtVw67vtvu2Wd54NGuY= -github.com/Azure/azure-sdk-for-go v61.4.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/azure-sdk-for-go v62.3.0+incompatible h1:Ctfsn9UoA/BB4HMYQlbPPgNXdX0tZ4tmb85+KFb2+RE= +github.com/Azure/azure-sdk-for-go v62.3.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go/sdk/azcore v0.21.0/go.mod h1:fBF9PQNqB8scdgpZ3ufzaLntG0AG7C1WjPMsiFOmfHM= github.com/Azure/azure-sdk-for-go/sdk/azcore v0.22.0 h1:zBJcBJwte0x6PcPK7XaWDMvK2o2ZM2f1sMaqNNavQ5g= github.com/Azure/azure-sdk-for-go/sdk/azcore v0.22.0/go.mod h1:fBF9PQNqB8scdgpZ3ufzaLntG0AG7C1WjPMsiFOmfHM= diff --git a/package/crds/azure.crossplane.io_providerconfigs.yaml b/package/crds/azure.crossplane.io_providerconfigs.yaml index df726d5c..285580e1 100644 --- a/package/crds/azure.crossplane.io_providerconfigs.yaml +++ b/package/crds/azure.crossplane.io_providerconfigs.yaml @@ -45,6 +45,10 @@ spec: spec: description: A ProviderConfigSpec defines the desired state of a ProviderConfig. properties: + armEndpoint: + description: ARMEndpoint is the Azure Resource Manager endpoint to + use. Defaults to ARM public cloud endpoint. + type: string clientID: description: ClientID is the user-assigned managed identity's ID when Credentials.Source is `InjectedIdentity`. If unset and Credentials.Source @@ -103,6 +107,11 @@ spec: required: - source type: object + subscriptionID: + description: SubscriptionID is the Azure subscription ID to be used. + If unset, subscription ID from Credentials will be used. Required + if Credentials.Source is not Secret. + type: string required: - credentials type: object diff --git a/package/crds/compute.azure.crossplane.io_aksclusters.yaml b/package/crds/compute.azure.crossplane.io_aksclusters.yaml index ea504341..e4668bb0 100644 --- a/package/crds/compute.azure.crossplane.io_aksclusters.yaml +++ b/package/crds/compute.azure.crossplane.io_aksclusters.yaml @@ -74,6 +74,28 @@ spec: to the Kubernetes API when managing containers after creating the cluster. type: string + identity: + description: Identity is the managed identity configuration for the + control-plane. + properties: + identityNames: + description: IdentityNames are the names of the user-assigned + managed identity resources to be used by the control-plane. + Required if Type is `UserAssigned`. + items: + type: string + type: array + type: + description: 'Type specifies the type of the managed identity + to be used by the control-plane. Allowed values are: `SystemAssigned` + or `UserAssigned`.' + enum: + - SystemAssigned + - UserAssigned + type: string + required: + - type + type: object location: description: Location is the Azure location that the cluster will be created in @@ -193,6 +215,7 @@ spec: - namespace type: object required: + - identity - location - version type: object diff --git a/pkg/clients/compute/aks.go b/pkg/clients/compute/aks.go index 78cd28ec..92c575cc 100644 --- a/pkg/clients/compute/aks.go +++ b/pkg/clients/compute/aks.go @@ -19,21 +19,13 @@ package compute import ( "context" "fmt" - "time" - "github.com/Azure/azure-sdk-for-go/services/authorization/mgmt/2015-07-01/authorization" - authorizationmgmt "github.com/Azure/azure-sdk-for-go/services/authorization/mgmt/2015-07-01/authorization" - "github.com/Azure/azure-sdk-for-go/services/containerservice/mgmt/2018-03-31/containerservice" - "github.com/Azure/azure-sdk-for-go/services/graphrbac/1.6/graphrbac" + "github.com/Azure/azure-sdk-for-go/services/containerservice/mgmt/2022-01-01/containerservice" "github.com/Azure/go-autorest/autorest" - "github.com/Azure/go-autorest/autorest/adal" - "github.com/Azure/go-autorest/autorest/date" "github.com/Azure/go-autorest/autorest/to" - "github.com/google/uuid" "github.com/pkg/errors" "github.com/crossplane/crossplane-runtime/pkg/meta" - "github.com/crossplane/crossplane-runtime/pkg/resource" "github.com/crossplane/provider-azure/apis/compute/v1alpha3" azure "github.com/crossplane/provider-azure/pkg/clients" @@ -43,72 +35,39 @@ const ( // AgentPoolProfileName is a format string for the name of the automatically // created cluster agent pool profile AgentPoolProfileName = "agentpool" +) - // NetworkContributorRoleID lets the AKS cluster managed networks, but not - // access them. - NetworkContributorRoleID = "/providers/Microsoft.Authorization/roleDefinitions/4d97b98b-1d4f-4787-a291-c67834d212e7" +const ( + // error strings + errInvalidUserAssignedManagedIdentity = "at least one user-assigned managed identity resource name must be specified when identity.type is UserAssigned" - appCredsValidYears = 5 + fmtUserAssignedManagedIdentityID = "/subscriptions/%s/resourceGroups/%s/providers/Microsoft.ManagedIdentity/userAssignedIdentities/%s" ) // An AKSClient can create, read, and delete AKS clusters and the various other // resources they require. type AKSClient interface { GetManagedCluster(ctx context.Context, ac *v1alpha3.AKSCluster) (containerservice.ManagedCluster, error) - EnsureManagedCluster(ctx context.Context, ac *v1alpha3.AKSCluster, secret string) error + EnsureManagedCluster(ctx context.Context, ac *v1alpha3.AKSCluster) error DeleteManagedCluster(ctx context.Context, ac *v1alpha3.AKSCluster) error GetKubeConfig(ctx context.Context, ac *v1alpha3.AKSCluster) ([]byte, error) } // An AggregateClient aggregates the various clients used by the AKS controller. type AggregateClient struct { - ManagedClusters containerservice.ManagedClustersClient - Applications graphrbac.ApplicationsClient - ServicePrincipals graphrbac.ServicePrincipalsClient - RoleAssignments authorization.RoleAssignmentsClient + ManagedClusters containerservice.ManagedClustersClient + subscriptionID string } // NewAggregateClient produces the various clients used by the AKS controller. -func NewAggregateClient(creds map[string]string, auth autorest.Authorizer) (AKSClient, error) { - mcc := containerservice.NewManagedClustersClient(creds[azure.CredentialsKeySubscriptionID]) +func NewAggregateClient(subscriptionID string, auth autorest.Authorizer) (AKSClient, error) { + mcc := containerservice.NewManagedClustersClient(subscriptionID) mcc.Authorizer = auth _ = mcc.AddToUserAgent(azure.UserAgent) - rac := authorization.NewRoleAssignmentsClient(creds[azure.CredentialsKeySubscriptionID]) - rac.Authorizer = auth - _ = rac.AddToUserAgent(azure.UserAgent) - - cfg, err := adal.NewOAuthConfig(creds[azure.CredentialsKeyActiveDirectoryEndpointURL], creds[azure.CredentialsKeyTenantID]) - if err != nil { - return nil, errors.Wrap(err, "cannot create OAuth configuration") - } - - token, err := adal.NewServicePrincipalToken(*cfg, - creds[azure.CredentialsKeyClientID], - creds[azure.CredentialsKeyClientSecret], - creds[azure.CredentialsKeyActiveDirectoryGraphResourceID]) - if err != nil { - return nil, errors.Wrap(err, "cannot create service principal token") - } - if err := token.Refresh(); err != nil { - return nil, errors.Wrap(err, "cannot refresh service principal token") - } - - ta := autorest.NewBearerAuthorizer(token) - - ac := graphrbac.NewApplicationsClient(creds[azure.CredentialsKeyTenantID]) - ac.Authorizer = ta - _ = ac.AddToUserAgent(azure.UserAgent) - - spc := graphrbac.NewServicePrincipalsClient(creds[azure.CredentialsKeyTenantID]) - spc.Authorizer = ta - _ = spc.AddToUserAgent(azure.UserAgent) - return AggregateClient{ - ManagedClusters: mcc, - Applications: ac, - ServicePrincipals: spc, - RoleAssignments: rac, + ManagedClusters: mcc, + subscriptionID: subscriptionID, }, nil } @@ -119,22 +78,11 @@ func (c AggregateClient) GetManagedCluster(ctx context.Context, ac *v1alpha3.AKS // EnsureManagedCluster ensures the supplied AKS cluster exists, including // ensuring any required service principals and role assignments exist. -func (c AggregateClient) EnsureManagedCluster(ctx context.Context, ac *v1alpha3.AKSCluster, secret string) error { - app, err := c.ensureApplication(ctx, meta.GetExternalName(ac), secret) - if err != nil { - return err - } - - sp, err := c.ensureServicePrincipal(ctx, to.String(app.AppID)) +func (c AggregateClient) EnsureManagedCluster(ctx context.Context, ac *v1alpha3.AKSCluster) error { + mc, err := newManagedCluster(ac, c.subscriptionID) if err != nil { return err } - - if err := c.ensureRoleAssignment(ctx, to.String(sp.ObjectID), NetworkContributorRoleID, ac.Spec.VnetSubnetID); err != nil { - return err - } - - mc := newManagedCluster(ac, to.String(app.AppID), secret) _, err = c.ManagedClusters.CreateOrUpdate(ctx, ac.Spec.ResourceGroupName, meta.GetExternalName(ac), mc) return err } @@ -142,9 +90,6 @@ func (c AggregateClient) EnsureManagedCluster(ctx context.Context, ac *v1alpha3. // DeleteManagedCluster deletes the supplied AKS cluster, including its service // principals and any role assignments. func (c AggregateClient) DeleteManagedCluster(ctx context.Context, ac *v1alpha3.AKSCluster) error { - if err := c.deleteApplication(ctx, meta.GetExternalName(ac)); err != nil { - return err - } _, err := c.ManagedClusters.Delete(ctx, ac.Spec.ResourceGroupName, meta.GetExternalName(ac)) return err } @@ -152,7 +97,7 @@ func (c AggregateClient) DeleteManagedCluster(ctx context.Context, ac *v1alpha3. // GetKubeConfig produces a kubeconfig file that configures access to the // supplied AKS cluster. func (c AggregateClient) GetKubeConfig(ctx context.Context, ac *v1alpha3.AKSCluster) ([]byte, error) { - creds, err := c.ManagedClusters.ListClusterAdminCredentials(ctx, ac.Spec.ResourceGroupName, meta.GetExternalName(ac)) + creds, err := c.ManagedClusters.ListClusterAdminCredentials(ctx, ac.Spec.ResourceGroupName, meta.GetExternalName(ac), "") if err != nil { return nil, err } @@ -168,103 +113,7 @@ func (c AggregateClient) GetKubeConfig(ctx context.Context, ac *v1alpha3.AKSClus return *((*creds.Kubeconfigs)[0].Value), nil } -func (c AggregateClient) ensureApplication(ctx context.Context, name, secret string) (graphrbac.Application, error) { - pc, err := newPasswordCredential(secret) - if err != nil { - return graphrbac.Application{}, err - } - - filter := fmt.Sprintf("displayName eq '%s'", name) - for l, err := c.Applications.ListComplete(ctx, filter); l.NotDone(); err = l.NextWithContext(ctx) { - if err != nil { - return graphrbac.Application{}, err - } - p := graphrbac.PasswordCredentialsUpdateParameters{Value: &[]graphrbac.PasswordCredential{pc}} - if _, err := c.Applications.UpdatePasswordCredentials(ctx, to.String(l.Value().ObjectID), p); err != nil { - return graphrbac.Application{}, err - } - - // We really do want to stop here if we found an app with our desired - // display name. We presume it's one we created earlier. - return l.Value(), nil // nolint:staticcheck - } - - url := fmt.Sprintf("api://%s.aks.crossplane.io", name) - p := graphrbac.ApplicationCreateParameters{ - AvailableToOtherTenants: to.BoolPtr(false), - DisplayName: to.StringPtr(name), - Homepage: to.StringPtr(url), - IdentifierUris: &[]string{url}, - PasswordCredentials: &[]graphrbac.PasswordCredential{pc}, - } - if err != nil { - return graphrbac.Application{}, err - } - - return c.Applications.Create(ctx, p) -} - -func (c AggregateClient) ensureServicePrincipal(ctx context.Context, appID string) (graphrbac.ServicePrincipal, error) { - r, err := c.Applications.GetServicePrincipalsIDByAppID(ctx, appID) - if azure.IsNotFound(err) { - // Create it. - p := graphrbac.ServicePrincipalCreateParameters{AppID: to.StringPtr(appID), AccountEnabled: to.BoolPtr(true)} - return c.ServicePrincipals.Create(ctx, p) - } - if err != nil { - return graphrbac.ServicePrincipal{}, err - } - - return c.ServicePrincipals.Get(ctx, to.String(r.Value)) -} - -func (c AggregateClient) ensureRoleAssignment(ctx context.Context, principalID, roleID, scope string) error { - // If scope was the empty string we probably needed a role assignment for - // an optional scope, for example a subnetwork. - if scope == "" { - return nil - } - - name, err := uuid.NewRandom() - if err != nil { - return err - } - - filter := fmt.Sprintf("principalId eq '%s'", principalID) - for l, err := c.RoleAssignments.ListForScopeComplete(ctx, scope, filter); l.NotDone(); err = l.NextWithContext(ctx) { - if err != nil { - return err - } - - // We really do want to stop here if our principal already has a role - // definition for this scope; we presume it's one we created earlier. - return nil // nolint:staticcheck - } - - p := authorizationmgmt.RoleAssignmentCreateParameters{Properties: &authorizationmgmt.RoleAssignmentProperties{ - RoleDefinitionID: azure.ToStringPtr(fmt.Sprintf("/subscriptions/%s%s", c.RoleAssignments.SubscriptionID, roleID)), - PrincipalID: azure.ToStringPtr(principalID), - }} - _, err = c.RoleAssignments.Create(ctx, scope, name.String(), p) - return err -} - -func (c AggregateClient) deleteApplication(ctx context.Context, name string) error { - filter := fmt.Sprintf("displayName eq '%s'", name) - for l, err := c.Applications.ListComplete(ctx, filter); l.NotDone(); err = l.NextWithContext(ctx) { - if err != nil { - return err - } - - // We really do want to delete the first matching application we find. - _, err := c.Applications.Delete(ctx, to.String(l.Value().ObjectID)) - return resource.Ignore(azure.IsNotFound, err) // nolint:staticcheck - } - - return nil -} - -func newManagedCluster(c *v1alpha3.AKSCluster, appID, secret string) containerservice.ManagedCluster { +func newManagedCluster(c *v1alpha3.AKSCluster, subscriptionID string) (containerservice.ManagedCluster, error) { nodeCount := int32(v1alpha3.DefaultNodeCount) if c.Spec.NodeCount != nil { nodeCount = int32(*c.Spec.NodeCount) @@ -280,38 +129,38 @@ func newManagedCluster(c *v1alpha3.AKSCluster, appID, secret string) containerse { Name: to.StringPtr(AgentPoolProfileName), Count: &nodeCount, - VMSize: containerservice.VMSizeTypes(c.Spec.NodeVMSize), + VMSize: to.StringPtr(c.Spec.NodeVMSize), }, }, - ServicePrincipalProfile: &containerservice.ManagedClusterServicePrincipalProfile{ - ClientID: to.StringPtr(appID), - Secret: to.StringPtr(secret), - }, EnableRBAC: to.BoolPtr(!c.Spec.DisableRBAC), }, } + switch containerservice.ResourceIdentityType(c.Spec.Identity.Type) { + case containerservice.ResourceIdentityTypeSystemAssigned: + p.Identity.Type = containerservice.ResourceIdentityTypeSystemAssigned + case containerservice.ResourceIdentityTypeUserAssigned: + p.Identity.Type = containerservice.ResourceIdentityTypeUserAssigned + if len(c.Spec.Identity.IdentityNames) == 0 { + return p, errors.New(errInvalidUserAssignedManagedIdentity) + } + p.Identity.UserAssignedIdentities = make(map[string]*containerservice.ManagedClusterIdentityUserAssignedIdentitiesValue, len(c.Spec.Identity.IdentityNames)) + for _, n := range c.Spec.Identity.IdentityNames { + resourceID := fmt.Sprintf(fmtUserAssignedManagedIdentityID, subscriptionID, c.Spec.ResourceGroupName, n) + p.Identity.UserAssignedIdentities[resourceID] = &containerservice.ManagedClusterIdentityUserAssignedIdentitiesValue{} + } + } if c.Spec.VnetSubnetID != "" { - p.ManagedClusterProperties.NetworkProfile = &containerservice.NetworkProfile{NetworkPlugin: containerservice.Azure} + p.ManagedClusterProperties.NetworkProfile = &containerservice.NetworkProfile{NetworkPlugin: containerservice.NetworkPluginAzure} p.ManagedClusterProperties.AgentPoolProfiles = &[]containerservice.ManagedClusterAgentPoolProfile{ { Name: to.StringPtr(AgentPoolProfileName), Count: &nodeCount, - VMSize: containerservice.VMSizeTypes(c.Spec.NodeVMSize), + VMSize: to.StringPtr(c.Spec.NodeVMSize), VnetSubnetID: to.StringPtr(c.Spec.VnetSubnetID), }, } } - return p -} - -func newPasswordCredential(secret string) (graphrbac.PasswordCredential, error) { - keyID, err := uuid.NewRandom() - return graphrbac.PasswordCredential{ - StartDate: &date.Time{Time: time.Now()}, - EndDate: &date.Time{Time: time.Now().AddDate(appCredsValidYears, 0, 0)}, - KeyID: to.StringPtr(keyID.String()), - Value: to.StringPtr(secret), - }, err + return p, nil } diff --git a/pkg/controller/compute/managed.go b/pkg/controller/compute/managed.go index fd4e9b49..3c41465b 100644 --- a/pkg/controller/compute/managed.go +++ b/pkg/controller/compute/managed.go @@ -21,8 +21,6 @@ import ( "github.com/Azure/go-autorest/autorest/to" "github.com/pkg/errors" - v1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/tools/clientcmd" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -150,38 +148,7 @@ func (e *external) Create(ctx context.Context, mg resource.Managed) (managed.Ext } cr.SetConditions(xpv1.Creating()) - pw, err := e.getPassword(ctx, cr) - if err != nil { - return managed.ExternalCreation{}, err - } - if pw == "" { - pw, err = e.newPasswordFn() - if err != nil { - return managed.ExternalCreation{}, errors.Wrap(err, errGenPassword) - } - } - return managed.ExternalCreation{ - ConnectionDetails: managed.ConnectionDetails{ - xpv1.ResourceCredentialsSecretPasswordKey: []byte(pw), - }, - }, errors.Wrap(e.client.EnsureManagedCluster(ctx, cr, pw), errCreateAKSCluster) -} - -func (e *external) getPassword(ctx context.Context, cr *v1alpha3.AKSCluster) (string, error) { - if cr.Spec.WriteConnectionSecretToReference == nil || - cr.Spec.WriteConnectionSecretToReference.Name == "" || cr.Spec.WriteConnectionSecretToReference.Namespace == "" { - return "", nil - } - - s := &v1.Secret{} - if err := e.kube.Get(ctx, types.NamespacedName{ - Namespace: cr.Spec.WriteConnectionSecretToReference.Namespace, - Name: cr.Spec.WriteConnectionSecretToReference.Name, - }, s); err != nil { - return "", errors.Wrap(err, errGetConnSecret) - } - - return string(s.Data[xpv1.ResourceCredentialsSecretPasswordKey]), nil + return managed.ExternalCreation{}, errors.Wrap(e.client.EnsureManagedCluster(ctx, cr), errCreateAKSCluster) } func (e *external) Update(_ context.Context, _ resource.Managed) (managed.ExternalUpdate, error) {