internal/api/v20240610preview/hcpopenshiftclusters_methods.go (422 lines of code) (raw):

// Copyright 2025 Microsoft Corporation // // 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. package v20240610preview import ( "fmt" "net/http" "github.com/Azure/ARO-HCP/internal/api" "github.com/Azure/ARO-HCP/internal/api/arm" "github.com/Azure/ARO-HCP/internal/api/v20240610preview/generated" ) type HcpOpenShiftCluster struct { generated.HcpOpenShiftCluster } func newVersionProfile(from *api.VersionProfile) *generated.VersionProfile { return &generated.VersionProfile{ ID: api.Ptr(from.ID), ChannelGroup: api.Ptr(from.ChannelGroup), AvailableUpgrades: api.StringSliceToStringPtrSlice(from.AvailableUpgrades), } } func newDNSProfile(from *api.DNSProfile) *generated.DNSProfile { return &generated.DNSProfile{ BaseDomain: api.Ptr(from.BaseDomain), BaseDomainPrefix: api.Ptr(from.BaseDomainPrefix), } } func newNetworkProfile(from *api.NetworkProfile) *generated.NetworkProfile { return &generated.NetworkProfile{ NetworkType: api.Ptr(generated.NetworkType(from.NetworkType)), PodCidr: api.Ptr(from.PodCIDR), ServiceCidr: api.Ptr(from.ServiceCIDR), MachineCidr: api.Ptr(from.MachineCIDR), HostPrefix: api.Ptr(from.HostPrefix), } } func newConsoleProfile(from *api.ConsoleProfile) *generated.ConsoleProfile { return &generated.ConsoleProfile{ URL: api.Ptr(from.URL), } } func newAPIProfile(from *api.APIProfile) *generated.APIProfile { return &generated.APIProfile{ URL: api.Ptr(from.URL), Visibility: api.Ptr(generated.Visibility(from.Visibility)), } } func newPlatformProfile(from *api.PlatformProfile) *generated.PlatformProfile { return &generated.PlatformProfile{ ManagedResourceGroup: api.Ptr(from.ManagedResourceGroup), SubnetID: api.Ptr(from.SubnetID), OutboundType: api.Ptr(generated.OutboundType(from.OutboundType)), NetworkSecurityGroupID: api.Ptr(from.NetworkSecurityGroupID), OperatorsAuthentication: newOperatorsAuthenticationProfile(&from.OperatorsAuthentication), IssuerURL: api.Ptr(from.IssuerURL), } } func newOperatorsAuthenticationProfile(from *api.OperatorsAuthenticationProfile) *generated.OperatorsAuthenticationProfile { return &generated.OperatorsAuthenticationProfile{ UserAssignedIdentities: newUserAssignedIdentitiesProfile(&from.UserAssignedIdentities), } } func newUserAssignedIdentitiesProfile(from *api.UserAssignedIdentitiesProfile) *generated.UserAssignedIdentitiesProfile { return &generated.UserAssignedIdentitiesProfile{ ControlPlaneOperators: api.StringMapToStringPtrMap(from.ControlPlaneOperators), DataPlaneOperators: api.StringMapToStringPtrMap(from.DataPlaneOperators), ServiceManagedIdentity: api.Ptr(from.ServiceManagedIdentity), } } func newClusterCapabilitiesProfile(from *api.ClusterCapabilitiesProfile) *generated.ClusterCapabilitiesProfile { out := &generated.ClusterCapabilitiesProfile{ Disabled: make([]*generated.OptionalClusterCapability, len(from.Disabled)), } for index, item := range from.Disabled { out.Disabled[index] = api.Ptr(generated.OptionalClusterCapability(item)) } return out } func (v version) NewHCPOpenShiftCluster(from *api.HCPOpenShiftCluster) api.VersionedHCPOpenShiftCluster { if from == nil { from = api.NewDefaultHCPOpenShiftCluster() } out := &HcpOpenShiftCluster{ generated.HcpOpenShiftCluster{ ID: api.Ptr(from.ID), Name: api.Ptr(from.Name), Type: api.Ptr(from.Type), Location: api.Ptr(from.Location), Tags: api.StringMapToStringPtrMap(from.Tags), Identity: &generated.ManagedServiceIdentity{ Type: api.Ptr(generated.ManagedServiceIdentityType(from.Identity.Type)), PrincipalID: api.Ptr(from.Identity.PrincipalID), TenantID: api.Ptr(from.Identity.TenantID), //as UserAssignedIdentities is of a different type so using convertUserAssignedIdentities instead of StringMapToStringPtrMap UserAssignedIdentities: convertUserAssignedIdentities(from.Identity.UserAssignedIdentities), }, Properties: &generated.HcpOpenShiftClusterProperties{ ProvisioningState: api.Ptr(generated.ProvisioningState(from.Properties.ProvisioningState)), Version: newVersionProfile(&from.Properties.Version), DNS: newDNSProfile(&from.Properties.DNS), Network: newNetworkProfile(&from.Properties.Network), Console: newConsoleProfile(&from.Properties.Console), API: newAPIProfile(&from.Properties.API), Platform: newPlatformProfile(&from.Properties.Platform), Capabilities: newClusterCapabilitiesProfile(&from.Properties.Capabilities), }, }, } if from.SystemData != nil { out.SystemData = &generated.SystemData{ CreatedBy: api.Ptr(from.SystemData.CreatedBy), CreatedByType: api.Ptr(generated.CreatedByType(from.SystemData.CreatedByType)), CreatedAt: from.SystemData.CreatedAt, LastModifiedBy: api.Ptr(from.SystemData.LastModifiedBy), LastModifiedByType: api.Ptr(generated.CreatedByType(from.SystemData.LastModifiedByType)), LastModifiedAt: from.SystemData.LastModifiedAt, } } return out } func (v version) MarshalHCPOpenShiftCluster(from *api.HCPOpenShiftCluster) ([]byte, error) { return arm.MarshalJSON(v.NewHCPOpenShiftCluster(from)) } func (c *HcpOpenShiftCluster) Normalize(out *api.HCPOpenShiftCluster) { if c.ID != nil { out.ID = *c.ID } if c.Name != nil { out.Name = *c.Name } if c.Type != nil { out.Type = *c.Type } if c.SystemData != nil { out.SystemData = &arm.SystemData{ CreatedAt: c.SystemData.CreatedAt, LastModifiedAt: c.SystemData.LastModifiedAt, } if c.SystemData.CreatedBy != nil { out.SystemData.CreatedBy = *c.SystemData.CreatedBy } if c.SystemData.CreatedByType != nil { out.SystemData.CreatedByType = arm.CreatedByType(*c.SystemData.CreatedByType) } if c.SystemData.LastModifiedBy != nil { out.SystemData.LastModifiedBy = *c.SystemData.LastModifiedBy } if c.SystemData.LastModifiedByType != nil { out.SystemData.LastModifiedByType = arm.CreatedByType(*c.SystemData.LastModifiedByType) } } if c.Location != nil { out.Location = *c.Location } if c.Identity != nil { if c.Identity.PrincipalID != nil { out.Identity.PrincipalID = *c.Identity.PrincipalID } if c.Identity.TenantID != nil { out.Identity.TenantID = *c.Identity.TenantID } if c.Identity.Type != nil { out.Identity.Type = (arm.ManagedServiceIdentityType)(*c.Identity.Type) } if c.Identity.UserAssignedIdentities != nil { normalizeIdentityUserAssignedIdentities(c.Identity.UserAssignedIdentities, &out.Identity.UserAssignedIdentities) } } // Per RPC-Patch-V1-04, the Tags field does NOT follow // JSON merge-patch (RFC 7396) semantics: // // When Tags are patched, the tags from the request // replace all existing tags for the resource // out.Tags = api.StringPtrMapToStringMap(c.Tags) if c.Properties != nil { if c.Properties.ProvisioningState != nil { out.Properties.ProvisioningState = arm.ProvisioningState(*c.Properties.ProvisioningState) } if c.Properties != nil { if c.Properties.Version != nil { normalizeVersion(c.Properties.Version, &out.Properties.Version) } if c.Properties.DNS != nil { normailzeDNS(c.Properties.DNS, &out.Properties.DNS) } if c.Properties.Network != nil { normalizeNetwork(c.Properties.Network, &out.Properties.Network) } if c.Properties.Console != nil { normalizeConsole(c.Properties.Console, &out.Properties.Console) } if c.Properties.API != nil { normalizeAPI(c.Properties.API, &out.Properties.API) } if c.Properties.Platform != nil { normalizePlatform(c.Properties.Platform, &out.Properties.Platform) } if c.Properties.Capabilities != nil { normalizeCapabilities(c.Properties.Capabilities, &out.Properties.Capabilities) } } } } func (c *HcpOpenShiftCluster) validateVersion(normalized *api.HCPOpenShiftCluster) []arm.CloudErrorBody { var errorDetails []arm.CloudErrorBody // XXX For now, "stable" is the only accepted value. In the future, we may // allow unlocking other channel groups through Azure Feature Exposure // Control (AFEC) flags or some other mechanism. if normalized.Properties.Version.ChannelGroup != "stable" { errorDetails = append(errorDetails, arm.CloudErrorBody{ Code: arm.CloudErrorCodeInvalidRequestContent, Message: "Channel group must be 'stable'", Target: "properties.version.channelGroup", }) } return errorDetails } func (c *HcpOpenShiftCluster) validateUserAssignedIdentities(normalized *api.HCPOpenShiftCluster) []arm.CloudErrorBody { var errorDetails []arm.CloudErrorBody // Idea is to check every identity mentioned in the Identity.UserAssignedIdentities is // being declared under Properties.Platform.OperatorsAuthentication.UserAssignedIdentities. if normalized.Identity.UserAssignedIdentities != nil { // Initiate the map that will have the number occurence of ConstrolPlaneOperators fields. controlPlaneOpOccurrences := make(map[string]int) // Generate a Map of Resource IDs of ControlplaneOperators MI, disregard the DataPlaneOperators. for _, operatorResourceID := range normalized.Properties.Platform.OperatorsAuthentication.UserAssignedIdentities.ControlPlaneOperators { controlPlaneOpOccurrences[operatorResourceID]++ } // variable to hold serviceManagedIdentity smiResourceID := normalized.Properties.Platform.OperatorsAuthentication.UserAssignedIdentities.ServiceManagedIdentity for operatorName, resourceID := range normalized.Properties.Platform.OperatorsAuthentication.UserAssignedIdentities.ControlPlaneOperators { _, ok := normalized.Identity.UserAssignedIdentities[resourceID] if !ok { errorDetails = append(errorDetails, arm.CloudErrorBody{ Code: arm.CloudErrorCodeInvalidRequestContent, Message: fmt.Sprintf( "identity %s is not assigned to this resource", resourceID), Target: fmt.Sprintf("properties.platform.operatorsAuthentication.userAssignedIdentities.controlPlaneOperators[%s]", operatorName), }) } else if controlPlaneOpOccurrences[resourceID] > 1 { errorDetails = append(errorDetails, arm.CloudErrorBody{ Code: arm.CloudErrorCodeInvalidRequestContent, Message: fmt.Sprintf( "identity %s is used multiple times", resourceID), Target: fmt.Sprintf("properties.platform.operatorsAuthentication.userAssignedIdentities.controlPlaneOperators[%s]", operatorName), }) } } if smiResourceID != "" { _, ok := normalized.Identity.UserAssignedIdentities[smiResourceID] if !ok { errorDetails = append(errorDetails, arm.CloudErrorBody{ Code: arm.CloudErrorCodeInvalidRequestContent, Message: fmt.Sprintf( "identity %s is not assigned to this resource", smiResourceID), Target: "properties.platform.operatorsAuthentication.userAssignedIdentities.serviceManagedIdentity", }) } // Making sure serviceManagedIdentity is not already assigned to controlPlaneOperators. if _, ok := controlPlaneOpOccurrences[smiResourceID]; ok { errorDetails = append(errorDetails, arm.CloudErrorBody{ Code: arm.CloudErrorCodeInvalidRequestContent, Message: fmt.Sprintf( "identity %s is used multiple times", smiResourceID), Target: "properties.platform.operatorsAuthentication.userAssignedIdentities.serviceManagedIdentity", }) } } for resourceID := range normalized.Identity.UserAssignedIdentities { if _, ok := controlPlaneOpOccurrences[resourceID]; !ok { if smiResourceID != resourceID { errorDetails = append(errorDetails, arm.CloudErrorBody{ Code: arm.CloudErrorCodeInvalidRequestContent, Message: fmt.Sprintf( "identity %s is assigned to this resource but not used", resourceID), Target: "identity.UserAssignedIdentities", }) } } } } return errorDetails } // validateStaticComplex performs more complex, multi-field validations than // are possible with struct tag validation. The returned CloudErrorBody slice // contains structured but user-friendly details for all discovered errors. func (c *HcpOpenShiftCluster) validateStaticComplex(normalized *api.HCPOpenShiftCluster) []arm.CloudErrorBody { var errorDetails []arm.CloudErrorBody errorDetails = append(errorDetails, c.validateVersion(normalized)...) errorDetails = append(errorDetails, c.validateUserAssignedIdentities(normalized)...) return errorDetails } func (c *HcpOpenShiftCluster) ValidateStatic(current api.VersionedHCPOpenShiftCluster, updating bool, method string) *arm.CloudError { var normalized api.HCPOpenShiftCluster var errorDetails []arm.CloudErrorBody cloudError := arm.NewCloudError( http.StatusBadRequest, arm.CloudErrorCodeMultipleErrorsOccurred, "", "Content validation failed on multiple fields") cloudError.Details = make([]arm.CloudErrorBody, 0) // Pass the embedded HcpOpenShiftCluster so the // struct field names match the clusterStructTagMap keys. errorDetails = api.ValidateVisibility( c.HcpOpenShiftCluster, current.(*HcpOpenShiftCluster).HcpOpenShiftCluster, clusterStructTagMap, updating) if errorDetails != nil { cloudError.Details = append(cloudError.Details, errorDetails...) } c.Normalize(&normalized) errorDetails = api.ValidateRequest(validate, method, &normalized) if errorDetails != nil { cloudError.Details = append(cloudError.Details, errorDetails...) } // Proceed with complex, multi-field validation only if single-field // validation has passed. This avoids running further checks on data // we already know to be invalid and prevents the response body from // becoming overwhelming. if len(cloudError.Details) == 0 { errorDetails = c.validateStaticComplex(&normalized) if errorDetails != nil { cloudError.Details = append(cloudError.Details, errorDetails...) } } switch len(cloudError.Details) { case 0: cloudError = nil case 1: // Promote a single validation error out of details. cloudError.CloudErrorBody = &cloudError.Details[0] } return cloudError } func normalizeVersion(p *generated.VersionProfile, out *api.VersionProfile) { if p.ID != nil { out.ID = *p.ID } if p.ChannelGroup != nil { out.ChannelGroup = *p.ChannelGroup } out.AvailableUpgrades = api.StringPtrSliceToStringSlice(p.AvailableUpgrades) } func normailzeDNS(p *generated.DNSProfile, out *api.DNSProfile) { if p.BaseDomain != nil { out.BaseDomain = *p.BaseDomain } if p.BaseDomainPrefix != nil { out.BaseDomainPrefix = *p.BaseDomainPrefix } } func normalizeNetwork(p *generated.NetworkProfile, out *api.NetworkProfile) { if p.NetworkType != nil { out.NetworkType = api.NetworkType(*p.NetworkType) } if p.PodCidr != nil { out.PodCIDR = *p.PodCidr } if p.ServiceCidr != nil { out.ServiceCIDR = *p.ServiceCidr } if p.MachineCidr != nil { out.MachineCIDR = *p.MachineCidr } if p.HostPrefix != nil { out.HostPrefix = *p.HostPrefix } } func normalizeConsole(p *generated.ConsoleProfile, out *api.ConsoleProfile) { if p.URL != nil { out.URL = *p.URL } } func normalizeAPI(p *generated.APIProfile, out *api.APIProfile) { if p.URL != nil { out.URL = *p.URL } if p.Visibility != nil { out.Visibility = api.Visibility(*p.Visibility) } } func normalizePlatform(p *generated.PlatformProfile, out *api.PlatformProfile) { if p.ManagedResourceGroup != nil { out.ManagedResourceGroup = *p.ManagedResourceGroup } if p.SubnetID != nil { out.SubnetID = *p.SubnetID } if p.OutboundType != nil { out.OutboundType = api.OutboundType(*p.OutboundType) } if p.NetworkSecurityGroupID != nil { out.NetworkSecurityGroupID = *p.NetworkSecurityGroupID } if p.OperatorsAuthentication != nil { normalizeOperatorsAuthentication(p.OperatorsAuthentication, &out.OperatorsAuthentication) } if p.IssuerURL != nil { out.IssuerURL = *p.IssuerURL } } func normalizeOperatorsAuthentication(p *generated.OperatorsAuthenticationProfile, out *api.OperatorsAuthenticationProfile) { if p.UserAssignedIdentities != nil { normalizeUserAssignedIdentities(p.UserAssignedIdentities, &out.UserAssignedIdentities) } } func normalizeUserAssignedIdentities(p *generated.UserAssignedIdentitiesProfile, out *api.UserAssignedIdentitiesProfile) { api.MergeStringPtrMap(p.ControlPlaneOperators, &out.ControlPlaneOperators) api.MergeStringPtrMap(p.DataPlaneOperators, &out.DataPlaneOperators) if p.ServiceManagedIdentity != nil { out.ServiceManagedIdentity = *p.ServiceManagedIdentity } } func normalizeIdentityUserAssignedIdentities(p map[string]*generated.UserAssignedIdentity, out *map[string]*arm.UserAssignedIdentity) { if *out == nil { *out = make(map[string]*arm.UserAssignedIdentity) } for key, value := range p { if value != nil { (*out)[key] = &arm.UserAssignedIdentity{ ClientID: value.ClientID, PrincipalID: value.PrincipalID, } } } } func normalizeCapabilities(c *generated.ClusterCapabilitiesProfile, out *api.ClusterCapabilitiesProfile) { if out == nil { out = &api.ClusterCapabilitiesProfile{} } if c.Disabled != nil { for _, v := range api.NonNilSliceValues(c.Disabled) { out.Disabled = append(out.Disabled, api.OptionalClusterCapability(*v)) } } } func convertUserAssignedIdentities(from map[string]*arm.UserAssignedIdentity) map[string]*generated.UserAssignedIdentity { converted := make(map[string]*generated.UserAssignedIdentity) for key, value := range from { if value != nil { converted[key] = &generated.UserAssignedIdentity{ ClientID: value.ClientID, PrincipalID: value.PrincipalID, } } } return converted }