diff --git a/api/hypershift/v1beta1/hostedcluster_types.go b/api/hypershift/v1beta1/hostedcluster_types.go index 42a98277c51..2aa9d9960fc 100644 --- a/api/hypershift/v1beta1/hostedcluster_types.go +++ b/api/hypershift/v1beta1/hostedcluster_types.go @@ -1805,7 +1805,7 @@ type AzurePlatformSpec struct { // // Resource group naming requirements can be found here: https://azure.github.io/PSRule.Rules.Azure/en/rules/Azure.ResourceGroup.Name/. // - //Example: if your resource group ID is /subscriptions//resourceGroups/, your + // Example: if your resource group ID is /subscriptions//resourceGroups/, your // ResourceGroupName is . // // +kubebuilder:default:=default @@ -1857,8 +1857,98 @@ type AzurePlatformSpec struct { // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="SecurityGroupID is immutable" // +kubebuilder:validation:Required // +immutable - // +required SecurityGroupID string `json:"securityGroupID,omitempty"` + + // managedIdentities contains the client IDs related to the managed identities needed for HCP control plane + // and data plane components that authenticate with Azure's API. + // + // +kubebuilder:validation:Required + ManagedIdentities AzureResourceManagedIdentities `json:"managedIdentities,omitempty"` +} + +// AzureResourceManagedIdentities contains the client IDs related to the managed identities needed for HCP control plane +// and data plane components that authenticate with Azure's API. +type AzureResourceManagedIdentities struct { + // ControlPlaneManagedIdentities contains the client IDs of all the managed identities on the HCP control plane needing to + // authenticate with Azure's API. + // + // +kubebuilder:validation:Required + ControlPlaneManagedIdentities ControlPlaneManagedIdentities `json:"controlPlaneManagedIdentities"` + + // Future placeholder - DataPlaneMIs * DataPlaneManagedIdentities +} + +// ManagedIdentityClientID is a client ID of a managed identity +// +kubebuilder:validation:XValidation:rule="self.matches('^[{]?[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}[}]?$')",message="the client ID of a managed identity must be a valid UUID. It should be 5 groups of hyphen separated hexadecimal characters in the form 8-4-4-4-12." +type ManagedIdentityClientID string + +// ControlPlaneManagedIdentities contains the client IDs of all the managed identities on the HCP control plane needing +// to authenticate with Azure's API. +// Managed identity regex pattern is from Microsoft here - https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/resource-name-rules#microsoftmanagedidentity. +// The format a managed identity should be `/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{managedIdentityName}`. +type ControlPlaneManagedIdentities struct { + // azureCloudProviderManagedIdentityClientID is the client ID of a pre-existing managed identity ID associated with the azure + // cloud provider, aka ccm. The client ID of a managed identity must be a valid UUID. It should be 5 groups of + // hyphen separated hexadecimal characters in the form 8-4-4-4-12. + // + // +kubebuilder:validation:Required + AzureCloudProviderManagedIdentityClientID ManagedIdentityClientID `json:"azureCloudProviderManagedIdentityClientID"` + + // clusterAPIAzureManagedIdentityClientID is the client ID of a pre-existing managed identity ID associated with cluster-api + // azure. The client ID of a managed identity must be a valid UUID. It should be 5 groups of hyphen separated + // hexadecimal characters in the form 8-4-4-4-12. + // + // +kubebuilder:validation:Required + ClusterAPIAzureManagedIdentityClientID ManagedIdentityClientID `json:"clusterAPIAzureManagedIdentityClientID"` + + // controlPlaneManagedIdentityClientID is the client ID of a pre-existing managed identity ID associated with the control plane + // operator. The client ID of a managed identity must be a valid UUID. It should be 5 groups of hyphen separated + // hexadecimal characters in the form 8-4-4-4-12. + // + // +kubebuilder:validation:Required + ControlPlaneManagedIdentityClientID ManagedIdentityClientID `json:"controlPlaneManagedIdentityClientID"` + + // azureKMSManagedIdentityClientID is the client ID of a pre-existing managed identity ID associated with Azure KMS. The client + // ID of a managed identity must be a valid UUID. It should be 5 groups of hyphen separated hexadecimal characters + // in the form 8-4-4-4-12. + // + // +optional + AzureKMSManagedIdentityClientID ManagedIdentityClientID `json:"azureKMSManagedIdentityClientID,omitempty"` + + // imageRegistryManagedIdentityClientID is the client ID of a pre-existing managed identity ID associated with the + // cluster-image-registry-operator. The client ID of a managed identity must be a valid UUID. It should be 5 groups + // of hyphen separated hexadecimal characters in the form 8-4-4-4-12. + // + // +kubebuilder:validation:Required + ImageRegistryManagedIdentityClientID ManagedIdentityClientID `json:"imageRegistryManagedIdentityClientID"` + + // ingressManagedIdentityClientID is the client ID of a pre-existing managed identity ID associated with the + // cluster-ingress-operator. The client ID of a managed identity must be a valid UUID. It should be 5 groups of + // hyphen separated hexadecimal characters in the form 8-4-4-4-12. + // + // +kubebuilder:validation:Required + IngressManagedIdentityClientID ManagedIdentityClientID `json:"ingressManagedIdentityClientID"` + + // networkManagedIdentityClientID is the client ID of a pre-existing managed identity ID associated with the + // cluster-network-operator. The client ID of a managed identity must be a valid UUID. It should be 5 groups of + // hyphen separated hexadecimal characters in the form 8-4-4-4-12. + // + // +kubebuilder:validation:Required + NetworkManagedIdentityClientID ManagedIdentityClientID `json:"networkManagedIdentityClientID"` + + // azureDiskManagedIdentityClientID is the client ID of a pre-existing managed identity ID associated with the + // azure-disk-controller. The client ID of a managed identity must be a valid UUID. It should be 5 groups of hyphen + // separated hexadecimal characters in the form 8-4-4-4-12. + // + // +kubebuilder:validation:Required + AzureDiskManagedIdentityClientID ManagedIdentityClientID `json:"azureDiskManagedIdentityClientID"` + + // azureFileManagedIdentityClientID is the client ID of a pre-existing managed identity ID associated with the + // azure-disk-controller. The client ID of a managed identity must be a valid UUID. It should be 5 groups of hyphen + // separated hexadecimal characters in the form 8-4-4-4-12. + // + // +kubebuilder:validation:Required + AzureFileManagedIdentityClientID ManagedIdentityClientID `json:"azureFileManagedIdentityClientID"` } // OpenStackPlatformSpec specifies configuration for clusters running on OpenStack. diff --git a/api/hypershift/v1beta1/zz_generated.deepcopy.go b/api/hypershift/v1beta1/zz_generated.deepcopy.go index 14e4e51e549..ff6b0f7894b 100644 --- a/api/hypershift/v1beta1/zz_generated.deepcopy.go +++ b/api/hypershift/v1beta1/zz_generated.deepcopy.go @@ -564,6 +564,7 @@ func (in *AzureNodePoolPlatform) DeepCopy() *AzureNodePoolPlatform { func (in *AzurePlatformSpec) DeepCopyInto(out *AzurePlatformSpec) { *out = *in out.Credentials = in.Credentials + out.ManagedIdentities = in.ManagedIdentities } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AzurePlatformSpec. @@ -576,6 +577,22 @@ func (in *AzurePlatformSpec) DeepCopy() *AzurePlatformSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AzureResourceManagedIdentities) DeepCopyInto(out *AzureResourceManagedIdentities) { + *out = *in + out.ControlPlaneManagedIdentities = in.ControlPlaneManagedIdentities +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AzureResourceManagedIdentities. +func (in *AzureResourceManagedIdentities) DeepCopy() *AzureResourceManagedIdentities { + if in == nil { + return nil + } + out := new(AzureResourceManagedIdentities) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AzureVMImage) DeepCopyInto(out *AzureVMImage) { *out = *in @@ -879,6 +896,21 @@ func (in *ClusterVersionStatus) DeepCopy() *ClusterVersionStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ControlPlaneManagedIdentities) DeepCopyInto(out *ControlPlaneManagedIdentities) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControlPlaneManagedIdentities. +func (in *ControlPlaneManagedIdentities) DeepCopy() *ControlPlaneManagedIdentities { + if in == nil { + return nil + } + out := new(ControlPlaneManagedIdentities) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DNSSpec) DeepCopyInto(out *DNSSpec) { *out = *in diff --git a/client/applyconfiguration/hypershift/v1beta1/azureresourcemanagedidentities.go b/client/applyconfiguration/hypershift/v1beta1/azureresourcemanagedidentities.go new file mode 100644 index 00000000000..c662e88f3e8 --- /dev/null +++ b/client/applyconfiguration/hypershift/v1beta1/azureresourcemanagedidentities.go @@ -0,0 +1,38 @@ +/* + + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1beta1 + +// AzureResourceManagedIdentitiesApplyConfiguration represents an declarative configuration of the AzureResourceManagedIdentities type for use +// with apply. +type AzureResourceManagedIdentitiesApplyConfiguration struct { + ControlPlaneManagedIdentities *ControlPlaneManagedIdentitiesApplyConfiguration `json:"controlPlaneManagedIdentities,omitempty"` +} + +// AzureResourceManagedIdentitiesApplyConfiguration constructs an declarative configuration of the AzureResourceManagedIdentities type for use with +// apply. +func AzureResourceManagedIdentities() *AzureResourceManagedIdentitiesApplyConfiguration { + return &AzureResourceManagedIdentitiesApplyConfiguration{} +} + +// WithControlPlaneManagedIdentities sets the ControlPlaneManagedIdentities field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ControlPlaneManagedIdentities field is set to the value of the last call. +func (b *AzureResourceManagedIdentitiesApplyConfiguration) WithControlPlaneManagedIdentities(value *ControlPlaneManagedIdentitiesApplyConfiguration) *AzureResourceManagedIdentitiesApplyConfiguration { + b.ControlPlaneManagedIdentities = value + return b +} diff --git a/client/applyconfiguration/hypershift/v1beta1/controlplanemanagedidentities.go b/client/applyconfiguration/hypershift/v1beta1/controlplanemanagedidentities.go new file mode 100644 index 00000000000..1c7d273cb17 --- /dev/null +++ b/client/applyconfiguration/hypershift/v1beta1/controlplanemanagedidentities.go @@ -0,0 +1,114 @@ +/* + + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1beta1 + +import ( + v1beta1 "github.com/openshift/hypershift/api/hypershift/v1beta1" +) + +// ControlPlaneManagedIdentitiesApplyConfiguration represents an declarative configuration of the ControlPlaneManagedIdentities type for use +// with apply. +type ControlPlaneManagedIdentitiesApplyConfiguration struct { + AzureCloudProviderManagedIdentityClientID *v1beta1.ManagedIdentityClientID `json:"azureCloudProviderManagedIdentityClientID,omitempty"` + ClusterAPIAzureManagedIdentityClientID *v1beta1.ManagedIdentityClientID `json:"clusterAPIAzureManagedIdentityClientID,omitempty"` + ControlPlaneManagedIdentityClientID *v1beta1.ManagedIdentityClientID `json:"controlPlaneManagedIdentityClientID,omitempty"` + AzureKMSManagedIdentityClientID *v1beta1.ManagedIdentityClientID `json:"azureKMSManagedIdentityClientID,omitempty"` + ImageRegistryManagedIdentityClientID *v1beta1.ManagedIdentityClientID `json:"imageRegistryManagedIdentityClientID,omitempty"` + IngressManagedIdentityClientID *v1beta1.ManagedIdentityClientID `json:"ingressManagedIdentityClientID,omitempty"` + NetworkManagedIdentityClientID *v1beta1.ManagedIdentityClientID `json:"networkManagedIdentityClientID,omitempty"` + AzureDiskManagedIdentityClientID *v1beta1.ManagedIdentityClientID `json:"azureDiskManagedIdentityClientID,omitempty"` + AzureFileManagedIdentityClientID *v1beta1.ManagedIdentityClientID `json:"azureFileManagedIdentityClientID,omitempty"` +} + +// ControlPlaneManagedIdentitiesApplyConfiguration constructs an declarative configuration of the ControlPlaneManagedIdentities type for use with +// apply. +func ControlPlaneManagedIdentities() *ControlPlaneManagedIdentitiesApplyConfiguration { + return &ControlPlaneManagedIdentitiesApplyConfiguration{} +} + +// WithAzureCloudProviderManagedIdentityClientID sets the AzureCloudProviderManagedIdentityClientID field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the AzureCloudProviderManagedIdentityClientID field is set to the value of the last call. +func (b *ControlPlaneManagedIdentitiesApplyConfiguration) WithAzureCloudProviderManagedIdentityClientID(value v1beta1.ManagedIdentityClientID) *ControlPlaneManagedIdentitiesApplyConfiguration { + b.AzureCloudProviderManagedIdentityClientID = &value + return b +} + +// WithClusterAPIAzureManagedIdentityClientID sets the ClusterAPIAzureManagedIdentityClientID field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ClusterAPIAzureManagedIdentityClientID field is set to the value of the last call. +func (b *ControlPlaneManagedIdentitiesApplyConfiguration) WithClusterAPIAzureManagedIdentityClientID(value v1beta1.ManagedIdentityClientID) *ControlPlaneManagedIdentitiesApplyConfiguration { + b.ClusterAPIAzureManagedIdentityClientID = &value + return b +} + +// WithControlPlaneManagedIdentityClientID sets the ControlPlaneManagedIdentityClientID field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ControlPlaneManagedIdentityClientID field is set to the value of the last call. +func (b *ControlPlaneManagedIdentitiesApplyConfiguration) WithControlPlaneManagedIdentityClientID(value v1beta1.ManagedIdentityClientID) *ControlPlaneManagedIdentitiesApplyConfiguration { + b.ControlPlaneManagedIdentityClientID = &value + return b +} + +// WithAzureKMSManagedIdentityClientID sets the AzureKMSManagedIdentityClientID field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the AzureKMSManagedIdentityClientID field is set to the value of the last call. +func (b *ControlPlaneManagedIdentitiesApplyConfiguration) WithAzureKMSManagedIdentityClientID(value v1beta1.ManagedIdentityClientID) *ControlPlaneManagedIdentitiesApplyConfiguration { + b.AzureKMSManagedIdentityClientID = &value + return b +} + +// WithImageRegistryManagedIdentityClientID sets the ImageRegistryManagedIdentityClientID field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ImageRegistryManagedIdentityClientID field is set to the value of the last call. +func (b *ControlPlaneManagedIdentitiesApplyConfiguration) WithImageRegistryManagedIdentityClientID(value v1beta1.ManagedIdentityClientID) *ControlPlaneManagedIdentitiesApplyConfiguration { + b.ImageRegistryManagedIdentityClientID = &value + return b +} + +// WithIngressManagedIdentityClientID sets the IngressManagedIdentityClientID field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the IngressManagedIdentityClientID field is set to the value of the last call. +func (b *ControlPlaneManagedIdentitiesApplyConfiguration) WithIngressManagedIdentityClientID(value v1beta1.ManagedIdentityClientID) *ControlPlaneManagedIdentitiesApplyConfiguration { + b.IngressManagedIdentityClientID = &value + return b +} + +// WithNetworkManagedIdentityClientID sets the NetworkManagedIdentityClientID field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the NetworkManagedIdentityClientID field is set to the value of the last call. +func (b *ControlPlaneManagedIdentitiesApplyConfiguration) WithNetworkManagedIdentityClientID(value v1beta1.ManagedIdentityClientID) *ControlPlaneManagedIdentitiesApplyConfiguration { + b.NetworkManagedIdentityClientID = &value + return b +} + +// WithAzureDiskManagedIdentityClientID sets the AzureDiskManagedIdentityClientID field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the AzureDiskManagedIdentityClientID field is set to the value of the last call. +func (b *ControlPlaneManagedIdentitiesApplyConfiguration) WithAzureDiskManagedIdentityClientID(value v1beta1.ManagedIdentityClientID) *ControlPlaneManagedIdentitiesApplyConfiguration { + b.AzureDiskManagedIdentityClientID = &value + return b +} + +// WithAzureFileManagedIdentityClientID sets the AzureFileManagedIdentityClientID field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the AzureFileManagedIdentityClientID field is set to the value of the last call. +func (b *ControlPlaneManagedIdentitiesApplyConfiguration) WithAzureFileManagedIdentityClientID(value v1beta1.ManagedIdentityClientID) *ControlPlaneManagedIdentitiesApplyConfiguration { + b.AzureFileManagedIdentityClientID = &value + return b +} diff --git a/cmd/cluster/azure/create.go b/cmd/cluster/azure/create.go index 7387b8bc581..2e2b07ee26a 100644 --- a/cmd/cluster/azure/create.go +++ b/cmd/cluster/azure/create.go @@ -201,6 +201,22 @@ func (o *CreateOptions) ApplyPlatformSpecifics(cluster *hyperv1.HostedCluster) e VnetID: o.infra.VNetID, SubnetID: o.infra.SubnetID, SecurityGroupID: o.infra.SecurityGroupID, + ManagedIdentities: hyperv1.AzureResourceManagedIdentities{ + ControlPlaneManagedIdentities: hyperv1.ControlPlaneManagedIdentities{ + // TODO these are initialized with the client ID of the Service Principal at the moment. Once the + // Microsoft Adapter sidecar containers support Managed Identities, the CLI will create a new + // managed identity for each of these fields. + AzureCloudProviderManagedIdentityClientID: hyperv1.ManagedIdentityClientID(o.creds.ClientID), + ClusterAPIAzureManagedIdentityClientID: hyperv1.ManagedIdentityClientID(o.creds.ClientID), + AzureKMSManagedIdentityClientID: hyperv1.ManagedIdentityClientID(o.creds.ClientID), + ControlPlaneManagedIdentityClientID: hyperv1.ManagedIdentityClientID(o.creds.ClientID), + NetworkManagedIdentityClientID: hyperv1.ManagedIdentityClientID(o.creds.ClientID), + ImageRegistryManagedIdentityClientID: hyperv1.ManagedIdentityClientID(o.creds.ClientID), + IngressManagedIdentityClientID: hyperv1.ManagedIdentityClientID(o.creds.ClientID), + AzureFileManagedIdentityClientID: hyperv1.ManagedIdentityClientID(o.creds.ClientID), + AzureDiskManagedIdentityClientID: hyperv1.ManagedIdentityClientID(o.creds.ClientID), + }, + }, }, } diff --git a/cmd/cluster/azure/testdata/zz_fixture_TestCreateCluster_complicated_invocation_from_bryan.yaml b/cmd/cluster/azure/testdata/zz_fixture_TestCreateCluster_complicated_invocation_from_bryan.yaml index cc074969181..bc3c19d5ca9 100644 --- a/cmd/cluster/azure/testdata/zz_fixture_TestCreateCluster_complicated_invocation_from_bryan.yaml +++ b/cmd/cluster/azure/testdata/zz_fixture_TestCreateCluster_complicated_invocation_from_bryan.yaml @@ -52,6 +52,17 @@ spec: credentials: name: bryans-cluster-cloud-credentials location: fakeLocation + managedIdentities: + controlPlaneManagedIdentities: + azureCloudProviderManagedIdentityClientID: fakeClientID + azureDiskManagedIdentityClientID: fakeClientID + azureFileManagedIdentityClientID: fakeClientID + azureKMSManagedIdentityClientID: fakeClientID + clusterAPIAzureManagedIdentityClientID: fakeClientID + controlPlaneManagedIdentityClientID: fakeClientID + imageRegistryManagedIdentityClientID: fakeClientID + ingressManagedIdentityClientID: fakeClientID + networkManagedIdentityClientID: fakeClientID resourceGroup: fakeResourceGroupName securityGroupID: fakeSecurityGroupID subnetID: fakeSubnetID diff --git a/cmd/cluster/azure/testdata/zz_fixture_TestCreateCluster_create_with_a_ure_marketplace_image.yaml b/cmd/cluster/azure/testdata/zz_fixture_TestCreateCluster_create_with_a_ure_marketplace_image.yaml index a9cdc403ebb..24cae45ddae 100644 --- a/cmd/cluster/azure/testdata/zz_fixture_TestCreateCluster_create_with_a_ure_marketplace_image.yaml +++ b/cmd/cluster/azure/testdata/zz_fixture_TestCreateCluster_create_with_a_ure_marketplace_image.yaml @@ -41,6 +41,17 @@ spec: credentials: name: bryans-cluster-cloud-credentials location: fakeLocation + managedIdentities: + controlPlaneManagedIdentities: + azureCloudProviderManagedIdentityClientID: fakeClientID + azureDiskManagedIdentityClientID: fakeClientID + azureFileManagedIdentityClientID: fakeClientID + azureKMSManagedIdentityClientID: fakeClientID + clusterAPIAzureManagedIdentityClientID: fakeClientID + controlPlaneManagedIdentityClientID: fakeClientID + imageRegistryManagedIdentityClientID: fakeClientID + ingressManagedIdentityClientID: fakeClientID + networkManagedIdentityClientID: fakeClientID resourceGroup: fakeResourceGroupName securityGroupID: fakeSecurityGroupID subnetID: fakeSubnetID diff --git a/cmd/cluster/azure/testdata/zz_fixture_TestCreateCluster_minimal_flags_necessary_to_render.yaml b/cmd/cluster/azure/testdata/zz_fixture_TestCreateCluster_minimal_flags_necessary_to_render.yaml index 736f6791100..34240261323 100644 --- a/cmd/cluster/azure/testdata/zz_fixture_TestCreateCluster_minimal_flags_necessary_to_render.yaml +++ b/cmd/cluster/azure/testdata/zz_fixture_TestCreateCluster_minimal_flags_necessary_to_render.yaml @@ -52,6 +52,17 @@ spec: credentials: name: example-cloud-credentials location: fakeLocation + managedIdentities: + controlPlaneManagedIdentities: + azureCloudProviderManagedIdentityClientID: fakeClientID + azureDiskManagedIdentityClientID: fakeClientID + azureFileManagedIdentityClientID: fakeClientID + azureKMSManagedIdentityClientID: fakeClientID + clusterAPIAzureManagedIdentityClientID: fakeClientID + controlPlaneManagedIdentityClientID: fakeClientID + imageRegistryManagedIdentityClientID: fakeClientID + ingressManagedIdentityClientID: fakeClientID + networkManagedIdentityClientID: fakeClientID resourceGroup: fakeResourceGroupName securityGroupID: fakeSecurityGroupID subnetID: fakeSubnetID diff --git a/cmd/install/assets/hypershift-operator/hypershift.openshift.io_hostedclusters.yaml b/cmd/install/assets/hypershift-operator/hypershift.openshift.io_hostedclusters.yaml index 72c396dbcdc..94fd151abbc 100644 --- a/cmd/install/assets/hypershift-operator/hypershift.openshift.io_hostedclusters.yaml +++ b/cmd/install/assets/hypershift-operator/hypershift.openshift.io_hostedclusters.yaml @@ -8255,6 +8255,128 @@ spec: x-kubernetes-validations: - message: Location is immutable rule: self == oldSelf + managedIdentities: + description: |- + managedIdentities contains the client IDs related to the managed identities needed for HCP control plane + and data plane components that authenticate with Azure's API. + properties: + controlPlaneManagedIdentities: + description: |- + ControlPlaneManagedIdentities contains the client IDs of all the managed identities on the HCP control plane needing to + authenticate with Azure's API. + properties: + azureCloudProviderManagedIdentityClientID: + description: |- + azureCloudProviderManagedIdentityClientID is the client ID of a pre-existing managed identity ID associated with the azure + cloud provider, aka ccm. The client ID of a managed identity must be a valid UUID. It should be 5 groups of + hyphen separated hexadecimal characters in the form 8-4-4-4-12. + type: string + x-kubernetes-validations: + - message: the client ID of a managed identity must + be a valid UUID. It should be 5 groups of hyphen + separated hexadecimal characters in the form 8-4-4-4-12. + rule: self.matches('^[{]?[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}[}]?$') + azureDiskManagedIdentityClientID: + description: |- + azureDiskManagedIdentityClientID is the client ID of a pre-existing managed identity ID associated with the + azure-disk-controller. The client ID of a managed identity must be a valid UUID. It should be 5 groups of hyphen + separated hexadecimal characters in the form 8-4-4-4-12. + type: string + x-kubernetes-validations: + - message: the client ID of a managed identity must + be a valid UUID. It should be 5 groups of hyphen + separated hexadecimal characters in the form 8-4-4-4-12. + rule: self.matches('^[{]?[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}[}]?$') + azureFileManagedIdentityClientID: + description: |- + azureFileManagedIdentityClientID is the client ID of a pre-existing managed identity ID associated with the + azure-disk-controller. The client ID of a managed identity must be a valid UUID. It should be 5 groups of hyphen + separated hexadecimal characters in the form 8-4-4-4-12. + type: string + x-kubernetes-validations: + - message: the client ID of a managed identity must + be a valid UUID. It should be 5 groups of hyphen + separated hexadecimal characters in the form 8-4-4-4-12. + rule: self.matches('^[{]?[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}[}]?$') + azureKMSManagedIdentityClientID: + description: |- + azureKMSManagedIdentityClientID is the client ID of a pre-existing managed identity ID associated with Azure KMS. The client + ID of a managed identity must be a valid UUID. It should be 5 groups of hyphen separated hexadecimal characters + in the form 8-4-4-4-12. + type: string + x-kubernetes-validations: + - message: the client ID of a managed identity must + be a valid UUID. It should be 5 groups of hyphen + separated hexadecimal characters in the form 8-4-4-4-12. + rule: self.matches('^[{]?[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}[}]?$') + clusterAPIAzureManagedIdentityClientID: + description: |- + clusterAPIAzureManagedIdentityClientID is the client ID of a pre-existing managed identity ID associated with cluster-api + azure. The client ID of a managed identity must be a valid UUID. It should be 5 groups of hyphen separated + hexadecimal characters in the form 8-4-4-4-12. + type: string + x-kubernetes-validations: + - message: the client ID of a managed identity must + be a valid UUID. It should be 5 groups of hyphen + separated hexadecimal characters in the form 8-4-4-4-12. + rule: self.matches('^[{]?[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}[}]?$') + controlPlaneManagedIdentityClientID: + description: |- + controlPlaneManagedIdentityClientID is the client ID of a pre-existing managed identity ID associated with the control plane + operator. The client ID of a managed identity must be a valid UUID. It should be 5 groups of hyphen separated + hexadecimal characters in the form 8-4-4-4-12. + type: string + x-kubernetes-validations: + - message: the client ID of a managed identity must + be a valid UUID. It should be 5 groups of hyphen + separated hexadecimal characters in the form 8-4-4-4-12. + rule: self.matches('^[{]?[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}[}]?$') + imageRegistryManagedIdentityClientID: + description: |- + imageRegistryManagedIdentityClientID is the client ID of a pre-existing managed identity ID associated with the + cluster-image-registry-operator. The client ID of a managed identity must be a valid UUID. It should be 5 groups + of hyphen separated hexadecimal characters in the form 8-4-4-4-12. + type: string + x-kubernetes-validations: + - message: the client ID of a managed identity must + be a valid UUID. It should be 5 groups of hyphen + separated hexadecimal characters in the form 8-4-4-4-12. + rule: self.matches('^[{]?[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}[}]?$') + ingressManagedIdentityClientID: + description: |- + ingressManagedIdentityClientID is the client ID of a pre-existing managed identity ID associated with the + cluster-ingress-operator. The client ID of a managed identity must be a valid UUID. It should be 5 groups of + hyphen separated hexadecimal characters in the form 8-4-4-4-12. + type: string + x-kubernetes-validations: + - message: the client ID of a managed identity must + be a valid UUID. It should be 5 groups of hyphen + separated hexadecimal characters in the form 8-4-4-4-12. + rule: self.matches('^[{]?[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}[}]?$') + networkManagedIdentityClientID: + description: |- + networkManagedIdentityClientID is the client ID of a pre-existing managed identity ID associated with the + cluster-network-operator. The client ID of a managed identity must be a valid UUID. It should be 5 groups of + hyphen separated hexadecimal characters in the form 8-4-4-4-12. + type: string + x-kubernetes-validations: + - message: the client ID of a managed identity must + be a valid UUID. It should be 5 groups of hyphen + separated hexadecimal characters in the form 8-4-4-4-12. + rule: self.matches('^[{]?[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}[}]?$') + required: + - azureCloudProviderManagedIdentityClientID + - azureDiskManagedIdentityClientID + - azureFileManagedIdentityClientID + - clusterAPIAzureManagedIdentityClientID + - controlPlaneManagedIdentityClientID + - imageRegistryManagedIdentityClientID + - ingressManagedIdentityClientID + - networkManagedIdentityClientID + type: object + required: + - controlPlaneManagedIdentities + type: object resourceGroup: default: default description: |- diff --git a/cmd/install/assets/hypershift-operator/hypershift.openshift.io_hostedcontrolplanes.yaml b/cmd/install/assets/hypershift-operator/hypershift.openshift.io_hostedcontrolplanes.yaml index 684cb311f3c..07bbf77c4f4 100644 --- a/cmd/install/assets/hypershift-operator/hypershift.openshift.io_hostedcontrolplanes.yaml +++ b/cmd/install/assets/hypershift-operator/hypershift.openshift.io_hostedcontrolplanes.yaml @@ -8219,6 +8219,128 @@ spec: x-kubernetes-validations: - message: Location is immutable rule: self == oldSelf + managedIdentities: + description: |- + managedIdentities contains the client IDs related to the managed identities needed for HCP control plane + and data plane components that authenticate with Azure's API. + properties: + controlPlaneManagedIdentities: + description: |- + ControlPlaneManagedIdentities contains the client IDs of all the managed identities on the HCP control plane needing to + authenticate with Azure's API. + properties: + azureCloudProviderManagedIdentityClientID: + description: |- + azureCloudProviderManagedIdentityClientID is the client ID of a pre-existing managed identity ID associated with the azure + cloud provider, aka ccm. The client ID of a managed identity must be a valid UUID. It should be 5 groups of + hyphen separated hexadecimal characters in the form 8-4-4-4-12. + type: string + x-kubernetes-validations: + - message: the client ID of a managed identity must + be a valid UUID. It should be 5 groups of hyphen + separated hexadecimal characters in the form 8-4-4-4-12. + rule: self.matches('^[{]?[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}[}]?$') + azureDiskManagedIdentityClientID: + description: |- + azureDiskManagedIdentityClientID is the client ID of a pre-existing managed identity ID associated with the + azure-disk-controller. The client ID of a managed identity must be a valid UUID. It should be 5 groups of hyphen + separated hexadecimal characters in the form 8-4-4-4-12. + type: string + x-kubernetes-validations: + - message: the client ID of a managed identity must + be a valid UUID. It should be 5 groups of hyphen + separated hexadecimal characters in the form 8-4-4-4-12. + rule: self.matches('^[{]?[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}[}]?$') + azureFileManagedIdentityClientID: + description: |- + azureFileManagedIdentityClientID is the client ID of a pre-existing managed identity ID associated with the + azure-disk-controller. The client ID of a managed identity must be a valid UUID. It should be 5 groups of hyphen + separated hexadecimal characters in the form 8-4-4-4-12. + type: string + x-kubernetes-validations: + - message: the client ID of a managed identity must + be a valid UUID. It should be 5 groups of hyphen + separated hexadecimal characters in the form 8-4-4-4-12. + rule: self.matches('^[{]?[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}[}]?$') + azureKMSManagedIdentityClientID: + description: |- + azureKMSManagedIdentityClientID is the client ID of a pre-existing managed identity ID associated with Azure KMS. The client + ID of a managed identity must be a valid UUID. It should be 5 groups of hyphen separated hexadecimal characters + in the form 8-4-4-4-12. + type: string + x-kubernetes-validations: + - message: the client ID of a managed identity must + be a valid UUID. It should be 5 groups of hyphen + separated hexadecimal characters in the form 8-4-4-4-12. + rule: self.matches('^[{]?[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}[}]?$') + clusterAPIAzureManagedIdentityClientID: + description: |- + clusterAPIAzureManagedIdentityClientID is the client ID of a pre-existing managed identity ID associated with cluster-api + azure. The client ID of a managed identity must be a valid UUID. It should be 5 groups of hyphen separated + hexadecimal characters in the form 8-4-4-4-12. + type: string + x-kubernetes-validations: + - message: the client ID of a managed identity must + be a valid UUID. It should be 5 groups of hyphen + separated hexadecimal characters in the form 8-4-4-4-12. + rule: self.matches('^[{]?[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}[}]?$') + controlPlaneManagedIdentityClientID: + description: |- + controlPlaneManagedIdentityClientID is the client ID of a pre-existing managed identity ID associated with the control plane + operator. The client ID of a managed identity must be a valid UUID. It should be 5 groups of hyphen separated + hexadecimal characters in the form 8-4-4-4-12. + type: string + x-kubernetes-validations: + - message: the client ID of a managed identity must + be a valid UUID. It should be 5 groups of hyphen + separated hexadecimal characters in the form 8-4-4-4-12. + rule: self.matches('^[{]?[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}[}]?$') + imageRegistryManagedIdentityClientID: + description: |- + imageRegistryManagedIdentityClientID is the client ID of a pre-existing managed identity ID associated with the + cluster-image-registry-operator. The client ID of a managed identity must be a valid UUID. It should be 5 groups + of hyphen separated hexadecimal characters in the form 8-4-4-4-12. + type: string + x-kubernetes-validations: + - message: the client ID of a managed identity must + be a valid UUID. It should be 5 groups of hyphen + separated hexadecimal characters in the form 8-4-4-4-12. + rule: self.matches('^[{]?[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}[}]?$') + ingressManagedIdentityClientID: + description: |- + ingressManagedIdentityClientID is the client ID of a pre-existing managed identity ID associated with the + cluster-ingress-operator. The client ID of a managed identity must be a valid UUID. It should be 5 groups of + hyphen separated hexadecimal characters in the form 8-4-4-4-12. + type: string + x-kubernetes-validations: + - message: the client ID of a managed identity must + be a valid UUID. It should be 5 groups of hyphen + separated hexadecimal characters in the form 8-4-4-4-12. + rule: self.matches('^[{]?[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}[}]?$') + networkManagedIdentityClientID: + description: |- + networkManagedIdentityClientID is the client ID of a pre-existing managed identity ID associated with the + cluster-network-operator. The client ID of a managed identity must be a valid UUID. It should be 5 groups of + hyphen separated hexadecimal characters in the form 8-4-4-4-12. + type: string + x-kubernetes-validations: + - message: the client ID of a managed identity must + be a valid UUID. It should be 5 groups of hyphen + separated hexadecimal characters in the form 8-4-4-4-12. + rule: self.matches('^[{]?[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}[}]?$') + required: + - azureCloudProviderManagedIdentityClientID + - azureDiskManagedIdentityClientID + - azureFileManagedIdentityClientID + - clusterAPIAzureManagedIdentityClientID + - controlPlaneManagedIdentityClientID + - imageRegistryManagedIdentityClientID + - ingressManagedIdentityClientID + - networkManagedIdentityClientID + type: object + required: + - controlPlaneManagedIdentities + type: object resourceGroup: default: default description: |- diff --git a/control-plane-operator/controllers/hostedcontrolplane/cloud/azure/params.go b/control-plane-operator/controllers/hostedcontrolplane/cloud/azure/params.go index 27073cd9b94..58d83e04ce9 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/cloud/azure/params.go +++ b/control-plane-operator/controllers/hostedcontrolplane/cloud/azure/params.go @@ -42,6 +42,7 @@ func NewAzureParams(hcp *hyperv1.HostedControlPlane) *AzureParams { p.DeploymentConfig.Scheduling.PriorityClass = hcp.Annotations[hyperv1.ControlPlanePriorityClass] } p.DeploymentConfig.SetRestartAnnotation(hcp.ObjectMeta) + p.DeploymentConfig.SetDefaultSecurityContext = false return p } diff --git a/control-plane-operator/controllers/hostedcontrolplane/cloud/azure/providerconfig.go b/control-plane-operator/controllers/hostedcontrolplane/cloud/azure/providerconfig.go index 0cf682ebfe4..64d2e00ffab 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/cloud/azure/providerconfig.go +++ b/control-plane-operator/controllers/hostedcontrolplane/cloud/azure/providerconfig.go @@ -17,7 +17,7 @@ const ( // ReconcileCloudConfig reconciles as expected by Nodes Kubelet. func ReconcileCloudConfig(cm *corev1.ConfigMap, hcp *hyperv1.HostedControlPlane, credentialsSecret *corev1.Secret) error { - cfg, err := azureConfigWithoutCredentials(hcp, credentialsSecret) + cfg, err := AzureConfigWithoutCredentials(hcp, credentialsSecret) if err != nil { return err } @@ -37,14 +37,13 @@ func ReconcileCloudConfig(cm *corev1.ConfigMap, hcp *hyperv1.HostedControlPlane, // ReconcileCloudConfigWithCredentials reconciles as expected by KAS/KCM. func ReconcileCloudConfigWithCredentials(secret *corev1.Secret, hcp *hyperv1.HostedControlPlane, credentialsSecret *corev1.Secret) error { - cfg, err := azureConfigWithoutCredentials(hcp, credentialsSecret) + cfg, err := AzureConfigWithoutCredentials(hcp, credentialsSecret) if err != nil { return err } - cfg.AADClientID = string(credentialsSecret.Data["AZURE_CLIENT_ID"]) - cfg.AADClientSecret = string(credentialsSecret.Data["AZURE_CLIENT_SECRET"]) - cfg.UseManagedIdentityExtension = false + cfg.UserAssignedIdentityID = string(hcp.Spec.Platform.Azure.ManagedIdentities.ControlPlaneManagedIdentities.AzureCloudProviderManagedIdentityClientID) + cfg.UseInstanceMetadata = false serializedConfig, err := json.MarshalIndent(cfg, "", " ") if err != nil { @@ -58,7 +57,7 @@ func ReconcileCloudConfigWithCredentials(secret *corev1.Secret, hcp *hyperv1.Hos return nil } -func azureConfigWithoutCredentials(hcp *hyperv1.HostedControlPlane, credentialsSecret *corev1.Secret) (AzureConfig, error) { +func AzureConfigWithoutCredentials(hcp *hyperv1.HostedControlPlane, credentialsSecret *corev1.Secret) (AzureConfig, error) { subnetName, err := azureutil.GetSubnetNameFromSubnetID(hcp.Spec.Platform.Azure.SubnetID) if err != nil { return AzureConfig{}, fmt.Errorf("failed to determine subnet name from SubnetID: %w", err) @@ -106,9 +105,8 @@ type AzureConfig struct { Cloud string `json:"cloud"` TenantID string `json:"tenantId"` UseManagedIdentityExtension bool `json:"useManagedIdentityExtension"` + UserAssignedIdentityID string `json:"userAssignedIdentityID"` SubscriptionID string `json:"subscriptionId"` - AADClientID string `json:"aadClientId"` - AADClientSecret string `json:"aadClientSecret"` ResourceGroup string `json:"resourceGroup"` Location string `json:"location"` VnetName string `json:"vnetName"` diff --git a/control-plane-operator/controllers/hostedcontrolplane/cloud/azure/reconcile.go b/control-plane-operator/controllers/hostedcontrolplane/cloud/azure/reconcile.go index 69c33f0737f..df1280e96a1 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/cloud/azure/reconcile.go +++ b/control-plane-operator/controllers/hostedcontrolplane/cloud/azure/reconcile.go @@ -1,12 +1,15 @@ package azure import ( + "context" "fmt" + "sigs.k8s.io/controller-runtime/pkg/client" hyperv1 "github.com/openshift/hypershift/api/hypershift/v1beta1" "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/imageprovider" "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/manifests" "github.com/openshift/hypershift/hypershift-operator/controllers/manifests/controlplaneoperator" + "github.com/openshift/hypershift/support/azureutil" "github.com/openshift/hypershift/support/config" "github.com/openshift/hypershift/support/proxy" "github.com/openshift/hypershift/support/util" @@ -22,7 +25,12 @@ func ReconcileCCMServiceAccount(sa *corev1.ServiceAccount, ownerRef config.Owner return nil } -func ReconcileDeployment(deployment *appsv1.Deployment, hcp *hyperv1.HostedControlPlane, p *AzureParams, serviceAccountName string, releaseImageProvider *imageprovider.ReleaseImageProvider) error { +func ReconcileDeployment(ctx context.Context, c client.Client, deployment *appsv1.Deployment, hcp *hyperv1.HostedControlPlane, p *AzureParams, serviceAccountName string, releaseImageProvider *imageprovider.ReleaseImageProvider) error { + azureCredentials, err := azureutil.GetAzureCredentialsFromSecret(ctx, c, hcp.Namespace, hcp.Spec.Platform.Azure.Credentials.Name) + if err != nil { + return err + } + deployment.Spec = appsv1.DeploymentSpec{ Selector: &metav1.LabelSelector{ MatchLabels: ccmLabels(), @@ -35,8 +43,12 @@ func ReconcileDeployment(deployment *appsv1.Deployment, hcp *hyperv1.HostedContr Labels: ccmLabels(), }, Spec: corev1.PodSpec{ + InitContainers: []corev1.Container{ + azureutil.AdapterInitContainer(), + }, Containers: []corev1.Container{ util.BuildContainer(ccmContainer(), buildCCMContainer(p, releaseImageProvider.GetImage("azure-cloud-controller-manager"), hcp.Namespace)), + azureutil.AdapterServerContainer(string(azureCredentials.Data["AZURE_CLIENT_ID"]), string(azureCredentials.Data["AZURE_CLIENT_SECRET"]), string(azureCredentials.Data["AZURE_TENANT_ID"])), }, Volumes: []corev1.Volume{}, ServiceAccountName: serviceAccountName, diff --git a/control-plane-operator/controllers/hostedcontrolplane/cno/clusternetworkoperator.go b/control-plane-operator/controllers/hostedcontrolplane/cno/clusternetworkoperator.go index f9d35a587dc..69125cd76d4 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/cno/clusternetworkoperator.go +++ b/control-plane-operator/controllers/hostedcontrolplane/cno/clusternetworkoperator.go @@ -6,19 +6,20 @@ import ( "os" "strconv" - "github.com/openshift/hypershift/support/proxy" - "github.com/openshift/hypershift/support/rhobsmonitoring" - "sigs.k8s.io/controller-runtime/pkg/client" - - "github.com/blang/semver" routev1 "github.com/openshift/api/route/v1" hyperv1 "github.com/openshift/hypershift/api/hypershift/v1beta1" "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/common" "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/imageprovider" "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/kas" "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/manifests" + "github.com/openshift/hypershift/support/azureutil" "github.com/openshift/hypershift/support/config" + "github.com/openshift/hypershift/support/proxy" + "github.com/openshift/hypershift/support/rhobsmonitoring" "github.com/openshift/hypershift/support/util" + + "github.com/blang/semver" + appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" @@ -27,6 +28,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" utilpointer "k8s.io/utils/pointer" + "sigs.k8s.io/controller-runtime/pkg/client" ) const ( @@ -74,9 +76,12 @@ type Params struct { DeploymentConfig config.DeploymentConfig IsPrivate bool DefaultIngressDomain string + NetworkManagedIdentity string + ClientIDSecret string + TenantID string } -func NewParams(hcp *hyperv1.HostedControlPlane, version string, releaseImageProvider *imageprovider.ReleaseImageProvider, userReleaseImageProvider *imageprovider.ReleaseImageProvider, setDefaultSecurityContext bool, defaultIngressDomain string) Params { +func NewParams(ctx context.Context, c client.Client, hcp *hyperv1.HostedControlPlane, version string, releaseImageProvider *imageprovider.ReleaseImageProvider, userReleaseImageProvider *imageprovider.ReleaseImageProvider, setDefaultSecurityContext bool, defaultIngressDomain string) (Params, error) { p := Params{ Images: Images{ NetworkOperator: releaseImageProvider.GetImage("cluster-network-operator"), @@ -131,7 +136,18 @@ func NewParams(hcp *hyperv1.HostedControlPlane, version string, releaseImageProv p.APIServerPort = hcp.Status.ControlPlaneEndpoint.Port } - return p + if os.Getenv("MANAGED_SERVICE") == hyperv1.AroHCP { + azureCredentials, err := azureutil.GetAzureCredentialsFromSecret(ctx, c, hcp.Namespace, hcp.Spec.Platform.Azure.Credentials.Name) + if err != nil { + return p, fmt.Errorf("failed to get Azure credentials: %w", err) + } + + p.NetworkManagedIdentity = string(hcp.Spec.Platform.Azure.ManagedIdentities.ControlPlaneManagedIdentities.NetworkManagedIdentityClientID) + p.ClientIDSecret = string(azureCredentials.Data["AZURE_CLIENT_SECRET"]) + p.TenantID = string(azureCredentials.Data["AZURE_TENANT_ID"]) + } + + return p, nil } func ReconcileRole(role *rbacv1.Role, ownerRef config.OwnerRef, networkType hyperv1.NetworkType) error { @@ -473,8 +489,11 @@ if [[ -n $sc ]]; then kubectl --kubeconfig $kc delete --ignore-not-found validat {Name: "CA_CONFIG_MAP", Value: params.CAConfigMap}, {Name: "CA_CONFIG_MAP_KEY", Value: params.CAConfigMapKey}, {Name: "TOKEN_AUDIENCE", Value: params.TokenAudience}, - - {Name: "RELEASE_VERSION", Value: params.ReleaseVersion}, + {Name: "ARO_HCP_MI_CLIENT_ID", Value: params.NetworkManagedIdentity}, + {Name: "AZURE_ADAPTER_INIT_IMAGE", Value: azureutil.AdapterInitImage}, + {Name: "AZURE_ADAPTER_SERVER_IMAGE", Value: azureutil.AdapterServerImage}, + {Name: "CLIENT_ID_SECRET", Value: params.ClientIDSecret}, + {Name: "TENANT_ID", Value: params.TenantID}, {Name: "APISERVER_OVERRIDE_HOST", Value: params.APIServerAddress}, // We need to pass this down to networking components on the nodes {Name: "APISERVER_OVERRIDE_PORT", Value: fmt.Sprint(params.APIServerPort)}, {Name: "OVN_NB_RAFT_ELECTION_TIMER", Value: "10"}, @@ -514,6 +533,7 @@ if [[ -n $sc ]]; then kubectl --kubeconfig $kc delete --ignore-not-found validat {Name: "CLI_IMAGE", Value: params.Images.CLI}, {Name: "SOCKS5_PROXY_IMAGE", Value: params.Images.Socks5Proxy}, {Name: "OPENSHIFT_RELEASE_IMAGE", Value: params.DeploymentConfig.AdditionalAnnotations[hyperv1.ReleaseImageAnnotation]}, + {Name: "RELEASE_VERSION", Value: params.ReleaseVersion}, }...), Name: operatorName, Image: params.Images.NetworkOperator, diff --git a/control-plane-operator/controllers/hostedcontrolplane/hostedcontrolplane_controller.go b/control-plane-operator/controllers/hostedcontrolplane/hostedcontrolplane_controller.go index 07e43a69603..989e787a345 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/hostedcontrolplane_controller.go +++ b/control-plane-operator/controllers/hostedcontrolplane/hostedcontrolplane_controller.go @@ -22,6 +22,7 @@ import ( "github.com/aws/aws-sdk-go/service/ec2/ec2iface" "github.com/aws/aws-sdk-go/service/kms" "github.com/go-logr/logr" + routev1 "github.com/openshift/api/route/v1" hyperv1 "github.com/openshift/hypershift/api/hypershift/v1beta1" awsutil "github.com/openshift/hypershift/cmd/infra/aws/util" @@ -46,6 +47,7 @@ import ( "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/ingress" "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/ingressoperator" "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/kas" + hcpkms "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/kas/kms" "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/kcm" "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/konnectivity" "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/machineapprover" @@ -64,8 +66,9 @@ import ( "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/snapshotcontroller" "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/storage" pkimanifests "github.com/openshift/hypershift/control-plane-pki-operator/manifests" - sharedingress "github.com/openshift/hypershift/hypershift-operator/controllers/sharedingress" + "github.com/openshift/hypershift/hypershift-operator/controllers/sharedingress" supportawsutil "github.com/openshift/hypershift/support/awsutil" + hyperazureutil "github.com/openshift/hypershift/support/azureutil" "github.com/openshift/hypershift/support/capabilities" "github.com/openshift/hypershift/support/certs" "github.com/openshift/hypershift/support/conditions" @@ -79,7 +82,9 @@ import ( "github.com/openshift/hypershift/support/thirdparty/library-go/pkg/image/reference" "github.com/openshift/hypershift/support/upsert" "github.com/openshift/hypershift/support/util" + prometheusoperatorv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" + appsv1 "k8s.io/api/apps/v1" batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" @@ -391,7 +396,7 @@ func (r *HostedControlPlaneReconciler) Reconcile(ctx context.Context, req ctrl.R Type: string(hyperv1.ValidHostedControlPlaneConfiguration), ObservedGeneration: hostedControlPlane.Generation, } - if err := r.validateConfigAndClusterCapabilities(hostedControlPlane); err != nil { + if err := r.validateConfigAndClusterCapabilities(ctx, hostedControlPlane); err != nil { condition.Status = metav1.ConditionFalse condition.Message = err.Error() condition.Reason = hyperv1.InsufficientClusterCapabilitiesReason @@ -844,12 +849,19 @@ func healthCheckKASEndpoint(ingressPoint string, port int) error { return nil } -func (r *HostedControlPlaneReconciler) validateConfigAndClusterCapabilities(hc *hyperv1.HostedControlPlane) error { - for _, svc := range hc.Spec.Services { +func (r *HostedControlPlaneReconciler) validateConfigAndClusterCapabilities(ctx context.Context, hcp *hyperv1.HostedControlPlane) error { + for _, svc := range hcp.Spec.Services { if svc.Type == hyperv1.Route && !r.ManagementClusterCapabilities.Has(capabilities.CapabilityRoute) { return fmt.Errorf("cluster does not support Routes, but service %q is exposed via a Route", svc.Service) } } + + if os.Getenv("MANAGED_SERVICE") == hyperv1.AroHCP { + if err := verifyResourceGroupLocationsMatch(ctx, hcp); err != nil { + return err + } + } + return nil } @@ -2983,6 +2995,20 @@ func (r *HostedControlPlaneReconciler) reconcileKubeAPIServer(ctx context.Contex if hcp.Spec.SecretEncryption.KMS == nil { return fmt.Errorf("kms metadata not specified") } + if hcp.Spec.Platform.Type == hyperv1.AzurePlatform { + azureCreds, err := hyperazureutil.GetAzureCredentialsFromSecret(ctx, r.Client, hcp.Namespace, hcp.Spec.Platform.Azure.Credentials.Name) + if err != nil { + return err + } + + // Reconcile KMS config secret + kmsConfigSecret := manifests.AzureKMSConfigSecret(hcp.Namespace) + if _, err := createOrUpdate(ctx, r, kmsConfigSecret, func() error { + return hcpkms.ReconcileKMSConfigWithCredentials(kmsConfigSecret, hcp, azureCreds) + }); err != nil { + return fmt.Errorf("failed to reconcile Azure cloud config with credentials: %w", err) + } + } if _, err := createOrUpdate(ctx, r, encryptionConfigFile, func() error { return kas.ReconcileKMSEncryptionConfig(encryptionConfigFile, p.OwnerRef, hcp.Spec.SecretEncryption.KMS) }); err != nil { @@ -3065,7 +3091,7 @@ func (r *HostedControlPlaneReconciler) reconcileKubeAPIServer(ctx context.Contex } if _, err := createOrUpdate(ctx, r, kubeAPIServerDeployment, func() error { - return kas.ReconcileKubeAPIServerDeployment(kubeAPIServerDeployment, + return kas.ReconcileKubeAPIServerDeployment(ctx, r.Client, kubeAPIServerDeployment, hcp, p.OwnerRef, p.DeploymentConfig, @@ -3509,7 +3535,10 @@ func (r *HostedControlPlaneReconciler) reconcileClusterVersionOperator(ctx conte } func (r *HostedControlPlaneReconciler) reconcileClusterNetworkOperator(ctx context.Context, hcp *hyperv1.HostedControlPlane, releaseImageProvider *imageprovider.ReleaseImageProvider, userReleaseImageProvider *imageprovider.ReleaseImageProvider, hasRouteCap bool, createOrUpdate upsert.CreateOrUpdateFN) error { - p := cno.NewParams(hcp, userReleaseImageProvider.Version(), releaseImageProvider, userReleaseImageProvider, r.SetDefaultSecurityContext, r.DefaultIngressDomain) + p, err := cno.NewParams(ctx, r.Client, hcp, userReleaseImageProvider.Version(), releaseImageProvider, userReleaseImageProvider, r.SetDefaultSecurityContext, r.DefaultIngressDomain) + if err != nil { + return err + } sa := manifests.ClusterNetworkOperatorServiceAccount(hcp.Namespace) if _, err := createOrUpdate(ctx, r.Client, sa, func() error { @@ -3676,7 +3705,9 @@ func (r *HostedControlPlaneReconciler) reconcileIngressOperator(ctx context.Cont deployment := manifests.IngressOperatorDeployment(hcp.Namespace) if _, err := createOrUpdate(ctx, r, deployment, func() error { - ingressoperator.ReconcileDeployment(deployment, p, hcp.Spec.Platform.Type) + if err := ingressoperator.ReconcileDeployment(ctx, r.Client, hcp, deployment, p, hcp.Spec.Platform.Type); err != nil { + return err + } return nil }); err != nil { return fmt.Errorf("failed to reconcile ingressoperator deployment: %w", err) @@ -3994,7 +4025,7 @@ func (r *HostedControlPlaneReconciler) reconcileImageRegistryOperator(ctx contex deployment := manifests.ImageRegistryOperatorDeployment(hcp.Namespace) if _, err := createOrUpdate(ctx, r, deployment, func() error { - return registryoperator.ReconcileDeployment(deployment, params) + return registryoperator.ReconcileDeployment(ctx, r.Client, hcp, deployment, params) }); err != nil { return fmt.Errorf("failed to reconcile image registry operator deployment: %w", err) } @@ -4564,7 +4595,7 @@ func (r *HostedControlPlaneReconciler) reconcileCloudControllerManager(ctx conte p := azure.NewAzureParams(hcp) deployment := azure.CCMDeployment(hcp.Namespace) if _, err := createOrUpdate(ctx, r, deployment, func() error { - return azure.ReconcileDeployment(deployment, hcp, p, sa.Name, releaseImageProvider) + return azure.ReconcileDeployment(ctx, r.Client, deployment, hcp, p, sa.Name, releaseImageProvider) }); err != nil { return fmt.Errorf("failed to reconcile %s cloud controller manager deployment: %w", hcp.Spec.Platform.Type, err) } @@ -4777,7 +4808,10 @@ func (r *HostedControlPlaneReconciler) reconcileCSISnapshotControllerOperator(ct } func (r *HostedControlPlaneReconciler) reconcileClusterStorageOperator(ctx context.Context, hcp *hyperv1.HostedControlPlane, releaseImageProvider *imageprovider.ReleaseImageProvider, userReleaseImageProvider *imageprovider.ReleaseImageProvider, createOrUpdate upsert.CreateOrUpdateFN) error { - params := storage.NewParams(hcp, userReleaseImageProvider.Version(), releaseImageProvider, userReleaseImageProvider, r.SetDefaultSecurityContext) + params, err := storage.NewParams(ctx, r.Client, hcp, userReleaseImageProvider.Version(), releaseImageProvider, userReleaseImageProvider, r.SetDefaultSecurityContext) + if err != nil { + return err + } deployment := manifests.ClusterStorageOperatorDeployment(hcp.Namespace) if _, err := createOrUpdate(ctx, r, deployment, func() error { @@ -4807,6 +4841,34 @@ func (r *HostedControlPlaneReconciler) reconcileClusterStorageOperator(ctx conte return fmt.Errorf("failed to reconcile cluster storage operator roleBinding: %w", err) } + // Reconcile azure-disk-csi-controller and azure-file-csi-controller configuration secrets for ARO HCP. This is + // needed so we can specify a unique managed identity for each controller to authenticate with the Managed Identity + // Azure API. + if os.Getenv("MANAGED_SERVICE") == hyperv1.AroHCP { + // Get the credentials secret so we can retrieve the tenant ID for the configuration + credentialsSecret := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Namespace: hcp.Namespace, Name: hcp.Spec.Platform.Azure.Credentials.Name}} + if err := r.Client.Get(ctx, client.ObjectKeyFromObject(credentialsSecret), credentialsSecret); err != nil { + return fmt.Errorf("failed to get Azure credentials secret: %w", err) + } + tenantID := string(credentialsSecret.Data["AZURE_TENANT_ID"]) + + // Reconcile the secret needed for azure-disk-csi-controller + azureDiskCSISecret := manifests.AzureDiskCSIConfig(hcp.Namespace) + if _, err := createOrUpdate(ctx, r, azureDiskCSISecret, func() error { + return storage.ReconcileAzureDiskCSISecret(azureDiskCSISecret, hcp, tenantID) + }); err != nil { + return fmt.Errorf("failed to reconcile Azure Disk CSI config: %w", err) + } + + // Reconcile the secret needed for azure-disk-csi-controller + azureFileCSISecret := manifests.AzureFileCSIConfig(hcp.Namespace) + if _, err := createOrUpdate(ctx, r, azureDiskCSISecret, func() error { + return storage.ReconcileAzureFileCSISecret(azureFileCSISecret, hcp, tenantID) + }); err != nil { + return fmt.Errorf("failed to reconcile Azure File CSI config: %w", err) + } + } + // TODO: create custom kubeconfig to the guest cluster + RBAC return nil @@ -5228,24 +5290,11 @@ func (r *HostedControlPlaneReconciler) validateAzureKMSConfig(ctx context.Contex } azureKmsSpec := hcp.Spec.SecretEncryption.KMS.Azure - credentialsSecret := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Namespace: hcp.Namespace, Name: hcp.Spec.Platform.Azure.Credentials.Name}} - if err := r.Client.Get(ctx, client.ObjectKeyFromObject(credentialsSecret), credentialsSecret); err != nil { - condition := metav1.Condition{ - Type: string(hyperv1.ValidAzureKMSConfig), - ObservedGeneration: hcp.Generation, - Status: metav1.ConditionUnknown, - Message: fmt.Sprintf("failed to get azure credentials secret: %v", err), - Reason: hyperv1.StatusUnknownReason, - } - meta.SetStatusCondition(&hcp.Status.Conditions, condition) - return + options := &azidentity.ManagedIdentityCredentialOptions{ + ID: azidentity.ClientID(hcp.Spec.Platform.Azure.ManagedIdentities.ControlPlaneManagedIdentities.AzureKMSManagedIdentityClientID), } - tenantID := string(credentialsSecret.Data["AZURE_TENANT_ID"]) - clientID := string(credentialsSecret.Data["AZURE_CLIENT_ID"]) - clientSecret := string(credentialsSecret.Data["AZURE_CLIENT_SECRET"]) - - cred, err := azidentity.NewClientSecretCredential(tenantID, clientID, clientSecret, nil) + cred, err := azidentity.NewManagedIdentityCredential(options) if err != nil { conditions.SetFalseCondition(hcp, hyperv1.ValidAzureKMSConfig, hyperv1.InvalidAzureCredentialsReason, fmt.Sprintf("failed to obtain azure client credential: %v", err)) @@ -5347,3 +5396,40 @@ func doesOpenShiftTrustedCABundleConfigMapForCPOExist(ctx context.Context, c cli } return false, nil } + +// verifyResourceGroupLocationsMatch verifies the locations match for the VNET, network security group, and managed resource groups +func verifyResourceGroupLocationsMatch(ctx context.Context, hcp *hyperv1.HostedControlPlane) error { + options := &azidentity.ManagedIdentityCredentialOptions{ + ID: azidentity.ClientID(hcp.Spec.Platform.Azure.ManagedIdentities.ControlPlaneManagedIdentities.ControlPlaneManagedIdentityClientID), + } + + creds, err := azidentity.NewManagedIdentityCredential(options) + if err != nil { + return fmt.Errorf("failed to create azure creds to verify resource group locations: %v", err) + } + + // Retrieve full vnet information from the VNET ID + vnet, err := hyperazureutil.GetVnetInfoFromVnetID(ctx, hcp.Spec.Platform.Azure.VnetID, hcp.Spec.Platform.Azure.SubscriptionID, creds) + if err != nil { + return fmt.Errorf("failed to get vnet info to verify its location: %v", err) + } + + // Retrieve full network security group information from the network security group ID + nsg, err := hyperazureutil.GetNetworkSecurityGroupInfo(ctx, hcp.Spec.Platform.Azure.SecurityGroupID, hcp.Spec.Platform.Azure.SubscriptionID, creds) + if err != nil { + return fmt.Errorf("failed to get network security group info to verify its location: %v", err) + } + + // Retrieve full resource group information from the resource group name + rg, err := hyperazureutil.GetResourceGroupInfo(ctx, hcp.Spec.Platform.Azure.ResourceGroupName, hcp.Spec.Platform.Azure.SubscriptionID, creds) + if err != nil { + return fmt.Errorf("failed to get resource group info to verify its location: %v", err) + } + + // Verify the vnet resource group location, network security group resource group location, and the managed resource group location match + if ptr.Deref(vnet.Location, "") != ptr.Deref(nsg.Location, "") || ptr.Deref(nsg.Location, "") != ptr.Deref(rg.Location, "") { + return fmt.Errorf("the locations of the resource groups do not match - vnet location: %v; network security group location: %v; managed resource group location: %v", ptr.Deref(vnet.Location, ""), ptr.Deref(nsg.Location, ""), ptr.Deref(rg.Location, "")) + } + + return nil +} diff --git a/control-plane-operator/controllers/hostedcontrolplane/ingressoperator/ingressoperator.go b/control-plane-operator/controllers/hostedcontrolplane/ingressoperator/ingressoperator.go index e818ed1e815..482c039263f 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/ingressoperator/ingressoperator.go +++ b/control-plane-operator/controllers/hostedcontrolplane/ingressoperator/ingressoperator.go @@ -1,30 +1,34 @@ package ingressoperator import ( + "context" "fmt" - configv1 "github.com/openshift/api/config/v1" hyperv1 "github.com/openshift/hypershift/api/hypershift/v1beta1" "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/imageprovider" "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/kas" "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/manifests" + "github.com/openshift/hypershift/support/azureutil" "github.com/openshift/hypershift/support/config" "github.com/openshift/hypershift/support/metrics" "github.com/openshift/hypershift/support/proxy" "github.com/openshift/hypershift/support/util" + "os" + prometheusoperatorv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" + appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" ) const ( operatorName = "ingress-operator" ingressOperatorContainerName = "ingress-operator" - metricsHostname = "ingress-operator" konnectivityProxyContainerName = "konnectivity-proxy" ingressOperatorMetricsPort = 60000 konnectivityProxyPort = 8090 @@ -70,14 +74,19 @@ func NewParams(hcp *hyperv1.HostedControlPlane, version string, releaseImageProv return p } -func ReconcileDeployment(dep *appsv1.Deployment, params Params, platformType hyperv1.PlatformType) { +func ReconcileDeployment(ctx context.Context, c client.Client, hcp *hyperv1.HostedControlPlane, dep *appsv1.Deployment, params Params, platformType hyperv1.PlatformType) error { + // Determine if the deployment will be placed on ARO HCP + aroHCPDeployment := os.Getenv("MANAGED_SERVICE") == hyperv1.AroHCP + + // Initialize resource requests ingressOpResources := corev1.ResourceRequirements{ Requests: corev1.ResourceList{ corev1.ResourceMemory: resource.MustParse("80Mi"), corev1.ResourceCPU: resource.MustParse("10m"), }, } - // preserve existing resource requirements + + // Preserve existing resource requirements mainContainer := util.FindContainer(ingressOperatorContainerName, dep.Spec.Template.Spec.Containers) if mainContainer != nil { if len(mainContainer.Resources.Requests) > 0 || len(mainContainer.Resources.Limits) > 0 { @@ -85,6 +94,12 @@ func ReconcileDeployment(dep *appsv1.Deployment, params Params, platformType hyp } } + // Initialize NO_PROXY environment variable and add the Azure Managed Identity API endpoint if ingress operator is being deployed on ARO HCP + noProxyValue := manifests.KubeAPIServerService("").Name + if aroHCPDeployment { + noProxyValue = noProxyValue + ",169.254.169.254" + } + dep.Spec.Replicas = ptr.To[int32](1) dep.Spec.Selector = &metav1.LabelSelector{MatchLabels: map[string]string{"name": operatorName}} dep.Spec.Strategy.Type = appsv1.RecreateDeploymentStrategyType @@ -132,7 +147,7 @@ func ReconcileDeployment(dep *appsv1.Deployment, params Params, platformType hyp }, { Name: "NO_PROXY", - Value: manifests.KubeAPIServerService("").Name, + Value: noProxyValue, }, }, Name: ingressOperatorContainerName, @@ -199,7 +214,29 @@ func ReconcileDeployment(dep *appsv1.Deployment, params Params, platformType hyp }, ) + if os.Getenv("MANAGED_SERVICE") == hyperv1.AroHCP { + dep.Spec.Template.Spec.Containers[0].Env = append(dep.Spec.Template.Spec.Containers[0].Env, + corev1.EnvVar{ + Name: "ARO_HCP_MI_CLIENT_ID", + Value: string(hcp.Spec.Platform.Azure.ManagedIdentities.ControlPlaneManagedIdentities.IngressManagedIdentityClientID), + }) + + if dep.Spec.Template.Spec.InitContainers == nil { + dep.Spec.Template.Spec.InitContainers = []corev1.Container{} + } + dep.Spec.Template.Spec.InitContainers = append(dep.Spec.Template.Spec.InitContainers, azureutil.AdapterInitContainer()) + + azureCredentials, err := azureutil.GetAzureCredentialsFromSecret(ctx, c, hcp.Namespace, hcp.Spec.Platform.Azure.Credentials.Name) + if err != nil { + return err + } + + dep.Spec.Template.Spec.Containers = append(dep.Spec.Template.Spec.Containers, azureutil.AdapterServerContainer(string(azureCredentials.Data["AZURE_CLIENT_ID"]), string(azureCredentials.Data["AZURE_CLIENT_SECRET"]), string(azureCredentials.Data["AZURE_TENANT_ID"]))) + params.DeploymentConfig.SetDefaultSecurityContext = false + } + params.DeploymentConfig.ApplyTo(dep) + return nil } func ingressOperatorKonnectivityProxyContainer(proxyImage string, proxyConfig *configv1.ProxySpec, noProxy string) corev1.Container { diff --git a/control-plane-operator/controllers/hostedcontrolplane/kas/deployment.go b/control-plane-operator/controllers/hostedcontrolplane/kas/deployment.go index 0562511d123..854caba4ac9 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/kas/deployment.go +++ b/control-plane-operator/controllers/hostedcontrolplane/kas/deployment.go @@ -2,7 +2,9 @@ package kas import ( "bytes" + "context" "fmt" + "os" "path" "strconv" "strings" @@ -14,6 +16,7 @@ import ( "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/pointer" + "sigs.k8s.io/controller-runtime/pkg/client" configv1 "github.com/openshift/api/config/v1" "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/cloud/aws" @@ -21,6 +24,7 @@ import ( "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/common" "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/manifests" "github.com/openshift/hypershift/support/api" + "github.com/openshift/hypershift/support/azureutil" "github.com/openshift/hypershift/support/certs" "github.com/openshift/hypershift/support/config" "github.com/openshift/hypershift/support/proxy" @@ -99,7 +103,10 @@ func kasLabels() map[string]string { } } -func ReconcileKubeAPIServerDeployment(deployment *appsv1.Deployment, +func ReconcileKubeAPIServerDeployment( + ctx context.Context, + c client.Client, + deployment *appsv1.Deployment, hcp *hyperv1.HostedControlPlane, ownerRef config.OwnerRef, deploymentConfig config.DeploymentConfig, @@ -319,6 +326,21 @@ func ReconcileKubeAPIServerDeployment(deployment *appsv1.Deployment, if err := applyKMSConfig(&deployment.Spec.Template.Spec, secretEncryptionData, images); err != nil { return err } + + // Add the adapter-init and adapter-server containers for ARO HCP + if os.Getenv("MANAGED_SERVICE") == hyperv1.AroHCP { + deployment.Spec.Template.Spec.InitContainers = append(deployment.Spec.Template.Spec.InitContainers, azureutil.AdapterInitContainer()) + + azureCredentials, err := azureutil.GetAzureCredentialsFromSecret(ctx, c, hcp.Namespace, hcp.Spec.Platform.Azure.Credentials.Name) + if err != nil { + return err + } + + deployment.Spec.Template.Spec.Containers = append(deployment.Spec.Template.Spec.Containers, azureutil.AdapterServerContainer(string(azureCredentials.Data["AZURE_CLIENT_ID"]), string(azureCredentials.Data["AZURE_CLIENT_SECRET"]), string(azureCredentials.Data["AZURE_TENANT_ID"]))) + + // ARO HCP needs elevated privileges in order to run the adapter-init container + deploymentConfig.SetDefaultSecurityContext = false + } case hyperv1.AESCBC: err := applyAESCBCKeyHashAnnotation(&deployment.Spec.Template, aesCBCActiveKey, aesCBCBackupKey) if err != nil { diff --git a/control-plane-operator/controllers/hostedcontrolplane/kas/deployment_test.go b/control-plane-operator/controllers/hostedcontrolplane/kas/deployment_test.go index e616a598c4d..7eb437b7f1e 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/kas/deployment_test.go +++ b/control-plane-operator/controllers/hostedcontrolplane/kas/deployment_test.go @@ -1,6 +1,7 @@ package kas import ( + "golang.org/x/net/context" "testing" . "github.com/onsi/gomega" @@ -57,7 +58,7 @@ func TestReconcileKubeAPIServerDeploymentNoChanges(t *testing.T) { tc.config.Data = map[string]string{"config.json": "test-json"} tc.auditConfig.Data = map[string]string{"policy.yaml": "test-data"} tc.authConfig.Data = map[string]string{"auth.json": "test-data"} - err := ReconcileKubeAPIServerDeployment(kubeAPIDeployment, hcp, ownerRef, tc.deploymentConfig, tc.params.NamedCertificates(), tc.params.CloudProvider, + err := ReconcileKubeAPIServerDeployment(context.TODO(), nil, kubeAPIDeployment, hcp, ownerRef, tc.deploymentConfig, tc.params.NamedCertificates(), tc.params.CloudProvider, tc.params.CloudProviderConfig, tc.params.CloudProviderCreds, tc.params.Images, tc.config, tc.auditConfig, tc.authConfig, tc.params.AuditWebhookRef, tc.activeKey, tc.backupKey, 6443, "test-payload-version", tc.params.FeatureGate, nil, tc.params.CipherSuites()) g.Expect(err).To(BeNil()) g.Expect(expectedMinReadySeconds).To(Equal(kubeAPIDeployment.Spec.MinReadySeconds)) diff --git a/control-plane-operator/controllers/hostedcontrolplane/kas/kms/azure.go b/control-plane-operator/controllers/hostedcontrolplane/kas/kms/azure.go index 81ee7b4dacb..c0d1ed98b6a 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/kas/kms/azure.go +++ b/control-plane-operator/controllers/hostedcontrolplane/kas/kms/azure.go @@ -1,6 +1,7 @@ package kms import ( + "encoding/json" "fmt" "time" @@ -216,7 +217,7 @@ func kasVolumeAzureKMSCredentials() *corev1.Volume { func buildVolumeAzureKMSCredentials(v *corev1.Volume) { v.Secret = &corev1.SecretVolumeSource{ - SecretName: manifests.AzureProviderConfigWithCredentials("").Name, + SecretName: manifests.AzureKMSConfigSecret("").Name, Items: []corev1.KeyToPath{ { Key: azure.CloudConfigKey, @@ -225,3 +226,24 @@ func buildVolumeAzureKMSCredentials(v *corev1.Volume) { }, } } + +func ReconcileKMSConfigWithCredentials(secret *corev1.Secret, hcp *hyperv1.HostedControlPlane, credentialsSecret *corev1.Secret) error { + cfg, err := azure.AzureConfigWithoutCredentials(hcp, credentialsSecret) + if err != nil { + return err + } + + cfg.UserAssignedIdentityID = string(hcp.Spec.Platform.Azure.ManagedIdentities.ControlPlaneManagedIdentities.AzureKMSManagedIdentityClientID) + + cfg.UseInstanceMetadata = false + serializedConfig, err := json.MarshalIndent(cfg, "", " ") + if err != nil { + return fmt.Errorf("failed to serialize cloudconfig: %w", err) + } + + if secret.Data == nil { + secret.Data = map[string][]byte{} + } + secret.Data[azure.CloudConfigKey] = serializedConfig + return nil +} diff --git a/control-plane-operator/controllers/hostedcontrolplane/manifests/azure.go b/control-plane-operator/controllers/hostedcontrolplane/manifests/azure.go index 39a373f72f4..bfb5491d91b 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/manifests/azure.go +++ b/control-plane-operator/controllers/hostedcontrolplane/manifests/azure.go @@ -25,3 +25,12 @@ func AzureProviderConfigWithCredentials(ns string) *corev1.Secret { }, } } + +func AzureKMSConfigSecret(ns string) *corev1.Secret { + return &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "azure-kms-cloud-config", + Namespace: ns, + }, + } +} diff --git a/control-plane-operator/controllers/hostedcontrolplane/manifests/storage.go b/control-plane-operator/controllers/hostedcontrolplane/manifests/storage.go index e09fbc8e570..fcc085100a8 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/manifests/storage.go +++ b/control-plane-operator/controllers/hostedcontrolplane/manifests/storage.go @@ -4,6 +4,7 @@ import ( appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func ClusterStorageOperatorDeployment(ns string) *appsv1.Deployment { @@ -33,3 +34,21 @@ func ClusterStorageOperatorServiceAccount(ns string) *corev1.ServiceAccount { sa.Namespace = ns return sa } + +func AzureDiskCSIConfig(ns string) *corev1.Secret { + return &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "azure-disk-csi-config", + Namespace: ns, + }, + } +} + +func AzureFileCSIConfig(ns string) *corev1.Secret { + return &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "azure-file-csi-config", + Namespace: ns, + }, + } +} diff --git a/control-plane-operator/controllers/hostedcontrolplane/registryoperator/reconcile.go b/control-plane-operator/controllers/hostedcontrolplane/registryoperator/reconcile.go index 31f1a347447..ae769b38733 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/registryoperator/reconcile.go +++ b/control-plane-operator/controllers/hostedcontrolplane/registryoperator/reconcile.go @@ -2,6 +2,8 @@ package registryoperator import ( "bytes" + "context" + "os" "path" "text/template" @@ -11,11 +13,13 @@ import ( "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/pointer" + "sigs.k8s.io/controller-runtime/pkg/client" hyperv1 "github.com/openshift/hypershift/api/hypershift/v1beta1" "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/imageprovider" "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/kas" "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/manifests" + "github.com/openshift/hypershift/support/azureutil" "github.com/openshift/hypershift/support/certs" "github.com/openshift/hypershift/support/config" "github.com/openshift/hypershift/support/metrics" @@ -152,7 +156,7 @@ func NewParams(hcp *hyperv1.HostedControlPlane, version string, releaseImageProv return params } -func ReconcileDeployment(deployment *appsv1.Deployment, params Params) error { +func ReconcileDeployment(ctx context.Context, c client.Client, hcp *hyperv1.HostedControlPlane, deployment *appsv1.Deployment, params Params) error { deployment.Spec = appsv1.DeploymentSpec{ Selector: &metav1.LabelSelector{ MatchLabels: selectorLabels(), @@ -194,6 +198,27 @@ func ReconcileDeployment(deployment *appsv1.Deployment, params Params) error { ) } + if os.Getenv("MANAGED_SERVICE") == hyperv1.AroHCP { + deployment.Spec.Template.Spec.Containers[0].Env = append(deployment.Spec.Template.Spec.Containers[0].Env, + corev1.EnvVar{ + Name: "ARO_HCP_MI_CLIENT_ID", + Value: "true", + }) + + if deployment.Spec.Template.Spec.InitContainers == nil { + deployment.Spec.Template.Spec.InitContainers = []corev1.Container{} + } + deployment.Spec.Template.Spec.InitContainers = append(deployment.Spec.Template.Spec.InitContainers, azureutil.AdapterInitContainer()) + + azureCredentials, err := azureutil.GetAzureCredentialsFromSecret(ctx, c, hcp.Namespace, hcp.Spec.Platform.Azure.Credentials.Name) + if err != nil { + return err + } + + deployment.Spec.Template.Spec.Containers = append(deployment.Spec.Template.Spec.Containers, azureutil.AdapterServerContainer(string(azureCredentials.Data["AZURE_CLIENT_ID"]), string(azureCredentials.Data["AZURE_CLIENT_SECRET"]), string(azureCredentials.Data["AZURE_TENANT_ID"]))) + params.deploymentConfig.SetDefaultSecurityContext = false + } + params.deploymentConfig.ApplyTo(deployment) return nil } diff --git a/control-plane-operator/controllers/hostedcontrolplane/registryoperator/reconcile_test.go b/control-plane-operator/controllers/hostedcontrolplane/registryoperator/reconcile_test.go index 3255fb7a13e..5640db74d80 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/registryoperator/reconcile_test.go +++ b/control-plane-operator/controllers/hostedcontrolplane/registryoperator/reconcile_test.go @@ -1,6 +1,8 @@ package registryoperator import ( + "context" + "sigs.k8s.io/controller-runtime/pkg/client/fake" "testing" hyperv1 "github.com/openshift/hypershift/api/hypershift/v1beta1" @@ -35,8 +37,9 @@ func TestReconcileDeployment(t *testing.T) { } deployment := manifests.ImageRegistryOperatorDeployment("test-namespace") imageProvider := imageprovider.NewFromImages(images) + fakeClient := fake.NewClientBuilder().WithScheme(api.Scheme).Build() params := NewParams(hcp, "1.0.0", imageProvider, imageProvider, true) - if err := ReconcileDeployment(deployment, params); err != nil { + if err := ReconcileDeployment(context.TODO(), fakeClient, hcp, deployment, params); err != nil { t.Fatalf("unexpected error: %v", err) } deploymentYaml, err := util.SerializeResource(deployment, api.Scheme) diff --git a/control-plane-operator/controllers/hostedcontrolplane/storage/azure.go b/control-plane-operator/controllers/hostedcontrolplane/storage/azure.go new file mode 100644 index 00000000000..fd8a6a514ac --- /dev/null +++ b/control-plane-operator/controllers/hostedcontrolplane/storage/azure.go @@ -0,0 +1,67 @@ +package storage + +import ( + "encoding/json" + "fmt" + + hyperv1 "github.com/openshift/hypershift/api/hypershift/v1beta1" + "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/cloud/azure" + + corev1 "k8s.io/api/core/v1" +) + +// initializeAzureCSIControllerConfig initializes an AzureConfig object which will be used to populate the secrets +// needed by azure-disk-csi-controller and azure-file-csi-controller. +// The source of truth for which fields are required by azure-disk is located here - https://github.com/kubernetes-sigs/azuredisk-csi-driver/blob/master/deploy/example/azure.json. +// The source of truth for which fields are required by azure-file is located here - https://github.com/kubernetes-sigs/azurefile-csi-driver/blob/master/deploy/example/azure.json. +// As of Sept 2024, the fields we need in HyperShift are the same between both of these sources of truth. +func initializeAzureCSIControllerConfig(hcp *hyperv1.HostedControlPlane, tenantID string) azure.AzureConfig { + azureConfig := azure.AzureConfig{ + // These fields are mandatory + Cloud: hcp.Spec.Platform.Azure.Cloud, + TenantID: tenantID, + SubscriptionID: hcp.Spec.Platform.Azure.SubscriptionID, + ResourceGroup: hcp.Spec.Platform.Azure.ResourceGroupName, + Location: hcp.Spec.Platform.Azure.Location, + + // These fields are mandatory when using managed identity; the user assigned identity ID is populated after this function call + UseManagedIdentityExtension: true, + UserAssignedIdentityID: "", + } + + return azureConfig +} + +// ReconcileAzureDiskCSISecret reconciles the configuration for the secret as expected by azure-disk-csi-controller +func ReconcileAzureDiskCSISecret(secret *corev1.Secret, hcp *hyperv1.HostedControlPlane, tenantID string) error { + config := initializeAzureCSIControllerConfig(hcp, tenantID) + config.UserAssignedIdentityID = string(hcp.Spec.Platform.Azure.ManagedIdentities.ControlPlaneManagedIdentities.AzureDiskManagedIdentityClientID) + + serializedConfig, err := json.MarshalIndent(config, "", " ") + if err != nil { + return fmt.Errorf("failed to serialize cloudconfig: %w", err) + } + + if secret.Data == nil { + secret.Data = map[string][]byte{} + } + secret.Data[azure.CloudConfigKey] = serializedConfig + return nil +} + +// ReconcileAzureFileCSISecret reconciles the configuration for the secret as expected by azure-file-csi-controller +func ReconcileAzureFileCSISecret(secret *corev1.Secret, hcp *hyperv1.HostedControlPlane, tenantID string) error { + config := initializeAzureCSIControllerConfig(hcp, tenantID) + config.UserAssignedIdentityID = string(hcp.Spec.Platform.Azure.ManagedIdentities.ControlPlaneManagedIdentities.AzureFileManagedIdentityClientID) + + serializedConfig, err := json.MarshalIndent(config, "", " ") + if err != nil { + return fmt.Errorf("failed to serialize cloudconfig: %w", err) + } + + if secret.Data == nil { + secret.Data = map[string][]byte{} + } + secret.Data[azure.CloudConfigKey] = serializedConfig + return nil +} diff --git a/control-plane-operator/controllers/hostedcontrolplane/storage/operator.go b/control-plane-operator/controllers/hostedcontrolplane/storage/operator.go index 03f559846de..384c020cd38 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/storage/operator.go +++ b/control-plane-operator/controllers/hostedcontrolplane/storage/operator.go @@ -1,18 +1,25 @@ package storage import ( + "fmt" + "os" + hyperv1 "github.com/openshift/hypershift/api/hypershift/v1beta1" "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/common" "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/kas" "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/storage/assets" assets2 "github.com/openshift/hypershift/support/assets" + "github.com/openshift/hypershift/support/azureutil" "github.com/openshift/hypershift/support/util" + appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/runtime/schema" ) +const ClusterStorageOperatorContainerName = "cluster-storage-operator" + var ( operatorDeployment = assets2.MustDeployment(assets.ReadFile, "10_deployment-hypershift.yaml") operatorRole = assets2.MustRole(assets.ReadFile, "role.yaml") @@ -26,12 +33,26 @@ func ReconcileOperatorDeployment( params.OwnerRef.ApplyTo(deployment) deployment.Spec = operatorDeployment.DeepCopy().Spec - for i, container := range deployment.Spec.Template.Spec.Containers { - switch container.Name { - case "cluster-storage-operator": - deployment.Spec.Template.Spec.Containers[i].Image = params.StorageOperatorImage - params.ImageReplacer.replaceEnvVars(deployment.Spec.Template.Spec.Containers[i].Env) + + csoContainer := util.FindContainer(ClusterStorageOperatorContainerName, deployment.Spec.Template.Spec.Containers) + if csoContainer == nil { + return fmt.Errorf("could not find ClusterStorageOperator container for Deployment") + } + + csoContainer.Image = params.StorageOperatorImage + params.ImageReplacer.replaceEnvVars(csoContainer.Env) + + if os.Getenv("MANAGED_SERVICE") == hyperv1.AroHCP { + aroHCPEnvs := []corev1.EnvVar{ + {Name: "AZURE_ADAPTER_INIT_IMAGE", Value: azureutil.AdapterInitImage}, + {Name: "AZURE_ADAPTER_SERVER_IMAGE", Value: azureutil.AdapterServerImage}, + {Name: "ARO_HCP_DISK_MI_CLIENT_ID", Value: params.AzureDiskManagedIdentity}, + {Name: "ARO_HCP_FILE_MI_CLIENT_ID", Value: params.AzureFileManagedIdentity}, + {Name: "CLIENT_ID_SECRET", Value: params.ClientIDSecret}, + {Name: "TENANT_ID", Value: params.TenantID}, } + + csoContainer.Env = append(csoContainer.Env, aroHCPEnvs...) } params.DeploymentConfig.ApplyTo(deployment) diff --git a/control-plane-operator/controllers/hostedcontrolplane/storage/params.go b/control-plane-operator/controllers/hostedcontrolplane/storage/params.go index be4c3c6615d..3505962a383 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/storage/params.go +++ b/control-plane-operator/controllers/hostedcontrolplane/storage/params.go @@ -1,11 +1,17 @@ package storage import ( + "context" + "fmt" + "os" + hyperv1 "github.com/openshift/hypershift/api/hypershift/v1beta1" "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/imageprovider" + "github.com/openshift/hypershift/support/azureutil" "github.com/openshift/hypershift/support/config" "github.com/openshift/hypershift/support/util" utilpointer "k8s.io/utils/pointer" + "sigs.k8s.io/controller-runtime/pkg/client" ) const ( @@ -13,20 +19,26 @@ const ( ) type Params struct { - OwnerRef config.OwnerRef - StorageOperatorImage string - ImageReplacer *environmentReplacer + OwnerRef config.OwnerRef + StorageOperatorImage string + AzureDiskManagedIdentity string + AzureFileManagedIdentity string + ClientIDSecret string + TenantID string + ImageReplacer *environmentReplacer AvailabilityProberImage string config.DeploymentConfig } func NewParams( + ctx context.Context, + c client.Client, hcp *hyperv1.HostedControlPlane, version string, releaseImageProvider *imageprovider.ReleaseImageProvider, userReleaseImageProvider *imageprovider.ReleaseImageProvider, - setDefaultSecurityContext bool) *Params { + setDefaultSecurityContext bool) (*Params, error) { ir := newEnvironmentReplacer() ir.setVersions(version) @@ -51,5 +63,17 @@ func NewParams( params.DeploymentConfig.SetDefaults(hcp, nil, utilpointer.Int(1)) params.DeploymentConfig.SetRestartAnnotation(hcp.ObjectMeta) - return ¶ms + if os.Getenv("MANAGED_SERVICE") == hyperv1.AroHCP { + azureCredentials, err := azureutil.GetAzureCredentialsFromSecret(ctx, c, hcp.Namespace, hcp.Spec.Platform.Azure.Credentials.Name) + if err != nil { + return nil, fmt.Errorf("failed to get Azure credentials: %w", err) + } + + params.AzureDiskManagedIdentity = string(hcp.Spec.Platform.Azure.ManagedIdentities.ControlPlaneManagedIdentities.AzureDiskManagedIdentityClientID) + params.AzureFileManagedIdentity = string(hcp.Spec.Platform.Azure.ManagedIdentities.ControlPlaneManagedIdentities.AzureFileManagedIdentityClientID) + params.ClientIDSecret = string(azureCredentials.Data["AZURE_CLIENT_SECRET"]) + params.TenantID = string(azureCredentials.Data["AZURE_TENANT_ID"]) + } + + return ¶ms, nil } diff --git a/docs/content/reference/api.md b/docs/content/reference/api.md index 673584152de..626d16df52b 100644 --- a/docs/content/reference/api.md +++ b/docs/content/reference/api.md @@ -2737,6 +2737,53 @@ configuration for the Azure cloud provider, aka Azure cloud controller manager ( expected to exist under the same subscription as SubscriptionID.

+ + +managedIdentities
+ + +AzureResourceManagedIdentities + + + + +

managedIdentities contains the client IDs related to the managed identities needed for HCP control plane +and data plane components that authenticate with Azure’s API.

+ + + + +###AzureResourceManagedIdentities { #hypershift.openshift.io/v1beta1.AzureResourceManagedIdentities } +

+(Appears on: +AzurePlatformSpec) +

+

+

AzureResourceManagedIdentities contains the client IDs related to the managed identities needed for HCP control plane +and data plane components that authenticate with Azure’s API.

+

+ + + + + + + + + + + +
FieldDescription
+controlPlaneManagedIdentities
+ + +ControlPlaneManagedIdentities + + +
+

ControlPlaneManagedIdentities contains the client IDs of all the managed identities on the HCP control plane needing to +authenticate with Azure’s API.

+
###AzureVMImage { #hypershift.openshift.io/v1beta1.AzureVMImage } @@ -3542,6 +3589,163 @@ and reports missing images if any.

+###ControlPlaneManagedIdentities { #hypershift.openshift.io/v1beta1.ControlPlaneManagedIdentities } +

+(Appears on: +AzureResourceManagedIdentities) +

+

+

ControlPlaneManagedIdentities contains the client IDs of all the managed identities on the HCP control plane needing +to authenticate with Azure’s API. +Managed identity regex pattern is from Microsoft here - https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/resource-name-rules#microsoftmanagedidentity. +The format a managed identity should be /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{managedIdentityName}.

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+azureCloudProviderManagedIdentityClientID
+ + +ManagedIdentityClientID + + +
+

azureCloudProviderManagedIdentityClientID is the client ID of a pre-existing managed identity ID associated with the azure +cloud provider, aka ccm. The client ID of a managed identity must be a valid UUID. It should be 5 groups of +hyphen separated hexadecimal characters in the form 8-4-4-4-12.

+
+clusterAPIAzureManagedIdentityClientID
+ + +ManagedIdentityClientID + + +
+

clusterAPIAzureManagedIdentityClientID is the client ID of a pre-existing managed identity ID associated with cluster-api +azure. The client ID of a managed identity must be a valid UUID. It should be 5 groups of hyphen separated +hexadecimal characters in the form 8-4-4-4-12.

+
+controlPlaneManagedIdentityClientID
+ + +ManagedIdentityClientID + + +
+

controlPlaneManagedIdentityClientID is the client ID of a pre-existing managed identity ID associated with the control plane +operator. The client ID of a managed identity must be a valid UUID. It should be 5 groups of hyphen separated +hexadecimal characters in the form 8-4-4-4-12.

+
+azureKMSManagedIdentityClientID
+ + +ManagedIdentityClientID + + +
+(Optional) +

azureKMSManagedIdentityClientID is the client ID of a pre-existing managed identity ID associated with Azure KMS. The client +ID of a managed identity must be a valid UUID. It should be 5 groups of hyphen separated hexadecimal characters +in the form 8-4-4-4-12.

+
+imageRegistryManagedIdentityClientID
+ + +ManagedIdentityClientID + + +
+

imageRegistryManagedIdentityClientID is the client ID of a pre-existing managed identity ID associated with the +cluster-image-registry-operator. The client ID of a managed identity must be a valid UUID. It should be 5 groups +of hyphen separated hexadecimal characters in the form 8-4-4-4-12.

+
+ingressManagedIdentityClientID
+ + +ManagedIdentityClientID + + +
+

ingressManagedIdentityClientID is the client ID of a pre-existing managed identity ID associated with the +cluster-ingress-operator. The client ID of a managed identity must be a valid UUID. It should be 5 groups of +hyphen separated hexadecimal characters in the form 8-4-4-4-12.

+
+networkManagedIdentityClientID
+ + +ManagedIdentityClientID + + +
+

networkManagedIdentityClientID is the client ID of a pre-existing managed identity ID associated with the +cluster-network-operator. The client ID of a managed identity must be a valid UUID. It should be 5 groups of +hyphen separated hexadecimal characters in the form 8-4-4-4-12.

+
+azureDiskManagedIdentityClientID
+ + +ManagedIdentityClientID + + +
+

azureDiskManagedIdentityClientID is the client ID of a pre-existing managed identity ID associated with the +azure-disk-controller. The client ID of a managed identity must be a valid UUID. It should be 5 groups of hyphen +separated hexadecimal characters in the form 8-4-4-4-12.

+
+azureFileManagedIdentityClientID
+ + +ManagedIdentityClientID + + +
+

azureFileManagedIdentityClientID is the client ID of a pre-existing managed identity ID associated with the +azure-disk-controller. The client ID of a managed identity must be a valid UUID. It should be 5 groups of hyphen +separated hexadecimal characters in the form 8-4-4-4-12.

+
###DNSSpec { #hypershift.openshift.io/v1beta1.DNSSpec }

(Appears on: @@ -6729,6 +6933,14 @@ is empty.

+###ManagedIdentityClientID { #hypershift.openshift.io/v1beta1.ManagedIdentityClientID } +

+(Appears on: +ControlPlaneManagedIdentities) +

+

+

ManagedIdentityClientID is a client ID of a managed identity

+

###MarketplaceImage { #hypershift.openshift.io/v1beta1.MarketplaceImage }

(Appears on: diff --git a/hypershift-operator/controllers/hostedcluster/hostedcluster_controller.go b/hypershift-operator/controllers/hostedcluster/hostedcluster_controller.go index 317a031fe95..ceb6f2e2da8 100644 --- a/hypershift-operator/controllers/hostedcluster/hostedcluster_controller.go +++ b/hypershift-operator/controllers/hostedcluster/hostedcluster_controller.go @@ -1586,6 +1586,16 @@ func (r *HostedClusterReconciler) reconcile(ctx context.Context, req ctrl.Reques proxy.SetEnvVars(&capiProviderDeploymentSpec.Template.Spec.Containers[0].Env) } + // For ARO HCP, we need to add the Microsoft init and sidecar containers to the CAPI Provider deployment + if os.Getenv("MANAGED_SERVICE") == hyperv1.AroHCP { + azureCredentials, err := azureutil.GetAzureCredentialsFromSecret(ctx, r.Client, hcluster.Namespace, hcluster.Spec.Platform.Azure.Credentials.Name) + if err != nil { + return ctrl.Result{}, err + } + + capiProviderDeploymentSpec.Template.Spec.Containers = append(capiProviderDeploymentSpec.Template.Spec.Containers, azureutil.AdapterServerContainer(string(azureCredentials.Data["AZURE_CLIENT_ID"]), string(azureCredentials.Data["AZURE_CLIENT_SECRET"]), string(azureCredentials.Data["AZURE_TENANT_ID"]))) + } + // Reconcile cluster prometheus RBAC resources if enabled if r.EnableOCPClusterMonitoring { if err := r.reconcileClusterPrometheusRBAC(ctx, createOrUpdate, hcp.Namespace); err != nil { @@ -2229,6 +2239,8 @@ func (r *HostedClusterReconciler) reconcileControlPlaneOperator(ctx context.Cont controlPlaneOperatorDeployment := controlplaneoperator.OperatorDeployment(controlPlaneNamespace.Name) _, err = createOrUpdate(ctx, r.Client, controlPlaneOperatorDeployment, func() error { return reconcileControlPlaneOperatorDeployment( + ctx, + r.Client, controlPlaneOperatorDeployment, openShiftTrustedCABundleConfigMapExists, hcluster, @@ -2478,6 +2490,8 @@ func GetControlPlaneOperatorImageLabels(ctx context.Context, hc *hyperv1.HostedC } func reconcileControlPlaneOperatorDeployment( + ctx context.Context, + c client.Client, deployment *appsv1.Deployment, openShiftTrustedCABundleConfigMapExists bool, hc *hyperv1.HostedCluster, @@ -2819,6 +2833,26 @@ func reconcileControlPlaneOperatorDeployment( if hcp.Annotations[hyperv1.ControlPlanePriorityClass] != "" { deploymentConfig.Scheduling.PriorityClass = hcp.Annotations[hyperv1.ControlPlanePriorityClass] } + + if os.Getenv("MANAGED_SERVICE") == hyperv1.AroHCP { + if deployment.Spec.Template.Spec.InitContainers == nil { + deployment.Spec.Template.Spec.InitContainers = []corev1.Container{} + } + + deployment.Spec.Template.Spec.InitContainers = append(deployment.Spec.Template.Spec.InitContainers, azureutil.AdapterInitContainer()) + + azureCredentials, err := azureutil.GetAzureCredentialsFromSecret(ctx, c, hcp.Namespace, hcp.Spec.Platform.Azure.Credentials.Name) + if err != nil { + return err + } + + deployment.Spec.Template.Spec.Containers = append(deployment.Spec.Template.Spec.Containers, azureutil.AdapterServerContainer(string(azureCredentials.Data["AZURE_CLIENT_ID"]), string(azureCredentials.Data["AZURE_CLIENT_SECRET"]), string(azureCredentials.Data["AZURE_TENANT_ID"]))) + + // ARO HCP needs elevated privileges in order to run the adapter-init container + deployment.Spec.Template.Spec.SecurityContext = nil + deploymentConfig.SetDefaultSecurityContext = false + } + deploymentConfig.SetDefaults(hcp, nil, k8sutilspointer.Int(1)) deploymentConfig.SetRestartAnnotation(hc.ObjectMeta) deploymentConfig.ApplyTo(deployment) @@ -3188,11 +3222,18 @@ func reconcileCAPIProviderDeployment(deployment *appsv1.Deployment, capiProvider // Enforce ServiceAccount. deployment.Spec.Template.Spec.ServiceAccountName = sa.Name + defaultSecurityContext := setDefaultSecurityContext + + // For ARO HCP, the MI sidecar containers need privileged permissions to run + if os.Getenv("MANAGED_SERVICE") == hyperv1.AroHCP { + defaultSecurityContext = false + } + deploymentConfig := config.DeploymentConfig{ Scheduling: config.Scheduling{ PriorityClass: config.DefaultPriorityClass, }, - SetDefaultSecurityContext: setDefaultSecurityContext, + SetDefaultSecurityContext: defaultSecurityContext, AdditionalLabels: map[string]string{ config.NeedManagementKASAccessLabel: "true", }, @@ -4333,12 +4374,9 @@ func (r *HostedClusterReconciler) validateAzureConfig(ctx context.Context, hc *h } // Verify the credentials secret contains the data fields we expect - credentialsSecret := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{ - Namespace: hc.Namespace, - Name: hc.Spec.Platform.Azure.Credentials.Name, - }} - if err := r.Get(ctx, client.ObjectKeyFromObject(credentialsSecret), credentialsSecret); err != nil { - return fmt.Errorf("failed to get credentials secret for cluster: %w", err) + credentialsSecret, err := azureutil.GetAzureCredentialsFromSecret(ctx, r.Client, hc.Namespace, hc.Spec.Platform.Azure.Credentials.Name) + if err != nil { + return err } var errs []error @@ -4348,12 +4386,6 @@ func (r *HostedClusterReconciler) validateAzureConfig(ctx context.Context, hc *h } } - // Verify the resource group locations match - err := azureutil.VerifyResourceGroupLocationsMatch(ctx, hc, credentialsSecret) - if err != nil { - errs = append(errs, err) - } - return utilerrors.NewAggregate(errs) } diff --git a/hypershift-operator/controllers/hostedcluster/internal/platform/azure/azure.go b/hypershift-operator/controllers/hostedcluster/internal/platform/azure/azure.go index 21770c592a5..4db23d0dc2d 100644 --- a/hypershift-operator/controllers/hostedcluster/internal/platform/azure/azure.go +++ b/hypershift-operator/controllers/hostedcluster/internal/platform/azure/azure.go @@ -56,7 +56,7 @@ func (a Azure) ReconcileCAPIInfraCR( } if _, err := createOrUpdate(ctx, client, azureClusterIdentity, func() error { - return reconcileAzureClusterIdentity(ctx, client, hcluster, azureClusterIdentity, controlPlaneNamespace) + return reconcileAzureClusterIdentity(hcluster, azureClusterIdentity, controlPlaneNamespace) }); err != nil { return nil, fmt.Errorf("failed to reconcile Azure cluster identity: %w", err) } @@ -83,6 +83,7 @@ func (a Azure) CAPIProviderDeploymentSpec(hcluster *hyperv1.HostedCluster, _ *hy Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ TerminationGracePeriodSeconds: k8sutilspointer.Int64(10), + InitContainers: []corev1.Container{azureutil.AdapterInitContainer()}, Containers: []corev1.Container{{ Name: "manager", Image: image, @@ -164,23 +165,6 @@ func (a Azure) ReconcileCredentials(ctx context.Context, c client.Client, create return err } - // Sync Azure Client Secret in its own secret for since CAPZ needs it in a specific key value - // https://capz.sigs.k8s.io/topics/multitenancy#manual-service-principal-identity - azureClientSecret := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "azure-client-secret", Namespace: controlPlaneNamespace}} - if _, err := createOrUpdate(ctx, c, azureClientSecret, func() error { - if azureClientSecret.Data == nil { - azureClientSecret.Data = map[string][]byte{} - } - for k, v := range source.Data { - if k == "AZURE_CLIENT_SECRET" { - azureClientSecret.Data["clientSecret"] = v - } - } - return nil - }); err != nil { - return err - } - // Sync CNCC secret cloudNetworkConfigCreds := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Namespace: controlPlaneNamespace, Name: "cloud-network-config-controller-creds"}} secretData := map[string][]byte{ @@ -247,17 +231,10 @@ func reconcileAzureCluster(azureCluster *capiazure.AzureCluster, hcluster *hyper return nil } -func reconcileAzureClusterIdentity(ctx context.Context, c client.Client, hcluster *hyperv1.HostedCluster, azureClusterIdentity *capiazure.AzureClusterIdentity, controlPlaneNamespace string) error { - credentialsSecret := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: hcluster.Spec.Platform.Azure.Credentials.Name, Namespace: controlPlaneNamespace}} - if err := c.Get(ctx, client.ObjectKeyFromObject(credentialsSecret), credentialsSecret); err != nil { - return fmt.Errorf("failed to get secret %s: %w", credentialsSecret, err) - } - +func reconcileAzureClusterIdentity(hcluster *hyperv1.HostedCluster, azureClusterIdentity *capiazure.AzureClusterIdentity, controlPlaneNamespace string) error { azureClusterIdentity.Spec = capiazure.AzureClusterIdentitySpec{ - ClientID: string(credentialsSecret.Data["AZURE_CLIENT_ID"]), - ClientSecret: corev1.SecretReference{Name: "azure-client-secret", Namespace: controlPlaneNamespace}, - TenantID: string(credentialsSecret.Data["AZURE_TENANT_ID"]), - Type: capiazure.ServicePrincipal, + ClientID: string(hcluster.Spec.Platform.Azure.ManagedIdentities.ControlPlaneManagedIdentities.ClusterAPIAzureManagedIdentityClientID), + Type: capiazure.UserAssignedMSI, AllowedNamespaces: &capiazure.AllowedNamespaces{ NamespaceList: []string{ controlPlaneNamespace, diff --git a/support/azureutil/azureutil.go b/support/azureutil/azureutil.go index 89ed85aa60d..aeec7778dbc 100644 --- a/support/azureutil/azureutil.go +++ b/support/azureutil/azureutil.go @@ -3,20 +3,25 @@ package azureutil import ( "context" "fmt" - "k8s.io/utils/ptr" "strings" - hyperv1 "github.com/openshift/hypershift/api/hypershift/v1beta1" - corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" "github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" - "github.com/Azure/azure-sdk-for-go/sdk/azidentity" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v5" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources" ) +// We received this images directly from Microsoft; we should expect them to change as Microsoft continues development on both containers. +// We are scheduled to receive new updates to these containers in October 2024 to support Managed Identities. They currently only support Service Principal. +// TODO past October, will we receive new versions? +const ( + AdapterInitImage = "aromiwi.azurecr.io/artifact/b8e9ef87-cd63-4085-ab14-1c637806568c/buddy/adapter-init:20240905.9" + AdapterServerImage = "aromiwi.azurecr.io/artifact/b8e9ef87-cd63-4085-ab14-1c637806568c/buddy/adapter-server:20240905.5" +) + // GetSubnetNameFromSubnetID extracts the subnet name from a subnet ID // Example subnet ID: /subscriptions//resourceGroups//providers/Microsoft.Network/virtualNetworks//subnets/ func GetSubnetNameFromSubnetID(subnetID string) (string, error) { @@ -145,8 +150,8 @@ func getFullVnetInfo(ctx context.Context, subscriptionID string, vnetResourceGro return vnet, nil } -// getNetworkSecurityGroupInfo gets the full information on a network security group based on its ID -func getNetworkSecurityGroupInfo(ctx context.Context, nsgID string, subscriptionID string, azureCreds azcore.TokenCredential) (armnetwork.SecurityGroupsClientGetResponse, error) { +// GetNetworkSecurityGroupInfo gets the full information on a network security group based on its ID +func GetNetworkSecurityGroupInfo(ctx context.Context, nsgID string, subscriptionID string, azureCreds azcore.TokenCredential) (armnetwork.SecurityGroupsClientGetResponse, error) { partialNSGInfo, err := arm.ParseResourceID(nsgID) if err != nil { return armnetwork.SecurityGroupsClientGetResponse{}, fmt.Errorf("failed to parse network security group id %q: %v", nsgID, err) @@ -165,8 +170,8 @@ func getNetworkSecurityGroupInfo(ctx context.Context, nsgID string, subscription return nsg, nil } -// getResourceGroupInfo gets the full information on a resource group based on its name -func getResourceGroupInfo(ctx context.Context, rgName string, subscriptionID string, azureCreds azcore.TokenCredential) (armresources.ResourceGroupsClientGetResponse, error) { +// GetResourceGroupInfo gets the full information on a resource group based on its name +func GetResourceGroupInfo(ctx context.Context, rgName string, subscriptionID string, azureCreds azcore.TokenCredential) (armresources.ResourceGroupsClientGetResponse, error) { resourceGroupClient, err := armresources.NewResourceGroupsClient(subscriptionID, azureCreds, nil) if err != nil { return armresources.ResourceGroupsClientGetResponse{}, fmt.Errorf("failed to create new resource groups client: %w", err) @@ -180,40 +185,63 @@ func getResourceGroupInfo(ctx context.Context, rgName string, subscriptionID str return rg, nil } -// VerifyResourceGroupLocationsMatch verifies the locations match for the VNET, network security group, and managed resource groups -func VerifyResourceGroupLocationsMatch(ctx context.Context, hc *hyperv1.HostedCluster, credentialsSecret *corev1.Secret) error { - // Setup azureCreds so we can retrieve the locations of the resource groups - tenantID := string(credentialsSecret.Data["AZURE_TENANT_ID"]) - clientID := string(credentialsSecret.Data["AZURE_CLIENT_ID"]) - clientSecret := string(credentialsSecret.Data["AZURE_CLIENT_SECRET"]) +// GetAzureCredentialsFromSecret gets the Service Principal client ID, client secret, and tenant ID from the credentials +// secret. This function will be modified a bit once the Microsoft sidecar containers support Managed Identity are +// delivered (expected Oct 2024). +func GetAzureCredentialsFromSecret(ctx context.Context, c client.Client, namespace, credsName string) (*corev1.Secret, error) { + var azureCredentials corev1.Secret - creds, err := azidentity.NewClientSecretCredential(tenantID, clientID, clientSecret, nil) - if err != nil { - return fmt.Errorf("failed to create azure creds to verify resource group locations: %v", err) + // Retrieve the Azure credentials secret to extract the needed fields for the managed identity containers + credentialsSecretName := client.ObjectKey{Namespace: namespace, Name: credsName} + if err := c.Get(ctx, credentialsSecretName, &azureCredentials); err != nil { + return nil, fmt.Errorf("failed to get secret %s: %w", credentialsSecretName, err) } - // Retrieve full vnet information from the VNET ID - vnet, err := GetVnetInfoFromVnetID(ctx, hc.Spec.Platform.Azure.VnetID, hc.Spec.Platform.Azure.SubscriptionID, creds) - if err != nil { - return fmt.Errorf("failed to get vnet info to verify its location: %v", err) + for _, expectedKey := range []string{"AZURE_CLIENT_ID", "AZURE_CLIENT_SECRET", "AZURE_TENANT_ID"} { + if _, found := azureCredentials.Data[expectedKey]; !found { + return nil, fmt.Errorf("credentials secret for cluster doesn't have required key %s", expectedKey) + } } - // Retrieve full network security group information from the network security group ID - nsg, err := getNetworkSecurityGroupInfo(ctx, hc.Spec.Platform.Azure.SecurityGroupID, hc.Spec.Platform.Azure.SubscriptionID, creds) - if err != nil { - return fmt.Errorf("failed to get network security group info to verify its location: %v", err) - } - - // Retrieve full resource group information from the resource group name - rg, err := getResourceGroupInfo(ctx, hc.Spec.Platform.Azure.ResourceGroupName, hc.Spec.Platform.Azure.SubscriptionID, creds) - if err != nil { - return fmt.Errorf("failed to get resource group info to verify its location: %v", err) - } + return &azureCredentials, nil +} - // Verify the vnet resource group location, network security group resource group location, and the managed resource group location match - if ptr.Deref(vnet.Location, "") != ptr.Deref(nsg.Location, "") || ptr.Deref(nsg.Location, "") != ptr.Deref(rg.Location, "") { - return fmt.Errorf("the locations of the resource groups do not match - vnet location: %v; network security group location: %v; managed resource group location: %v", ptr.Deref(vnet.Location, ""), ptr.Deref(nsg.Location, ""), ptr.Deref(rg.Location, "")) - } +// AdapterInitContainer returns the Microsoft adapter-init init container. This container needs the NET_ADMIN permission +// so the adapter-server sidecar container can intercept the Managed Identity Azure API authentication calls. +func AdapterInitContainer() corev1.Container { + return corev1.Container{ + Name: "adapter-init", + Image: AdapterInitImage, + ImagePullPolicy: corev1.PullIfNotPresent, + SecurityContext: &corev1.SecurityContext{ + Capabilities: &corev1.Capabilities{ + Add: []corev1.Capability{ + "NET_ADMIN", + }, + }, + }} +} - return nil +// AdapterServerContainer returns the Microsoft adapter-server sidecar container. Currently, this container mimics Azure +// Managed Identity approval and returns an authentication token. The container currently needs a Service Principal to +// do this. Future versions of this container will be able to take a Managed Identity instead. +func AdapterServerContainer(clientID, clientSecret, tenantID string) corev1.Container { + return corev1.Container{Name: "adapter-server", + Image: AdapterServerImage, + ImagePullPolicy: corev1.PullIfNotPresent, + Args: []string{"sp"}, + Env: []corev1.EnvVar{ + { + Name: "AZURE_CLIENT_ID", + Value: clientID, + }, + { + Name: "AZURE_CLIENT_SECRET", + Value: clientSecret, + }, + { + Name: "AZURE_TENANT_ID", + Value: tenantID, + }, + }} } diff --git a/support/azureutil/azureutil_test.go b/support/azureutil/azureutil_test.go index 6a5da3ad2da..9882749ef2d 100644 --- a/support/azureutil/azureutil_test.go +++ b/support/azureutil/azureutil_test.go @@ -1,9 +1,17 @@ package azureutil import ( + "context" "testing" + hyperv1 "github.com/openshift/hypershift/api/hypershift/v1beta1" + . "github.com/onsi/gomega" + "github.com/openshift/hypershift/support/api" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + crclient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" ) func TestGetSubnetNameFromSubnetID(t *testing.T) { @@ -138,3 +146,116 @@ func TestGetVnetNameAndResourceGroupFromVnetID(t *testing.T) { }) } } + +func TestGetAzureCredentialsFromSecret(t *testing.T) { + tests := []struct { + testCaseName string + hc *hyperv1.HostedCluster + secret *corev1.Secret + expectedErr bool + }{ + { + testCaseName: "nominal test case", + hc: &hyperv1.HostedCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "clusters", + }, + Spec: hyperv1.HostedClusterSpec{ + Platform: hyperv1.PlatformSpec{ + Type: hyperv1.AzurePlatform, + Azure: &hyperv1.AzurePlatformSpec{ + Credentials: corev1.LocalObjectReference{Name: "cloud-credentials"}, + }, + }, + }, + }, + secret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cloud-credentials", + Namespace: "clusters", + }, + Data: map[string][]byte{ + "AZURE_CLIENT_ID": []byte("46fb37b5"), + "AZURE_CLIENT_SECRET": []byte("46fb37b5"), + "AZURE_TENANT_ID": []byte("46fb37b5"), + }, + }, + expectedErr: false, + }, + { + testCaseName: "wrong secret name, err", + hc: &hyperv1.HostedCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "clusters", + }, + Spec: hyperv1.HostedClusterSpec{ + Platform: hyperv1.PlatformSpec{ + Type: hyperv1.AzurePlatform, + Azure: &hyperv1.AzurePlatformSpec{ + Credentials: corev1.LocalObjectReference{Name: "cloud-credentialss"}, + }, + }, + }, + }, + secret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cloud-credentials", + Namespace: "clusters", + }, + Data: map[string][]byte{ + "AZURE_CLIENT_ID": []byte("46fb37b5"), + "AZURE_CLIENT_SECRET": []byte("46fb37b5"), + "AZURE_TENANT_ID": []byte("46fb37b5"), + }, + }, + expectedErr: true, + }, + { + testCaseName: "missing date from secret, err", + hc: &hyperv1.HostedCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "clusters", + }, + Spec: hyperv1.HostedClusterSpec{ + Platform: hyperv1.PlatformSpec{ + Type: hyperv1.AzurePlatform, + Azure: &hyperv1.AzurePlatformSpec{ + Credentials: corev1.LocalObjectReference{Name: "cloud-credentialss"}, + }, + }, + }, + }, + secret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cloud-credentials", + Namespace: "clusters", + }, + Data: map[string][]byte{ + "AZURE_CLIENT_ID": []byte("46fb37b5"), + "AZURE_TENANT_ID": []byte("46fb37b5"), + }, + }, + expectedErr: true, + }, + } + for _, tc := range tests { + t.Run(tc.testCaseName, func(t *testing.T) { + g := NewGomegaWithT(t) + + objs := []crclient.Object{tc.hc, tc.secret} + + client := fake.NewClientBuilder().WithScheme(api.Scheme).WithObjects(objs...).Build() + + creds, err := GetAzureCredentialsFromSecret(context.TODO(), client, tc.hc.Namespace, tc.hc.Spec.Platform.Azure.Credentials.Name) + if !tc.expectedErr { + g.Expect(err).To(BeNil()) + g.Expect(creds.Name).To(Equal(tc.hc.Spec.Platform.Azure.Credentials.Name)) + } else { + g.Expect(err).To(Not(BeNil())) + } + }) + } +} diff --git a/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/hostedcluster_types.go b/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/hostedcluster_types.go index 42a98277c51..2aa9d9960fc 100644 --- a/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/hostedcluster_types.go +++ b/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/hostedcluster_types.go @@ -1805,7 +1805,7 @@ type AzurePlatformSpec struct { // // Resource group naming requirements can be found here: https://azure.github.io/PSRule.Rules.Azure/en/rules/Azure.ResourceGroup.Name/. // - //Example: if your resource group ID is /subscriptions//resourceGroups/, your + // Example: if your resource group ID is /subscriptions//resourceGroups/, your // ResourceGroupName is . // // +kubebuilder:default:=default @@ -1857,8 +1857,98 @@ type AzurePlatformSpec struct { // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="SecurityGroupID is immutable" // +kubebuilder:validation:Required // +immutable - // +required SecurityGroupID string `json:"securityGroupID,omitempty"` + + // managedIdentities contains the client IDs related to the managed identities needed for HCP control plane + // and data plane components that authenticate with Azure's API. + // + // +kubebuilder:validation:Required + ManagedIdentities AzureResourceManagedIdentities `json:"managedIdentities,omitempty"` +} + +// AzureResourceManagedIdentities contains the client IDs related to the managed identities needed for HCP control plane +// and data plane components that authenticate with Azure's API. +type AzureResourceManagedIdentities struct { + // ControlPlaneManagedIdentities contains the client IDs of all the managed identities on the HCP control plane needing to + // authenticate with Azure's API. + // + // +kubebuilder:validation:Required + ControlPlaneManagedIdentities ControlPlaneManagedIdentities `json:"controlPlaneManagedIdentities"` + + // Future placeholder - DataPlaneMIs * DataPlaneManagedIdentities +} + +// ManagedIdentityClientID is a client ID of a managed identity +// +kubebuilder:validation:XValidation:rule="self.matches('^[{]?[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}[}]?$')",message="the client ID of a managed identity must be a valid UUID. It should be 5 groups of hyphen separated hexadecimal characters in the form 8-4-4-4-12." +type ManagedIdentityClientID string + +// ControlPlaneManagedIdentities contains the client IDs of all the managed identities on the HCP control plane needing +// to authenticate with Azure's API. +// Managed identity regex pattern is from Microsoft here - https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/resource-name-rules#microsoftmanagedidentity. +// The format a managed identity should be `/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{managedIdentityName}`. +type ControlPlaneManagedIdentities struct { + // azureCloudProviderManagedIdentityClientID is the client ID of a pre-existing managed identity ID associated with the azure + // cloud provider, aka ccm. The client ID of a managed identity must be a valid UUID. It should be 5 groups of + // hyphen separated hexadecimal characters in the form 8-4-4-4-12. + // + // +kubebuilder:validation:Required + AzureCloudProviderManagedIdentityClientID ManagedIdentityClientID `json:"azureCloudProviderManagedIdentityClientID"` + + // clusterAPIAzureManagedIdentityClientID is the client ID of a pre-existing managed identity ID associated with cluster-api + // azure. The client ID of a managed identity must be a valid UUID. It should be 5 groups of hyphen separated + // hexadecimal characters in the form 8-4-4-4-12. + // + // +kubebuilder:validation:Required + ClusterAPIAzureManagedIdentityClientID ManagedIdentityClientID `json:"clusterAPIAzureManagedIdentityClientID"` + + // controlPlaneManagedIdentityClientID is the client ID of a pre-existing managed identity ID associated with the control plane + // operator. The client ID of a managed identity must be a valid UUID. It should be 5 groups of hyphen separated + // hexadecimal characters in the form 8-4-4-4-12. + // + // +kubebuilder:validation:Required + ControlPlaneManagedIdentityClientID ManagedIdentityClientID `json:"controlPlaneManagedIdentityClientID"` + + // azureKMSManagedIdentityClientID is the client ID of a pre-existing managed identity ID associated with Azure KMS. The client + // ID of a managed identity must be a valid UUID. It should be 5 groups of hyphen separated hexadecimal characters + // in the form 8-4-4-4-12. + // + // +optional + AzureKMSManagedIdentityClientID ManagedIdentityClientID `json:"azureKMSManagedIdentityClientID,omitempty"` + + // imageRegistryManagedIdentityClientID is the client ID of a pre-existing managed identity ID associated with the + // cluster-image-registry-operator. The client ID of a managed identity must be a valid UUID. It should be 5 groups + // of hyphen separated hexadecimal characters in the form 8-4-4-4-12. + // + // +kubebuilder:validation:Required + ImageRegistryManagedIdentityClientID ManagedIdentityClientID `json:"imageRegistryManagedIdentityClientID"` + + // ingressManagedIdentityClientID is the client ID of a pre-existing managed identity ID associated with the + // cluster-ingress-operator. The client ID of a managed identity must be a valid UUID. It should be 5 groups of + // hyphen separated hexadecimal characters in the form 8-4-4-4-12. + // + // +kubebuilder:validation:Required + IngressManagedIdentityClientID ManagedIdentityClientID `json:"ingressManagedIdentityClientID"` + + // networkManagedIdentityClientID is the client ID of a pre-existing managed identity ID associated with the + // cluster-network-operator. The client ID of a managed identity must be a valid UUID. It should be 5 groups of + // hyphen separated hexadecimal characters in the form 8-4-4-4-12. + // + // +kubebuilder:validation:Required + NetworkManagedIdentityClientID ManagedIdentityClientID `json:"networkManagedIdentityClientID"` + + // azureDiskManagedIdentityClientID is the client ID of a pre-existing managed identity ID associated with the + // azure-disk-controller. The client ID of a managed identity must be a valid UUID. It should be 5 groups of hyphen + // separated hexadecimal characters in the form 8-4-4-4-12. + // + // +kubebuilder:validation:Required + AzureDiskManagedIdentityClientID ManagedIdentityClientID `json:"azureDiskManagedIdentityClientID"` + + // azureFileManagedIdentityClientID is the client ID of a pre-existing managed identity ID associated with the + // azure-disk-controller. The client ID of a managed identity must be a valid UUID. It should be 5 groups of hyphen + // separated hexadecimal characters in the form 8-4-4-4-12. + // + // +kubebuilder:validation:Required + AzureFileManagedIdentityClientID ManagedIdentityClientID `json:"azureFileManagedIdentityClientID"` } // OpenStackPlatformSpec specifies configuration for clusters running on OpenStack. diff --git a/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/zz_generated.deepcopy.go b/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/zz_generated.deepcopy.go index 14e4e51e549..ff6b0f7894b 100644 --- a/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/zz_generated.deepcopy.go +++ b/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/zz_generated.deepcopy.go @@ -564,6 +564,7 @@ func (in *AzureNodePoolPlatform) DeepCopy() *AzureNodePoolPlatform { func (in *AzurePlatformSpec) DeepCopyInto(out *AzurePlatformSpec) { *out = *in out.Credentials = in.Credentials + out.ManagedIdentities = in.ManagedIdentities } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AzurePlatformSpec. @@ -576,6 +577,22 @@ func (in *AzurePlatformSpec) DeepCopy() *AzurePlatformSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AzureResourceManagedIdentities) DeepCopyInto(out *AzureResourceManagedIdentities) { + *out = *in + out.ControlPlaneManagedIdentities = in.ControlPlaneManagedIdentities +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AzureResourceManagedIdentities. +func (in *AzureResourceManagedIdentities) DeepCopy() *AzureResourceManagedIdentities { + if in == nil { + return nil + } + out := new(AzureResourceManagedIdentities) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AzureVMImage) DeepCopyInto(out *AzureVMImage) { *out = *in @@ -879,6 +896,21 @@ func (in *ClusterVersionStatus) DeepCopy() *ClusterVersionStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ControlPlaneManagedIdentities) DeepCopyInto(out *ControlPlaneManagedIdentities) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControlPlaneManagedIdentities. +func (in *ControlPlaneManagedIdentities) DeepCopy() *ControlPlaneManagedIdentities { + if in == nil { + return nil + } + out := new(ControlPlaneManagedIdentities) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DNSSpec) DeepCopyInto(out *DNSSpec) { *out = *in