From 57d9d254e408e5003e734a48703b8c576e25b44d Mon Sep 17 00:00:00 2001 From: Matthew Barnes Date: Mon, 18 Mar 2024 09:57:53 -0400 Subject: [PATCH 1/6] api: Synchronize with API specification This uses OpenShift API types for the internal representation of a cluster's ExternalAuths. The types are not a 100% match to our representation (data is stored in what are supposed to be config map or secret name references) but it's close enough. Adds a dependency on github.com/openshift/api. --- go.work.sum | 19 + internal/api/enums.go | 40 ++ internal/api/enums_test.go | 65 ++ internal/api/hcpopenshiftcluster.go | 142 ++--- internal/api/hcpopenshiftclusternodepool.go | 28 + internal/api/registry.go | 15 +- internal/api/utils.go | 35 + .../hcpopenshiftclusters_methods.go | 596 ++++++++++++++++++ internal/api/v20240610preview/register.go | 19 + internal/go.mod | 15 + internal/go.sum | 81 +++ 11 files changed, 973 insertions(+), 82 deletions(-) create mode 100644 go.work.sum create mode 100644 internal/api/utils.go create mode 100644 internal/api/v20240610preview/hcpopenshiftclusters_methods.go create mode 100644 internal/api/v20240610preview/register.go diff --git a/go.work.sum b/go.work.sum new file mode 100644 index 000000000..2a3453643 --- /dev/null +++ b/go.work.sum @@ -0,0 +1,19 @@ +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= +github.com/emicklei/go-restful/v3 v3.8.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM= diff --git a/internal/api/enums.go b/internal/api/enums.go index caaeaa27f..2cbf6757f 100644 --- a/internal/api/enums.go +++ b/internal/api/enums.go @@ -5,6 +5,46 @@ package api import "fmt" +// NetworkType represents an OpenShift cluster network plugin. +type NetworkType int + +const ( + NetworkTypeOpenShiftSDN NetworkType = iota + NetworkTypeOVNKubernetes + + NetworkTypeOther // catch-all, must be last +) + +func (v NetworkType) String() string { + switch v { + case NetworkTypeOpenShiftSDN: + return "OpenShiftSDN" + case NetworkTypeOVNKubernetes: + return "OVNKubernetes" + default: + return "Other" + } +} + +func (v NetworkType) MarshalText() (text []byte, err error) { + // NetworkTypeOther is a catch-all value. + text = []byte(v.String()) + return +} + +func (v *NetworkType) UnmarshalText(text []byte) error { + for i := range NetworkTypeOther { + if i.String() == string(text) { + *v = i + return nil + } + } + + // NetworkTypeOther is a catch-all value. + *v = NetworkTypeOther + return nil +} + // OutboundType represents a routing strategy to provide egress to the Internet. type OutboundType int diff --git a/internal/api/enums_test.go b/internal/api/enums_test.go index 505305290..5ed5966c6 100644 --- a/internal/api/enums_test.go +++ b/internal/api/enums_test.go @@ -10,6 +10,71 @@ import ( "testing" ) +func TestNetworkType(t *testing.T) { + // Ensure NetworkType implementes these interfaces + var i NetworkType + _ = fmt.Stringer(i) + _ = encoding.TextMarshaler(i) + _ = encoding.TextUnmarshaler(&i) + + for _, tt := range []struct { + name string + val int + str string + skipMarshal bool + skipUnmarshal bool + }{ + { + name: "NetworkTypeOpenShiftSDN", + val: int(NetworkTypeOpenShiftSDN), + str: fmt.Sprintf("%q", NetworkTypeOpenShiftSDN), + }, + { + name: "NetworkTypeOVNKubernetes", + val: int(NetworkTypeOVNKubernetes), + str: fmt.Sprintf("%q", NetworkTypeOVNKubernetes), + }, + { + name: "NetworkTypeOther", + val: int(NetworkTypeOther), + str: fmt.Sprintf("%q", NetworkTypeOther), + }, + { + name: "Unknown NetworkType string", + val: int(NetworkTypeOther), + str: "\"unknown\"", + skipMarshal: true, + }, + { + name: "Unknown NetworkType value", + val: -1, + str: fmt.Sprintf("%q", NetworkTypeOther), + skipUnmarshal: true, + }, + } { + if !tt.skipMarshal { + t.Logf("Marshaling %d", tt.val) + data, err := json.Marshal(NetworkType(tt.val)) + if err != nil { + t.Fatalf("Marshal: Unexpected error: %s", err) + } else if string(data) != tt.str { + t.Fatalf("Marshal: Expected %s, got %s", tt.str, string(data)) + } + } + + if !tt.skipUnmarshal { + var val NetworkType + t.Logf("Unmarshaling %s", tt.str) + err := json.Unmarshal([]byte(tt.str), &val) + if err != nil { + t.Fatalf("Unmarshal: Unexpected error: %s", err) + } else if int(val) != tt.val { + t.Fatalf("Unmarshal: Expected %d, got %d", tt.val, val) + } + } + } +} + func TestOutboundType(t *testing.T) { // Ensure OutboundType implements these interfaces var i OutboundType diff --git a/internal/api/hcpopenshiftcluster.go b/internal/api/hcpopenshiftcluster.go index ec268c598..a5f2a6992 100644 --- a/internal/api/hcpopenshiftcluster.go +++ b/internal/api/hcpopenshiftcluster.go @@ -5,6 +5,9 @@ package api import ( "net" + "net/url" + + configv1 "github.com/openshift/api/config/v1" "github.com/Azure/ARO-HCP/internal/api/arm" "github.com/Azure/ARO-HCP/internal/api/json" @@ -19,106 +22,103 @@ type HCPOpenShiftCluster struct { // HCPOpenShiftClusterProperties represents the property bag of a HCPOpenShiftCluster resource. type HCPOpenShiftClusterProperties struct { ProvisioningState arm.ProvisioningState `json:"provisioningState,omitempty" visibility:"read"` - ClusterProfile ClusterProfile `json:"clusterProfile,omitempty" visibility:"read,create,update"` - ProxyProfile ProxyProfile `json:"proxyProfile,omitempty" visibility:"read,create,update"` - APIProfile APIProfile `json:"apiProfile,omitempty" visibility:"read,create"` - ConsoleProfile ConsoleProfile `json:"consoleProfile,omitempty" visibility:"read,create,update"` - IngressProfile IngressProfile `json:"ingressProfile,omitempty" visibility:"read,create"` - NetworkProfile NetworkProfile `json:"networkProfile,omitempty" visibility:"read,create"` - NodePoolProfiles []*NodePoolProfile `json:"nodePoolProfiles,omitempty" visibility:"read"` - EtcdEncryption EtcdEncryptionProfile `json:"etcdEncryption,omitempty" visibility:"read,create"` + Spec ClusterSpec `json:"spec,omitempty" visibility:"read,create,update"` } -// ClusterProfile represents a high level cluster configuration. -type ClusterProfile struct { - ControlPlaneVersion string `json:"controlPlaneVersion,omitempty" visibility:"read,create,update"` - SubnetID string `json:"subnetId,omitempty" visibility:"read,create"` - ManagedResourceGroup string `json:"managedResourceGroup,omitempty" visibility:"read,create"` - OIDCIssuerURL json.URL `json:"oidcIssuerUrl,omitempty" visibility:"read"` +// ClusterSpec represents a high level cluster configuration. +type ClusterSpec struct { + Version VersionProfile `json:"version,omitempty" visibility:"read,create,update"` + DNS DNSProfile `json:"dns,omitempty" visibility:"read,create,update"` + Network NetworkProfile `json:"network,omitempty" visibility:"read,create"` + Console ConsoleProfile `json:"console,omitempty" visibility:"read"` + API APIProfile `json:"api,omitempty" visibility:"read,create"` + FIPS bool `json:"fips,omitempty" visibility:"read,create"` + EtcdEncryption bool `json:"etcdEncryption,omitempty" visibility:"read,create"` + DisableUserWorkloadMonitoring bool `json:"disableUserWorkloadMonitoring,omitempty" visibility:"read,create,update"` + Proxy ProxyProfile `json:"proxy,omitempty" visibility:"read,create,update"` + Platform PlatformProfile `json:"platform,omitempty" visibility:"read,create"` + IssuerURL url.URL `json:"issuerUrl,omitempty" visibility:"read"` + ExternalAuth ExternalAuthConfigProfile `json:"externalAuth,omitempty" visibility:"read,create"` + Ingress []*IngressProfile `json:"ingressProfile,omitempty" visibility:"read,create"` } -// ProxyProfile represents the cluster proxy configuration. -// Visibility for the entire struct is "read,create,update". -type ProxyProfile struct { - HTTPProxy string `json:"httpProxy,omitempty"` - HTTPSProxy string `json:"httpsProxy,omitempty"` - NoProxy string `json:"noProxy,omitempty"` - TrustedCA string `json:"trustedCa,omitempty"` +// VersionProfile represents the cluster control plane version. +type VersionProfile struct { + ID string `json:"id,omitempty" visibility:"read,create,update"` + ChannelGroup string `json:"channelGroup,omitempty" visibility:"read,create"` + AvailableUpgrades []string `json:"availableUpgrades,omitempty" visibility:"read"` } -// APIProfile represents a cluster API server configuration. +// DNSProfile represents the DNS configuration of the cluster. +type DNSProfile struct { + BaseDomain string `json:"baseDomain,omitempty" visibility:"read"` + BaseDomainPrefix string `json:"baseDomainPrefix,omitempty" visibility:"read,create"` +} + +// NetworkProfile represents a cluster network configuration. // Visibility for the entire struct is "read,create". -type APIProfile struct { - URL json.URL `json:"url,omitempty"` - IP net.IP `json:"ip,omitempty"` - Visibility Visibility `json:"visibility,omitempty"` +type NetworkProfile struct { + NetworkType NetworkType `json:"networkType,omitempty"` + PodCIDR json.IPNet `json:"podCidr,omitempty"` + ServiceCIDR json.IPNet `json:"serviceCidr,omitempty"` + MachineCIDR json.IPNet `json:"machineCidr,omitempty"` + HostPrefix int32 `json:"hostPrefix,omitempty"` } // ConsoleProfile represents a cluster web console configuration. +// Visibility for the entire struct is "read". type ConsoleProfile struct { - URL json.URL `json:"url,omitempty" visibility:"read"` - FIPS bool `json:"fips,omitempty" visibility:"read,create,update"` + URL url.URL `json:"url,omitempty"` } -// IngressProfile represents a cluster ingress configuration. -type IngressProfile struct { +// APIProfile represents a cluster API server configuration. +type APIProfile struct { + URL url.URL `json:"url,omitempty" visibility:"read"` IP net.IP `json:"ip,omitempty" visibility:"read"` - URL json.URL `json:"url,omitempty" visibility:"read"` Visibility Visibility `json:"visibility,omitempty" visibility:"read,create"` } -// NetworkProfile represents a cluster network configuration. -// Visibility for the entire struct is "read,create". -type NetworkProfile struct { - PodCIDR json.IPNet `json:"podCidr,omitempty"` - ServiceCIDR json.IPNet `json:"serviceCidr,omitempty"` - MachineCIDR json.IPNet `json:"machineCidr,omitempty"` - HostPrefix int32 `json:"hostPrefix,omitempty"` - OutboundType OutboundType `json:"outboundType,omitempty"` - PreconfiguredNSGs bool `json:"preconfiguredNsgs,omitempty"` +// ProxyProfile represents the cluster proxy configuration. +// Visibility for the entire struct is "read,create,update". +type ProxyProfile struct { + HTTPProxy string `json:"httpProxy,omitempty"` + HTTPSProxy string `json:"httpsProxy,omitempty"` + NoProxy string `json:"noProxy,omitempty"` + TrustedCA string `json:"trustedCa,omitempty"` } -// NodePoolAutoscaling represents a node pool autoscaling configuration. -// Visibility for the entire struct is "read". -type NodePoolAutoscaling struct { - MinReplicas int32 `json:"minReplicas,omitempty"` - MaxReplicas int32 `json:"maxReplicas,omitempty"` +// PlatformProfile represents the Azure platform configuration. +// Visibility for the entire struct is "read,create". +type PlatformProfile struct { + ManagedResourceGroup string `json:"managedResourceGroup,omitempty"` + SubnetID string `json:"subnetId,omitempty"` + OutboundType OutboundType `json:"outboundType,omitempty"` + PreconfiguredNSGs bool `json:"preconfiguredNsgs,omitempty"` + EtcdEncryptionSetID string `json:"etcdEncryptionSetId,omitempty"` } -// NodePoolProfile represents a worker node pool configuration. -// Visibility for the entire struct is "read". -type NodePoolProfile struct { - Name string `json:"name,omitempty"` - Version string `json:"version,omitempty"` - Labels []string `json:"labels,omitempty"` - Taints []string `json:"taints,omitempty"` - DiskSize int32 `json:"diskSize,omitempty"` - EphemeralOSDisk bool `json:"ephemeralOsDisk,omitempty"` - Replicas int32 `json:"replicas,omitempty"` - SubnetID string `json:"subnetId,omitempty"` - EncryptionAtHost bool `json:"encryptionAtHost,omitempty"` - AutoRepair bool `json:"autoRepair,omitempty"` - DiscEncryptionSetID string `json:"discEncryptionSetId,omitempty"` - TuningConfigs []string `json:"tuningConfigs,omitempty"` - AvailabilityZone string `json:"availabilityZone,omitempty"` - DiscStorageAccountType string `json:"discStorageAccountType,omitempty"` - VMSize string `json:"vmSize,omitempty"` - Autoscaling NodePoolAutoscaling `json:"autoscaling,omitempty"` +// ExternalAuthConfigProfile represents the external authentication configuration. +type ExternalAuthConfigProfile struct { + Enabled bool `json:"enabled,omitempty" visibility:"read,create"` + ExternalAuths []*configv1.OIDCProvider `json:"externalAuths,omitempty" visibility:"read"` } -// EtcdEncryptionProfile represents the configuration needed for customer -// provided keys to encrypt etcd storage. -// Visibility for the entire struct is "read,create". -type EtcdEncryptionProfile struct { - DiscEncryptionSetID string `json:"discEncryptionSetId,omitempty"` +// IngressProfile represents a cluster ingress configuration. +type IngressProfile struct { + IP net.IP `json:"ip,omitempty" visibility:"read"` + URL url.URL `json:"url,omitempty" visibility:"read"` + Visibility Visibility `json:"visibility,omitempty" visibility:"read,create"` } // Creates an HCPOpenShiftCluster with any non-zero default values. func NewDefaultHCPOpenShiftCluster() *HCPOpenShiftCluster { return &HCPOpenShiftCluster{ Properties: HCPOpenShiftClusterProperties{ - NetworkProfile: NetworkProfile{ - HostPrefix: 23, + Spec: ClusterSpec{ + Network: NetworkProfile{ + NetworkType: NetworkTypeOVNKubernetes, + HostPrefix: 23, + }, }, }, } diff --git a/internal/api/hcpopenshiftclusternodepool.go b/internal/api/hcpopenshiftclusternodepool.go index b502b34ed..1c049092b 100644 --- a/internal/api/hcpopenshiftclusternodepool.go +++ b/internal/api/hcpopenshiftclusternodepool.go @@ -20,3 +20,31 @@ type HCPOpenShiftClusterNodePoolProperties struct { ProvisioningState arm.ProvisioningState `json:"provisioningState,omitempty" visibility:"read"` Profile NodePoolProfile `json:"profile,omitempty" visibility:"read,create,update"` } + +// NodePoolProfile represents a worker node pool configuration. +// Visibility for the entire struct is "read". +type NodePoolProfile struct { + Name string `json:"name,omitempty"` + Version string `json:"version,omitempty"` + Labels []string `json:"labels,omitempty"` + Taints []string `json:"taints,omitempty"` + DiskSize int32 `json:"diskSize,omitempty"` + EphemeralOSDisk bool `json:"ephemeralOsDisk,omitempty"` + Replicas int32 `json:"replicas,omitempty"` + SubnetID string `json:"subnetId,omitempty"` + EncryptionAtHost bool `json:"encryptionAtHost,omitempty"` + AutoRepair bool `json:"autoRepair,omitempty"` + DiscEncryptionSetID string `json:"discEncryptionSetId,omitempty"` + TuningConfigs []string `json:"tuningConfigs,omitempty"` + AvailabilityZone string `json:"availabilityZone,omitempty"` + DiscStorageAccountType string `json:"discStorageAccountType,omitempty"` + VMSize string `json:"vmSize,omitempty"` + Autoscaling NodePoolAutoscaling `json:"autoscaling,omitempty"` +} + +// NodePoolAutoscaling represents a node pool autoscaling configuration. +// Visibility for the entire struct is "read". +type NodePoolAutoscaling struct { + MinReplicas int32 `json:"minReplicas,omitempty"` + MaxReplicas int32 `json:"maxReplicas,omitempty"` +} diff --git a/internal/api/registry.go b/internal/api/registry.go index 30b894980..544cd44e7 100644 --- a/internal/api/registry.go +++ b/internal/api/registry.go @@ -15,17 +15,12 @@ const ( ) type VersionedHCPOpenShiftCluster interface { - Normalize(*HCPOpenShiftCluster) + Normalize(*HCPOpenShiftCluster) error ValidateStatic() error } type VersionedHCPOpenShiftClusterNodePool interface { - Normalize(*HCPOpenShiftClusterNodePool) - ValidateStatic() error -} - -type VersionedNodePoolProfile interface { - Normalize(*NodePoolProfile) + Normalize(*HCPOpenShiftClusterNodePool) error ValidateStatic() error } @@ -34,10 +29,8 @@ type Version interface { // Resource Types NewHCPOpenShiftCluster(*HCPOpenShiftCluster) VersionedHCPOpenShiftCluster - NewHCPOpenShiftClusterNodePool(*HCPOpenShiftClusterNodePool) VersionedHCPOpenShiftClusterNodePool - - // Component Types - NewNodePoolProfile(*NodePoolProfile) VersionedNodePoolProfile + // FIXME Disable until we have generated structs for node pools. + //NewHCPOpenShiftClusterNodePool(*HCPOpenShiftClusterNodePool) VersionedHCPOpenShiftClusterNodePool } // apiRegistry is the map of registered API versions diff --git a/internal/api/utils.go b/internal/api/utils.go new file mode 100644 index 000000000..0c5faac3d --- /dev/null +++ b/internal/api/utils.go @@ -0,0 +1,35 @@ +package api + +import "slices" + +// Copyright (c) Microsoft Corporation. +// Licensed under the Apache License 2.0. + +// Ptr returns a pointer to p. +func Ptr[T any](p T) *T { + return &p +} + +// DeleteNilsFromPtrSlice returns a slice with nil pointers removed. +func DeleteNilsFromPtrSlice[S ~[]*E, E any](s S) S { + return slices.DeleteFunc(s, func(e *E) bool { return e == nil }) +} + +// StringSliceToStringPtrSlice converts a slice of strings to a slice of string pointers. +func StringSliceToStringPtrSlice(s []string) []*string { + out := make([]*string, len(s)) + for index, item := range s { + out[index] = Ptr(item) + } + return out +} + +// StringPtrSliceToStringSlice converts a slice of string pointers to a slice of strings. +func StringPtrSliceToStringSlice(s []*string) []string { + s = DeleteNilsFromPtrSlice(s) + out := make([]string, 0, len(s)) + for _, item := range s { + out = append(out, *item) + } + return out +} diff --git a/internal/api/v20240610preview/hcpopenshiftclusters_methods.go b/internal/api/v20240610preview/hcpopenshiftclusters_methods.go new file mode 100644 index 000000000..683dee84a --- /dev/null +++ b/internal/api/v20240610preview/hcpopenshiftclusters_methods.go @@ -0,0 +1,596 @@ +package v20240610preview + +// Copyright (c) Microsoft Corporation. +// Licensed under the Apache License 2.0. + +import ( + configv1 "github.com/openshift/api/config/v1" + + "github.com/Azure/ARO-HCP/internal/api" + "github.com/Azure/ARO-HCP/internal/api/arm" +) + +func newVersionProfile(from *api.VersionProfile) *VersionProfile { + return &VersionProfile{ + ID: api.Ptr(from.ID), + ChannelGroup: api.Ptr(from.ChannelGroup), + AvailableUpgrades: api.StringSliceToStringPtrSlice(from.AvailableUpgrades), + } +} + +func newDNSProfile(from *api.DNSProfile) *DNSProfile { + return &DNSProfile{ + BaseDomain: api.Ptr(from.BaseDomain), + BaseDomainPrefix: api.Ptr(from.BaseDomainPrefix), + } +} + +func newNetworkProfile(from *api.NetworkProfile) *NetworkProfile { + return &NetworkProfile{ + NetworkType: api.Ptr(NetworkType(from.NetworkType.String())), + PodCidr: api.Ptr(from.PodCIDR.String()), + ServiceCidr: api.Ptr(from.ServiceCIDR.String()), + MachineCidr: api.Ptr(from.MachineCIDR.String()), + HostPrefix: api.Ptr(from.HostPrefix), + } +} + +func newConsoleProfile(from *api.ConsoleProfile) *ConsoleProfile { + return &ConsoleProfile{ + URL: api.Ptr(from.URL.String()), + } +} + +func newAPIProfile(from *api.APIProfile) *APIProfile { + return &APIProfile{ + URL: api.Ptr(from.URL.String()), + IP: api.Ptr(from.IP.String()), + Visibility: api.Ptr(Visibility(from.Visibility.String())), + } +} + +func newProxyProfile(from *api.ProxyProfile) *ProxyProfile { + return &ProxyProfile{ + HTTPProxy: api.Ptr(from.HTTPProxy), + HTTPSProxy: api.Ptr(from.HTTPSProxy), + NoProxy: api.Ptr(from.NoProxy), + TrustedCa: api.Ptr(from.TrustedCA), + } +} + +func newPlatformProfile(from *api.PlatformProfile) *PlatformProfile { + return &PlatformProfile{ + ManagedResourceGroup: api.Ptr(from.ManagedResourceGroup), + SubnetID: api.Ptr(from.SubnetID), + OutboundType: api.Ptr(OutboundType(from.OutboundType.String())), + PreconfiguredNsgs: api.Ptr(from.PreconfiguredNSGs), + EtcdEncryptionSetID: api.Ptr(from.EtcdEncryptionSetID), + } +} + +func newIngressProfile(from *api.IngressProfile) *IngressProfile { + return &IngressProfile{ + IP: api.Ptr(from.IP.String()), + URL: api.Ptr(from.URL.String()), + Visibility: api.Ptr(Visibility(from.Visibility.String())), + } +} + +func newExternalAuthProfile(from *configv1.OIDCProvider) *ExternalAuthProfile { + out := &ExternalAuthProfile{ + Issuer: &TokenIssuerProfile{ + URL: api.Ptr(from.Issuer.URL), + Audiences: make([]*string, len(from.Issuer.Audiences)), + Ca: api.Ptr(from.Issuer.CertificateAuthority.Name), + }, + Clients: make([]*ExternalAuthClientProfile, len(from.OIDCClients)), + Claim: &ExternalAuthClaimProfile{ + Mappings: &TokenClaimMappingsProfile{ + Username: &ClaimProfile{ + Claim: api.Ptr(from.ClaimMappings.Username.Claim), + PrefixPolicy: api.Ptr(string(from.ClaimMappings.Username.PrefixPolicy)), + }, + Groups: &ClaimProfile{ + Claim: api.Ptr(from.ClaimMappings.Groups.Claim), + Prefix: api.Ptr(from.ClaimMappings.Groups.Prefix), + }, + }, + ValidationRules: make([]*TokenClaimValidationRuleProfile, len(from.ClaimValidationRules)), + }, + } + + for index, item := range from.Issuer.Audiences { + out.Issuer.Audiences[index] = api.Ptr(string(item)) + } + + for index, item := range from.OIDCClients { + out.Clients[index] = newExternalAuthClientProfile(item) + } + + if from.ClaimMappings.Username.Prefix != nil { + out.Claim.Mappings.Username.Prefix = api.Ptr(from.ClaimMappings.Username.Prefix.PrefixString) + } + + for index, item := range from.ClaimValidationRules { + out.Claim.ValidationRules[index] = newTokenClaimValidationRuleProfile(item) + } + + return out +} + +func newTokenClaimValidationRuleProfile(from configv1.TokenClaimValidationRule) *TokenClaimValidationRuleProfile { + if from.RequiredClaim == nil { + // Should never happen since we create these rules. + panic("TokenClaimValidationRule has no RequiredClaim") + } + + return &TokenClaimValidationRuleProfile{ + Claim: api.Ptr(from.RequiredClaim.Claim), + RequiredValue: api.Ptr(from.RequiredClaim.RequiredValue), + } +} + +func newExternalAuthClientProfile(from configv1.OIDCClientConfig) *ExternalAuthClientProfile { + return &ExternalAuthClientProfile{ + Component: &ExternalAuthClientComponentProfile{ + Name: api.Ptr(from.ComponentName), + AuthClientNamespace: api.Ptr(from.ComponentNamespace), + }, + ID: api.Ptr(from.ClientID), + Secret: api.Ptr(from.ClientSecret.Name), + ExtraScopes: api.StringSliceToStringPtrSlice(from.ExtraScopes), + } +} + +func (v version) NewHCPOpenShiftCluster(from *api.HCPOpenShiftCluster) api.VersionedHCPOpenShiftCluster { + out := &HcpOpenShiftClusterResource{ + ID: api.Ptr(from.Resource.ID), + Name: api.Ptr(from.Resource.Name), + Type: api.Ptr(from.Resource.Type), + Location: api.Ptr(from.TrackedResource.Location), + Tags: map[string]*string{}, + // FIXME Skipping ManagedServiceIdentity + Properties: &HcpOpenShiftClusterProperties{ + ProvisioningState: api.Ptr(ProvisioningState(from.Properties.ProvisioningState.String())), + Spec: &ClusterSpec{ + Version: newVersionProfile(&from.Properties.Spec.Version), + DNS: newDNSProfile(&from.Properties.Spec.DNS), + Network: newNetworkProfile(&from.Properties.Spec.Network), + Console: newConsoleProfile(&from.Properties.Spec.Console), + API: newAPIProfile(&from.Properties.Spec.API), + Fips: api.Ptr(from.Properties.Spec.FIPS), + EtcdEncryption: api.Ptr(from.Properties.Spec.EtcdEncryption), + DisableUserWorkloadMonitoring: api.Ptr(from.Properties.Spec.DisableUserWorkloadMonitoring), + Proxy: newProxyProfile(&from.Properties.Spec.Proxy), + Platform: newPlatformProfile(&from.Properties.Spec.Platform), + IssuerURL: api.Ptr(from.Properties.Spec.IssuerURL.String()), + ExternalAuth: &ExternalAuthConfigProfile{ + Enabled: api.Ptr(from.Properties.Spec.ExternalAuth.Enabled), + ExternalAuths: make([]*ExternalAuthProfile, len(from.Properties.Spec.ExternalAuth.ExternalAuths)), + }, + Ingress: make([]*IngressProfile, len(from.Properties.Spec.Ingress)), + }, + }, + } + + if from.Resource.SystemData != nil { + out.SystemData = &SystemData{ + CreatedBy: api.Ptr(from.Resource.SystemData.CreatedBy), + CreatedByType: api.Ptr(CreatedByType(from.Resource.SystemData.CreatedByType.String())), + CreatedAt: from.Resource.SystemData.CreatedAt, + LastModifiedBy: api.Ptr(from.Resource.SystemData.LastModifiedBy), + LastModifiedByType: api.Ptr(CreatedByType(from.Resource.SystemData.LastModifiedByType.String())), + LastModifiedAt: from.Resource.SystemData.LastModifiedAt, + } + } + + for key, val := range from.TrackedResource.Tags { + out.Tags[key] = api.Ptr(val) + } + + for index, item := range from.Properties.Spec.ExternalAuth.ExternalAuths { + out.Properties.Spec.ExternalAuth.ExternalAuths[index] = newExternalAuthProfile(item) + } + + for index, item := range from.Properties.Spec.Ingress { + out.Properties.Spec.Ingress[index] = newIngressProfile(item) + } + + return out +} + +func (c *HcpOpenShiftClusterResource) Normalize(out *api.HCPOpenShiftCluster) error { + if c.ID != nil { + out.Resource.ID = *c.ID + } + if c.Name != nil { + out.Resource.Name = *c.Name + } + if c.Type != nil { + out.Resource.Type = *c.Type + } + if c.SystemData != nil { + out.Resource.SystemData = &arm.SystemData{ + CreatedAt: c.SystemData.CreatedAt, + LastModifiedAt: c.SystemData.LastModifiedAt, + } + if c.SystemData.CreatedBy != nil { + out.Resource.SystemData.CreatedBy = *c.SystemData.CreatedBy + } + if c.SystemData.CreatedByType != nil { + text := []byte(*c.SystemData.CreatedByType) + err := out.Resource.SystemData.CreatedByType.UnmarshalText(text) + if err != nil { + return err + } + } + if c.SystemData.LastModifiedBy != nil { + out.Resource.SystemData.LastModifiedBy = *c.SystemData.LastModifiedBy + } + if c.SystemData.LastModifiedByType != nil { + text := []byte(*c.SystemData.LastModifiedByType) + err := out.Resource.SystemData.LastModifiedByType.UnmarshalText(text) + if err != nil { + return err + } + } + } + // FIXME Skipping ManagedServiceIdentity + if c.Location != nil { + out.TrackedResource.Location = *c.Location + } + out.Tags = make(map[string]string) + for k, v := range c.Tags { + if v != nil { + out.Tags[k] = *v + } + } + if c.Properties != nil { + if c.Properties.ProvisioningState != nil { + text := []byte(*c.Properties.ProvisioningState) + err := out.Properties.ProvisioningState.UnmarshalText(text) + if err != nil { + return err + } + } + if c.Properties.Spec != nil { + if c.Properties.Spec.Version != nil { + err := c.Properties.Spec.Version.Normalize(&out.Properties.Spec.Version) + if err != nil { + return err + } + } + if c.Properties.Spec.DNS != nil { + err := c.Properties.Spec.DNS.Normalize(&out.Properties.Spec.DNS) + if err != nil { + return err + } + } + if c.Properties.Spec.Network != nil { + err := c.Properties.Spec.Network.Normalize(&out.Properties.Spec.Network) + if err != nil { + return err + } + } + if c.Properties.Spec.Console != nil { + err := c.Properties.Spec.Console.Normalize(&out.Properties.Spec.Console) + if err != nil { + return err + } + } + if c.Properties.Spec.API != nil { + err := c.Properties.Spec.API.Normalize(&out.Properties.Spec.API) + if err != nil { + return err + } + } + if c.Properties.Spec.Fips != nil { + out.Properties.Spec.FIPS = *c.Properties.Spec.Fips + } + if c.Properties.Spec.EtcdEncryption != nil { + out.Properties.Spec.EtcdEncryption = *c.Properties.Spec.EtcdEncryption + } + if c.Properties.Spec.DisableUserWorkloadMonitoring != nil { + out.Properties.Spec.DisableUserWorkloadMonitoring = *c.Properties.Spec.DisableUserWorkloadMonitoring + } + if c.Properties.Spec.Proxy != nil { + err := c.Properties.Spec.Proxy.Normalize(&out.Properties.Spec.Proxy) + if err != nil { + return err + } + } + if c.Properties.Spec.Platform != nil { + err := c.Properties.Spec.Platform.Normalize(&out.Properties.Spec.Platform) + if err != nil { + return err + } + } + if c.Properties.Spec.IssuerURL != nil { + text := []byte(*c.Properties.Spec.IssuerURL) + err := out.Properties.Spec.IssuerURL.UnmarshalBinary(text) + if err != nil { + return err + } + } + if c.Properties.Spec.ExternalAuth != nil { + err := c.Properties.Spec.ExternalAuth.Normalize(&out.Properties.Spec.ExternalAuth) + if err != nil { + return err + } + } + ingressSequence := api.DeleteNilsFromPtrSlice(c.Properties.Spec.Ingress) + out.Properties.Spec.Ingress = make([]*api.IngressProfile, len(ingressSequence)) + for index, item := range ingressSequence { + out.Properties.Spec.Ingress[index] = &api.IngressProfile{} + err := item.Normalize(out.Properties.Spec.Ingress[index]) + if err != nil { + return err + } + } + } + } + + return nil +} + +func (c *HcpOpenShiftClusterResource) ValidateStatic() error { + return nil +} + +func (p *VersionProfile) Normalize(out *api.VersionProfile) error { + if p.ID != nil { + out.ID = *p.ID + } + if p.ChannelGroup != nil { + out.ChannelGroup = *p.ChannelGroup + } + out.AvailableUpgrades = api.StringPtrSliceToStringSlice(p.AvailableUpgrades) + + return nil +} + +func (p *DNSProfile) Normalize(out *api.DNSProfile) error { + if p.BaseDomain != nil { + out.BaseDomain = *p.BaseDomain + } + if p.BaseDomainPrefix != nil { + out.BaseDomainPrefix = *p.BaseDomainPrefix + } + + return nil +} + +func (p *NetworkProfile) Normalize(out *api.NetworkProfile) error { + if p.NetworkType != nil { + text := []byte(*p.NetworkType) + err := out.NetworkType.UnmarshalText(text) + if err != nil { + return err + } + } + if p.PodCidr != nil { + text := []byte(*p.PodCidr) + err := out.PodCIDR.UnmarshalText(text) + if err != nil { + return err + } + } + if p.ServiceCidr != nil { + text := []byte(*p.ServiceCidr) + err := out.ServiceCIDR.UnmarshalText(text) + if err != nil { + return err + } + } + if p.MachineCidr != nil { + text := []byte(*p.MachineCidr) + err := out.MachineCIDR.UnmarshalText(text) + if err != nil { + return err + } + } + if p.HostPrefix != nil { + out.HostPrefix = *p.HostPrefix + } + + return nil +} + +func (p *ConsoleProfile) Normalize(out *api.ConsoleProfile) error { + if p.URL != nil { + text := []byte(*p.URL) + err := out.URL.UnmarshalBinary(text) + if err != nil { + return err + } + } + + return nil +} + +func (p *APIProfile) Normalize(out *api.APIProfile) error { + if p.URL != nil { + text := []byte(*p.URL) + err := out.URL.UnmarshalBinary(text) + if err != nil { + return err + } + } + if p.IP != nil { + text := []byte(*p.IP) + err := out.IP.UnmarshalText(text) + if err != nil { + return err + } + } + if p.Visibility != nil { + text := []byte(*p.Visibility) + err := out.Visibility.UnmarshalText(text) + if err != nil { + return err + } + } + + return nil +} + +func (p *ProxyProfile) Normalize(out *api.ProxyProfile) error { + if p.HTTPProxy != nil { + out.HTTPProxy = *p.HTTPProxy + } + if p.HTTPSProxy != nil { + out.HTTPSProxy = *p.HTTPSProxy + } + if p.NoProxy != nil { + out.NoProxy = *p.NoProxy + } + if p.TrustedCa != nil { + out.TrustedCA = *p.TrustedCa + } + + return nil +} + +func (p *PlatformProfile) Normalize(out *api.PlatformProfile) error { + if p.ManagedResourceGroup != nil { + out.ManagedResourceGroup = *p.ManagedResourceGroup + } + if p.SubnetID != nil { + out.SubnetID = *p.SubnetID + } + if p.OutboundType != nil { + text := []byte(*p.OutboundType) + err := out.OutboundType.UnmarshalText(text) + if err != nil { + return err + } + } + if p.PreconfiguredNsgs != nil { + out.PreconfiguredNSGs = *p.PreconfiguredNsgs + } + if p.EtcdEncryptionSetID != nil { + out.EtcdEncryptionSetID = *p.EtcdEncryptionSetID + } + + return nil +} + +func (p *ExternalAuthConfigProfile) Normalize(out *api.ExternalAuthConfigProfile) error { + if p.Enabled != nil { + out.Enabled = *p.Enabled + } + out.ExternalAuths = []*configv1.OIDCProvider{} + for _, item := range api.DeleteNilsFromPtrSlice(p.ExternalAuths) { + provider := &configv1.OIDCProvider{} + + if item.Issuer != nil { + if item.Issuer.URL != nil { + provider.Issuer.URL = *item.Issuer.URL + } + provider.Issuer.Audiences = make([]configv1.TokenAudience, len(item.Issuer.Audiences)) + for index, audience := range item.Issuer.Audiences { + if audience != nil { + provider.Issuer.Audiences[index] = configv1.TokenAudience(*audience) + } + } + if item.Issuer.Ca != nil { + // Slight misuse of the field. It's meant to name a config map holding a + // "ca-bundle.crt" key, whereas we store the data directly in the Name field. + provider.Issuer.CertificateAuthority = configv1.ConfigMapNameReference{ + Name: *item.Issuer.Ca, + } + } + } + + clientSequence := api.DeleteNilsFromPtrSlice(item.Clients) + provider.OIDCClients = make([]configv1.OIDCClientConfig, len(clientSequence)) + for index, client := range clientSequence { + if client.Component != nil { + if client.Component.Name != nil { + provider.OIDCClients[index].ComponentName = *client.Component.Name + } + if client.Component.AuthClientNamespace != nil { + provider.OIDCClients[index].ComponentNamespace = *client.Component.AuthClientNamespace + } + } + if client.ID != nil { + provider.OIDCClients[index].ClientID = *client.ID + } + if client.Secret != nil { + // Slight misuse of the field. It's meant to name a secret holding a + // "clientSecret" key, whereas we store the data directly in the Name field. + provider.OIDCClients[index].ClientSecret.Name = *client.Secret + } + provider.OIDCClients[index].ExtraScopes = api.StringPtrSliceToStringSlice(client.ExtraScopes) + } + + if item.Claim != nil { + if item.Claim.Mappings != nil { + if item.Claim.Mappings.Username != nil { + if item.Claim.Mappings.Username.Claim != nil { + provider.ClaimMappings.Username.TokenClaimMapping.Claim = *item.Claim.Mappings.Username.Claim + } + if item.Claim.Mappings.Username.PrefixPolicy != nil { + provider.ClaimMappings.Username.PrefixPolicy = configv1.UsernamePrefixPolicy(*item.Claim.Mappings.Username.PrefixPolicy) + } + if item.Claim.Mappings.Username.Prefix != nil { + provider.ClaimMappings.Username.Prefix.PrefixString = *item.Claim.Mappings.Username.Prefix + } + } + if item.Claim.Mappings.Groups != nil { + if item.Claim.Mappings.Groups.Claim != nil { + provider.ClaimMappings.Groups.TokenClaimMapping.Claim = *item.Claim.Mappings.Groups.Claim + } + if item.Claim.Mappings.Groups.Prefix != nil { + provider.ClaimMappings.Groups.Prefix = *item.Claim.Mappings.Groups.Prefix + } + } + } + } + + validationRuleSequence := api.DeleteNilsFromPtrSlice(item.Claim.ValidationRules) + provider.ClaimValidationRules = make([]configv1.TokenClaimValidationRule, len(validationRuleSequence)) + for index, rule := range validationRuleSequence { + provider.ClaimValidationRules[index] = configv1.TokenClaimValidationRule{ + Type: configv1.TokenValidationRuleTypeRequiredClaim, + RequiredClaim: &configv1.TokenRequiredClaim{}, + } + if rule.Claim != nil { + provider.ClaimValidationRules[index].RequiredClaim.Claim = *rule.Claim + } + if rule.RequiredValue != nil { + provider.ClaimValidationRules[index].RequiredClaim.RequiredValue = *rule.RequiredValue + } + } + + out.ExternalAuths = append(out.ExternalAuths, provider) + } + + return nil +} + +func (p *IngressProfile) Normalize(out *api.IngressProfile) error { + if p.IP != nil { + text := []byte(*p.IP) + err := out.IP.UnmarshalText(text) + if err != nil { + return err + } + } + if p.URL != nil { + text := []byte(*p.URL) + err := out.URL.UnmarshalBinary(text) + if err != nil { + return err + } + } + if p.Visibility != nil { + text := []byte(*p.Visibility) + err := out.Visibility.UnmarshalText(text) + if err != nil { + return err + } + } + + return nil +} diff --git a/internal/api/v20240610preview/register.go b/internal/api/v20240610preview/register.go new file mode 100644 index 000000000..185347cd6 --- /dev/null +++ b/internal/api/v20240610preview/register.go @@ -0,0 +1,19 @@ +package v20240610preview + +// Copyright (c) Microsoft Corporation. +// Licensed under the Apache License 2.0. + +import ( + "github.com/Azure/ARO-HCP/internal/api" +) + +type version struct{} + +// String returns the api-version parameter value for this API. +func (v version) String() string { + return "2024-06-10-preview" +} + +func init() { + api.Register(version{}) +} diff --git a/internal/go.mod b/internal/go.mod index a06730a54..c51467b6a 100644 --- a/internal/go.mod +++ b/internal/go.mod @@ -9,6 +9,21 @@ require ( require ( github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2 // indirect + github.com/go-logr/logr v1.3.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/openshift/api v0.0.0-20240316014254-8ebde957e3a6 golang.org/x/net v0.21.0 // indirect golang.org/x/text v0.14.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + k8s.io/api v0.29.0 // indirect + k8s.io/apimachinery v0.29.0 // indirect + k8s.io/klog/v2 v2.110.1 // indirect + k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect ) diff --git a/internal/go.sum b/internal/go.sum index 28ff26ee6..d59ede2ba 100644 --- a/internal/go.sum +++ b/internal/go.sum @@ -2,17 +2,98 @@ github.com/Azure/azure-sdk-for-go/sdk/azcore v1.10.0 h1:n1DH8TPV4qqPTje2RcUBYwtr github.com/Azure/azure-sdk-for-go/sdk/azcore v1.10.0/go.mod h1:HDcZnuGbiyppErN6lB+idp4CKhjbc8gwjto6OPpyggM= github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2 h1:LqbJ/WzJUwBf8UiaSzgX7aMclParm9/5Vgp+TY51uBQ= github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2/go.mod h1:yInRyqWXAuaPrgI7p70+lDDgh3mlBohis29jGMISnmc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/openshift/api v0.0.0-20240316014254-8ebde957e3a6 h1:G2VumOYEbLx6aBBSZ5czPAw2DWrqZH8bi3qdfbhU/m8= +github.com/openshift/api v0.0.0-20240316014254-8ebde957e3a6/go.mod h1:CxgbWAlvu2iQB0UmKTtRu1YfepRg1/vJ64n2DlIEVz4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.29.0 h1:NiCdQMY1QOp1H8lfRyeEf8eOwV6+0xA6XEE44ohDX2A= +k8s.io/api v0.29.0/go.mod h1:sdVmXoz2Bo/cb77Pxi71IPTSErEW32xa4aXwKH7gfBA= +k8s.io/apimachinery v0.29.0 h1:+ACVktwyicPz0oc6MTMLwa2Pw3ouLAfAon1wPLtG48o= +k8s.io/apimachinery v0.29.0/go.mod h1:eVBxQ/cwiJxH58eK/jd/vAk4mrxmVlnpBH5J2GbMeis= +k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0= +k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= From 544de631925ac6a8391eade765d97113a22fff57 Mon Sep 17 00:00:00 2001 From: Matthew Barnes Date: Wed, 3 Apr 2024 08:27:48 -0400 Subject: [PATCH 2/6] frontend: Store struct pointers in cache --- frontend/cache.go | 8 ++++---- frontend/frontend.go | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/frontend/cache.go b/frontend/cache.go index 85c8a3df9..21c4ccf97 100644 --- a/frontend/cache.go +++ b/frontend/cache.go @@ -6,22 +6,22 @@ package main import "github.com/Azure/ARO-HCP/internal/api" type cache struct { - cluster map[string]api.HCPOpenShiftCluster + cluster map[string]*api.HCPOpenShiftCluster } // NewCache returns a new cache. func NewCache() *cache { return &cache{ - cluster: make(map[string]api.HCPOpenShiftCluster), + cluster: make(map[string]*api.HCPOpenShiftCluster), } } -func (c *cache) GetCluster(id string) (api.HCPOpenShiftCluster, bool) { +func (c *cache) GetCluster(id string) (*api.HCPOpenShiftCluster, bool) { cluster, found := c.cluster[id] return cluster, found } -func (c *cache) SetCluster(id string, cluster api.HCPOpenShiftCluster) { +func (c *cache) SetCluster(id string, cluster *api.HCPOpenShiftCluster) { c.cluster[id] = cluster } diff --git a/frontend/frontend.go b/frontend/frontend.go index e89e73489..4e408cd1a 100644 --- a/frontend/frontend.go +++ b/frontend/frontend.go @@ -266,11 +266,11 @@ func (f *Frontend) ArmResourceAction(writer http.ResponseWriter, request *http.R writer.WriteHeader(http.StatusOK) } -func clusterFromRequest(body []byte) (api.HCPOpenShiftCluster, error) { +func clusterFromRequest(body []byte) (*api.HCPOpenShiftCluster, error) { var cluster api.HCPOpenShiftCluster err := json.Unmarshal(body, &cluster) if err != nil { - return api.HCPOpenShiftCluster{}, err + return nil, err } - return cluster, nil + return &cluster, nil } From 3d4d34bd170d68d467606dfe9fed7d2de784664f Mon Sep 17 00:00:00 2001 From: Matthew Barnes Date: Wed, 3 Apr 2024 08:51:11 -0400 Subject: [PATCH 3/6] api: Add unmarshal method for versioned interfaces --- internal/api/registry.go | 2 ++ .../hcpopenshiftclusters_methods.go | 14 ++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/internal/api/registry.go b/internal/api/registry.go index 544cd44e7..6f193608a 100644 --- a/internal/api/registry.go +++ b/internal/api/registry.go @@ -31,6 +31,8 @@ type Version interface { NewHCPOpenShiftCluster(*HCPOpenShiftCluster) VersionedHCPOpenShiftCluster // FIXME Disable until we have generated structs for node pools. //NewHCPOpenShiftClusterNodePool(*HCPOpenShiftClusterNodePool) VersionedHCPOpenShiftClusterNodePool + + UnmarshalHCPOpenShiftCluster([]byte, bool, *HCPOpenShiftCluster) error } // apiRegistry is the map of registered API versions diff --git a/internal/api/v20240610preview/hcpopenshiftclusters_methods.go b/internal/api/v20240610preview/hcpopenshiftclusters_methods.go index 683dee84a..0a5cb6048 100644 --- a/internal/api/v20240610preview/hcpopenshiftclusters_methods.go +++ b/internal/api/v20240610preview/hcpopenshiftclusters_methods.go @@ -4,6 +4,8 @@ package v20240610preview // Licensed under the Apache License 2.0. import ( + "encoding/json" + configv1 "github.com/openshift/api/config/v1" "github.com/Azure/ARO-HCP/internal/api" @@ -199,6 +201,18 @@ func (v version) NewHCPOpenShiftCluster(from *api.HCPOpenShiftCluster) api.Versi return out } +func (v version) UnmarshalHCPOpenShiftCluster(data []byte, updating bool, out *api.HCPOpenShiftCluster) error { + var resource HcpOpenShiftClusterResource + + err := json.Unmarshal(data, &resource) + if err != nil { + return err + } + + // FIXME Pass updating flag and possibly other flags. + return resource.Normalize(out) +} + func (c *HcpOpenShiftClusterResource) Normalize(out *api.HCPOpenShiftCluster) error { if c.ID != nil { out.Resource.ID = *c.ID From 3254325ee49daed8809afb99b56cd535aa2ae0e0 Mon Sep 17 00:00:00 2001 From: Matthew Barnes Date: Wed, 3 Apr 2024 08:16:22 -0400 Subject: [PATCH 4/6] frontend: Use versioned interface to read and write JSON data --- frontend/frontend.go | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/frontend/frontend.go b/frontend/frontend.go index 4e408cd1a..a92783d15 100644 --- a/frontend/frontend.go +++ b/frontend/frontend.go @@ -183,13 +183,15 @@ func (f *Frontend) ArmResourceRead(writer http.ResponseWriter, request *http.Req versionedInterface, _ := ctx.Value(ContextKeyVersion).(api.Version) logger.Info(fmt.Sprintf("%s: ArmResourceRead", versionedInterface)) - resourceID := strings.ToLower(request.URL.Path) + // URL path is already lowercased by middleware. + resourceID := request.URL.Path cluster, found := f.cache.GetCluster(resourceID) if !found { writer.WriteHeader(http.StatusNotFound) return } - resp, err := json.Marshal(cluster) + versionedResource := versionedInterface.NewHCPOpenShiftCluster(cluster) + resp, err := json.Marshal(versionedResource) if err != nil { f.logger.Error(err.Error()) writer.WriteHeader(http.StatusInternalServerError) @@ -208,15 +210,23 @@ func (f *Frontend) ArmResourceCreateOrUpdate(writer http.ResponseWriter, request versionedInterface, _ := ctx.Value(ContextKeyVersion).(api.Version) logger.Info(fmt.Sprintf("%s: ArmResourceCreateOrUpdate", versionedInterface)) - resourceID := strings.ToLower(request.URL.Path) - cluster, err := clusterFromRequest(ctx.Value(ContextKeyBody).([]byte)) + + // URL path is already lowercased by middleware. + resourceID := request.URL.Path + cluster, updating := f.cache.GetCluster(resourceID) + if !updating { + cluster = api.NewDefaultHCPOpenShiftCluster() + } + body := ctx.Value(ContextKeyBody).([]byte) + err := versionedInterface.UnmarshalHCPOpenShiftCluster(body, updating, cluster) if err != nil { f.logger.Error(err.Error()) writer.WriteHeader(http.StatusBadRequest) } f.cache.SetCluster(resourceID, cluster) - resp, err := json.Marshal(cluster) + versionedResource := versionedInterface.NewHCPOpenShiftCluster(cluster) + resp, err := json.Marshal(versionedResource) if err != nil { f.logger.Error(err.Error()) writer.WriteHeader(http.StatusInternalServerError) @@ -245,7 +255,8 @@ func (f *Frontend) ArmResourceDelete(writer http.ResponseWriter, request *http.R versionedInterface, _ := ctx.Value(ContextKeyVersion).(api.Version) logger.Info(fmt.Sprintf("%s: ArmResourceDelete", versionedInterface)) - resourceID := strings.ToLower(request.URL.Path) + // URL path is already lowercased by middleware. + resourceID := request.URL.Path _, found := f.cache.GetCluster(resourceID) if !found { writer.WriteHeader(http.StatusNotFound) From 59ca2eebaf2968626a31139dae0d26cdc3862fdd Mon Sep 17 00:00:00 2001 From: Matthew Barnes Date: Wed, 3 Apr 2024 08:31:25 -0400 Subject: [PATCH 5/6] frontend: Remove temporary test hacks for cache Remove clusterFromRequest() and "cache" as a valid API version. --- frontend/frontend.go | 9 --------- frontend/middleware_validateapi.go | 4 ---- 2 files changed, 13 deletions(-) diff --git a/frontend/frontend.go b/frontend/frontend.go index a92783d15..0a64564cf 100644 --- a/frontend/frontend.go +++ b/frontend/frontend.go @@ -276,12 +276,3 @@ func (f *Frontend) ArmResourceAction(writer http.ResponseWriter, request *http.R writer.WriteHeader(http.StatusOK) } - -func clusterFromRequest(body []byte) (*api.HCPOpenShiftCluster, error) { - var cluster api.HCPOpenShiftCluster - err := json.Unmarshal(body, &cluster) - if err != nil { - return nil, err - } - return &cluster, nil -} diff --git a/frontend/middleware_validateapi.go b/frontend/middleware_validateapi.go index 452ccdb6a..74af6aecc 100644 --- a/frontend/middleware_validateapi.go +++ b/frontend/middleware_validateapi.go @@ -6,7 +6,6 @@ package main import ( "context" "net/http" - "strings" "github.com/Azure/ARO-HCP/internal/api" "github.com/Azure/ARO-HCP/internal/api/arm" @@ -20,9 +19,6 @@ func MiddlewareValidateAPIVersion(w http.ResponseWriter, r *http.Request, next h arm.CloudErrorCodeInvalidParameter, "", "The request is missing required parameter '%s'.", APIVersionKey) - } else if strings.EqualFold(apiVersion, "cache") { - r = r.WithContext(context.WithValue(r.Context(), ContextKeyVersion, "cache")) - next(w, r) } else if version, ok := api.Lookup(apiVersion); !ok { arm.WriteError( w, http.StatusBadRequest, From 7f0f5221651e45aea9151d41d1746bff3f8a44c8 Mon Sep 17 00:00:00 2001 From: Matthew Barnes Date: Thu, 4 Apr 2024 09:18:23 -0400 Subject: [PATCH 6/6] frontend: Assume versioned interface lookup succeeds The API version middleware will catch an invalid API version and complete the request before the final handler function executes, so handler functions can safely get the versioned interface from the request context. --- frontend/frontend.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/frontend.go b/frontend/frontend.go index 0a64564cf..20cdb9868 100644 --- a/frontend/frontend.go +++ b/frontend/frontend.go @@ -180,7 +180,7 @@ func (f *Frontend) ArmResourceListByResourceGroup(writer http.ResponseWriter, re func (f *Frontend) ArmResourceRead(writer http.ResponseWriter, request *http.Request) { ctx := request.Context() logger := ctx.Value(ContextKeyLogger).(*slog.Logger) - versionedInterface, _ := ctx.Value(ContextKeyVersion).(api.Version) + versionedInterface := ctx.Value(ContextKeyVersion).(api.Version) logger.Info(fmt.Sprintf("%s: ArmResourceRead", versionedInterface)) // URL path is already lowercased by middleware. @@ -207,7 +207,7 @@ func (f *Frontend) ArmResourceRead(writer http.ResponseWriter, request *http.Req func (f *Frontend) ArmResourceCreateOrUpdate(writer http.ResponseWriter, request *http.Request) { ctx := request.Context() logger := ctx.Value(ContextKeyLogger).(*slog.Logger) - versionedInterface, _ := ctx.Value(ContextKeyVersion).(api.Version) + versionedInterface := ctx.Value(ContextKeyVersion).(api.Version) logger.Info(fmt.Sprintf("%s: ArmResourceCreateOrUpdate", versionedInterface)) @@ -252,7 +252,7 @@ func (f *Frontend) ArmResourcePatch(writer http.ResponseWriter, request *http.Re func (f *Frontend) ArmResourceDelete(writer http.ResponseWriter, request *http.Request) { ctx := request.Context() logger := ctx.Value(ContextKeyLogger).(*slog.Logger) - versionedInterface, _ := ctx.Value(ContextKeyVersion).(api.Version) + versionedInterface := ctx.Value(ContextKeyVersion).(api.Version) logger.Info(fmt.Sprintf("%s: ArmResourceDelete", versionedInterface)) // URL path is already lowercased by middleware.