ARO-6425 v20240812preview validation (#3563)

This commit is contained in:
Alex Chvatal 2024-05-21 13:44:10 -04:00 коммит произвёл GitHub
Родитель cceb396a67
Коммит e71343ad58
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
6 изменённых файлов: 281 добавлений и 33 удалений

Просмотреть файл

@ -339,8 +339,10 @@ func (c openShiftClusterConverter) ExternalNoReadOnly(_oc interface{}) {
for i := range oc.Properties.IngressProfiles {
oc.Properties.IngressProfiles[i].IP = ""
}
for i := range oc.Properties.PlatformWorkloadIdentityProfile.PlatformWorkloadIdentities {
oc.Properties.PlatformWorkloadIdentityProfile.PlatformWorkloadIdentities[i].ClientID = ""
oc.Properties.PlatformWorkloadIdentityProfile.PlatformWorkloadIdentities[i].ObjectID = ""
if oc.Properties.PlatformWorkloadIdentityProfile != nil {
for i := range oc.Properties.PlatformWorkloadIdentityProfile.PlatformWorkloadIdentities {
oc.Properties.PlatformWorkloadIdentityProfile.PlatformWorkloadIdentities[i].ClientID = ""
oc.Properties.PlatformWorkloadIdentityProfile.PlatformWorkloadIdentities[i].ObjectID = ""
}
}
}

Просмотреть файл

@ -10,6 +10,7 @@ import (
"net/url"
"strings"
azcorearm "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
"github.com/Azure/go-autorest/autorest/azure"
"github.com/Azure/ARO-RP/pkg/api"
@ -78,6 +79,10 @@ func (sv openShiftClusterStaticValidator) validate(oc *OpenShiftCluster, isCreat
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, "location", "The provided location '%s' is invalid.", oc.Location)
}
if err := sv.validatePlatformIdentities(oc); err != nil {
return err
}
return sv.validateProperties("properties", &oc.Properties, isCreate, architectureVersion)
}
@ -110,6 +115,9 @@ func (sv openShiftClusterStaticValidator) validateProperties(path string, p *Ope
if err := sv.validateAPIServerProfile(path+".apiserverProfile", &p.APIServerProfile); err != nil {
return err
}
if err := sv.validatePlatformWorkloadIdentityProfile(path+".platformWorkloadIdentityProfile", p.PlatformWorkloadIdentityProfile); err != nil {
return err
}
if isCreate {
if len(p.WorkerProfilesStatus) != 0 {
@ -412,3 +420,48 @@ func (sv openShiftClusterStaticValidator) validateDelta(oc, current *OpenShiftCl
return nil
}
func (sv openShiftClusterStaticValidator) validatePlatformWorkloadIdentityProfile(path string, pwip *PlatformWorkloadIdentityProfile) error {
// PlatformWorkloadIdentityProfile being empty is acceptable
if pwip == nil {
return nil
}
// Validate the PlatformWorkloadIdentities
for n, p := range pwip.PlatformWorkloadIdentities {
resource, err := azcorearm.ParseResourceID(p.ResourceID)
if err != nil {
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, fmt.Sprintf("%s.PlatformWorkloadIdentities[%d].resourceID", path, n), "ResourceID %s formatted incorrectly.", p.ResourceID)
}
if p.OperatorName == "" {
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, fmt.Sprintf("%s.PlatformWorkloadIdentities[%d].resourceID", path, n), "Operator name is empty.")
}
if resource.ResourceType.Type != "userAssignedIdentities" {
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, fmt.Sprintf("%s.PlatformWorkloadIdentities[%d].resourceID", path, n), "Resource must be a user assigned identity.")
}
}
return nil
}
func (sv openShiftClusterStaticValidator) validatePlatformIdentities(oc *OpenShiftCluster) error {
pwip := oc.Properties.PlatformWorkloadIdentityProfile
spp := oc.Properties.ServicePrincipalProfile
if pwip == nil && spp == nil {
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, "properties.servicePrincipalProfile", "Must provide either an identity or service principal credentials.")
}
if pwip != nil && spp != nil && (spp.ClientID != "" || spp.ClientSecret != "") {
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, "properties.servicePrincipalProfile", "Cannot use identities and service principal credentials at the same time.")
}
clusterIdentityPresent := oc.Identity != nil
operatorRolePresent := pwip != nil
if clusterIdentityPresent != operatorRolePresent {
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, "identity", "Cluster identity and platform workload identities require each other.")
}
return nil
}

Просмотреть файл

@ -1161,3 +1161,169 @@ func TestOpenShiftClusterStaticValidateDelta(t *testing.T) {
runTests(t, testModeUpdate, tests)
}
func TestOpenShiftClusterStaticValidatePlatformWorkloadIdentityProfile(t *testing.T) {
createTests := []*validateTest{
{
name: "valid empty workloadIdentityProfile",
modify: func(oc *OpenShiftCluster) {
oc.Properties.PlatformWorkloadIdentityProfile = nil
},
},
{
name: "valid workloadIdentityProfile",
modify: func(oc *OpenShiftCluster) {
oc.Properties.PlatformWorkloadIdentityProfile = &PlatformWorkloadIdentityProfile{
PlatformWorkloadIdentities: []PlatformWorkloadIdentity{
{
OperatorName: "FAKE-OPERATOR",
ResourceID: "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/a-fake-group/providers/Microsoft.RedHatOpenShift/userAssignedIdentities/fake-cluster-name",
},
},
}
oc.Identity = &Identity{
UserAssignedIdentities: UserAssignedIdentities{
"first": {
ClientID: "11111111-1111-1111-1111-111111111111",
PrincipalID: "SOMETHING",
},
},
}
oc.Properties.ServicePrincipalProfile = nil
},
},
{
name: "invalid resourceID",
modify: func(oc *OpenShiftCluster) {
oc.Identity = &Identity{
UserAssignedIdentities: UserAssignedIdentities{
"first": {
ClientID: "11111111-1111-1111-1111-111111111111",
PrincipalID: "SOMETHING",
},
},
}
oc.Properties.PlatformWorkloadIdentityProfile = &PlatformWorkloadIdentityProfile{
PlatformWorkloadIdentities: []PlatformWorkloadIdentity{
{
OperatorName: "FAKE-OPERATOR",
ResourceID: "BAD",
},
},
}
oc.Properties.ServicePrincipalProfile = nil
},
wantErr: "400: InvalidParameter: properties.platformWorkloadIdentityProfile.PlatformWorkloadIdentities[0].resourceID: ResourceID BAD formatted incorrectly.",
},
{
name: "wrong resource type",
modify: func(oc *OpenShiftCluster) {
oc.Properties.PlatformWorkloadIdentityProfile = &PlatformWorkloadIdentityProfile{
PlatformWorkloadIdentities: []PlatformWorkloadIdentity{
{
OperatorName: "FAKE-OPERATOR",
ResourceID: "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/a-fake-group/providers/Microsoft.RedHatOpenShift/otherThing/fake-cluster-name",
},
},
}
oc.Properties.ServicePrincipalProfile = nil
oc.Identity = &Identity{
UserAssignedIdentities: UserAssignedIdentities{
"first": {
ClientID: "11111111-1111-1111-1111-111111111111",
PrincipalID: "SOMETHING",
},
},
}
},
wantErr: "400: InvalidParameter: properties.platformWorkloadIdentityProfile.PlatformWorkloadIdentities[0].resourceID: Resource must be a user assigned identity.",
},
{
name: "no credentials with identities",
modify: func(oc *OpenShiftCluster) {
oc.Properties.PlatformWorkloadIdentityProfile = &PlatformWorkloadIdentityProfile{
PlatformWorkloadIdentities: []PlatformWorkloadIdentity{
{
OperatorName: "FAKE-OPERATOR",
ResourceID: "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/a-fake-group/providers/Microsoft.RedHatOpenShift/userAssignedIdentities/fake-cluster-name",
},
},
}
oc.Identity = &Identity{
UserAssignedIdentities: UserAssignedIdentities{
"first": {
ClientID: "11111111-1111-1111-1111-111111111111",
PrincipalID: "SOMETHING",
},
},
}
oc.Properties.ServicePrincipalProfile = &ServicePrincipalProfile{
ClientID: "11111111-1111-1111-1111-111111111111",
ClientSecret: "BAD",
}
},
wantErr: "400: InvalidParameter: properties.servicePrincipalProfile: Cannot use identities and service principal credentials at the same time.",
},
{
name: "cluster identity missing platform workload identity",
modify: func(oc *OpenShiftCluster) {
oc.Identity = &Identity{
UserAssignedIdentities: UserAssignedIdentities{
"first": {
ClientID: "11111111-1111-1111-1111-111111111111",
PrincipalID: "SOMETHING",
},
},
}
},
wantErr: "400: InvalidParameter: identity: Cluster identity and platform workload identities require each other.",
},
{
name: "platform workload identity missing cluster identity",
modify: func(oc *OpenShiftCluster) {
oc.Properties.PlatformWorkloadIdentityProfile = &PlatformWorkloadIdentityProfile{
PlatformWorkloadIdentities: []PlatformWorkloadIdentity{
{
OperatorName: "operator_name",
},
},
}
oc.Properties.ServicePrincipalProfile = nil
},
wantErr: "400: InvalidParameter: identity: Cluster identity and platform workload identities require each other.",
},
{
name: "operator name missing",
modify: func(oc *OpenShiftCluster) {
oc.Identity = &Identity{
UserAssignedIdentities: UserAssignedIdentities{
"first": {
ClientID: "11111111-1111-1111-1111-111111111111",
PrincipalID: "SOMETHING",
},
},
}
oc.Properties.PlatformWorkloadIdentityProfile = &PlatformWorkloadIdentityProfile{
PlatformWorkloadIdentities: []PlatformWorkloadIdentity{
{
ResourceID: "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/a-fake-group/providers/Microsoft.RedHatOpenShift/userAssignedIdentities/fake-cluster-name",
OperatorName: "",
},
},
}
oc.Properties.ServicePrincipalProfile = nil
},
wantErr: "400: InvalidParameter: properties.platformWorkloadIdentityProfile.PlatformWorkloadIdentities[0].resourceID: Operator name is empty.",
},
{
name: "identity and service principal missing",
modify: func(oc *OpenShiftCluster) {
oc.Properties.PlatformWorkloadIdentityProfile = nil
oc.Properties.ServicePrincipalProfile = nil
},
wantErr: "400: InvalidParameter: properties.servicePrincipalProfile: Must provide either an identity or service principal credentials.",
},
}
runTests(t, testModeCreate, createTests)
}

Просмотреть файл

@ -2,7 +2,6 @@ package arm
import (
"fmt"
"regexp"
"strings"
)
@ -40,27 +39,31 @@ func (r ArmResource) String() string {
return fmt.Sprintf("/subscriptions/%s/resourcegroups/%s/providers/%s/%s/%s/%s/%s", r.SubscriptionID, r.ResourceGroup, r.Provider, r.ResourceType, r.ResourceName, r.SubResource.ResourceType, r.SubResource.ResourceName)
}
// ParseArmResourceId take the resourceID of a child resource to an OpenShiftCluster
// TODO refactor this function to support an additional layer of child resources if we ever get to that point, right now only supports 1 child resource
// ParseArmResourceId takes in an ARM resource ID and returns an ArmResource object representing that resource. It supports up to two levels of subresource nesting.
func ParseArmResourceId(resourceId string) (*ArmResource, error) {
const resourceIDPatternText = `(?i)subscriptions/(.+)/resourcegroups/(.+)/providers/(.+?)/(.+?)/(.+?)/(.+?)/(.+)`
resourceIDPattern := regexp.MustCompile(resourceIDPatternText)
match := resourceIDPattern.FindStringSubmatch(resourceId)
if len(match) != 8 || strings.Contains(match[7], "/") {
resourceComponents := strings.Split(strings.TrimPrefix(resourceId, "/"), "/")
if len(resourceComponents) < 8 || !strings.EqualFold(resourceComponents[0], "subscriptions") || !strings.EqualFold(resourceComponents[2], "resourceGroups") || !strings.EqualFold(resourceComponents[4], "providers") {
return nil, fmt.Errorf("parsing failed for %s. Invalid resource Id format", resourceId)
}
result := &ArmResource{
SubscriptionID: match[1],
ResourceGroup: match[2],
Provider: match[3],
ResourceType: match[4],
ResourceName: match[5],
SubResource: SubResource{
ResourceType: match[6],
ResourceName: match[7],
},
SubscriptionID: resourceComponents[1],
ResourceGroup: resourceComponents[3],
Provider: resourceComponents[5],
ResourceType: resourceComponents[6],
ResourceName: resourceComponents[7],
}
if len(resourceComponents) > 8 {
result.SubResource = SubResource{
ResourceType: resourceComponents[8],
ResourceName: resourceComponents[9],
}
if len(resourceComponents) > 10 {
result.SubResource.SubResource = &SubResource{
ResourceType: resourceComponents[8], // same subresource type as the first subresource
ResourceName: resourceComponents[10],
}
}
}
return result, nil
}

Просмотреть файл

@ -31,9 +31,15 @@ func TestArmResources(t *testing.T) {
},
},
{
name: "sad path - bad input - missing subresources",
name: "happy path - missing subresources",
input: "/subscriptions/abc/resourcegroups/v4-eastus/providers/Microsoft.RedHatOpenShift/openshiftclusters/cluster1",
err: "parsing failed for /subscriptions/abc/resourcegroups/v4-eastus/providers/Microsoft.RedHatOpenShift/openshiftclusters/cluster1. Invalid resource Id format",
want: &ArmResource{
SubscriptionID: "abc",
ResourceGroup: "v4-eastus",
Provider: "Microsoft.RedHatOpenShift",
ResourceName: "cluster1",
ResourceType: "openshiftclusters",
},
},
{
name: "sad path - bad input - missing cluster resource",
@ -41,21 +47,37 @@ func TestArmResources(t *testing.T) {
err: "parsing failed for /subscriptions/abc/resourcegroups/v4-eastus/providers. Invalid resource Id format",
},
{
name: "sad path - bad input - too many nested resource",
name: "happy path - two subresources",
input: "/subscriptions/abc/resourcegroups/v4-eastus/providers/Microsoft.RedHatOpenShift/openshiftclusters/cluster1/syncSets/syncset1/nextResource",
err: "parsing failed for /subscriptions/abc/resourcegroups/v4-eastus/providers/Microsoft.RedHatOpenShift/openshiftclusters/cluster1/syncSets/syncset1/nextResource. Invalid resource Id format",
want: &ArmResource{
SubscriptionID: "abc",
ResourceGroup: "v4-eastus",
Provider: "Microsoft.RedHatOpenShift",
ResourceName: "cluster1",
ResourceType: "openshiftclusters",
SubResource: SubResource{
ResourceName: "syncset1",
ResourceType: "syncSets",
SubResource: &SubResource{
ResourceName: "nextResource",
ResourceType: "syncSets",
},
},
},
},
}
for _, test := range tests {
actual, err := ParseArmResourceId(test.input)
if err != nil {
if test.err != err.Error() {
t.Fatalf("want %v, got %v", test.err, err)
t.Run(test.name, func(t *testing.T) {
actual, err := ParseArmResourceId(test.input)
if err != nil {
if test.err != err.Error() {
t.Errorf("%s: want %v, got %v", test.name, test.err, err)
}
}
}
if !reflect.DeepEqual(actual, test.want) {
t.Fatalf("want %v, got %v", test.want, actual)
}
if !reflect.DeepEqual(actual, test.want) {
t.Errorf("%s: want %v, got %v", test.name, test.want, actual)
}
})
}
}

Просмотреть файл

@ -3,7 +3,9 @@ package stringutils
// Copyright (c) Microsoft Corporation.
// Licensed under the Apache License 2.0.
import "strings"
import (
"strings"
)
// LastTokenByte splits s on sep and returns the last token
func LastTokenByte(s string, sep byte) string {