From e97cc96348adeb4df89cb7bd724b4b841061e7b3 Mon Sep 17 00:00:00 2001 From: rknaur Date: Tue, 22 Apr 2025 10:58:32 +0200 Subject: [PATCH 1/4] Add RosaRoleConfig API and CRD. --- PROJECT | 3 + ...ne.cluster.x-k8s.io_rosacontrolplanes.yaml | 41 +- ...ture.cluster.x-k8s.io_rosaroleconfigs.yaml | 428 +++++++++++ config/crd/kustomization.yaml | 3 + .../cainjection_in_rosaroleconfigs.yaml | 8 + .../patches/webhook_in_rosaroleconfigs.yaml | 16 + config/rbac/role.yaml | 3 + config/webhook/manifests.yaml | 44 ++ .../rosa/api/v1beta2/conditions_consts.go | 9 + .../api/v1beta2/rosacontrolplane_types.go | 39 +- .../api/v1beta2/rosacontrolplane_webhook.go | 59 ++ .../rosa/api/v1beta2/zz_generated.deepcopy.go | 23 +- .../rosacontrolplane_controller.go | 123 +++- .../rosacontrolplane_controller_test.go | 17 +- exp/api/v1beta2/finalizers.go | 3 + exp/api/v1beta2/rosamachinepool_types.go | 11 +- exp/api/v1beta2/rosaroleconfig_types.go | 173 +++++ exp/api/v1beta2/rosaroleconfig_webhook.go | 48 ++ exp/api/v1beta2/zz_generated.deepcopy.go | 191 ++++- exp/controllers/rosamachinepool_controller.go | 2 +- .../rosamachinepool_controller_test.go | 17 +- exp/controllers/rosaroleconfig_controller.go | 662 ++++++++++++++++++ .../rosaroleconfig_controller_test.go | 55 ++ exp/controllers/suite_test.go | 3 + go.mod | 9 + go.sum | 14 + main.go | 17 +- pkg/cloud/scope/rosacontrolplane.go | 5 + pkg/cloud/scope/rosaroleconfig.go | 170 +++++ pkg/rosa/client.go | 37 +- pkg/rosa/helpers.go | 35 + pkg/rosa/ocmclient.go | 17 +- test/mocks/ocm_client_mock.go | 15 + 33 files changed, 2228 insertions(+), 72 deletions(-) create mode 100644 config/crd/bases/infrastructure.cluster.x-k8s.io_rosaroleconfigs.yaml create mode 100644 config/crd/patches/cainjection_in_rosaroleconfigs.yaml create mode 100644 config/crd/patches/webhook_in_rosaroleconfigs.yaml create mode 100644 exp/api/v1beta2/rosaroleconfig_types.go create mode 100644 exp/api/v1beta2/rosaroleconfig_webhook.go create mode 100644 exp/controllers/rosaroleconfig_controller.go create mode 100644 exp/controllers/rosaroleconfig_controller_test.go create mode 100644 pkg/cloud/scope/rosaroleconfig.go diff --git a/PROJECT b/PROJECT index 44c9df3c2c..0430d782a8 100644 --- a/PROJECT +++ b/PROJECT @@ -58,3 +58,6 @@ resources: - group: infrastructure version: v1beta2 kind: AWSManagedCluster +- group: infrastructure + kind: ROSARoleConfig + version: v1beta2 diff --git a/config/crd/bases/controlplane.cluster.x-k8s.io_rosacontrolplanes.yaml b/config/crd/bases/controlplane.cluster.x-k8s.io_rosacontrolplanes.yaml index 9f68e97eae..afa7fb5131 100644 --- a/config/crd/bases/controlplane.cluster.x-k8s.io_rosacontrolplanes.yaml +++ b/config/crd/bases/controlplane.cluster.x-k8s.io_rosacontrolplanes.yaml @@ -524,8 +524,9 @@ spec: - name type: object installerRoleARN: - description: InstallerRoleARN is an AWS IAM role that OpenShift Cluster - Manager will assume to create the cluster.. + description: |- + InstallerRoleARN is an AWS IAM role that OpenShift Cluster Manager will assume to create the cluster. + Required if RosaRoleConfigRef is not specified. type: string network: description: Network config for the ROSA HCP cluster. @@ -559,7 +560,9 @@ spec: type: string type: object oidcID: - description: The ID of the internal OpenID Connect Provider. + description: |- + The ID of the internal OpenID Connect Provider. + Required if RosaRoleConfigRef is not specified. type: string x-kubernetes-validations: - message: oidcID is immutable @@ -575,8 +578,9 @@ spec: description: The AWS Region the cluster lives in. type: string rolesRef: - description: AWS IAM roles used to perform credential requests by - the openshift operators. + description: |- + AWS IAM roles used to perform credential requests by the openshift operators. + Required if RosaRoleConfigRef is not specified. properties: controlPlaneOperatorARN: description: "ControlPlaneOperatorARN is an ARN value referencing @@ -776,6 +780,22 @@ spec: x-kubernetes-validations: - message: rosaClusterName is immutable rule: self == oldSelf + rosaRoleConfigRef: + description: |- + RosaRoleConfigRef is a reference to a RosaRoleConfig resource that contains account and operator roles and OIDC configuration. + If specified, the roles and OIDC configuration will be taken from the referenced RosaRoleConfig instead of the direct fields. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic subnets: description: |- The Subnet IDs to use when installing the cluster. @@ -787,6 +807,7 @@ spec: description: |- SupportRoleARN is an AWS IAM role used by Red Hat SREs to enable access to the cluster account in order to provide support. + Required if RosaRoleConfigRef is not specified. type: string version: description: OpenShift semantic version, for example "4.14.5". @@ -805,22 +826,18 @@ spec: - AlwaysAcknowledge type: string workerRoleARN: - description: WorkerRoleARN is an AWS IAM role that will be attached - to worker instances. + description: |- + WorkerRoleARN is an AWS IAM role that will be attached to worker instances. + Required if RosaRoleConfigRef is not specified. type: string required: - availabilityZones - channelGroup - - installerRoleARN - - oidcID - region - - rolesRef - rosaClusterName - subnets - - supportRoleARN - version - versionGate - - workerRoleARN type: object status: description: RosaControlPlaneStatus defines the observed state of ROSAControlPlane. diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_rosaroleconfigs.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_rosaroleconfigs.yaml new file mode 100644 index 0000000000..2eee276154 --- /dev/null +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_rosaroleconfigs.yaml @@ -0,0 +1,428 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.3 + name: rosaroleconfigs.infrastructure.cluster.x-k8s.io +spec: + group: infrastructure.cluster.x-k8s.io + names: + categories: + - cluster-api + kind: ROSARoleConfig + listKind: ROSARoleConfigList + plural: rosaroleconfigs + shortNames: + - rosarole + singular: rosaroleconfig + scope: Namespaced + versions: + - name: v1beta2 + schema: + openAPIV3Schema: + description: ROSARoleConfig is the Schema for the rosaroleconfigs API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: ROSARoleConfigSpec defines the desired state of ROSARoleConfig + properties: + accountRoleConfig: + description: AccountRoleConfig defines account-wide IAM roles before + creating your ROSA cluster. + properties: + path: + type: string + permissionsBoundaryARN: + type: string + prefix: + description: User-defined prefix for all generated AWS resources + maxLength: 4 + type: string + sharedVPCConfig: + description: SharedVPCConfig is used to set up shared VPC. + properties: + routeRoleARN: + description: ' Role ARN associated with the private hosted + zone used for Hosted Control Plane cluster shared VPC, this + role contains policies to be used with Route 53' + type: string + vpcEndpointRoleArn: + description: ' Role ARN associated with the shared VPC used + for Hosted Control Plane clusters, this role contains policies + to be used with the VPC endpoint' + type: string + type: object + version: + description: ' Version of OpenShift that will be used to setup + policy tag, for example "4.11"' + type: string + required: + - prefix + - version + type: object + credentialsSecretRef: + description: CredentialsSecretRef references a secret with necessary + credentials to connect to the OCM API. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + identityRef: + description: AWSIdentityReference specifies a identity. + properties: + kind: + description: Kind of the identity. + enum: + - AWSClusterControllerIdentity + - AWSClusterRoleIdentity + - AWSClusterStaticIdentity + type: string + name: + description: Name of the identity. + minLength: 1 + type: string + required: + - kind + - name + type: object + operatorRoleConfig: + description: OperatorRoleConfig defines cluster-specific operator + IAM roles based on your cluster configuration. + properties: + oidcID: + description: |- + OIDCID is the ID of the OIDC config that will be used to create the operator roles. + A managed OIDC-provider will be created if the OIDCID not specified + type: string + permissionsBoundaryARN: + description: The ARN of the policy that is used to set the permissions + boundary for the operator roles. + type: string + prefix: + description: ' User-defined prefix for generated AWS operator + policies.' + maxLength: 4 + type: string + sharedVPCConfig: + description: SharedVPCConfig is used to set up shared VPC. + properties: + routeRoleARN: + description: ' Role ARN associated with the private hosted + zone used for Hosted Control Plane cluster shared VPC, this + role contains policies to be used with Route 53' + type: string + vpcEndpointRoleArn: + description: ' Role ARN associated with the shared VPC used + for Hosted Control Plane clusters, this role contains policies + to be used with the VPC endpoint' + type: string + type: object + required: + - prefix + type: object + required: + - accountRoleConfig + - operatorRoleConfig + type: object + status: + description: ROSARoleConfigStatus defines the observed state of ROSARoleConfig + properties: + accountRolesRef: + description: Created Account roles that can be used to + properties: + installerRoleARN: + description: InstallerRoleARN is an AWS IAM role that OpenShift + Cluster Manager will assume to create the cluster.. + type: string + supportRoleARN: + description: |- + SupportRoleARN is an AWS IAM role used by Red Hat SREs to enable + access to the cluster account in order to provide support. + type: string + workerRoleARN: + description: WorkerRoleARN is an AWS IAM role that will be attached + to worker instances. + type: string + type: object + conditions: + description: Conditions provide observations of the operational state + of a Cluster API resource. + items: + description: Condition defines an observation of a Cluster API resource + operational state. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when + the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This field may be empty. + maxLength: 10240 + minLength: 1 + type: string + reason: + description: |- + reason is the reason for the condition's last transition in CamelCase. + The specific API may choose whether or not this field is considered a guaranteed API. + This field may be empty. + maxLength: 256 + minLength: 1 + type: string + severity: + description: |- + severity provides an explicit classification of Reason code, so the users or machines can immediately + understand the current situation and act accordingly. + The Severity field MUST be set only when Status=False. + maxLength: 32 + type: string + status: + description: status of the condition, one of True, False, Unknown. + type: string + type: + description: |- + type of condition in CamelCase or in foo.example.com/CamelCase. + Many .condition.type values are consistent across resources like Available, but because arbitrary conditions + can be useful (see .node.status.conditions), the ability to deconflict is important. + maxLength: 256 + minLength: 1 + type: string + required: + - lastTransitionTime + - status + - type + type: object + type: array + oidcID: + description: ID of created OIDC config + type: string + oidcProviderARN: + description: Create OIDC provider for operators to authenticate against + in an STS cluster. + type: string + operatorRolesRef: + description: AWS IAM roles used to perform credential requests by + the openshift operators. + properties: + controlPlaneOperatorARN: + description: "ControlPlaneOperatorARN is an ARN value referencing + a role appropriate for the Control Plane Operator.\n\nThe following + is an example of a valid policy document:\n\n{\n\t\"Version\": + \"2012-10-17\",\n\t\"Statement\": [\n\t\t{\n\t\t\t\"Effect\": + \"Allow\",\n\t\t\t\"Action\": [\n\t\t\t\t\"ec2:CreateVpcEndpoint\",\n\t\t\t\t\"ec2:DescribeVpcEndpoints\",\n\t\t\t\t\"ec2:ModifyVpcEndpoint\",\n\t\t\t\t\"ec2:DeleteVpcEndpoints\",\n\t\t\t\t\"ec2:CreateTags\",\n\t\t\t\t\"route53:ListHostedZones\",\n\t\t\t\t\"ec2:CreateSecurityGroup\",\n\t\t\t\t\"ec2:AuthorizeSecurityGroupIngress\",\n\t\t\t\t\"ec2:AuthorizeSecurityGroupEgress\",\n\t\t\t\t\"ec2:DeleteSecurityGroup\",\n\t\t\t\t\"ec2:RevokeSecurityGroupIngress\",\n\t\t\t\t\"ec2:RevokeSecurityGroupEgress\",\n\t\t\t\t\"ec2:DescribeSecurityGroups\",\n\t\t\t\t\"ec2:DescribeVpcs\",\n\t\t\t],\n\t\t\t\"Resource\": + \"*\"\n\t\t},\n\t\t{\n\t\t\t\"Effect\": \"Allow\",\n\t\t\t\"Action\": + [\n\t\t\t\t\"route53:ChangeResourceRecordSets\",\n\t\t\t\t\"route53:ListResourceRecordSets\"\n\t\t\t],\n\t\t\t\"Resource\": + \"arn:aws:route53:::%s\"\n\t\t}\n\t]\n}" + type: string + imageRegistryARN: + description: "ImageRegistryARN is an ARN value referencing a role + appropriate for the Image Registry Operator.\n\nThe following + is an example of a valid policy document:\n\n{\n\t\"Version\": + \"2012-10-17\",\n\t\"Statement\": [\n\t\t{\n\t\t\t\"Effect\": + \"Allow\",\n\t\t\t\"Action\": [\n\t\t\t\t\"s3:CreateBucket\",\n\t\t\t\t\"s3:DeleteBucket\",\n\t\t\t\t\"s3:PutBucketTagging\",\n\t\t\t\t\"s3:GetBucketTagging\",\n\t\t\t\t\"s3:PutBucketPublicAccessBlock\",\n\t\t\t\t\"s3:GetBucketPublicAccessBlock\",\n\t\t\t\t\"s3:PutEncryptionConfiguration\",\n\t\t\t\t\"s3:GetEncryptionConfiguration\",\n\t\t\t\t\"s3:PutLifecycleConfiguration\",\n\t\t\t\t\"s3:GetLifecycleConfiguration\",\n\t\t\t\t\"s3:GetBucketLocation\",\n\t\t\t\t\"s3:ListBucket\",\n\t\t\t\t\"s3:GetObject\",\n\t\t\t\t\"s3:PutObject\",\n\t\t\t\t\"s3:DeleteObject\",\n\t\t\t\t\"s3:ListBucketMultipartUploads\",\n\t\t\t\t\"s3:AbortMultipartUpload\",\n\t\t\t\t\"s3:ListMultipartUploadParts\"\n\t\t\t],\n\t\t\t\"Resource\": + \"*\"\n\t\t}\n\t]\n}" + type: string + ingressARN: + description: "The referenced role must have a trust relationship + that allows it to be assumed via web identity.\nhttps://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_oidc.html.\nExample:\n{\n\t\t\"Version\": + \"2012-10-17\",\n\t\t\"Statement\": [\n\t\t\t{\n\t\t\t\t\"Effect\": + \"Allow\",\n\t\t\t\t\"Principal\": {\n\t\t\t\t\t\"Federated\": + \"{{ .ProviderARN }}\"\n\t\t\t\t},\n\t\t\t\t\t\"Action\": \"sts:AssumeRoleWithWebIdentity\",\n\t\t\t\t\"Condition\": + {\n\t\t\t\t\t\"StringEquals\": {\n\t\t\t\t\t\t\"{{ .ProviderName + }}:sub\": {{ .ServiceAccounts }}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t]\n\t}\n\nIngressARN + is an ARN value referencing a role appropriate for the Ingress + Operator.\n\nThe following is an example of a valid policy document:\n\n{\n\t\"Version\": + \"2012-10-17\",\n\t\"Statement\": [\n\t\t{\n\t\t\t\"Effect\": + \"Allow\",\n\t\t\t\"Action\": [\n\t\t\t\t\"elasticloadbalancing:DescribeLoadBalancers\",\n\t\t\t\t\"tag:GetResources\",\n\t\t\t\t\"route53:ListHostedZones\"\n\t\t\t],\n\t\t\t\"Resource\": + \"*\"\n\t\t},\n\t\t{\n\t\t\t\"Effect\": \"Allow\",\n\t\t\t\"Action\": + [\n\t\t\t\t\"route53:ChangeResourceRecordSets\"\n\t\t\t],\n\t\t\t\"Resource\": + [\n\t\t\t\t\"arn:aws:route53:::PUBLIC_ZONE_ID\",\n\t\t\t\t\"arn:aws:route53:::PRIVATE_ZONE_ID\"\n\t\t\t]\n\t\t}\n\t]\n}" + type: string + kmsProviderARN: + type: string + kubeCloudControllerARN: + description: |- + KubeCloudControllerARN is an ARN value referencing a role appropriate for the KCM/KCC. + Source: https://cloud-provider-aws.sigs.k8s.io/prerequisites/#iam-policies + + The following is an example of a valid policy document: + + { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "autoscaling:DescribeAutoScalingGroups", + "autoscaling:DescribeLaunchConfigurations", + "autoscaling:DescribeTags", + "ec2:DescribeAvailabilityZones", + "ec2:DescribeInstances", + "ec2:DescribeImages", + "ec2:DescribeRegions", + "ec2:DescribeRouteTables", + "ec2:DescribeSecurityGroups", + "ec2:DescribeSubnets", + "ec2:DescribeVolumes", + "ec2:CreateSecurityGroup", + "ec2:CreateTags", + "ec2:CreateVolume", + "ec2:ModifyInstanceAttribute", + "ec2:ModifyVolume", + "ec2:AttachVolume", + "ec2:AuthorizeSecurityGroupIngress", + "ec2:CreateRoute", + "ec2:DeleteRoute", + "ec2:DeleteSecurityGroup", + "ec2:DeleteVolume", + "ec2:DetachVolume", + "ec2:RevokeSecurityGroupIngress", + "ec2:DescribeVpcs", + "elasticloadbalancing:AddTags", + "elasticloadbalancing:AttachLoadBalancerToSubnets", + "elasticloadbalancing:ApplySecurityGroupsToLoadBalancer", + "elasticloadbalancing:CreateLoadBalancer", + "elasticloadbalancing:CreateLoadBalancerPolicy", + "elasticloadbalancing:CreateLoadBalancerListeners", + "elasticloadbalancing:ConfigureHealthCheck", + "elasticloadbalancing:DeleteLoadBalancer", + "elasticloadbalancing:DeleteLoadBalancerListeners", + "elasticloadbalancing:DescribeLoadBalancers", + "elasticloadbalancing:DescribeLoadBalancerAttributes", + "elasticloadbalancing:DetachLoadBalancerFromSubnets", + "elasticloadbalancing:DeregisterInstancesFromLoadBalancer", + "elasticloadbalancing:ModifyLoadBalancerAttributes", + "elasticloadbalancing:RegisterInstancesWithLoadBalancer", + "elasticloadbalancing:SetLoadBalancerPoliciesForBackendServer", + "elasticloadbalancing:AddTags", + "elasticloadbalancing:CreateListener", + "elasticloadbalancing:CreateTargetGroup", + "elasticloadbalancing:DeleteListener", + "elasticloadbalancing:DeleteTargetGroup", + "elasticloadbalancing:DeregisterTargets", + "elasticloadbalancing:DescribeListeners", + "elasticloadbalancing:DescribeLoadBalancerPolicies", + "elasticloadbalancing:DescribeTargetGroups", + "elasticloadbalancing:DescribeTargetHealth", + "elasticloadbalancing:ModifyListener", + "elasticloadbalancing:ModifyTargetGroup", + "elasticloadbalancing:RegisterTargets", + "elasticloadbalancing:SetLoadBalancerPoliciesOfListener", + "iam:CreateServiceLinkedRole", + "kms:DescribeKey" + ], + "Resource": [ + "*" + ], + "Effect": "Allow" + } + ] + } + type: string + networkARN: + description: "NetworkARN is an ARN value referencing a role appropriate + for the Network Operator.\n\nThe following is an example of + a valid policy document:\n\n{\n\t\"Version\": \"2012-10-17\",\n\t\"Statement\": + [\n\t\t{\n\t\t\t\"Effect\": \"Allow\",\n\t\t\t\"Action\": [\n\t\t\t\t\"ec2:DescribeInstances\",\n + \ \"ec2:DescribeInstanceStatus\",\n \"ec2:DescribeInstanceTypes\",\n + \ \"ec2:UnassignPrivateIpAddresses\",\n \"ec2:AssignPrivateIpAddresses\",\n + \ \"ec2:UnassignIpv6Addresses\",\n \"ec2:AssignIpv6Addresses\",\n + \ \"ec2:DescribeSubnets\",\n \"ec2:DescribeNetworkInterfaces\"\n\t\t\t],\n\t\t\t\"Resource\": + \"*\"\n\t\t}\n\t]\n}" + type: string + nodePoolManagementARN: + description: "NodePoolManagementARN is an ARN value referencing + a role appropriate for the CAPI Controller.\n\nThe following + is an example of a valid policy document:\n\n{\n \"Version\": + \"2012-10-17\",\n \"Statement\": [\n {\n \"Action\": [\n + \ \"ec2:AssociateRouteTable\",\n \"ec2:AttachInternetGateway\",\n + \ \"ec2:AuthorizeSecurityGroupIngress\",\n \"ec2:CreateInternetGateway\",\n + \ \"ec2:CreateNatGateway\",\n \"ec2:CreateRoute\",\n + \ \"ec2:CreateRouteTable\",\n \"ec2:CreateSecurityGroup\",\n + \ \"ec2:CreateSubnet\",\n \"ec2:CreateTags\",\n \"ec2:DeleteInternetGateway\",\n + \ \"ec2:DeleteNatGateway\",\n \"ec2:DeleteRouteTable\",\n + \ \"ec2:DeleteSecurityGroup\",\n \"ec2:DeleteSubnet\",\n + \ \"ec2:DeleteTags\",\n \"ec2:DescribeAccountAttributes\",\n + \ \"ec2:DescribeAddresses\",\n \"ec2:DescribeAvailabilityZones\",\n + \ \"ec2:DescribeImages\",\n \"ec2:DescribeInstances\",\n + \ \"ec2:DescribeInternetGateways\",\n \"ec2:DescribeNatGateways\",\n + \ \"ec2:DescribeNetworkInterfaces\",\n \"ec2:DescribeNetworkInterfaceAttribute\",\n + \ \"ec2:DescribeRouteTables\",\n \"ec2:DescribeSecurityGroups\",\n + \ \"ec2:DescribeSubnets\",\n \"ec2:DescribeVpcs\",\n + \ \"ec2:DescribeVpcAttribute\",\n \"ec2:DescribeVolumes\",\n + \ \"ec2:DetachInternetGateway\",\n \"ec2:DisassociateRouteTable\",\n + \ \"ec2:DisassociateAddress\",\n \"ec2:ModifyInstanceAttribute\",\n + \ \"ec2:ModifyNetworkInterfaceAttribute\",\n \"ec2:ModifySubnetAttribute\",\n + \ \"ec2:RevokeSecurityGroupIngress\",\n \"ec2:RunInstances\",\n + \ \"ec2:TerminateInstances\",\n \"tag:GetResources\",\n + \ \"ec2:CreateLaunchTemplate\",\n \"ec2:CreateLaunchTemplateVersion\",\n + \ \"ec2:DescribeLaunchTemplates\",\n \"ec2:DescribeLaunchTemplateVersions\",\n + \ \"ec2:DeleteLaunchTemplate\",\n \"ec2:DeleteLaunchTemplateVersions\"\n + \ ],\n \"Resource\": [\n \"*\"\n ],\n \"Effect\": + \"Allow\"\n },\n {\n \"Condition\": {\n \"StringLike\": + {\n \"iam:AWSServiceName\": \"elasticloadbalancing.amazonaws.com\"\n + \ }\n },\n \"Action\": [\n \"iam:CreateServiceLinkedRole\"\n + \ ],\n \"Resource\": [\n \"arn:*:iam::*:role/aws-service-role/elasticloadbalancing.amazonaws.com/AWSServiceRoleForElasticLoadBalancing\"\n + \ ],\n \"Effect\": \"Allow\"\n },\n {\n \"Action\": + [\n \"iam:PassRole\"\n ],\n \"Resource\": [\n + \ \"arn:*:iam::*:role/*-worker-role\"\n ],\n \"Effect\": + \"Allow\"\n },\n\t {\n\t \t\"Effect\": \"Allow\",\n\t \t\"Action\": + [\n\t \t\t\"kms:Decrypt\",\n\t \t\t\"kms:ReEncrypt\",\n\t + \ \t\t\"kms:GenerateDataKeyWithoutPlainText\",\n\t \t\t\"kms:DescribeKey\"\n\t + \ \t],\n\t \t\"Resource\": \"*\"\n\t },\n\t {\n\t \t\"Effect\": + \"Allow\",\n\t \t\"Action\": [\n\t \t\t\"kms:CreateGrant\"\n\t + \ \t],\n\t \t\"Resource\": \"*\",\n\t \t\"Condition\": {\n\t + \ \t\t\"Bool\": {\n\t \t\t\t\"kms:GrantIsForAWSResource\": + true\n\t \t\t}\n\t \t}\n\t }\n ]\n}" + type: string + storageARN: + description: "StorageARN is an ARN value referencing a role appropriate + for the Storage Operator.\n\nThe following is an example of + a valid policy document:\n\n{\n\t\"Version\": \"2012-10-17\",\n\t\"Statement\": + [\n\t\t{\n\t\t\t\"Effect\": \"Allow\",\n\t\t\t\"Action\": [\n\t\t\t\t\"ec2:AttachVolume\",\n\t\t\t\t\"ec2:CreateSnapshot\",\n\t\t\t\t\"ec2:CreateTags\",\n\t\t\t\t\"ec2:CreateVolume\",\n\t\t\t\t\"ec2:DeleteSnapshot\",\n\t\t\t\t\"ec2:DeleteTags\",\n\t\t\t\t\"ec2:DeleteVolume\",\n\t\t\t\t\"ec2:DescribeInstances\",\n\t\t\t\t\"ec2:DescribeSnapshots\",\n\t\t\t\t\"ec2:DescribeTags\",\n\t\t\t\t\"ec2:DescribeVolumes\",\n\t\t\t\t\"ec2:DescribeVolumesModifications\",\n\t\t\t\t\"ec2:DetachVolume\",\n\t\t\t\t\"ec2:ModifyVolume\"\n\t\t\t],\n\t\t\t\"Resource\": + \"*\"\n\t\t}\n\t]\n}" + type: string + required: + - controlPlaneOperatorARN + - imageRegistryARN + - ingressARN + - kmsProviderARN + - kubeCloudControllerARN + - networkARN + - nodePoolManagementARN + - storageARN + type: object + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index c3f6177556..607d7176a9 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -26,6 +26,7 @@ resources: - bases/controlplane.cluster.x-k8s.io_rosacontrolplanes.yaml - bases/infrastructure.cluster.x-k8s.io_rosaclusters.yaml - bases/infrastructure.cluster.x-k8s.io_rosamachinepools.yaml +- bases/infrastructure.cluster.x-k8s.io_rosaroleconfigs.yaml # +kubebuilder:scaffold:crdkustomizeresource patchesStrategicMerge: @@ -41,6 +42,7 @@ patchesStrategicMerge: - patches/webhook_in_awsmanagedcontrolplanetemplates.yaml - patches/webhook_in_eksconfigs.yaml - patches/webhook_in_eksconfigtemplates.yaml +- patches/webhook_in_rosaroleconfigs.yaml # +kubebuilder:scaffold:crdkustomizewebhookpatch # [CERTMANAGER] To enable webhook, uncomment all the sections with [CERTMANAGER] prefix. @@ -57,6 +59,7 @@ patchesStrategicMerge: - patches/cainjection_in_awsmanagedclustertemplates.yaml - patches/cainjection_in_eksconfigs.yaml - patches/cainjection_in_eksconfigtemplates.yaml +- patches/cainjection_in_rosaroleconfigs.yaml # +kubebuilder:scaffold:crdkustomizecainjectionpatch # [LABEL] To enable label, uncomment all the sections with [LABEL] prefix. diff --git a/config/crd/patches/cainjection_in_rosaroleconfigs.yaml b/config/crd/patches/cainjection_in_rosaroleconfigs.yaml new file mode 100644 index 0000000000..8a3a3e05ee --- /dev/null +++ b/config/crd/patches/cainjection_in_rosaroleconfigs.yaml @@ -0,0 +1,8 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +# CRD conversion requires k8s 1.13 or later. +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: rosaroleconfigs.infrastructure.cluster.x-k8s.io diff --git a/config/crd/patches/webhook_in_rosaroleconfigs.yaml b/config/crd/patches/webhook_in_rosaroleconfigs.yaml new file mode 100644 index 0000000000..c3f9b8706a --- /dev/null +++ b/config/crd/patches/webhook_in_rosaroleconfigs.yaml @@ -0,0 +1,16 @@ +# The following patch enables conversion webhook for CRD +# CRD conversion requires k8s 1.13 or later. +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: rosaroleconfigs.infrastructure.cluster.x-k8s.io +spec: + conversion: + strategy: Webhook + webhook: + conversionReviewVersions: ["v1", "v1beta1"] + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index cb04a602d7..b5225e6e11 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -176,6 +176,7 @@ rules: - awsfargateprofiles/status - rosaclusters/status - rosamachinepools/status + - rosaroleconfigs/status verbs: - get - patch @@ -197,6 +198,7 @@ rules: - infrastructure.cluster.x-k8s.io resources: - awsmachines + - rosaroleconfigs verbs: - create - delete @@ -209,5 +211,6 @@ rules: - infrastructure.cluster.x-k8s.io resources: - rosamachinepools/finalizers + - rosaroleconfigs/finalizers verbs: - update diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml index 91dd9aa54b..d48065c09f 100644 --- a/config/webhook/manifests.yaml +++ b/config/webhook/manifests.yaml @@ -223,6 +223,28 @@ webhooks: resources: - rosamachinepools sideEffects: None +- admissionReviewVersions: + - v1 + - v1beta1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /mutate-infrastructure-cluster-x-k8s-io-v1beta2-rosaroleconfig + failurePolicy: Fail + matchPolicy: Equivalent + name: default.rosaroleconfig.infrastructure.cluster.x-k8s.io + rules: + - apiGroups: + - infrastructure.cluster.x-k8s.io + apiVersions: + - v1beta2 + operations: + - CREATE + - UPDATE + resources: + - rosaroleconfigs + sideEffects: None - admissionReviewVersions: - v1 - v1beta1 @@ -581,6 +603,28 @@ webhooks: resources: - rosamachinepools sideEffects: None +- admissionReviewVersions: + - v1 + - v1beta1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-infrastructure-cluster-x-k8s-io-v1beta2-rosaroleconfig + failurePolicy: Fail + matchPolicy: Equivalent + name: validation.rosaroleconfig.infrastructure.cluster.x-k8s.io + rules: + - apiGroups: + - infrastructure.cluster.x-k8s.io + apiVersions: + - v1beta2 + operations: + - CREATE + - UPDATE + resources: + - rosaroleconfigs + sideEffects: None - admissionReviewVersions: - v1 - v1beta1 diff --git a/controlplane/rosa/api/v1beta2/conditions_consts.go b/controlplane/rosa/api/v1beta2/conditions_consts.go index 8bb0f50427..f094348440 100644 --- a/controlplane/rosa/api/v1beta2/conditions_consts.go +++ b/controlplane/rosa/api/v1beta2/conditions_consts.go @@ -31,6 +31,9 @@ const ( // ExternalAuthConfiguredCondition condition reports whether external auth has beed correctly configured. ExternalAuthConfiguredCondition clusterv1.ConditionType = "ExternalAuthConfigured" + // ROSARoleConfigReadyCondition condition reports whether the referenced RosaRoleConfig is ready. + ROSARoleConfigReadyCondition clusterv1.ConditionType = "ROSARoleConfigReady" + // ReconciliationFailedReason used to report reconciliation failures. ReconciliationFailedReason = "ReconciliationFailed" @@ -39,4 +42,10 @@ const ( // ROSAControlPlaneInvalidConfigurationReason used to report invalid user input. ROSAControlPlaneInvalidConfigurationReason = "InvalidConfiguration" + + // ROSARoleConfigNotReadyReason used to report when referenced RosaRoleConfig is not ready. + ROSARoleConfigNotReadyReason = "ROSARoleConfigNotReady" + + // ROSARoleConfigNotFoundReason used to report when referenced RosaRoleConfig is not found. + ROSARoleConfigNotFoundReason = "ROSARoleConfigNotFound" ) diff --git a/controlplane/rosa/api/v1beta2/rosacontrolplane_types.go b/controlplane/rosa/api/v1beta2/rosacontrolplane_types.go index 95d06281ff..0ecb34fbd8 100644 --- a/controlplane/rosa/api/v1beta2/rosacontrolplane_types.go +++ b/controlplane/rosa/api/v1beta2/rosacontrolplane_types.go @@ -21,7 +21,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" infrav1 "sigs.k8s.io/cluster-api-provider-aws/v2/api/v1beta2" - expinfrav1 "sigs.k8s.io/cluster-api-provider-aws/v2/exp/api/v1beta2" clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" ) @@ -124,13 +123,23 @@ type RosaControlPlaneSpec struct { //nolint: maligned // +kubebuilder:default=WaitForAcknowledge VersionGate VersionGateAckType `json:"versionGate"` + // RosaRoleConfigRef is a reference to a RosaRoleConfig resource that contains account and operator roles and OIDC configuration. + // If specified, the roles and OIDC configuration will be taken from the referenced RosaRoleConfig instead of the direct fields. + // + // +optional + RosaRoleConfigRef *corev1.LocalObjectReference `json:"rosaRoleConfigRef,omitempty"` + // AWS IAM roles used to perform credential requests by the openshift operators. - RolesRef AWSRolesRef `json:"rolesRef"` + // Required if RosaRoleConfigRef is not specified. + // +optional + RolesRef AWSRolesRef `json:"rolesRef,omitempty"` // The ID of the internal OpenID Connect Provider. + // Required if RosaRoleConfigRef is not specified. // // +kubebuilder:validation:XValidation:rule="self == oldSelf", message="oidcID is immutable" - OIDCID string `json:"oidcID"` + // +optional + OIDCID string `json:"oidcID,omitempty"` // EnableExternalAuthProviders enables external authentication configuration for the cluster. // @@ -149,13 +158,19 @@ type RosaControlPlaneSpec struct { //nolint: maligned // +kubebuilder:validation:MaxItems=1 ExternalAuthProviders []ExternalAuthProvider `json:"externalAuthProviders,omitempty"` - // InstallerRoleARN is an AWS IAM role that OpenShift Cluster Manager will assume to create the cluster.. - InstallerRoleARN string `json:"installerRoleARN"` + // InstallerRoleARN is an AWS IAM role that OpenShift Cluster Manager will assume to create the cluster. + // Required if RosaRoleConfigRef is not specified. + // +optional + InstallerRoleARN string `json:"installerRoleARN,omitempty"` // SupportRoleARN is an AWS IAM role used by Red Hat SREs to enable // access to the cluster account in order to provide support. - SupportRoleARN string `json:"supportRoleARN"` + // Required if RosaRoleConfigRef is not specified. + // +optional + SupportRoleARN string `json:"supportRoleARN,omitempty"` // WorkerRoleARN is an AWS IAM role that will be attached to worker instances. - WorkerRoleARN string `json:"workerRoleARN"` + // Required if RosaRoleConfigRef is not specified. + // +optional + WorkerRoleARN string `json:"workerRoleARN,omitempty"` // BillingAccount is an optional AWS account to use for billing the subscription fees for ROSA HCP clusters. // The cost of running each ROSA HCP cluster will be billed to the infrastructure account in which the cluster @@ -330,7 +345,7 @@ type DefaultMachinePoolSpec struct { // Autoscaling specifies auto scaling behaviour for the default MachinePool. Autoscaling min/max value // must be equal or multiple of the availability zones count. // +optional - Autoscaling *expinfrav1.RosaMachinePoolAutoScaling `json:"autoscaling,omitempty"` + Autoscaling *AutoScaling `json:"autoscaling,omitempty"` // VolumeSize set the disk volume size for the default workers machine pool in Gib. The default is 300 GiB. // +kubebuilder:validation:Minimum=75 @@ -340,6 +355,14 @@ type DefaultMachinePoolSpec struct { VolumeSize int `json:"volumeSize,omitempty"` } +// AutoScaling specifies scaling options. +type AutoScaling struct { + // +kubebuilder:validation:Minimum=1 + MinReplicas int `json:"minReplicas,omitempty"` + // +kubebuilder:validation:Minimum=1 + MaxReplicas int `json:"maxReplicas,omitempty"` +} + // AWSRolesRef contains references to various AWS IAM roles required for operators to make calls against the AWS API. type AWSRolesRef struct { // The referenced role must have a trust relationship that allows it to be assumed via web identity. diff --git a/controlplane/rosa/api/v1beta2/rosacontrolplane_webhook.go b/controlplane/rosa/api/v1beta2/rosacontrolplane_webhook.go index 56071a878e..0103f020e2 100644 --- a/controlplane/rosa/api/v1beta2/rosacontrolplane_webhook.go +++ b/controlplane/rosa/api/v1beta2/rosacontrolplane_webhook.go @@ -15,6 +15,9 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook/admission" ) +// log is for logging in this package. +var rosacpLog = ctrl.Log.WithName("rosacontrolplane-resource") + // SetupWebhookWithManager will setup the webhooks for the ROSAControlPlane. func (r *ROSAControlPlane) SetupWebhookWithManager(mgr ctrl.Manager) error { w := new(rosaControlPlaneWebhook) @@ -58,6 +61,10 @@ func (*rosaControlPlaneWebhook) ValidateCreate(_ context.Context, obj runtime.Ob allErrs = append(allErrs, err) } + if err := r.validateRosaRoleConfig(); err != nil { + allErrs = append(allErrs, err) + } + allErrs = append(allErrs, r.validateNetwork()...) allErrs = append(allErrs, r.Spec.AdditionalTags.Validate()...) @@ -179,6 +186,58 @@ func (r *ROSAControlPlane) validateExternalAuthProviders() *field.Error { return nil } +func (r *ROSAControlPlane) validateRosaRoleConfig() *field.Error { + hasAnyDirectRoleFields := r.Spec.OIDCID != "" || r.Spec.InstallerRoleARN != "" || r.Spec.SupportRoleARN != "" || r.Spec.WorkerRoleARN != "" || + r.Spec.RolesRef.IngressARN != "" || r.Spec.RolesRef.ImageRegistryARN != "" || r.Spec.RolesRef.StorageARN != "" || + r.Spec.RolesRef.NetworkARN != "" || r.Spec.RolesRef.KubeCloudControllerARN != "" || r.Spec.RolesRef.NodePoolManagementARN != "" || + r.Spec.RolesRef.ControlPlaneOperatorARN != "" || r.Spec.RolesRef.KMSProviderARN != "" + + if r.Spec.RosaRoleConfigRef != nil { + if hasAnyDirectRoleFields { + rosacpLog.Info("rosaRoleConfigRef and direct role fields (oidcID, installerRoleARN, supportRoleARN, workerRoleARN, rolesRef) are mutually exclusive") + } + return nil + } + + if r.Spec.OIDCID == "" { + return field.Invalid(field.NewPath("spec.oidcID"), r.Spec.OIDCID, "must be specified") + } + if r.Spec.InstallerRoleARN == "" { + return field.Invalid(field.NewPath("spec.installerRoleARN"), r.Spec.InstallerRoleARN, "must be specified") + } + if r.Spec.SupportRoleARN == "" { + return field.Invalid(field.NewPath("spec.supportRoleARN"), r.Spec.SupportRoleARN, "must be specified") + } + if r.Spec.WorkerRoleARN == "" { + return field.Invalid(field.NewPath("spec.workerRoleARN"), r.Spec.WorkerRoleARN, "must be specified") + } + if r.Spec.RolesRef.IngressARN == "" { + return field.Invalid(field.NewPath("spec.rolesRef.ingressARN"), r.Spec.RolesRef.IngressARN, "must be specified") + } + if r.Spec.RolesRef.ImageRegistryARN == "" { + return field.Invalid(field.NewPath("spec.rolesRef.imageRegistryARN"), r.Spec.RolesRef.ImageRegistryARN, "must be specified") + } + if r.Spec.RolesRef.StorageARN == "" { + return field.Invalid(field.NewPath("spec.rolesRef.storageARN"), r.Spec.RolesRef.StorageARN, "must be specified") + } + if r.Spec.RolesRef.NetworkARN == "" { + return field.Invalid(field.NewPath("spec.rolesRef.networkARN"), r.Spec.RolesRef.NetworkARN, "must be specified") + } + if r.Spec.RolesRef.KubeCloudControllerARN == "" { + return field.Invalid(field.NewPath("spec.rolesRef.kubeCloudControllerARN"), r.Spec.RolesRef.KubeCloudControllerARN, "must be specified") + } + if r.Spec.RolesRef.NodePoolManagementARN == "" { + return field.Invalid(field.NewPath("spec.rolesRef.nodePoolManagementARN"), r.Spec.RolesRef.NodePoolManagementARN, "must be specified") + } + if r.Spec.RolesRef.ControlPlaneOperatorARN == "" { + return field.Invalid(field.NewPath("spec.rolesRef.controlPlaneOperatorARN"), r.Spec.RolesRef.ControlPlaneOperatorARN, "must be specified") + } + if r.Spec.RolesRef.KMSProviderARN == "" { + return field.Invalid(field.NewPath("spec.rolesRef.kmsProviderARN"), r.Spec.RolesRef.KMSProviderARN, "must be specified") + } + return nil +} + // Default implements admission.Defaulter. func (*rosaControlPlaneWebhook) Default(_ context.Context, obj runtime.Object) error { r, ok := obj.(*ROSAControlPlane) diff --git a/controlplane/rosa/api/v1beta2/zz_generated.deepcopy.go b/controlplane/rosa/api/v1beta2/zz_generated.deepcopy.go index 3e4dfdf8cf..fbc10d0117 100644 --- a/controlplane/rosa/api/v1beta2/zz_generated.deepcopy.go +++ b/controlplane/rosa/api/v1beta2/zz_generated.deepcopy.go @@ -24,7 +24,6 @@ import ( "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" apiv1beta2 "sigs.k8s.io/cluster-api-provider-aws/v2/api/v1beta2" - expapiv1beta2 "sigs.k8s.io/cluster-api-provider-aws/v2/exp/api/v1beta2" "sigs.k8s.io/cluster-api/api/v1beta1" ) @@ -43,12 +42,27 @@ func (in *AWSRolesRef) DeepCopy() *AWSRolesRef { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AutoScaling) DeepCopyInto(out *AutoScaling) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AutoScaling. +func (in *AutoScaling) DeepCopy() *AutoScaling { + if in == nil { + return nil + } + out := new(AutoScaling) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DefaultMachinePoolSpec) DeepCopyInto(out *DefaultMachinePoolSpec) { *out = *in if in.Autoscaling != nil { in, out := &in.Autoscaling, &out.Autoscaling - *out = new(expapiv1beta2.RosaMachinePoolAutoScaling) + *out = new(AutoScaling) **out = **in } } @@ -311,6 +325,11 @@ func (in *RosaControlPlaneSpec) DeepCopyInto(out *RosaControlPlaneSpec) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.RosaRoleConfigRef != nil { + in, out := &in.RosaRoleConfigRef, &out.RosaRoleConfigRef + *out = new(v1.LocalObjectReference) + **out = **in + } out.RolesRef = in.RolesRef if in.ExternalAuthProviders != nil { in, out := &in.ExternalAuthProviders, &out.ExternalAuthProviders diff --git a/controlplane/rosa/controllers/rosacontrolplane_controller.go b/controlplane/rosa/controllers/rosacontrolplane_controller.go index 37143bc3f9..1f46795351 100644 --- a/controlplane/rosa/controllers/rosacontrolplane_controller.go +++ b/controlplane/rosa/controllers/rosacontrolplane_controller.go @@ -141,6 +141,8 @@ func (r *ROSAControlPlaneReconciler) SetupWithManager(ctx context.Context, mgr c // +kubebuilder:rbac:groups=controlplane.cluster.x-k8s.io,resources=rosacontrolplanes,verbs=get;list;watch;update;patch;delete // +kubebuilder:rbac:groups=controlplane.cluster.x-k8s.io,resources=rosacontrolplanes/status,verbs=get;update;patch // +kubebuilder:rbac:groups=controlplane.cluster.x-k8s.io,resources=rosacontrolplanes/finalizers,verbs=update +// +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=rosaroleconfigs,verbs=get;list;watch; +// +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=rosaroleconfigs/status,verbs=get; // Reconcile will reconcile RosaControlPlane Resources. func (r *ROSAControlPlaneReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.Result, reterr error) { @@ -167,7 +169,6 @@ func (r *ROSAControlPlaneReconciler) Reconcile(ctx context.Context, req ctrl.Req } log = log.WithValues("cluster", klog.KObj(cluster)) - if isPaused, conditionChanged, err := paused.EnsurePausedCondition(ctx, r.Client, cluster, rosaControlPlane); err != nil || isPaused || conditionChanged { return ctrl.Result{}, err } @@ -197,10 +198,10 @@ func (r *ROSAControlPlaneReconciler) Reconcile(ctx context.Context, req ctrl.Req } // Handle normal reconciliation loop. - return r.reconcileNormal(ctx, rosaScope) + return r.reconcileNormal(ctx, rosaScope, log) } -func (r *ROSAControlPlaneReconciler) reconcileNormal(ctx context.Context, rosaScope *scope.ROSAControlPlaneScope) (res ctrl.Result, reterr error) { +func (r *ROSAControlPlaneReconciler) reconcileNormal(ctx context.Context, rosaScope *scope.ROSAControlPlaneScope, log *logger.Logger) (res ctrl.Result, reterr error) { rosaScope.Info("Reconciling ROSAControlPlane") if controllerutil.AddFinalizer(rosaScope.ControlPlane, ROSAControlPlaneFinalizer) { @@ -227,11 +228,61 @@ func (r *ROSAControlPlaneReconciler) reconcileNormal(ctx context.Context, rosaSc return ctrl.Result{}, fmt.Errorf("failed to transform caller identity to creator: %w", err) } + rosaRoleConfig := &expinfrav1.ROSARoleConfig{} + // Get role configuration from either RosaRoleConfig or direct fields + if rosaScope.ControlPlane.Spec.RosaRoleConfigRef != nil { + // Get configuration from RosaRoleConfig + + key := client.ObjectKey{ + Name: rosaScope.ControlPlane.Spec.RosaRoleConfigRef.Name, + Namespace: rosaScope.ControlPlane.Namespace, + } + + if err := r.Client.Get(ctx, key, rosaRoleConfig); err != nil { + if apierrors.IsNotFound(err) { + conditions.MarkFalse(rosaScope.ControlPlane, + rosacontrolplanev1.ROSARoleConfigReadyCondition, + rosacontrolplanev1.ROSARoleConfigNotFoundReason, + clusterv1.ConditionSeverityError, + "RosaRoleConfig %s/%s not found", rosaScope.ControlPlane.Namespace, rosaScope.ControlPlane.Spec.RosaRoleConfigRef.Name) + log.Error(err, fmt.Sprintf("RosaRoleConfig %s/%s not found: %s", rosaScope.ControlPlane.Namespace, rosaScope.ControlPlane.Spec.RosaRoleConfigRef.Name, err.Error())) + return ctrl.Result{RequeueAfter: time.Second * 60}, nil + } + log.Error(err, fmt.Sprintf("failed to get RosaRoleConfig %s/%s: %s", rosaScope.ControlPlane.Namespace, rosaScope.ControlPlane.Spec.RosaRoleConfigRef.Name, err.Error())) + return ctrl.Result{RequeueAfter: time.Second * 60}, nil + } + + // Check if RosaRoleConfig is ready + if !conditions.IsTrue(rosaRoleConfig, expinfrav1.RosaRoleConfigReadyCondition) { + conditions.MarkFalse(rosaScope.ControlPlane, + rosacontrolplanev1.ROSARoleConfigReadyCondition, + rosacontrolplanev1.ROSARoleConfigNotReadyReason, + clusterv1.ConditionSeverityWarning, + "RosaRoleConfig %s/%s is not ready", rosaScope.ControlPlane.Namespace, rosaScope.ControlPlane.Spec.RosaRoleConfigRef.Name) + log.Error(err, fmt.Sprintf("RosaRoleConfig %s/%s is not ready", rosaScope.ControlPlane.Namespace, rosaScope.ControlPlane.Spec.RosaRoleConfigRef.Name)) + + return ctrl.Result{RequeueAfter: time.Second * 60}, nil + } + + conditions.MarkTrue(rosaScope.ControlPlane, rosacontrolplanev1.ROSARoleConfigReadyCondition) + } else { + rosaRoleConfig.Status.OIDCID = rosaScope.ControlPlane.Spec.OIDCID + rosaRoleConfig.Status.AccountRolesRef.InstallerRoleARN = rosaScope.ControlPlane.Spec.InstallerRoleARN + rosaRoleConfig.Status.AccountRolesRef.SupportRoleARN = rosaScope.ControlPlane.Spec.SupportRoleARN + rosaRoleConfig.Status.AccountRolesRef.WorkerRoleARN = rosaScope.ControlPlane.Spec.WorkerRoleARN + rosaRoleConfig.Status.OperatorRolesRef = rosaScope.ControlPlane.Spec.RolesRef + } + validationMessage, err := validateControlPlaneSpec(ocmClient, rosaScope) if err != nil { return ctrl.Result{}, fmt.Errorf("failed to validate ROSAControlPlane.spec: %w", err) } + err = validateRoleConfigSpec(rosaRoleConfig) + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed to validate ROSAControlPlane.spec: %w", err) + } + conditions.MarkTrue(rosaScope.ControlPlane, rosacontrolplanev1.ROSAControlPlaneValidCondition) if validationMessage != "" { conditions.MarkFalse(rosaScope.ControlPlane, @@ -314,7 +365,7 @@ func (r *ROSAControlPlaneReconciler) reconcileNormal(ctx context.Context, rosaSc return ctrl.Result{RequeueAfter: time.Second * 60}, nil } - ocmClusterSpec, err := buildOCMClusterSpec(rosaScope.ControlPlane.Spec, creator) + ocmClusterSpec, err := buildOCMClusterSpec(rosaScope.ControlPlane.Spec, rosaRoleConfig, creator) if err != nil { return ctrl.Result{}, err } @@ -915,7 +966,7 @@ func validateControlPlaneSpec(ocmClient rosa.OCMClient, rosaScope *scope.ROSACon return "", nil } -func buildOCMClusterSpec(controlPlaneSpec rosacontrolplanev1.RosaControlPlaneSpec, creator *rosaaws.Creator) (ocm.Spec, error) { +func buildOCMClusterSpec(controlPlaneSpec rosacontrolplanev1.RosaControlPlaneSpec, roleConfig *expinfrav1.ROSARoleConfig, creator *rosaaws.Creator) (ocm.Spec, error) { billingAccount := controlPlaneSpec.BillingAccount if billingAccount == "" { billingAccount = creator.AccountID @@ -939,11 +990,11 @@ func buildOCMClusterSpec(controlPlaneSpec rosacontrolplanev1.RosaControlPlaneSpe SubnetIds: controlPlaneSpec.Subnets, IsSTS: true, - RoleARN: controlPlaneSpec.InstallerRoleARN, - SupportRoleARN: controlPlaneSpec.SupportRoleARN, - WorkerRoleARN: controlPlaneSpec.WorkerRoleARN, - OperatorIAMRoles: operatorIAMRoles(controlPlaneSpec.RolesRef), - OidcConfigId: controlPlaneSpec.OIDCID, + RoleARN: roleConfig.Status.AccountRolesRef.InstallerRoleARN, + SupportRoleARN: roleConfig.Status.AccountRolesRef.SupportRoleARN, + WorkerRoleARN: roleConfig.Status.AccountRolesRef.WorkerRoleARN, + OperatorIAMRoles: operatorIAMRoles(roleConfig.Status.OperatorRolesRef), + OidcConfigId: roleConfig.Status.OIDCID, Mode: "auto", Hypershift: ocm.Hypershift{ Enabled: true, @@ -1138,3 +1189,55 @@ func buildAPIEndpoint(cluster *cmv1.Cluster) (*clusterv1.APIEndpoint, error) { Port: int32(port), //#nosec G109 G115 }, nil } + +func validateRoleConfigSpec(roleConfig *expinfrav1.ROSARoleConfig) error { + if roleConfig.Status.OIDCID == "" { + return fmt.Errorf("OIDCID is required") + } + + if roleConfig.Status.AccountRolesRef.InstallerRoleARN == "" { + return fmt.Errorf("InstallerRoleARN is required") + } + + if roleConfig.Status.AccountRolesRef.SupportRoleARN == "" { + return fmt.Errorf("SupportRoleARN is required") + } + + if roleConfig.Status.AccountRolesRef.WorkerRoleARN == "" { + return fmt.Errorf("WorkerRoleARN is required") + } + + if roleConfig.Status.OperatorRolesRef.IngressARN == "" { + return fmt.Errorf("IngressARN is required") + } + + if roleConfig.Status.OperatorRolesRef.ImageRegistryARN == "" { + return fmt.Errorf("ImageRegistryARN is required") + } + + if roleConfig.Status.OperatorRolesRef.StorageARN == "" { + return fmt.Errorf("StorageARN is required") + } + + if roleConfig.Status.OperatorRolesRef.NetworkARN == "" { + return fmt.Errorf("NetworkARN is required") + } + + if roleConfig.Status.OperatorRolesRef.KubeCloudControllerARN == "" { + return fmt.Errorf("KubeCloudControllerARN is required") + } + + if roleConfig.Status.OperatorRolesRef.KMSProviderARN == "" { + return fmt.Errorf("KMSProviderARN is required") + } + + if roleConfig.Status.OperatorRolesRef.ControlPlaneOperatorARN == "" { + return fmt.Errorf("ControlPlaneOperatorARN is required") + } + + if roleConfig.Status.OperatorRolesRef.NodePoolManagementARN == "" { + return fmt.Errorf("NodePoolManagementARN is required") + } + + return nil +} diff --git a/controlplane/rosa/controllers/rosacontrolplane_controller_test.go b/controlplane/rosa/controllers/rosacontrolplane_controller_test.go index 61b8f9ce52..f7172744cb 100644 --- a/controlplane/rosa/controllers/rosacontrolplane_controller_test.go +++ b/controlplane/rosa/controllers/rosacontrolplane_controller_test.go @@ -250,10 +250,19 @@ func TestRosaControlPlaneReconcileStatusVersion(t *testing.T) { PodCIDR: "10.128.0.0/14", ServiceCIDR: "172.30.0.0/16", }, - Region: "us-east-1", - Version: "4.15.20", - ChannelGroup: "stable", - RolesRef: rosacontrolplanev1.AWSRolesRef{}, + Region: "us-east-1", + Version: "4.15.20", + ChannelGroup: "stable", + RolesRef: rosacontrolplanev1.AWSRolesRef{ + IngressARN: "op-arn1", + ImageRegistryARN: "op-arn2", + StorageARN: "op-arn3", + NetworkARN: "op-arn4", + KubeCloudControllerARN: "op-arn5", + NodePoolManagementARN: "op-arn6", + ControlPlaneOperatorARN: "op-arn7", + KMSProviderARN: "op-arn8", + }, OIDCID: "iodcid1", InstallerRoleARN: "arn1", WorkerRoleARN: "arn2", diff --git a/exp/api/v1beta2/finalizers.go b/exp/api/v1beta2/finalizers.go index 1125449285..f0cffa7958 100644 --- a/exp/api/v1beta2/finalizers.go +++ b/exp/api/v1beta2/finalizers.go @@ -28,4 +28,7 @@ const ( // RosaMachinePoolFinalizer allows the controller to clean up resources on delete. RosaMachinePoolFinalizer = "rosamachinepools.infrastructure.cluster.x-k8s.io" + + // RosaRoleConfigFinalizer allows the controller to clean up resources on delete. + RosaRoleConfigFinalizer = "rosaroleconfigs.infrastructure.cluster.x-k8s.io" ) diff --git a/exp/api/v1beta2/rosamachinepool_types.go b/exp/api/v1beta2/rosamachinepool_types.go index 0dc3af30ed..a3286a4a2e 100644 --- a/exp/api/v1beta2/rosamachinepool_types.go +++ b/exp/api/v1beta2/rosamachinepool_types.go @@ -22,6 +22,7 @@ import ( "k8s.io/apimachinery/pkg/util/intstr" infrav1 "sigs.k8s.io/cluster-api-provider-aws/v2/api/v1beta2" + rosacontrolplanev1 "sigs.k8s.io/cluster-api-provider-aws/v2/controlplane/rosa/api/v1beta2" clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" ) @@ -79,7 +80,7 @@ type RosaMachinePoolSpec struct { // Autoscaling specifies auto scaling behaviour for this MachinePool. // required if Replicas is not configured // +optional - Autoscaling *RosaMachinePoolAutoScaling `json:"autoscaling,omitempty"` + Autoscaling *rosacontrolplanev1.AutoScaling `json:"autoscaling,omitempty"` // TuningConfigs specifies the names of the tuning configs to be applied to this MachinePool. // Tuning configs must already exist. @@ -139,14 +140,6 @@ type RosaTaint struct { Effect corev1.TaintEffect `json:"effect"` } -// RosaMachinePoolAutoScaling specifies scaling options. -type RosaMachinePoolAutoScaling struct { - // +kubebuilder:validation:Minimum=1 - MinReplicas int `json:"minReplicas,omitempty"` - // +kubebuilder:validation:Minimum=1 - MaxReplicas int `json:"maxReplicas,omitempty"` -} - // RosaUpdateConfig specifies update configuration type RosaUpdateConfig struct { // RollingUpdate specifies MaxUnavailable & MaxSurge number of nodes during update. diff --git a/exp/api/v1beta2/rosaroleconfig_types.go b/exp/api/v1beta2/rosaroleconfig_types.go new file mode 100644 index 0000000000..2dd118394d --- /dev/null +++ b/exp/api/v1beta2/rosaroleconfig_types.go @@ -0,0 +1,173 @@ +/* +Copyright The Kubernetes Authors. + +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 v1beta2 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + infrav1 "sigs.k8s.io/cluster-api-provider-aws/v2/api/v1beta2" + rosacontrolplanev1 "sigs.k8s.io/cluster-api-provider-aws/v2/controlplane/rosa/api/v1beta2" + clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// ROSARoleConfigSpec defines the desired state of ROSARoleConfig +type ROSARoleConfigSpec struct { + AccountRoleConfig AccountRoleConfig `json:"accountRoleConfig"` + OperatorRoleConfig OperatorRoleConfig `json:"operatorRoleConfig"` + IdentityRef *infrav1.AWSIdentityReference `json:"identityRef,omitempty"` + // CredentialsSecretRef references a secret with necessary credentials to connect to the OCM API. + // +optional + CredentialsSecretRef *corev1.LocalObjectReference `json:"credentialsSecretRef,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:resource:path=rosaroleconfigs,scope=Namespaced,categories=cluster-api,shortName=rosarole +// +kubebuilder:storageversion +// +kubebuilder:subresource:status + +// ROSARoleConfig is the Schema for the rosaroleconfigs API +type ROSARoleConfig struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ROSARoleConfigSpec `json:"spec,omitempty"` + Status ROSARoleConfigStatus `json:"status,omitempty"` +} + +// AccountRoleConfig defines account-wide IAM roles before creating your ROSA cluster. +type AccountRoleConfig struct { + // User-defined prefix for all generated AWS resources + // +kubebuilder:validation:MaxLength:=4 + // +kubebuilder:validation:Required + // +immutable + Prefix string `json:"prefix"` + // The ARN of the policy that is used to set the permissions boundary for the account roles. + // +optional + // +immutable + + PermissionsBoundaryARN string `json:"permissionsBoundaryARN,omitempty"` + // The arn path for the account/operator roles as well as their policies. + // +optional + // +immutable + + Path string `json:"path,omitempty"` + // Version of OpenShift that will be used to setup policy tag, for example "4.11" + // +kubebuilder:validation:Required + // +immutable + Version string `json:"version"` + // SharedVPCConfig is used to set up shared VPC. + // +optional + // +immutable + SharedVPCConfig SharedVPCConfig `json:"sharedVPCConfig,omitempty"` +} + +// OperatorRoleConfig defines cluster-specific operator IAM roles based on your cluster configuration. +type OperatorRoleConfig struct { + // User-defined prefix for generated AWS operator policies. + // +kubebuilder:validation:MaxLength:=4 + // +kubebuilder:validation:Required + // +immutable + Prefix string `json:"prefix"` + // The ARN of the policy that is used to set the permissions boundary for the operator roles. + // +optional + // +immutable + PermissionsBoundaryARN string `json:"permissionsBoundaryARN,omitempty"` + // SharedVPCConfig is used to set up shared VPC. + // +optional + // +immutable + SharedVPCConfig SharedVPCConfig `json:"sharedVPCConfig,omitempty"` + // OIDCID is the ID of the OIDC config that will be used to create the operator roles. + // A managed OIDC-provider will be created if the OIDCID not specified + // +optional + // +immutable + OIDCID string `json:"oidcID,omitempty"` +} + +// SharedVPCConfig is used to set up shared VPC. +type SharedVPCConfig struct { + // Role ARN associated with the private hosted zone used for Hosted Control Plane cluster shared VPC, this role contains policies to be used with Route 53 + RouteRoleARN string `json:"routeRoleARN,omitempty"` + // Role ARN associated with the shared VPC used for Hosted Control Plane clusters, this role contains policies to be used with the VPC endpoint + VPCEndpointRoleARN string `json:"vpcEndpointRoleArn,omitempty"` +} + +// ROSARoleConfigStatus defines the observed state of ROSARoleConfig +type ROSARoleConfigStatus struct { + // ID of created OIDC config + OIDCID string `json:"oidcID,omitempty"` + // Create OIDC provider for operators to authenticate against in an STS cluster. + OIDCProviderARN string `json:"oidcProviderARN,omitempty"` + // Created Account roles that can be used to + AccountRolesRef AccountRolesRef `json:"accountRolesRef,omitempty"` + // AWS IAM roles used to perform credential requests by the openshift operators. + OperatorRolesRef rosacontrolplanev1.AWSRolesRef `json:"operatorRolesRef,omitempty"` + Conditions clusterv1.Conditions `json:"conditions,omitempty"` +} + +// AccountRolesRef defscribes ARNs used as Account roles. +type AccountRolesRef struct { + // InstallerRoleARN is an AWS IAM role that OpenShift Cluster Manager will assume to create the cluster.. + InstallerRoleARN string `json:"installerRoleARN,omitempty"` + // SupportRoleARN is an AWS IAM role used by Red Hat SREs to enable + // access to the cluster account in order to provide support. + SupportRoleARN string `json:"supportRoleARN,omitempty"` + // WorkerRoleARN is an AWS IAM role that will be attached to worker instances. + WorkerRoleARN string `json:"workerRoleARN,omitempty"` +} + +// ROSARoleConfigList contains a list of ROSARoleConfig +// +kubebuilder:object:root=true +type ROSARoleConfigList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []ROSARoleConfig `json:"items"` +} + +const ( + // RosaRoleConfigReadyCondition condition reports on the successful reconciliation of RosaRoleConfig. + RosaRoleConfigReadyCondition = "RosaRoleConfigReady" + + // RosaRoleConfigDeletionFailedReason used to report failures while deleting RosaRoleConfig. + RosaRoleConfigDeletionFailedReason = "DeletionFailed" + + // RosaRoleConfigReconciliationFailedReason used to report reconciliation failures. + RosaRoleConfigReconciliationFailedReason = "ReconciliationFailed" + + // RosaRoleConfigDeletionStarted used to indicate that the deletion of RosaRoleConfig has started. + RosaRoleConfigDeletionStarted = "DeletionStarted" + + // RosaRoleConfigCreatedReason used to indicate that the RosaRoleConfig has been created. + RosaRoleConfigCreatedReason = "Created" +) + +// SetConditions sets the conditions of the ROSARoleConfig. +func (r *ROSARoleConfig) SetConditions(conditions clusterv1.Conditions) { + r.Status.Conditions = conditions +} + +// GetConditions returns the observations of the operational state of the RosaNetwork resource. +func (r *ROSARoleConfig) GetConditions() clusterv1.Conditions { + return r.Status.Conditions +} + +func init() { + SchemeBuilder.Register(&ROSARoleConfig{}, &ROSARoleConfigList{}) +} diff --git a/exp/api/v1beta2/rosaroleconfig_webhook.go b/exp/api/v1beta2/rosaroleconfig_webhook.go new file mode 100644 index 0000000000..e0b3feb589 --- /dev/null +++ b/exp/api/v1beta2/rosaroleconfig_webhook.go @@ -0,0 +1,48 @@ +package v1beta2 + +import ( + "context" + + runtime "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +// SetupWebhookWithManager will setup the webhooks for the ROSARoleConfig. +func (r *ROSARoleConfig) SetupWebhookWithManager(mgr ctrl.Manager) error { + w := new(rosaRoleConfigWebhook) + return ctrl.NewWebhookManagedBy(mgr). + For(r). + WithValidator(w). + WithDefaulter(w). + Complete() +} + +// +kubebuilder:webhook:verbs=create;update,path=/validate-infrastructure-cluster-x-k8s-io-v1beta2-rosaroleconfig,mutating=false,failurePolicy=fail,matchPolicy=Equivalent,groups=infrastructure.cluster.x-k8s.io,resources=rosaroleconfigs,versions=v1beta2,name=validation.rosaroleconfig.infrastructure.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1;v1beta1 +// +kubebuilder:webhook:verbs=create;update,path=/mutate-infrastructure-cluster-x-k8s-io-v1beta2-rosaroleconfig,mutating=true,failurePolicy=fail,matchPolicy=Equivalent,groups=infrastructure.cluster.x-k8s.io,resources=rosaroleconfigs,versions=v1beta2,name=default.rosaroleconfig.infrastructure.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1;v1beta1 + +type rosaRoleConfigWebhook struct{} + +var _ webhook.CustomDefaulter = &rosaRoleConfigWebhook{} +var _ webhook.CustomValidator = &rosaRoleConfigWebhook{} + +// ValidateCreate implements admission.Validator. +func (r *rosaRoleConfigWebhook) ValidateCreate(ctx context.Context, obj runtime.Object) (warnings admission.Warnings, err error) { + return nil, nil +} + +// ValidateUpdate implements admission.Validator. +func (r *rosaRoleConfigWebhook) ValidateUpdate(ctx context.Context, old runtime.Object, updated runtime.Object) (warnings admission.Warnings, err error) { + return nil, nil +} + +// ValidateDelete implements admission.Validator. +func (r *rosaRoleConfigWebhook) ValidateDelete(ctx context.Context, obj runtime.Object) (warnings admission.Warnings, err error) { + return nil, nil +} + +// Default implements admission.Defaulter. +func (r *rosaRoleConfigWebhook) Default(ctx context.Context, obj runtime.Object) error { + return nil +} diff --git a/exp/api/v1beta2/zz_generated.deepcopy.go b/exp/api/v1beta2/zz_generated.deepcopy.go index 6885eb4c64..c08607265e 100644 --- a/exp/api/v1beta2/zz_generated.deepcopy.go +++ b/exp/api/v1beta2/zz_generated.deepcopy.go @@ -21,10 +21,12 @@ limitations under the License. package v1beta2 import ( + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/intstr" apiv1beta2 "sigs.k8s.io/cluster-api-provider-aws/v2/api/v1beta2" + rosaapiv1beta2 "sigs.k8s.io/cluster-api-provider-aws/v2/controlplane/rosa/api/v1beta2" "sigs.k8s.io/cluster-api/api/v1beta1" ) @@ -621,6 +623,37 @@ func (in *AWSManagedMachinePoolStatus) DeepCopy() *AWSManagedMachinePoolStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AccountRoleConfig) DeepCopyInto(out *AccountRoleConfig) { + *out = *in + out.SharedVPCConfig = in.SharedVPCConfig +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AccountRoleConfig. +func (in *AccountRoleConfig) DeepCopy() *AccountRoleConfig { + if in == nil { + return nil + } + out := new(AccountRoleConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AccountRolesRef) DeepCopyInto(out *AccountRolesRef) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AccountRolesRef. +func (in *AccountRolesRef) DeepCopy() *AccountRolesRef { + if in == nil { + return nil + } + out := new(AccountRolesRef) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AutoScalingGroup) DeepCopyInto(out *AutoScalingGroup) { *out = *in @@ -891,6 +924,22 @@ func (in *MixedInstancesPolicy) DeepCopy() *MixedInstancesPolicy { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OperatorRoleConfig) DeepCopyInto(out *OperatorRoleConfig) { + *out = *in + out.SharedVPCConfig = in.SharedVPCConfig +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OperatorRoleConfig. +func (in *OperatorRoleConfig) DeepCopy() *OperatorRoleConfig { + if in == nil { + return nil + } + out := new(OperatorRoleConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Overrides) DeepCopyInto(out *Overrides) { *out = *in @@ -1129,6 +1178,116 @@ func (in *ROSAMachinePoolList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ROSARoleConfig) DeepCopyInto(out *ROSARoleConfig) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ROSARoleConfig. +func (in *ROSARoleConfig) DeepCopy() *ROSARoleConfig { + if in == nil { + return nil + } + out := new(ROSARoleConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ROSARoleConfig) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ROSARoleConfigList) DeepCopyInto(out *ROSARoleConfigList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ROSARoleConfig, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ROSARoleConfigList. +func (in *ROSARoleConfigList) DeepCopy() *ROSARoleConfigList { + if in == nil { + return nil + } + out := new(ROSARoleConfigList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ROSARoleConfigList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ROSARoleConfigSpec) DeepCopyInto(out *ROSARoleConfigSpec) { + *out = *in + out.AccountRoleConfig = in.AccountRoleConfig + out.OperatorRoleConfig = in.OperatorRoleConfig + if in.IdentityRef != nil { + in, out := &in.IdentityRef, &out.IdentityRef + *out = new(apiv1beta2.AWSIdentityReference) + **out = **in + } + if in.CredentialsSecretRef != nil { + in, out := &in.CredentialsSecretRef, &out.CredentialsSecretRef + *out = new(corev1.LocalObjectReference) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ROSARoleConfigSpec. +func (in *ROSARoleConfigSpec) DeepCopy() *ROSARoleConfigSpec { + if in == nil { + return nil + } + out := new(ROSARoleConfigSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ROSARoleConfigStatus) DeepCopyInto(out *ROSARoleConfigStatus) { + *out = *in + out.AccountRolesRef = in.AccountRolesRef + out.OperatorRolesRef = in.OperatorRolesRef + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make(v1beta1.Conditions, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ROSARoleConfigStatus. +func (in *ROSARoleConfigStatus) DeepCopy() *ROSARoleConfigStatus { + if in == nil { + return nil + } + out := new(ROSARoleConfigStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RefreshPreferences) DeepCopyInto(out *RefreshPreferences) { *out = *in @@ -1189,21 +1348,6 @@ func (in *RollingUpdate) DeepCopy() *RollingUpdate { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RosaMachinePoolAutoScaling) DeepCopyInto(out *RosaMachinePoolAutoScaling) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RosaMachinePoolAutoScaling. -func (in *RosaMachinePoolAutoScaling) DeepCopy() *RosaMachinePoolAutoScaling { - if in == nil { - return nil - } - out := new(RosaMachinePoolAutoScaling) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RosaMachinePoolSpec) DeepCopyInto(out *RosaMachinePoolSpec) { *out = *in @@ -1228,7 +1372,7 @@ func (in *RosaMachinePoolSpec) DeepCopyInto(out *RosaMachinePoolSpec) { } if in.Autoscaling != nil { in, out := &in.Autoscaling, &out.Autoscaling - *out = new(RosaMachinePoolAutoScaling) + *out = new(rosaapiv1beta2.AutoScaling) **out = **in } if in.TuningConfigs != nil { @@ -1335,6 +1479,21 @@ func (in *RosaUpdateConfig) DeepCopy() *RosaUpdateConfig { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SharedVPCConfig) DeepCopyInto(out *SharedVPCConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SharedVPCConfig. +func (in *SharedVPCConfig) DeepCopy() *SharedVPCConfig { + if in == nil { + return nil + } + out := new(SharedVPCConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SuspendProcessesTypes) DeepCopyInto(out *SuspendProcessesTypes) { *out = *in diff --git a/exp/controllers/rosamachinepool_controller.go b/exp/controllers/rosamachinepool_controller.go index 4750369a9b..259a12fc99 100644 --- a/exp/controllers/rosamachinepool_controller.go +++ b/exp/controllers/rosamachinepool_controller.go @@ -539,7 +539,7 @@ func nodePoolToRosaMachinePoolSpec(nodePool *cmv1.NodePool) expinfrav1.RosaMachi } if nodePool.Autoscaling() != nil { - spec.Autoscaling = &expinfrav1.RosaMachinePoolAutoScaling{ + spec.Autoscaling = &rosacontrolplanev1.AutoScaling{ MinReplicas: nodePool.Autoscaling().MinReplica(), MaxReplicas: nodePool.Autoscaling().MaxReplica(), } diff --git a/exp/controllers/rosamachinepool_controller_test.go b/exp/controllers/rosamachinepool_controller_test.go index 553cc38922..778a13539d 100644 --- a/exp/controllers/rosamachinepool_controller_test.go +++ b/exp/controllers/rosamachinepool_controller_test.go @@ -127,10 +127,19 @@ func TestRosaMachinePoolReconcile(t *testing.T) { PodCIDR: "10.128.0.0/14", ServiceCIDR: "172.30.0.0/16", }, - Region: "us-east-1", - Version: "4.15.20", - ChannelGroup: "stable", - RolesRef: rosacontrolplanev1.AWSRolesRef{}, + Region: "us-east-1", + Version: "4.15.20", + ChannelGroup: "stable", + RolesRef: rosacontrolplanev1.AWSRolesRef{ + IngressARN: "op-arn1", + ImageRegistryARN: "op-arn2", + StorageARN: "op-arn3", + NetworkARN: "op-arn4", + KubeCloudControllerARN: "op-arn5", + NodePoolManagementARN: "op-arn6", + ControlPlaneOperatorARN: "op-arn7", + KMSProviderARN: "op-arn8", + }, OIDCID: "iodcid1", InstallerRoleARN: "arn1", WorkerRoleARN: "arn2", diff --git a/exp/controllers/rosaroleconfig_controller.go b/exp/controllers/rosaroleconfig_controller.go new file mode 100644 index 0000000000..778f5b54f7 --- /dev/null +++ b/exp/controllers/rosaroleconfig_controller.go @@ -0,0 +1,662 @@ +/* +Copyright The Kubernetes Authors. + +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 controllers + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/url" + "strings" + + "github.com/aws/aws-sdk-go/service/iam" + "github.com/go-logr/logr" + cmv1 "github.com/openshift-online/ocm-sdk-go/clustersmgmt/v1" + accountroles "github.com/openshift/rosa/cmd/create/accountroles" + oidcconfig "github.com/openshift/rosa/cmd/create/oidcconfig" + oidcprovider "github.com/openshift/rosa/cmd/create/oidcprovider" + operatorroles "github.com/openshift/rosa/cmd/create/operatorroles" + "github.com/openshift/rosa/pkg/aws" + "github.com/openshift/rosa/pkg/helper" + interactive "github.com/openshift/rosa/pkg/interactive" + rosalogging "github.com/openshift/rosa/pkg/logging" + "github.com/openshift/rosa/pkg/ocm" + rosacli "github.com/openshift/rosa/pkg/rosa" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/klog/v2" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + expinfrav1 "sigs.k8s.io/cluster-api-provider-aws/v2/exp/api/v1beta2" + "sigs.k8s.io/cluster-api-provider-aws/v2/pkg/cloud" + "sigs.k8s.io/cluster-api-provider-aws/v2/pkg/cloud/scope" + stsiface "sigs.k8s.io/cluster-api-provider-aws/v2/pkg/cloud/services/sts" + "sigs.k8s.io/cluster-api-provider-aws/v2/pkg/logger" + "sigs.k8s.io/cluster-api-provider-aws/v2/pkg/rosa" + clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + "sigs.k8s.io/cluster-api/util/conditions" + "sigs.k8s.io/cluster-api/util/predicates" +) + +// ROSARoleConfigReconciler reconciles a ROSARoleConfig object. +type ROSARoleConfigReconciler struct { + client.Client + Log logr.Logger + Scheme *runtime.Scheme + Endpoints []scope.ServiceEndpoint + WatchFilterValue string + NewStsClient func(cloud.ScopeUsage, cloud.Session, logger.Wrapper, runtime.Object) stsiface.STSClient + NewOCMClient func(ctx context.Context, scope rosa.OCMSecretsRetriever) (rosa.OCMClient, error) +} + +func (r *ROSARoleConfigReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, options controller.Options) error { + log := logger.FromContext(ctx) + r.NewOCMClient = rosa.NewWrappedOCMClientWithoutControlPlane + r.NewStsClient = scope.NewSTSClient + + return ctrl.NewControllerManagedBy(mgr). + For(&expinfrav1.ROSARoleConfig{}). + WithOptions(options). + WithEventFilter(predicates.ResourceHasFilterLabel(mgr.GetScheme(), log.GetLogger(), r.WatchFilterValue)). + Complete(r) +} + +// +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=rosaroleconfigs,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=rosaroleconfigs/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=rosaroleconfigs/finalizers,verbs=update + +func (r *ROSARoleConfigReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.Result, reterr error) { + log := logger.FromContext(ctx) + + roleConfig := &expinfrav1.ROSARoleConfig{} + if err := r.Get(ctx, req.NamespacedName, roleConfig); err != nil { + if apierrors.IsNotFound(err) { + return ctrl.Result{}, nil + } + return ctrl.Result{Requeue: true}, nil + } + + log = log.WithValues("cluster", klog.KObj(roleConfig)) + scope, err := scope.NewRosaRoleConfigScope(scope.RosaRoleConfigScopeParams{ + Client: r.Client, + RosaRoleConfig: roleConfig, + ControllerName: "rosaroleconfig", + Endpoints: r.Endpoints, + Logger: log, + }) + + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed to create rosaroleconfig scope: %w", err) + } + + // Always close the scope and set summary condition + defer func() { + conditions.SetSummary(scope.RosaRoleConfig, conditions.WithConditions(expinfrav1.RosaRoleConfigReadyCondition), conditions.WithStepCounter()) + + if err := scope.Close(); err != nil { + reterr = errors.Join(reterr, err) + } + }() + + ocm, err := r.NewOCMClient(ctx, scope) + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed to create OCM client: %w", err) + } + + ocmClient, err := rosa.ConvertToRosaOcmClient(ocm) + if err != nil || ocmClient == nil { + return ctrl.Result{}, fmt.Errorf("failed to create OCM client: %w", err) + } + + if !roleConfig.ObjectMeta.DeletionTimestamp.IsZero() { + conditions.MarkFalse(scope.RosaRoleConfig, expinfrav1.RosaRoleConfigReadyCondition, expinfrav1.RosaRoleConfigDeletionStarted, clusterv1.ConditionSeverityInfo, "Deletion of RosaRolesConfig started") + return ctrl.Result{}, r.reconcileDelete(scope, ocmClient) + } + + if controllerutil.AddFinalizer(scope.RosaRoleConfig, expinfrav1.RosaRoleConfigFinalizer) { + if err := scope.PatchObject(); err != nil { + return ctrl.Result{}, err + } + } + + err = r.createAccountRoles(ctx, roleConfig, scope, ocmClient) + if err != nil { + conditions.MarkFalse(scope.RosaRoleConfig, expinfrav1.RosaRoleConfigReadyCondition, expinfrav1.RosaRoleConfigReconciliationFailedReason, clusterv1.ConditionSeverityError, "Failed to create Account Roles: %v", err) + return ctrl.Result{}, fmt.Errorf("failed to Create AccountRoles: %w", err) + } + + err = r.reconcileOIDCConfig(roleConfig, scope, ocmClient) + if err != nil { + conditions.MarkFalse(scope.RosaRoleConfig, expinfrav1.RosaRoleConfigReadyCondition, expinfrav1.RosaRoleConfigReconciliationFailedReason, clusterv1.ConditionSeverityError, "Failed to create OIDC Config: %v", err) + return ctrl.Result{}, fmt.Errorf("failed to OICD Config: %w", err) + } + + err = r.createOperatorRoles(ctx, roleConfig, scope, ocmClient) + if err != nil { + conditions.MarkFalse(scope.RosaRoleConfig, expinfrav1.RosaRoleConfigReadyCondition, expinfrav1.RosaRoleConfigReconciliationFailedReason, clusterv1.ConditionSeverityError, "Failed to create Operator Roles: %v", err) + return ctrl.Result{}, fmt.Errorf("failed to Create OperatorRoles: %w", err) + } + + err = r.createOIDCProvider(scope, ocmClient) + if err != nil { + conditions.MarkFalse(scope.RosaRoleConfig, expinfrav1.RosaRoleConfigReadyCondition, expinfrav1.RosaRoleConfigReconciliationFailedReason, clusterv1.ConditionSeverityError, "Failed to create OIDC provider: %v", err) + return ctrl.Result{}, fmt.Errorf("failed to Create OIDC provider: %w", err) + } + + if r.rosaRolesConfigReady(scope) { + conditions.MarkTrue(scope.RosaRoleConfig, expinfrav1.RosaRoleConfigReadyCondition) + conditions.Set(scope.RosaRoleConfig, + &clusterv1.Condition{ + Type: expinfrav1.RosaRoleConfigReadyCondition, + Status: corev1.ConditionTrue, + Reason: expinfrav1.RosaRoleConfigCreatedReason, + Severity: clusterv1.ConditionSeverityInfo, + Message: "RosaRoleConfig is ready to be used.", + }) + } + return ctrl.Result{}, nil +} + +func (r *ROSARoleConfigReconciler) reconcileDelete(scope *scope.RosaRoleConfigScope, ocmClient *ocm.Client) error { + log := rosalogging.NewLogger() + awsClient, err := aws.NewClient().Logger(log).Build() + if err != nil { + conditions.MarkFalse(scope.RosaRoleConfig, expinfrav1.RosaRoleConfigReadyCondition, expinfrav1.RosaRoleConfigDeletionFailedReason, clusterv1.ConditionSeverityError, "Failed to create AWS client: %v", err) + return err + } + + oidcID := scope.RosaRoleConfig.Status.OIDCID + if scope.RosaRoleConfig.Spec.OperatorRoleConfig.OIDCID == "" { + err = r.deleteOIDCProvider(ocmClient, awsClient, oidcID) + if err != nil { + conditions.MarkFalse(scope.RosaRoleConfig, expinfrav1.RosaRoleConfigReadyCondition, expinfrav1.RosaRoleConfigDeletionFailedReason, clusterv1.ConditionSeverityError, "Failed to delete OIDC provider: %v", err) + return err + } + } + + err = r.deleteOperatorRoles(ocmClient, awsClient, scope.RosaRoleConfig.Spec.AccountRoleConfig.Prefix) + if err != nil { + conditions.MarkFalse(scope.RosaRoleConfig, expinfrav1.RosaRoleConfigReadyCondition, expinfrav1.RosaRoleConfigDeletionFailedReason, clusterv1.ConditionSeverityError, "Failed to delete operator roles: %v", err) + return err + } + + err = r.deleteAccountRoles(ocmClient, awsClient, scope) + if err != nil { + conditions.MarkFalse(scope.RosaRoleConfig, expinfrav1.RosaRoleConfigReadyCondition, expinfrav1.RosaRoleConfigDeletionFailedReason, clusterv1.ConditionSeverityError, "Failed to delete account roles: %v", err) + return err + } + + if scope.RosaRoleConfig.Spec.OperatorRoleConfig.OIDCID == "" { + err = r.deleteOIDCConfig(ocmClient, oidcID) + if err != nil { + conditions.MarkFalse(scope.RosaRoleConfig, expinfrav1.RosaRoleConfigReadyCondition, expinfrav1.RosaRoleConfigDeletionFailedReason, clusterv1.ConditionSeverityError, "Failed to delete OIDC config: %v", err) + return err + } + } + + controllerutil.RemoveFinalizer(scope.RosaRoleConfig, expinfrav1.RosaRoleConfigFinalizer) + // Explicitly patch the object to persist the finalizer removal + if err := scope.PatchObject(); err != nil { + conditions.MarkFalse(scope.RosaRoleConfig, expinfrav1.RosaRoleConfigReadyCondition, expinfrav1.RosaRoleConfigDeletionFailedReason, clusterv1.ConditionSeverityError, "Failed to remove finalizer: %v", err) + return fmt.Errorf("failed to remove finalizer from ROSARoleConfig: %w", err) + } + + return nil +} + +func (r *ROSARoleConfigReconciler) createOperatorRoles(ctx context.Context, roleConfig *expinfrav1.ROSARoleConfig, scope *scope.RosaRoleConfigScope, ocmClient *ocm.Client) error { + installerRoleArn := scope.RosaRoleConfig.Status.AccountRolesRef.InstallerRoleARN + if installerRoleArn == "" { + return fmt.Errorf("installer role is empty") + } + oidcConfigID := scope.RosaRoleConfig.Status.OIDCID + if oidcConfigID == "" { + return fmt.Errorf("OIDCID is empty") + } + + runtime := rosacli.NewRuntime() + policies, err := ocmClient.GetPolicies("OperatorRole") + if err != nil { + return err + } + runtime.OCMClient, err = rosa.NewOCMClient(ctx, scope) + if err != nil { + return err + } + + runtime.Reporter = (&rosa.Reporter{}) + runtime.Logger = rosalogging.NewLogger() + runtime.AWSClient, err = aws.NewClient().Logger(runtime.Logger).Build() + if err != nil { + return fmt.Errorf("failed to create aws client: %w", err) + } + + runtime.Creator, err = runtime.AWSClient.GetCreator() + if err != nil { + return err + } + + config := roleConfig.Spec.OperatorRoleConfig + version := roleConfig.Spec.AccountRoleConfig.Version + hostedCp := true + forcePolicyCreation := true + isSharedVpc := config.SharedVPCConfig.VPCEndpointRoleARN != "" && config.SharedVPCConfig.RouteRoleARN != "" + + operatorRoles, err := runtime.AWSClient.ListOperatorRoles(version, "", config.Prefix) + + if err != nil { + return err + } + + if len(operatorRoles) > 0 { + for _, roles := range operatorRoles { + for _, role := range roles { + if strings.Contains(role.RoleName, fmt.Sprintf("%s-openshift-ingress-operator-cloud-credentials", config.Prefix)) { + scope.RosaRoleConfig.Status.OperatorRolesRef.IngressARN = role.RoleARN + } + if strings.Contains(role.RoleName, fmt.Sprintf("%s-openshift-image-registry-installer-cloud-credentials", config.Prefix)) { + scope.RosaRoleConfig.Status.OperatorRolesRef.ImageRegistryARN = role.RoleARN + } + if strings.Contains(role.RoleName, fmt.Sprintf("%s-openshift-cluster-csi-drivers-ebs-cloud-credentials", config.Prefix)) { + scope.RosaRoleConfig.Status.OperatorRolesRef.StorageARN = role.RoleARN + } + if strings.Contains(role.RoleName, fmt.Sprintf("%s-openshift-cloud-network-config-controller-cloud-credentials", config.Prefix)) { + scope.RosaRoleConfig.Status.OperatorRolesRef.NetworkARN = role.RoleARN + } + if strings.Contains(role.RoleName, fmt.Sprintf("%s-kube-system-kube-controller-manager", config.Prefix)) { + scope.RosaRoleConfig.Status.OperatorRolesRef.KubeCloudControllerARN = role.RoleARN + } + if strings.Contains(role.RoleName, fmt.Sprintf("%s-kube-system-capa-controller-manager", config.Prefix)) { + scope.RosaRoleConfig.Status.OperatorRolesRef.NodePoolManagementARN = role.RoleARN + } + if strings.Contains(role.RoleName, fmt.Sprintf("%s-kube-system-control-plane-operator", config.Prefix)) { + scope.RosaRoleConfig.Status.OperatorRolesRef.ControlPlaneOperatorARN = role.RoleARN + } + if strings.Contains(role.RoleName, fmt.Sprintf("%s-kube-system-kms-provider", config.Prefix)) { + scope.RosaRoleConfig.Status.OperatorRolesRef.KMSProviderARN = role.RoleARN + } + } + } + } else { + err = operatorroles.CreateOperatorRoles(runtime, ocm.Production, config.PermissionsBoundaryARN, interactive.ModeAuto, policies, version, isSharedVpc, config.Prefix, hostedCp, installerRoleArn, forcePolicyCreation, + oidcConfigID, config.SharedVPCConfig.RouteRoleARN, ocm.DefaultChannelGroup, config.SharedVPCConfig.VPCEndpointRoleARN) + return err + } + + return nil +} + +func (r *ROSARoleConfigReconciler) reconcileOIDCConfig(roleConfig *expinfrav1.ROSARoleConfig, scope *scope.RosaRoleConfigScope, ocmClient *ocm.Client) error { + if scope.RosaRoleConfig.Status.OIDCID != "" { + return nil + } + if roleConfig.Spec.OperatorRoleConfig.OIDCID != "" { + scope.RosaRoleConfig.Status.OIDCID = roleConfig.Spec.OperatorRoleConfig.OIDCID + return nil + } + // Try to get OIDC UUID from some operator role policy document. + roleName := fmt.Sprintf("%s-openshift-ingress-operator-cloud-credentials", roleConfig.Spec.OperatorRoleConfig.Prefix) + roleDetails, err := scope.IAMClient().GetRole(&iam.GetRoleInput{ + RoleName: &roleName, + }) + if err != nil { + return r.createOIDCConfig(scope, ocmClient) + } + oidcID, err := r.GetOIDCIDFromOperatorRole(scope, roleDetails) + if err != nil { + return r.createOIDCConfig(scope, ocmClient) + } + scope.RosaRoleConfig.Status.OIDCID = oidcID + return nil +} + +func (r *ROSARoleConfigReconciler) createOIDCProvider(scope *scope.RosaRoleConfigScope, ocmClient *ocm.Client) error { + if scope.RosaRoleConfig.Status.OIDCProviderARN != "" { + return nil + } + + var err error + oidcID := scope.RosaRoleConfig.Status.OIDCID + if oidcID == "" { + return nil + } + runtime := rosacli.NewRuntime() + runtime.OCMClient = ocmClient + runtime.Reporter = (&rosa.Reporter{}) + + runtime.Logger = rosalogging.NewLogger() + runtime.AWSClient, err = aws.NewClient().Logger(runtime.Logger).Build() + if err != nil { + return fmt.Errorf("failed to create aws client: %w", err) + } + + oidcConfig, err := runtime.OCMClient.GetOidcConfig(oidcID) + if err != nil { + return err + } + + providers, err := runtime.AWSClient.ListOidcProviders("", oidcConfig) + if err != nil { + return err + } + for _, provider := range providers { + if strings.Contains(provider.Arn, oidcID) { + scope.RosaRoleConfig.Status.OIDCProviderARN = provider.Arn + return nil + } + } + + runtime.Creator, err = runtime.AWSClient.GetCreator() + if err != nil { + return err + } + + return oidcprovider.CreateOIDCProvider(runtime, oidcID, "", true) +} + +func (r *ROSARoleConfigReconciler) createAccountRoles(ctx context.Context, roleConfig *expinfrav1.ROSARoleConfig, scope *scope.RosaRoleConfigScope, ocmClient rosa.OCMClient) error { + config := roleConfig.Spec.AccountRoleConfig + runtime := rosacli.NewRuntime() + policies, err := ocmClient.GetPolicies("AccountRole") + if err != nil { + return err + } + runtime.OCMClient, err = rosa.NewOCMClient(ctx, scope) + if err != nil { + return err + } + + runtime.Reporter = (&rosa.Reporter{}) + runtime.Logger = rosalogging.NewLogger() + runtime.AWSClient, err = aws.NewClient().Logger(runtime.Logger).Build() + if err != nil { + return fmt.Errorf("failed to create aws client: %w", err) + } + + runtime.Creator, err = runtime.AWSClient.GetCreator() + if err != nil { + return err + } + + createRoles := true + accountRoles, err := runtime.AWSClient.ListAccountRoles(config.Version) + if err != nil { + // Let create account roles continue if no account roles are found + if !strings.Contains(err.Error(), "no account roles found") { + return err + } + } + + for _, role := range accountRoles { + if strings.Contains(role.RoleName, fmt.Sprintf("%s-HCP-ROSA-Installer", config.Prefix)) { + createRoles = false + scope.RosaRoleConfig.Status.AccountRolesRef.InstallerRoleARN = role.RoleARN + } + if strings.Contains(role.RoleName, fmt.Sprintf("%s-HCP-ROSA-Support", config.Prefix)) { + createRoles = false + scope.RosaRoleConfig.Status.AccountRolesRef.SupportRoleARN = role.RoleARN + } + if strings.Contains(role.RoleName, fmt.Sprintf("%s-HCP-ROSA-Worker", config.Prefix)) { + createRoles = false + scope.RosaRoleConfig.Status.AccountRolesRef.WorkerRoleARN = role.RoleARN + } + } + if createRoles { + runtime.Reporter = (&rosa.Reporter{}) + runtime.Logger = rosalogging.NewLogger() + runtime.AWSClient, err = aws.NewClient().Logger(runtime.Logger).Build() + if err != nil { + return fmt.Errorf("failed to create aws client: %w", err) + } + + managedPolicies := true + isSharedVpc := config.SharedVPCConfig.VPCEndpointRoleARN != "" && config.SharedVPCConfig.RouteRoleARN != "" + err := accountroles.CreateHCPRoles(runtime, config.Prefix, managedPolicies, config.PermissionsBoundaryARN, ocm.Production, policies, config.Version, config.Path, isSharedVpc, config.SharedVPCConfig.RouteRoleARN, config.SharedVPCConfig.VPCEndpointRoleARN) + return err + } + + return nil +} + +func (r *ROSARoleConfigReconciler) createOIDCConfig(scope *scope.RosaRoleConfigScope, ocmClient *ocm.Client) error { + runtime := rosacli.NewRuntime() + var err error + runtime.Reporter = (&rosa.Reporter{}) + runtime.OCMClient = ocmClient + runtime.Logger = rosalogging.NewLogger() + runtime.AWSClient, err = aws.NewClient().Logger(runtime.Logger).Build() + if err != nil { + return fmt.Errorf("failed to create aws client: %w", err) + } + + runtime.Creator, err = runtime.AWSClient.GetCreator() + if err != nil { + return err + } + + // userPrefix, region are used only for unmanaged OIDC config + oidcID, createErr := oidcconfig.CreateOIDCConfig(runtime, true, "", "") + if createErr != nil { + return fmt.Errorf("failed to Create OIDC config: %w", err) + } + + scope.RosaRoleConfig.Status.OIDCID = oidcID + return createErr +} + +func (r *ROSARoleConfigReconciler) deleteAccountRoles(ocmClient *ocm.Client, awsClient aws.Client, scope *scope.RosaRoleConfigScope) error { + roles := scope.RosaRoleConfig.Status.AccountRolesRef + config := scope.RosaRoleConfig.Spec.AccountRoleConfig + deleteHcpSharedVpcPolicies := config.SharedVPCConfig.VPCEndpointRoleARN != "" && config.SharedVPCConfig.RouteRoleARN != "" + creator, err := awsClient.GetCreator() + if err != nil { + return err + } + + clusters, err := ocmClient.GetAllClusters(creator) + if err != nil { + return err + } + + var err2, err3 error + if canDeleteRole(clusters, roles.InstallerRoleARN) { + err = awsClient.DeleteAccountRole(strings.Split(roles.InstallerRoleARN, "/")[1], config.Prefix, true, deleteHcpSharedVpcPolicies) + } + if canDeleteRole(clusters, roles.WorkerRoleARN) { + err2 = awsClient.DeleteAccountRole(strings.Split(roles.WorkerRoleARN, "/")[1], config.Prefix, true, deleteHcpSharedVpcPolicies) + } + if canDeleteRole(clusters, roles.SupportRoleARN) { + err3 = awsClient.DeleteAccountRole(strings.Split(roles.SupportRoleARN, "/")[1], config.Prefix, true, deleteHcpSharedVpcPolicies) + } + if err != nil { + return err + } + if err2 != nil { + return err2 + } + return err3 +} + +func (r *ROSARoleConfigReconciler) deleteOIDCProvider(ocmClient *ocm.Client, awsClient aws.Client, oidcConfigID string) error { + if oidcConfigID == "" { + return nil + } + + oidcConfig, err := ocmClient.GetOidcConfig(oidcConfigID) + if err != nil { + return err + } + + oidcEndpointURL := oidcConfig.IssuerUrl() + parsedURI, _ := url.ParseRequestURI(oidcEndpointURL) + if parsedURI.Scheme != helper.ProtocolHttps { + return fmt.Errorf("expected OIDC endpoint URL '%s' to use an https:// scheme", oidcEndpointURL) + } + providerArn, err := awsClient.GetOpenIDConnectProviderByOidcEndpointUrl(oidcEndpointURL) + if err != nil { + return err + } + + if providerArn == "" { + return nil + } + creator, err := awsClient.GetCreator() + if err != nil { + return err + } + hasClusterUsingOidcProvider, err := ocmClient.HasAClusterUsingOidcProvider(oidcEndpointURL, creator.AccountID) + if err != nil { + return err + } + + if hasClusterUsingOidcProvider { + return fmt.Errorf("there are clusters using OIDC config '%s', can't delete the provider", oidcEndpointURL) + } + + return awsClient.DeleteOpenIDConnectProvider(providerArn) +} + +func (r *ROSARoleConfigReconciler) deleteOperatorRoles(ocmClient *ocm.Client, awsClient aws.Client, prefix string) error { + hasClusterUsingOperatorRolesPrefix, err := ocmClient.HasAClusterUsingOperatorRolesPrefix(prefix) + if err != nil { + return err + } + if hasClusterUsingOperatorRolesPrefix { + return fmt.Errorf("there are clusters using Operator Roles Prefix '%s', can't delete the IAM roles", prefix) + } + + credRequests, err := ocmClient.GetAllCredRequests() + if err != nil { + return err + } + + foundOperatorRoles, err := awsClient.GetOperatorRolesFromAccountByPrefix(prefix, credRequests) + if err != nil { + return err + } + + if len(foundOperatorRoles) == 0 { + return nil + } + + _, roleARN, err := awsClient.CheckRoleExists(foundOperatorRoles[0]) + if err != nil { + return err + } + + managedPolicies, err := awsClient.HasManagedPolicies(roleARN) + if err != nil { + return err + } + + allSharedVpcPoliciesNotDeleted := make(map[string]bool) + for _, role := range foundOperatorRoles { + sharedVpcPoliciesNotDeleted, _ := awsClient.DeleteOperatorRole(role, managedPolicies, true) + for key, value := range sharedVpcPoliciesNotDeleted { + allSharedVpcPoliciesNotDeleted[key] = value + } + } + + for policyOutput, notDeleted := range allSharedVpcPoliciesNotDeleted { + if notDeleted { + return fmt.Errorf("unable to delete policy %s: Policy still attached to other resources", policyOutput) + } + } + return nil +} + +func (r *ROSARoleConfigReconciler) deleteOIDCConfig(ocmClient *ocm.Client, oidcConfigID string) error { + if oidcConfigID == "" { + return nil + } + return ocmClient.DeleteOidcConfig(oidcConfigID) +} + +func canDeleteRole(clusters []*cmv1.Cluster, roleARN string) bool { + if roleARN == "" { + return false + } + for _, cluster := range clusters { + if cluster.AWS().STS().RoleARN() == roleARN || + cluster.AWS().STS().SupportRoleARN() == roleARN || + cluster.AWS().STS().InstanceIAMRoles().MasterRoleARN() == roleARN || + cluster.AWS().STS().InstanceIAMRoles().WorkerRoleARN() == roleARN { + return false + } + } + return true +} + +func (r ROSARoleConfigReconciler) rosaRolesConfigReady(scope *scope.RosaRoleConfigScope) bool { + if scope.RosaRoleConfig.Status.OIDCID == "" || + scope.RosaRoleConfig.Status.OIDCProviderARN == "" || + scope.RosaRoleConfig.Status.AccountRolesRef.InstallerRoleARN == "" || + scope.RosaRoleConfig.Status.AccountRolesRef.SupportRoleARN == "" || + scope.RosaRoleConfig.Status.AccountRolesRef.WorkerRoleARN == "" || + scope.RosaRoleConfig.Status.OperatorRolesRef.ControlPlaneOperatorARN == "" || + scope.RosaRoleConfig.Status.OperatorRolesRef.ImageRegistryARN == "" || + scope.RosaRoleConfig.Status.OperatorRolesRef.IngressARN == "" || + scope.RosaRoleConfig.Status.OperatorRolesRef.KMSProviderARN == "" || + scope.RosaRoleConfig.Status.OperatorRolesRef.KubeCloudControllerARN == "" || + scope.RosaRoleConfig.Status.OperatorRolesRef.NetworkARN == "" || + scope.RosaRoleConfig.Status.OperatorRolesRef.NodePoolManagementARN == "" || + scope.RosaRoleConfig.Status.OperatorRolesRef.StorageARN == "" { + return false + } + return true +} + +// GetOIDCIDFromOperatorRole extracts the OIDC UUID from the operator role policy document. +func (r *ROSARoleConfigReconciler) GetOIDCIDFromOperatorRole(scope *scope.RosaRoleConfigScope, roleDetails *iam.GetRoleOutput) (string, error) { + decodedString, err := url.QueryUnescape(*roleDetails.Role.AssumeRolePolicyDocument) + if err != nil { + return "", err + } + + var policyDoc struct { + Statement []struct { + Principal struct { + Federated string `json:"Federated"` + } `json:"Principal"` + Condition map[string]map[string]string `json:"Condition"` + } `json:"Statement"` + } + + err = json.Unmarshal([]byte(decodedString), &policyDoc) + if err != nil { + return "", err + } + + // Extract from the 'Federated' ARN + if len(policyDoc.Statement) > 0 { + federatedARN := policyDoc.Statement[0].Principal.Federated + // The format is arn:aws:iam::ACCOUNT_ID:oidc-provider/OIDC_PROVIDER_URL + // OIDC_PROVIDER_URL ends with /OIDCID + parts := strings.Split(federatedARN, "/") + if len(parts) > 1 { + oidcUUID := parts[len(parts)-1] + return oidcUUID, nil + } + } + + return "", fmt.Errorf("cant extract oidc uuid from the %s policy document", *roleDetails.Role.RoleName) +} diff --git a/exp/controllers/rosaroleconfig_controller_test.go b/exp/controllers/rosaroleconfig_controller_test.go new file mode 100644 index 0000000000..53b665c0f4 --- /dev/null +++ b/exp/controllers/rosaroleconfig_controller_test.go @@ -0,0 +1,55 @@ +/* +Copyright The Kubernetes Authors. + +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 controllers + +import ( + "context" + "testing" + + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + + expinfrav1 "sigs.k8s.io/cluster-api-provider-aws/v2/exp/api/v1beta2" +) + +func TestROSARoleConfigReconciler_Reconcile(t *testing.T) { + g := NewWithT(t) + + ctx := context.TODO() + + rosaRoleConfig := &expinfrav1.ROSARoleConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-rosa-role", + Namespace: "test-namespace"}, + Spec: expinfrav1.ROSARoleConfigSpec{}, + } + + // Setup the reconciler with these mocks + reconciler := &ROSARoleConfigReconciler{ + Client: testEnv.Client, + } + + // Call the Reconcile function + req := ctrl.Request{} + req.NamespacedName = types.NamespacedName{Name: rosaRoleConfig.Name, Namespace: rosaRoleConfig.Namespace} + _, errReconcile := reconciler.Reconcile(ctx, req) + + // Assertions + g.Expect(errReconcile).ToNot(HaveOccurred()) +} diff --git a/exp/controllers/suite_test.go b/exp/controllers/suite_test.go index 9283f003e9..637cb6a19e 100644 --- a/exp/controllers/suite_test.go +++ b/exp/controllers/suite_test.go @@ -86,6 +86,9 @@ func setup() { if err := (&expinfrav1.ROSAMachinePool{}).SetupWebhookWithManager(testEnv); err != nil { panic(fmt.Sprintf("Unable to setup ROSAMachinePool webhook: %v", err)) } + if err := (&expinfrav1.ROSARoleConfig{}).SetupWebhookWithManager(testEnv); err != nil { + panic(fmt.Sprintf("Unable to setup ROSARoleConfig webhook: %v", err)) + } if err := (&rosacontrolplanev1.ROSAControlPlane{}).SetupWebhookWithManager(testEnv); err != nil { panic(fmt.Sprintf("Unable to setup ROSAControlPlane webhook: %v", err)) } diff --git a/go.mod b/go.mod index cfd3722878..fe525d6985 100644 --- a/go.mod +++ b/go.mod @@ -133,6 +133,7 @@ require ( github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-jose/go-jose/v4 v4.0.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/zapr v1.3.0 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect @@ -167,6 +168,7 @@ require ( github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect @@ -194,6 +196,7 @@ require ( github.com/prometheus/common v0.55.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/rivo/uniseg v0.4.2 // indirect + github.com/robfig/cron/v3 v3.0.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sagikazarmark/locafero v0.7.0 // indirect github.com/sanathkr/go-yaml v0.0.0-20170819195128-ed9d249f429b // indirect @@ -244,3 +247,9 @@ require ( sigs.k8s.io/kind v0.27.0 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect ) + +require ( + github.com/AlecAivazis/survey/v2 v2.2.15 // indirect + github.com/kr/pty v1.1.8 // indirect + github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect +) diff --git a/go.sum b/go.sum index ea099d0230..0fadc4c04f 100644 --- a/go.sum +++ b/go.sum @@ -28,6 +28,8 @@ github.com/Microsoft/go-winio v0.5.0 h1:Elr9Wn+sGKPlkaBvwu4mTrxtmOp3F3yV9qhaHbXG github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= +github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nBpB11F9br+3HUrpgb+fcm5iADzXXYEw= +github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc= github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 h1:wPbRQzjjwFc0ih8puEVAOFGELsn1zoIIYdxvML7mDxA= github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g= github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= @@ -167,6 +169,7 @@ github.com/coreos/vcontext v0.0.0-20230201181013-d72178a18687 h1:uSmlDgJGbUB0bwQ github.com/coreos/vcontext v0.0.0-20230201181013-d72178a18687/go.mod h1:Salmysdw7DAVuobBW/LwsKKgpyCPHUhjyJoMJD+ZJiI= github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= @@ -212,6 +215,8 @@ github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/ github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk= +github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -314,6 +319,8 @@ github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8 github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0= github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDGgJGQpNflI3+MJSBhsgT5PCtzBQ= +github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= @@ -361,6 +368,9 @@ github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI= +github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= @@ -378,6 +388,7 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58= @@ -497,6 +508,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -578,6 +590,7 @@ go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN8 go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 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/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -645,6 +658,7 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY= golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/main.go b/main.go index 8aac35b373..94a52ae06b 100644 --- a/main.go +++ b/main.go @@ -86,7 +86,9 @@ func init() { _ = rosacontrolplanev1.AddToScheme(scheme) _ = infrav1.AddToScheme(scheme) _ = infrav1beta1.AddToScheme(scheme) + _ = expinfrav1beta1.AddToScheme(scheme) + _ = expinfrav1.AddToScheme(scheme) // +kubebuilder:scaffold:scheme } @@ -279,8 +281,21 @@ func main() { setupLog.Error(err, "unable to create webhook", "webhook", "ROSAMachinePool") os.Exit(1) } - } + if err = (&expcontrollers.ROSARoleConfigReconciler{ + Client: mgr.GetClient(), + Log: ctrl.Log.WithName("controllers").WithName("ROSARoleConfig"), + Scheme: mgr.GetScheme(), + }).SetupWithManager(ctx, mgr, controller.Options{MaxConcurrentReconciles: awsClusterConcurrency, RecoverPanic: ptr.To[bool](true)}); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "ROSARoleConfig") + os.Exit(1) + } + + if err := (&expinfrav1.ROSARoleConfig{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "ROSARoleConfig") + os.Exit(1) + } + } // +kubebuilder:scaffold:builder if err := mgr.AddReadyzCheck("webhook", mgr.GetWebhookServer().StartedChecker()); err != nil { diff --git a/pkg/cloud/scope/rosacontrolplane.go b/pkg/cloud/scope/rosacontrolplane.go index 4aac52bd01..47073c2dad 100644 --- a/pkg/cloud/scope/rosacontrolplane.go +++ b/pkg/cloud/scope/rosacontrolplane.go @@ -162,6 +162,11 @@ func (s *ROSAControlPlaneScope) Namespace() string { return s.Cluster.Namespace } +// GetClient return Client of this scope. +func (s *ROSAControlPlaneScope) GetClient() client.Client { + return s.Client +} + // CredentialsSecret returns the CredentialsSecret object. func (s *ROSAControlPlaneScope) CredentialsSecret() *corev1.Secret { secretRef := s.ControlPlane.Spec.CredentialsSecretRef diff --git a/pkg/cloud/scope/rosaroleconfig.go b/pkg/cloud/scope/rosaroleconfig.go new file mode 100644 index 0000000000..14c2470acf --- /dev/null +++ b/pkg/cloud/scope/rosaroleconfig.go @@ -0,0 +1,170 @@ +/* + Copyright The Kubernetes Authors. + + 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 scope + +import ( + "context" + + awsclient "github.com/aws/aws-sdk-go/aws/client" + "github.com/aws/aws-sdk-go/service/iam" + "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/klog/v2" + "sigs.k8s.io/controller-runtime/pkg/client" + + infrav1 "sigs.k8s.io/cluster-api-provider-aws/v2/api/v1beta2" + expinfrav1 "sigs.k8s.io/cluster-api-provider-aws/v2/exp/api/v1beta2" + "sigs.k8s.io/cluster-api-provider-aws/v2/pkg/cloud" + "sigs.k8s.io/cluster-api-provider-aws/v2/pkg/cloud/throttle" + "sigs.k8s.io/cluster-api-provider-aws/v2/pkg/logger" + "sigs.k8s.io/cluster-api/util/patch" +) + +// RosaRoleConfigScopeParams defines the input parameters used to create a new RosaRoleConfigScope. +type RosaRoleConfigScopeParams struct { + Client client.Client + ControllerName string + Endpoints []ServiceEndpoint + Logger *logger.Logger + RosaRoleConfig *expinfrav1.ROSARoleConfig +} + +// RosaRoleConfigScope defines the basic context for an actuator to operate upon. +type RosaRoleConfigScope struct { + logger.Logger + Client client.Client + controllerName string + patchHelper *patch.Helper + RosaRoleConfig *expinfrav1.ROSARoleConfig + serviceLimiters throttle.ServiceLimiters + session awsclient.ConfigProvider + iamClient *iam.IAM +} + +// NewRosaRoleConfigScope creates a new RosaRoleConfigScope from the supplied parameters. +func NewRosaRoleConfigScope(params RosaRoleConfigScopeParams) (*RosaRoleConfigScope, error) { + if params.Logger == nil { + log := klog.Background() + params.Logger = logger.NewLogger(log) + } + + RosaRoleConfigScope := &RosaRoleConfigScope{ + Logger: *params.Logger, + Client: params.Client, + controllerName: params.ControllerName, + patchHelper: nil, + RosaRoleConfig: params.RosaRoleConfig, + } + + session, serviceLimiters, err := sessionForClusterWithRegion(params.Client, RosaRoleConfigScope, "", params.Endpoints, params.Logger) + if err != nil { + return nil, errors.Errorf("failed to create aws session: %v", err) + } + + iamClient := iam.New(session) + + patchHelper, err := patch.NewHelper(params.RosaRoleConfig, params.Client) + if err != nil { + return nil, errors.Wrap(err, "failed to init patch helper") + } + + RosaRoleConfigScope.patchHelper = patchHelper + RosaRoleConfigScope.session = session + RosaRoleConfigScope.serviceLimiters = serviceLimiters + RosaRoleConfigScope.iamClient = iamClient + + return RosaRoleConfigScope, nil +} + +// IdentityRef returns the AWSIdentityReference object. +func (s *RosaRoleConfigScope) IdentityRef() *infrav1.AWSIdentityReference { + return s.RosaRoleConfig.Spec.IdentityRef +} + +// Session returns the AWS SDK session (used for creating clients). +func (s *RosaRoleConfigScope) Session() awsclient.ConfigProvider { + return s.session +} + +// ServiceLimiter returns the AWS SDK session (used for creating clients). +func (s *RosaRoleConfigScope) ServiceLimiter(service string) *throttle.ServiceLimiter { + if sl, ok := s.serviceLimiters[service]; ok { + return sl + } + return nil +} + +// ControllerName returns the name of the controller. +func (s *RosaRoleConfigScope) ControllerName() string { + return s.controllerName +} + +// InfraCluster returns the RosaRoleConfig object. +// The method is then used in session.go to set proper Conditions for the RosaRoleConfig object. +func (s *RosaRoleConfigScope) InfraCluster() cloud.ClusterObject { + return s.RosaRoleConfig +} + +// InfraClusterName returns the name of the RosaRoleConfig object. +// The method is then used in session.go to set the key to the AWS session cache. +func (s *RosaRoleConfigScope) InfraClusterName() string { + return s.RosaRoleConfig.Name +} + +// Namespace returns the namespace of the RosaRoleConfig object. +// The method is then used in session.go to set the key to the AWS session cache. +func (s *RosaRoleConfigScope) Namespace() string { + return s.RosaRoleConfig.Namespace +} + +// GetClient Returns RosaRoleConfigScope client. +func (s *RosaRoleConfigScope) GetClient() client.Client { + return s.Client +} + +// PatchObject persists the RosaRoleConfig configuration and status. +func (s *RosaRoleConfigScope) PatchObject() error { + return s.patchHelper.Patch( + context.TODO(), + s.RosaRoleConfig) +} + +// Close closes the current scope persisting the RosaRoleConfig configuration and status. +func (s *RosaRoleConfigScope) Close() error { + return s.PatchObject() +} + +// CredentialsSecret returns the CredentialsSecret object. +func (s *RosaRoleConfigScope) CredentialsSecret() *corev1.Secret { + secretRef := s.RosaRoleConfig.Spec.CredentialsSecretRef + if secretRef == nil { + return nil + } + + return &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: s.RosaRoleConfig.Spec.CredentialsSecretRef.Name, + Namespace: s.RosaRoleConfig.Namespace, + }, + } +} + +// IAMClient returns the IAM client. +func (s *RosaRoleConfigScope) IAMClient() *iam.IAM { + return s.iamClient +} diff --git a/pkg/rosa/client.go b/pkg/rosa/client.go index 90670772c3..e86f5a99bd 100644 --- a/pkg/rosa/client.go +++ b/pkg/rosa/client.go @@ -28,8 +28,15 @@ const ( capaAgentName = "CAPA" ) +// OCMSecretsRetriever contains functions that are needed for creating OCM connection. +type OCMSecretsRetriever interface { + CredentialsSecret() *corev1.Secret + GetClient() client.Client // Or just Client, depending on your actual field + Info(msg string, keysAndValues ...interface{}) +} + // NewOCMClient creates a new OCM client. -func NewOCMClient(ctx context.Context, rosaScope *scope.ROSAControlPlaneScope) (*ocm.Client, error) { +func NewOCMClient(ctx context.Context, rosaScope OCMSecretsRetriever) (*ocm.Client, error) { token, url, clientID, clientSecret, err := ocmCredentials(ctx, rosaScope) if err != nil { return nil, err @@ -62,6 +69,25 @@ func NewWrappedOCMClient(ctx context.Context, rosaScope *scope.ROSAControlPlaneS return &c, err } +// NewWrappedOCMClientWithoutControlPlane creates OCM connection without controlplane. +func NewWrappedOCMClientWithoutControlPlane(ctx context.Context, rosaScope OCMSecretsRetriever) (OCMClient, error) { + ocmClient, err := NewOCMClient(ctx, rosaScope) + c := ocmclient{ + ocmClient: ocmClient, + } + + return &c, err +} + +// NewWrappedOCMClientFromOCMClient makes a wrapped OCM client from an existing OCM client. +func NewWrappedOCMClientFromOCMClient(ctx context.Context, ocmClient *ocm.Client) (OCMClient, error) { + c := ocmclient{ + ocmClient: ocmClient, + } + + return &c, nil +} + func newOCMRawConnection(ctx context.Context, rosaScope *scope.ROSAControlPlaneScope) (*sdk.Connection, error) { ocmSdkLogger, err := sdk.NewGoLoggerBuilder(). Debug(false). @@ -94,7 +120,9 @@ func newOCMRawConnection(ctx context.Context, rosaScope *scope.ROSAControlPlaneS return connection, nil } -func ocmCredentials(ctx context.Context, rosaScope *scope.ROSAControlPlaneScope) (string, string, string, string, error) { +// OCMSecretsRetriever defines the interface for types that can provide OCM credentials information. + +func ocmCredentials(ctx context.Context, rosaScope OCMSecretsRetriever) (string, string, string, string, error) { var token string // Offline SSO token var ocmClientID string // Service account client id var ocmClientSecret string // Service account client secret @@ -102,8 +130,9 @@ func ocmCredentials(ctx context.Context, rosaScope *scope.ROSAControlPlaneScope) var secret *corev1.Secret secret = rosaScope.CredentialsSecret() // We'll retrieve the OCM credentials ref from the ROSA control plane + if secret != nil { - if err := rosaScope.Client.Get(ctx, client.ObjectKeyFromObject(secret), secret); err != nil { + if err := rosaScope.GetClient().Get(ctx, client.ObjectKeyFromObject(secret), secret); err != nil { return "", "", "", "", fmt.Errorf("failed to get credentials secret: %w", err) } } else { // If the reference to OCM secret wasn't specified in the ROSA control plane, we'll try to use a predefined secret name from the capa namespace @@ -114,7 +143,7 @@ func ocmCredentials(ctx context.Context, rosaScope *scope.ROSAControlPlaneScope) }, } - err := rosaScope.Client.Get(ctx, client.ObjectKeyFromObject(secret), secret) + err := rosaScope.GetClient().Get(ctx, client.ObjectKeyFromObject(secret), secret) // We'll ignore non-existent secret so that we can try the ENV variable fallback below // TODO: once the ENV variable fallback is gone, we can no longer ignore non-existent secret here if err != nil && !apierrors.IsNotFound(err) { diff --git a/pkg/rosa/helpers.go b/pkg/rosa/helpers.go index e73493a703..8cf505d7d4 100644 --- a/pkg/rosa/helpers.go +++ b/pkg/rosa/helpers.go @@ -1,9 +1,13 @@ package rosa import ( + "fmt" + cmv1 "github.com/openshift-online/ocm-sdk-go/clustersmgmt/v1" ocmerrors "github.com/openshift-online/ocm-sdk-go/errors" errors "github.com/zgalor/weberr" + + "sigs.k8s.io/cluster-api-provider-aws/v2/pkg/cloud/scope" ) // IsNodePoolReady checkes whether the nodepool is provisoned and all replicas are available. @@ -38,3 +42,34 @@ func handleErr(res *ocmerrors.Error, err error) error { errType := errors.ErrorType(res.Status()) //#nosec G115 return errType.Set(errors.Errorf("%s", msg)) } + +// Reporter is a helper struct used by rosa CLI runtime to report errors. +// We need it to limit number of reports by rosa CLI runtime. +type Reporter struct { + scope scope.RosaRoleConfigScope +} + +// Debugf prints a debug message with the given format and arguments. +func (r *Reporter) Debugf(format string, args ...interface{}) { +} + +// Infof prints an informative message with the given format and arguments. +func (r *Reporter) Infof(format string, args ...interface{}) { +} + +// Warnf prints an warning message with the given format and arguments. +func (r *Reporter) Warnf(format string, args ...interface{}) { +} + +// Errorf prints an error message with the given format and arguments. It also return an error +// containing the same information, which will be usually discarded, except when the caller needs to +// report the error and also return it. +func (r *Reporter) Errorf(format string, args ...interface{}) error { + r.scope.Error(fmt.Errorf(format, args...), format, args...) + return nil +} + +// IsTerminal indicates that the reporter is terminal. +func (r *Reporter) IsTerminal() bool { + return true +} diff --git a/pkg/rosa/ocmclient.go b/pkg/rosa/ocmclient.go index ca7fa81fb1..4bdc335382 100644 --- a/pkg/rosa/ocmclient.go +++ b/pkg/rosa/ocmclient.go @@ -3,6 +3,7 @@ package rosa import ( "context" + "fmt" v1 "github.com/openshift-online/ocm-sdk-go/clustersmgmt/v1" "github.com/openshift/rosa/pkg/aws" @@ -30,10 +31,11 @@ type OCMClient interface { GetCluster(clusterKey string, creator *aws.Creator) (*v1.Cluster, error) GetControlPlaneUpgradePolicies(clusterID string) (controlPlaneUpgradePolicies []*v1.ControlPlaneUpgradePolicy, err error) GetHTPasswdUserList(clusterID string, htpasswdIDPId string) (*v1.HTPasswdUserList, error) + GetHypershiftNodePoolUpgrade(clusterID string, clusterKey string, nodePoolID string) (*v1.NodePool, *v1.NodePoolUpgradePolicy, error) GetIdentityProviders(clusterID string) ([]*v1.IdentityProvider, error) GetMissingGateAgreementsHypershift(clusterID string, upgradePolicy *v1.ControlPlaneUpgradePolicy) ([]*v1.VersionGate, error) GetNodePool(clusterID string, nodePoolID string) (*v1.NodePool, bool, error) - GetHypershiftNodePoolUpgrade(clusterID string, clusterKey string, nodePoolID string) (*v1.NodePool, *v1.NodePoolUpgradePolicy, error) + GetPolicies(policyType string) (map[string]*v1.AWSSTSPolicy, error) GetUser(clusterID string, group string, username string) (*v1.User, error) ScheduleHypershiftControlPlaneUpgrade(clusterID string, upgradePolicy *v1.ControlPlaneUpgradePolicy) (*v1.ControlPlaneUpgradePolicy, error) ScheduleNodePoolUpgrade(clusterID string, nodePoolID string, upgradePolicy *v1.NodePoolUpgradePolicy) (*v1.NodePoolUpgradePolicy, error) @@ -103,6 +105,10 @@ func (c *ocmclient) GetCluster(clusterKey string, creator *aws.Creator) (*v1.Clu return c.ocmClient.GetCluster(clusterKey, creator) } +func (c *ocmclient) GetPolicies(policyType string) (map[string]*v1.AWSSTSPolicy, error) { + return c.ocmClient.GetPolicies(policyType) +} + func (c *ocmclient) GetUser(clusterID string, group string, username string) (*v1.User, error) { return c.ocmClient.GetUser(clusterID, group, username) } @@ -131,3 +137,12 @@ func (c *ocmclient) ValidateHypershiftVersion(versionRawID string, channelGroup func NewMockOCMClient(ctx context.Context, rosaScope *scope.ROSAControlPlaneScope) (OCMClient, error) { return &ocmclient{ocmClient: &ocm.Client{}}, nil } + +// ConvertToRosaOcmClient convert OCMClient to *ocm.Client that is needed by rosa-cli lib. +func ConvertToRosaOcmClient(i OCMClient) (*ocm.Client, error) { + c, ok := i.(*ocmclient) + if !ok { + return nil, fmt.Errorf("failed to conver to Rosa OCM Client") + } + return c.ocmClient, nil +} diff --git a/test/mocks/ocm_client_mock.go b/test/mocks/ocm_client_mock.go index 4e948cf639..f10d7689a5 100644 --- a/test/mocks/ocm_client_mock.go +++ b/test/mocks/ocm_client_mock.go @@ -290,6 +290,21 @@ func (mr *MockOCMClientMockRecorder) GetNodePool(arg0, arg1 interface{}) *gomock return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNodePool", reflect.TypeOf((*MockOCMClient)(nil).GetNodePool), arg0, arg1) } +// GetPolicies mocks base method. +func (m *MockOCMClient) GetPolicies(arg0 string) (map[string]*v1.AWSSTSPolicy, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetPolicies", arg0) + ret0, _ := ret[0].(map[string]*v1.AWSSTSPolicy) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetPolicies indicates an expected call of GetPolicies. +func (mr *MockOCMClientMockRecorder) GetPolicies(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPolicies", reflect.TypeOf((*MockOCMClient)(nil).GetPolicies), arg0) +} + // GetUser mocks base method. func (m *MockOCMClient) GetUser(arg0, arg1, arg2 string) (*v1.User, error) { m.ctrl.T.Helper() From 432b48a704fb139b52f37a0190921e2750365ac1 Mon Sep 17 00:00:00 2001 From: rknaur Date: Wed, 13 Aug 2025 16:03:26 +0200 Subject: [PATCH 2/4] Enable partial reconcile of Rosa Operator Roles --- ...ne.cluster.x-k8s.io_rosacontrolplanes.yaml | 9 -- ...ture.cluster.x-k8s.io_rosaroleconfigs.yaml | 9 -- .../api/v1beta2/rosacontrolplane_types.go | 16 +-- exp/controllers/rosaroleconfig_controller.go | 109 +++++++++--------- pkg/cloud/scope/rosaroleconfig.go | 24 ++-- 5 files changed, 75 insertions(+), 92 deletions(-) diff --git a/config/crd/bases/controlplane.cluster.x-k8s.io_rosacontrolplanes.yaml b/config/crd/bases/controlplane.cluster.x-k8s.io_rosacontrolplanes.yaml index afa7fb5131..eccf71f704 100644 --- a/config/crd/bases/controlplane.cluster.x-k8s.io_rosacontrolplanes.yaml +++ b/config/crd/bases/controlplane.cluster.x-k8s.io_rosacontrolplanes.yaml @@ -759,15 +759,6 @@ spec: [\n\t\t{\n\t\t\t\"Effect\": \"Allow\",\n\t\t\t\"Action\": [\n\t\t\t\t\"ec2:AttachVolume\",\n\t\t\t\t\"ec2:CreateSnapshot\",\n\t\t\t\t\"ec2:CreateTags\",\n\t\t\t\t\"ec2:CreateVolume\",\n\t\t\t\t\"ec2:DeleteSnapshot\",\n\t\t\t\t\"ec2:DeleteTags\",\n\t\t\t\t\"ec2:DeleteVolume\",\n\t\t\t\t\"ec2:DescribeInstances\",\n\t\t\t\t\"ec2:DescribeSnapshots\",\n\t\t\t\t\"ec2:DescribeTags\",\n\t\t\t\t\"ec2:DescribeVolumes\",\n\t\t\t\t\"ec2:DescribeVolumesModifications\",\n\t\t\t\t\"ec2:DetachVolume\",\n\t\t\t\t\"ec2:ModifyVolume\"\n\t\t\t],\n\t\t\t\"Resource\": \"*\"\n\t\t}\n\t]\n}" type: string - required: - - controlPlaneOperatorARN - - imageRegistryARN - - ingressARN - - kmsProviderARN - - kubeCloudControllerARN - - networkARN - - nodePoolManagementARN - - storageARN type: object rosaClusterName: description: |- diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_rosaroleconfigs.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_rosaroleconfigs.yaml index 2eee276154..6a9ef5ecf7 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_rosaroleconfigs.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_rosaroleconfigs.yaml @@ -410,15 +410,6 @@ spec: [\n\t\t{\n\t\t\t\"Effect\": \"Allow\",\n\t\t\t\"Action\": [\n\t\t\t\t\"ec2:AttachVolume\",\n\t\t\t\t\"ec2:CreateSnapshot\",\n\t\t\t\t\"ec2:CreateTags\",\n\t\t\t\t\"ec2:CreateVolume\",\n\t\t\t\t\"ec2:DeleteSnapshot\",\n\t\t\t\t\"ec2:DeleteTags\",\n\t\t\t\t\"ec2:DeleteVolume\",\n\t\t\t\t\"ec2:DescribeInstances\",\n\t\t\t\t\"ec2:DescribeSnapshots\",\n\t\t\t\t\"ec2:DescribeTags\",\n\t\t\t\t\"ec2:DescribeVolumes\",\n\t\t\t\t\"ec2:DescribeVolumesModifications\",\n\t\t\t\t\"ec2:DetachVolume\",\n\t\t\t\t\"ec2:ModifyVolume\"\n\t\t\t],\n\t\t\t\"Resource\": \"*\"\n\t\t}\n\t]\n}" type: string - required: - - controlPlaneOperatorARN - - imageRegistryARN - - ingressARN - - kmsProviderARN - - kubeCloudControllerARN - - networkARN - - nodePoolManagementARN - - storageARN type: object type: object type: object diff --git a/controlplane/rosa/api/v1beta2/rosacontrolplane_types.go b/controlplane/rosa/api/v1beta2/rosacontrolplane_types.go index 0ecb34fbd8..aae8dd6ce7 100644 --- a/controlplane/rosa/api/v1beta2/rosacontrolplane_types.go +++ b/controlplane/rosa/api/v1beta2/rosacontrolplane_types.go @@ -414,7 +414,7 @@ type AWSRolesRef struct { // } // ] // } - IngressARN string `json:"ingressARN"` + IngressARN string `json:"ingressARN,omitempty"` // ImageRegistryARN is an ARN value referencing a role appropriate for the Image Registry Operator. // @@ -449,7 +449,7 @@ type AWSRolesRef struct { // } // ] // } - ImageRegistryARN string `json:"imageRegistryARN"` + ImageRegistryARN string `json:"imageRegistryARN,omitempty"` // StorageARN is an ARN value referencing a role appropriate for the Storage Operator. // @@ -480,7 +480,7 @@ type AWSRolesRef struct { // } // ] // } - StorageARN string `json:"storageARN"` + StorageARN string `json:"storageARN,omitempty"` // NetworkARN is an ARN value referencing a role appropriate for the Network Operator. // @@ -506,7 +506,7 @@ type AWSRolesRef struct { // } // ] // } - NetworkARN string `json:"networkARN"` + NetworkARN string `json:"networkARN,omitempty"` // KubeCloudControllerARN is an ARN value referencing a role appropriate for the KCM/KCC. // Source: https://cloud-provider-aws.sigs.k8s.io/prerequisites/#iam-policies @@ -584,7 +584,7 @@ type AWSRolesRef struct { // ] // } // +immutable - KubeCloudControllerARN string `json:"kubeCloudControllerARN"` + KubeCloudControllerARN string `json:"kubeCloudControllerARN,omitempty"` // NodePoolManagementARN is an ARN value referencing a role appropriate for the CAPI Controller. // @@ -697,7 +697,7 @@ type AWSRolesRef struct { // } // // +immutable - NodePoolManagementARN string `json:"nodePoolManagementARN"` + NodePoolManagementARN string `json:"nodePoolManagementARN,omitempty"` // ControlPlaneOperatorARN is an ARN value referencing a role appropriate for the Control Plane Operator. // @@ -737,8 +737,8 @@ type AWSRolesRef struct { // ] // } // +immutable - ControlPlaneOperatorARN string `json:"controlPlaneOperatorARN"` - KMSProviderARN string `json:"kmsProviderARN"` + ControlPlaneOperatorARN string `json:"controlPlaneOperatorARN,omitempty"` + KMSProviderARN string `json:"kmsProviderARN,omitempty"` } // RosaControlPlaneStatus defines the observed state of ROSAControlPlane. diff --git a/exp/controllers/rosaroleconfig_controller.go b/exp/controllers/rosaroleconfig_controller.go index 778f5b54f7..0f7c4612b7 100644 --- a/exp/controllers/rosaroleconfig_controller.go +++ b/exp/controllers/rosaroleconfig_controller.go @@ -24,7 +24,7 @@ import ( "net/url" "strings" - "github.com/aws/aws-sdk-go/service/iam" + iamv2 "github.com/aws/aws-sdk-go-v2/service/iam" "github.com/go-logr/logr" cmv1 "github.com/openshift-online/ocm-sdk-go/clustersmgmt/v1" accountroles "github.com/openshift/rosa/cmd/create/accountroles" @@ -46,6 +46,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/cluster-api-provider-aws/v2/controlplane/rosa/api/v1beta2" expinfrav1 "sigs.k8s.io/cluster-api-provider-aws/v2/exp/api/v1beta2" "sigs.k8s.io/cluster-api-provider-aws/v2/pkg/cloud" "sigs.k8s.io/cluster-api-provider-aws/v2/pkg/cloud/scope" @@ -150,18 +151,18 @@ func (r *ROSARoleConfigReconciler) Reconcile(ctx context.Context, req ctrl.Reque return ctrl.Result{}, fmt.Errorf("failed to OICD Config: %w", err) } - err = r.createOperatorRoles(ctx, roleConfig, scope, ocmClient) - if err != nil { - conditions.MarkFalse(scope.RosaRoleConfig, expinfrav1.RosaRoleConfigReadyCondition, expinfrav1.RosaRoleConfigReconciliationFailedReason, clusterv1.ConditionSeverityError, "Failed to create Operator Roles: %v", err) - return ctrl.Result{}, fmt.Errorf("failed to Create OperatorRoles: %w", err) - } - err = r.createOIDCProvider(scope, ocmClient) if err != nil { conditions.MarkFalse(scope.RosaRoleConfig, expinfrav1.RosaRoleConfigReadyCondition, expinfrav1.RosaRoleConfigReconciliationFailedReason, clusterv1.ConditionSeverityError, "Failed to create OIDC provider: %v", err) return ctrl.Result{}, fmt.Errorf("failed to Create OIDC provider: %w", err) } + err = r.createOperatorRoles(ctx, roleConfig, scope, ocmClient) + if err != nil { + conditions.MarkFalse(scope.RosaRoleConfig, expinfrav1.RosaRoleConfigReadyCondition, expinfrav1.RosaRoleConfigReconciliationFailedReason, clusterv1.ConditionSeverityError, "Failed to create Operator Roles: %v", err) + return ctrl.Result{}, fmt.Errorf("failed to Create OperatorRoles: %w", err) + } + if r.rosaRolesConfigReady(scope) { conditions.MarkTrue(scope.RosaRoleConfig, expinfrav1.RosaRoleConfigReadyCondition) conditions.Set(scope.RosaRoleConfig, @@ -184,6 +185,12 @@ func (r *ROSARoleConfigReconciler) reconcileDelete(scope *scope.RosaRoleConfigSc return err } + err = r.deleteOperatorRoles(ocmClient, awsClient, scope.RosaRoleConfig.Spec.AccountRoleConfig.Prefix) + if err != nil { + conditions.MarkFalse(scope.RosaRoleConfig, expinfrav1.RosaRoleConfigReadyCondition, expinfrav1.RosaRoleConfigDeletionFailedReason, clusterv1.ConditionSeverityError, "Failed to delete operator roles: %v", err) + return err + } + oidcID := scope.RosaRoleConfig.Status.OIDCID if scope.RosaRoleConfig.Spec.OperatorRoleConfig.OIDCID == "" { err = r.deleteOIDCProvider(ocmClient, awsClient, oidcID) @@ -193,12 +200,6 @@ func (r *ROSARoleConfigReconciler) reconcileDelete(scope *scope.RosaRoleConfigSc } } - err = r.deleteOperatorRoles(ocmClient, awsClient, scope.RosaRoleConfig.Spec.AccountRoleConfig.Prefix) - if err != nil { - conditions.MarkFalse(scope.RosaRoleConfig, expinfrav1.RosaRoleConfigReadyCondition, expinfrav1.RosaRoleConfigDeletionFailedReason, clusterv1.ConditionSeverityError, "Failed to delete operator roles: %v", err) - return err - } - err = r.deleteAccountRoles(ocmClient, awsClient, scope) if err != nil { conditions.MarkFalse(scope.RosaRoleConfig, expinfrav1.RosaRoleConfigReadyCondition, expinfrav1.RosaRoleConfigDeletionFailedReason, clusterv1.ConditionSeverityError, "Failed to delete account roles: %v", err) @@ -267,36 +268,29 @@ func (r *ROSARoleConfigReconciler) createOperatorRoles(ctx context.Context, role return err } - if len(operatorRoles) > 0 { - for _, roles := range operatorRoles { - for _, role := range roles { - if strings.Contains(role.RoleName, fmt.Sprintf("%s-openshift-ingress-operator-cloud-credentials", config.Prefix)) { - scope.RosaRoleConfig.Status.OperatorRolesRef.IngressARN = role.RoleARN - } - if strings.Contains(role.RoleName, fmt.Sprintf("%s-openshift-image-registry-installer-cloud-credentials", config.Prefix)) { - scope.RosaRoleConfig.Status.OperatorRolesRef.ImageRegistryARN = role.RoleARN - } - if strings.Contains(role.RoleName, fmt.Sprintf("%s-openshift-cluster-csi-drivers-ebs-cloud-credentials", config.Prefix)) { - scope.RosaRoleConfig.Status.OperatorRolesRef.StorageARN = role.RoleARN - } - if strings.Contains(role.RoleName, fmt.Sprintf("%s-openshift-cloud-network-config-controller-cloud-credentials", config.Prefix)) { - scope.RosaRoleConfig.Status.OperatorRolesRef.NetworkARN = role.RoleARN - } - if strings.Contains(role.RoleName, fmt.Sprintf("%s-kube-system-kube-controller-manager", config.Prefix)) { - scope.RosaRoleConfig.Status.OperatorRolesRef.KubeCloudControllerARN = role.RoleARN - } - if strings.Contains(role.RoleName, fmt.Sprintf("%s-kube-system-capa-controller-manager", config.Prefix)) { - scope.RosaRoleConfig.Status.OperatorRolesRef.NodePoolManagementARN = role.RoleARN - } - if strings.Contains(role.RoleName, fmt.Sprintf("%s-kube-system-control-plane-operator", config.Prefix)) { - scope.RosaRoleConfig.Status.OperatorRolesRef.ControlPlaneOperatorARN = role.RoleARN - } - if strings.Contains(role.RoleName, fmt.Sprintf("%s-kube-system-kms-provider", config.Prefix)) { - scope.RosaRoleConfig.Status.OperatorRolesRef.KMSProviderARN = role.RoleARN - } + for _, roles := range operatorRoles { + for _, role := range roles { + if role.RoleName == fmt.Sprintf("%s-openshift-ingress-operator-cloud-credentials", config.Prefix) { + scope.RosaRoleConfig.Status.OperatorRolesRef.IngressARN = role.RoleARN + } else if role.RoleName == fmt.Sprintf("%s-openshift-image-registry-installer-cloud-credentials", config.Prefix) { + scope.RosaRoleConfig.Status.OperatorRolesRef.ImageRegistryARN = role.RoleARN + } else if role.RoleName == fmt.Sprintf("%s-openshift-cluster-csi-drivers-ebs-cloud-credentials", config.Prefix) { + scope.RosaRoleConfig.Status.OperatorRolesRef.StorageARN = role.RoleARN + } else if role.RoleName == fmt.Sprintf("%s-openshift-cloud-network-config-controller-cloud-credentials", config.Prefix) { + scope.RosaRoleConfig.Status.OperatorRolesRef.NetworkARN = role.RoleARN + } else if role.RoleName == fmt.Sprintf("%s-kube-system-kube-controller-manager", config.Prefix) { + scope.RosaRoleConfig.Status.OperatorRolesRef.KubeCloudControllerARN = role.RoleARN + } else if role.RoleName == fmt.Sprintf("%s-kube-system-capa-controller-manager", config.Prefix) { + scope.RosaRoleConfig.Status.OperatorRolesRef.NodePoolManagementARN = role.RoleARN + } else if role.RoleName == fmt.Sprintf("%s-kube-system-control-plane-operator", config.Prefix) { + scope.RosaRoleConfig.Status.OperatorRolesRef.ControlPlaneOperatorARN = role.RoleARN + } else if role.RoleName == fmt.Sprintf("%s-kube-system-kms-provider", config.Prefix) { + scope.RosaRoleConfig.Status.OperatorRolesRef.KMSProviderARN = role.RoleARN } } - } else { + } + + if !r.operatorRolesReady(&scope.RosaRoleConfig.Status.OperatorRolesRef) { err = operatorroles.CreateOperatorRoles(runtime, ocm.Production, config.PermissionsBoundaryARN, interactive.ModeAuto, policies, version, isSharedVpc, config.Prefix, hostedCp, installerRoleArn, forcePolicyCreation, oidcConfigID, config.SharedVPCConfig.RouteRoleARN, ocm.DefaultChannelGroup, config.SharedVPCConfig.VPCEndpointRoleARN) return err @@ -315,7 +309,7 @@ func (r *ROSARoleConfigReconciler) reconcileOIDCConfig(roleConfig *expinfrav1.RO } // Try to get OIDC UUID from some operator role policy document. roleName := fmt.Sprintf("%s-openshift-ingress-operator-cloud-credentials", roleConfig.Spec.OperatorRoleConfig.Prefix) - roleDetails, err := scope.IAMClient().GetRole(&iam.GetRoleInput{ + roleDetails, err := scope.IAMClient().GetRole(context.TODO(), &iamv2.GetRoleInput{ RoleName: &roleName, }) if err != nil { @@ -407,15 +401,15 @@ func (r *ROSARoleConfigReconciler) createAccountRoles(ctx context.Context, roleC } for _, role := range accountRoles { - if strings.Contains(role.RoleName, fmt.Sprintf("%s-HCP-ROSA-Installer", config.Prefix)) { + if role.RoleName == fmt.Sprintf("%s-HCP-ROSA-Installer-Role", config.Prefix) { createRoles = false scope.RosaRoleConfig.Status.AccountRolesRef.InstallerRoleARN = role.RoleARN } - if strings.Contains(role.RoleName, fmt.Sprintf("%s-HCP-ROSA-Support", config.Prefix)) { + if role.RoleName == fmt.Sprintf("%s-HCP-ROSA-Support-Role", config.Prefix) { createRoles = false scope.RosaRoleConfig.Status.AccountRolesRef.SupportRoleARN = role.RoleARN } - if strings.Contains(role.RoleName, fmt.Sprintf("%s-HCP-ROSA-Worker", config.Prefix)) { + if role.RoleName == fmt.Sprintf("%s-HCP-ROSA-Worker-Role", config.Prefix) { createRoles = false scope.RosaRoleConfig.Status.AccountRolesRef.WorkerRoleARN = role.RoleARN } @@ -612,21 +606,28 @@ func (r ROSARoleConfigReconciler) rosaRolesConfigReady(scope *scope.RosaRoleConf scope.RosaRoleConfig.Status.AccountRolesRef.InstallerRoleARN == "" || scope.RosaRoleConfig.Status.AccountRolesRef.SupportRoleARN == "" || scope.RosaRoleConfig.Status.AccountRolesRef.WorkerRoleARN == "" || - scope.RosaRoleConfig.Status.OperatorRolesRef.ControlPlaneOperatorARN == "" || - scope.RosaRoleConfig.Status.OperatorRolesRef.ImageRegistryARN == "" || - scope.RosaRoleConfig.Status.OperatorRolesRef.IngressARN == "" || - scope.RosaRoleConfig.Status.OperatorRolesRef.KMSProviderARN == "" || - scope.RosaRoleConfig.Status.OperatorRolesRef.KubeCloudControllerARN == "" || - scope.RosaRoleConfig.Status.OperatorRolesRef.NetworkARN == "" || - scope.RosaRoleConfig.Status.OperatorRolesRef.NodePoolManagementARN == "" || - scope.RosaRoleConfig.Status.OperatorRolesRef.StorageARN == "" { + !r.operatorRolesReady(&scope.RosaRoleConfig.Status.OperatorRolesRef) { + return false + } + return true +} + +func (r ROSARoleConfigReconciler) operatorRolesReady(operatorRolesRef *v1beta2.AWSRolesRef) bool { + if operatorRolesRef.ControlPlaneOperatorARN == "" || + operatorRolesRef.ImageRegistryARN == "" || + operatorRolesRef.IngressARN == "" || + operatorRolesRef.KMSProviderARN == "" || + operatorRolesRef.KubeCloudControllerARN == "" || + operatorRolesRef.NetworkARN == "" || + operatorRolesRef.NodePoolManagementARN == "" || + operatorRolesRef.StorageARN == "" { return false } return true } // GetOIDCIDFromOperatorRole extracts the OIDC UUID from the operator role policy document. -func (r *ROSARoleConfigReconciler) GetOIDCIDFromOperatorRole(scope *scope.RosaRoleConfigScope, roleDetails *iam.GetRoleOutput) (string, error) { +func (r *ROSARoleConfigReconciler) GetOIDCIDFromOperatorRole(scope *scope.RosaRoleConfigScope, roleDetails *iamv2.GetRoleOutput) (string, error) { decodedString, err := url.QueryUnescape(*roleDetails.Role.AssumeRolePolicyDocument) if err != nil { return "", err diff --git a/pkg/cloud/scope/rosaroleconfig.go b/pkg/cloud/scope/rosaroleconfig.go index 14c2470acf..fa142a3e9f 100644 --- a/pkg/cloud/scope/rosaroleconfig.go +++ b/pkg/cloud/scope/rosaroleconfig.go @@ -19,8 +19,8 @@ package scope import ( "context" - awsclient "github.com/aws/aws-sdk-go/aws/client" - "github.com/aws/aws-sdk-go/service/iam" + awsv2 "github.com/aws/aws-sdk-go-v2/aws" + iamv2 "github.com/aws/aws-sdk-go-v2/service/iam" "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -52,8 +52,8 @@ type RosaRoleConfigScope struct { patchHelper *patch.Helper RosaRoleConfig *expinfrav1.ROSARoleConfig serviceLimiters throttle.ServiceLimiters - session awsclient.ConfigProvider - iamClient *iam.IAM + session awsv2.Config + iamClient *iamv2.Client } // NewRosaRoleConfigScope creates a new RosaRoleConfigScope from the supplied parameters. @@ -71,12 +71,12 @@ func NewRosaRoleConfigScope(params RosaRoleConfigScopeParams) (*RosaRoleConfigSc RosaRoleConfig: params.RosaRoleConfig, } - session, serviceLimiters, err := sessionForClusterWithRegion(params.Client, RosaRoleConfigScope, "", params.Endpoints, params.Logger) + sessionv2, serviceLimitersv2, err := sessionForClusterWithRegionV2(params.Client, RosaRoleConfigScope, "", params.Endpoints, params.Logger) if err != nil { - return nil, errors.Errorf("failed to create aws session: %v", err) + return nil, errors.Errorf("failed to create aws V2 session: %v", err) } - iamClient := iam.New(session) + iamClient := iamv2.NewFromConfig(*sessionv2) patchHelper, err := patch.NewHelper(params.RosaRoleConfig, params.Client) if err != nil { @@ -84,8 +84,8 @@ func NewRosaRoleConfigScope(params RosaRoleConfigScopeParams) (*RosaRoleConfigSc } RosaRoleConfigScope.patchHelper = patchHelper - RosaRoleConfigScope.session = session - RosaRoleConfigScope.serviceLimiters = serviceLimiters + RosaRoleConfigScope.session = *sessionv2 + RosaRoleConfigScope.serviceLimiters = serviceLimitersv2 RosaRoleConfigScope.iamClient = iamClient return RosaRoleConfigScope, nil @@ -96,8 +96,8 @@ func (s *RosaRoleConfigScope) IdentityRef() *infrav1.AWSIdentityReference { return s.RosaRoleConfig.Spec.IdentityRef } -// Session returns the AWS SDK session (used for creating clients). -func (s *RosaRoleConfigScope) Session() awsclient.ConfigProvider { +// Session returns the AWS SDK V2 session. Used for creating clients. +func (s *RosaRoleConfigScope) Session() awsv2.Config { return s.session } @@ -165,6 +165,6 @@ func (s *RosaRoleConfigScope) CredentialsSecret() *corev1.Secret { } // IAMClient returns the IAM client. -func (s *RosaRoleConfigScope) IAMClient() *iam.IAM { +func (s *RosaRoleConfigScope) IAMClient() *iamv2.Client { return s.iamClient } From 45b31e042ef15e7d9bfafc491672d47f634e63b5 Mon Sep 17 00:00:00 2001 From: rknaur Date: Mon, 18 Aug 2025 13:01:37 +0200 Subject: [PATCH 3/4] Review fixes --- ...ne.cluster.x-k8s.io_rosacontrolplanes.yaml | 9 ++ ...ture.cluster.x-k8s.io_rosaroleconfigs.yaml | 9 ++ .../api/v1beta2/rosacontrolplane_types.go | 16 +-- .../api/v1beta2/rosacontrolplane_webhook.go | 16 +++ exp/api/v1beta2/rosaroleconfig_types.go | 29 ++++- exp/controllers/rosamachinepool_controller.go | 16 +++ exp/controllers/rosaroleconfig_controller.go | 107 +++++++++--------- pkg/cloud/scope/rosaroleconfig.go | 31 +++-- pkg/rosa/client.go | 16 +++ pkg/rosa/externalauthproviders.go | 16 +++ pkg/rosa/helpers.go | 16 +++ pkg/rosa/idps.go | 16 +++ pkg/rosa/oauth.go | 16 +++ pkg/rosa/ocmclient.go | 16 +++ 14 files changed, 247 insertions(+), 82 deletions(-) diff --git a/config/crd/bases/controlplane.cluster.x-k8s.io_rosacontrolplanes.yaml b/config/crd/bases/controlplane.cluster.x-k8s.io_rosacontrolplanes.yaml index eccf71f704..afa7fb5131 100644 --- a/config/crd/bases/controlplane.cluster.x-k8s.io_rosacontrolplanes.yaml +++ b/config/crd/bases/controlplane.cluster.x-k8s.io_rosacontrolplanes.yaml @@ -759,6 +759,15 @@ spec: [\n\t\t{\n\t\t\t\"Effect\": \"Allow\",\n\t\t\t\"Action\": [\n\t\t\t\t\"ec2:AttachVolume\",\n\t\t\t\t\"ec2:CreateSnapshot\",\n\t\t\t\t\"ec2:CreateTags\",\n\t\t\t\t\"ec2:CreateVolume\",\n\t\t\t\t\"ec2:DeleteSnapshot\",\n\t\t\t\t\"ec2:DeleteTags\",\n\t\t\t\t\"ec2:DeleteVolume\",\n\t\t\t\t\"ec2:DescribeInstances\",\n\t\t\t\t\"ec2:DescribeSnapshots\",\n\t\t\t\t\"ec2:DescribeTags\",\n\t\t\t\t\"ec2:DescribeVolumes\",\n\t\t\t\t\"ec2:DescribeVolumesModifications\",\n\t\t\t\t\"ec2:DetachVolume\",\n\t\t\t\t\"ec2:ModifyVolume\"\n\t\t\t],\n\t\t\t\"Resource\": \"*\"\n\t\t}\n\t]\n}" type: string + required: + - controlPlaneOperatorARN + - imageRegistryARN + - ingressARN + - kmsProviderARN + - kubeCloudControllerARN + - networkARN + - nodePoolManagementARN + - storageARN type: object rosaClusterName: description: |- diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_rosaroleconfigs.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_rosaroleconfigs.yaml index 6a9ef5ecf7..2eee276154 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_rosaroleconfigs.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_rosaroleconfigs.yaml @@ -410,6 +410,15 @@ spec: [\n\t\t{\n\t\t\t\"Effect\": \"Allow\",\n\t\t\t\"Action\": [\n\t\t\t\t\"ec2:AttachVolume\",\n\t\t\t\t\"ec2:CreateSnapshot\",\n\t\t\t\t\"ec2:CreateTags\",\n\t\t\t\t\"ec2:CreateVolume\",\n\t\t\t\t\"ec2:DeleteSnapshot\",\n\t\t\t\t\"ec2:DeleteTags\",\n\t\t\t\t\"ec2:DeleteVolume\",\n\t\t\t\t\"ec2:DescribeInstances\",\n\t\t\t\t\"ec2:DescribeSnapshots\",\n\t\t\t\t\"ec2:DescribeTags\",\n\t\t\t\t\"ec2:DescribeVolumes\",\n\t\t\t\t\"ec2:DescribeVolumesModifications\",\n\t\t\t\t\"ec2:DetachVolume\",\n\t\t\t\t\"ec2:ModifyVolume\"\n\t\t\t],\n\t\t\t\"Resource\": \"*\"\n\t\t}\n\t]\n}" type: string + required: + - controlPlaneOperatorARN + - imageRegistryARN + - ingressARN + - kmsProviderARN + - kubeCloudControllerARN + - networkARN + - nodePoolManagementARN + - storageARN type: object type: object type: object diff --git a/controlplane/rosa/api/v1beta2/rosacontrolplane_types.go b/controlplane/rosa/api/v1beta2/rosacontrolplane_types.go index aae8dd6ce7..0ecb34fbd8 100644 --- a/controlplane/rosa/api/v1beta2/rosacontrolplane_types.go +++ b/controlplane/rosa/api/v1beta2/rosacontrolplane_types.go @@ -414,7 +414,7 @@ type AWSRolesRef struct { // } // ] // } - IngressARN string `json:"ingressARN,omitempty"` + IngressARN string `json:"ingressARN"` // ImageRegistryARN is an ARN value referencing a role appropriate for the Image Registry Operator. // @@ -449,7 +449,7 @@ type AWSRolesRef struct { // } // ] // } - ImageRegistryARN string `json:"imageRegistryARN,omitempty"` + ImageRegistryARN string `json:"imageRegistryARN"` // StorageARN is an ARN value referencing a role appropriate for the Storage Operator. // @@ -480,7 +480,7 @@ type AWSRolesRef struct { // } // ] // } - StorageARN string `json:"storageARN,omitempty"` + StorageARN string `json:"storageARN"` // NetworkARN is an ARN value referencing a role appropriate for the Network Operator. // @@ -506,7 +506,7 @@ type AWSRolesRef struct { // } // ] // } - NetworkARN string `json:"networkARN,omitempty"` + NetworkARN string `json:"networkARN"` // KubeCloudControllerARN is an ARN value referencing a role appropriate for the KCM/KCC. // Source: https://cloud-provider-aws.sigs.k8s.io/prerequisites/#iam-policies @@ -584,7 +584,7 @@ type AWSRolesRef struct { // ] // } // +immutable - KubeCloudControllerARN string `json:"kubeCloudControllerARN,omitempty"` + KubeCloudControllerARN string `json:"kubeCloudControllerARN"` // NodePoolManagementARN is an ARN value referencing a role appropriate for the CAPI Controller. // @@ -697,7 +697,7 @@ type AWSRolesRef struct { // } // // +immutable - NodePoolManagementARN string `json:"nodePoolManagementARN,omitempty"` + NodePoolManagementARN string `json:"nodePoolManagementARN"` // ControlPlaneOperatorARN is an ARN value referencing a role appropriate for the Control Plane Operator. // @@ -737,8 +737,8 @@ type AWSRolesRef struct { // ] // } // +immutable - ControlPlaneOperatorARN string `json:"controlPlaneOperatorARN,omitempty"` - KMSProviderARN string `json:"kmsProviderARN,omitempty"` + ControlPlaneOperatorARN string `json:"controlPlaneOperatorARN"` + KMSProviderARN string `json:"kmsProviderARN"` } // RosaControlPlaneStatus defines the observed state of ROSAControlPlane. diff --git a/controlplane/rosa/api/v1beta2/rosacontrolplane_webhook.go b/controlplane/rosa/api/v1beta2/rosacontrolplane_webhook.go index 0103f020e2..a22220a0e7 100644 --- a/controlplane/rosa/api/v1beta2/rosacontrolplane_webhook.go +++ b/controlplane/rosa/api/v1beta2/rosacontrolplane_webhook.go @@ -1,3 +1,19 @@ +/* +Copyright 2023 The Kubernetes Authors. + +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 v1beta2 import ( diff --git a/exp/api/v1beta2/rosaroleconfig_types.go b/exp/api/v1beta2/rosaroleconfig_types.go index 2dd118394d..c9e7894d53 100644 --- a/exp/api/v1beta2/rosaroleconfig_types.go +++ b/exp/api/v1beta2/rosaroleconfig_types.go @@ -1,5 +1,5 @@ /* -Copyright The Kubernetes Authors. +Copyright 2025 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -25,9 +25,6 @@ import ( clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" ) -// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! -// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. - // ROSARoleConfigSpec defines the desired state of ROSARoleConfig type ROSARoleConfigSpec struct { AccountRoleConfig AccountRoleConfig `json:"accountRoleConfig"` @@ -158,6 +155,25 @@ const ( RosaRoleConfigCreatedReason = "Created" ) +const ( + // IngressOperatorARNSuffix is the suffix for the ingress operator role. + IngressOperatorARNSuffix = "-openshift-ingress-operator-cloud-credentials" + // ImageRegistryARNSuffix is the suffix for the image registry operator role. + ImageRegistryARNSuffix = "-openshift-image-registry-installer-cloud-credentials" + // StorageARNSuffix is the suffix for the storage operator role. + StorageARNSuffix = "-openshift-cluster-csi-drivers-ebs-cloud-credentials" + // NetworkARNSuffix is the suffix for the network operator role. + NetworkARNSuffix = "-openshift-cloud-network-config-controller-cloud-credentials" + // KubeCloudControllerARNSuffix is the suffix for the kube cloud controller role. + KubeCloudControllerARNSuffix = "-kube-system-kube-controller-manager" + // NodePoolManagementARNSuffix is the suffix for the node pool management role. + NodePoolManagementARNSuffix = "-kube-system-capa-controller-manager" + // ControlPlaneOperatorARNSuffix is the suffix for the control plane operator role. + ControlPlaneOperatorARNSuffix = "-kube-system-control-plane-operator" + // KMSProviderARNSuffix is the suffix for the kms provider role. + KMSProviderARNSuffix = "-kube-system-kms-provider" +) + // SetConditions sets the conditions of the ROSARoleConfig. func (r *ROSARoleConfig) SetConditions(conditions clusterv1.Conditions) { r.Status.Conditions = conditions @@ -168,6 +184,11 @@ func (r *ROSARoleConfig) GetConditions() clusterv1.Conditions { return r.Status.Conditions } +// IsSharedVPC checks if the shared VPC config is set. +func (s SharedVPCConfig) IsSharedVPC() bool { + return s.VPCEndpointRoleARN != "" && s.RouteRoleARN != "" +} + func init() { SchemeBuilder.Register(&ROSARoleConfig{}, &ROSARoleConfigList{}) } diff --git a/exp/controllers/rosamachinepool_controller.go b/exp/controllers/rosamachinepool_controller.go index 259a12fc99..1b358def45 100644 --- a/exp/controllers/rosamachinepool_controller.go +++ b/exp/controllers/rosamachinepool_controller.go @@ -1,3 +1,19 @@ +/* +Copyright 2023 The Kubernetes Authors. + +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 controllers import ( diff --git a/exp/controllers/rosaroleconfig_controller.go b/exp/controllers/rosaroleconfig_controller.go index 0f7c4612b7..0487695575 100644 --- a/exp/controllers/rosaroleconfig_controller.go +++ b/exp/controllers/rosaroleconfig_controller.go @@ -1,5 +1,5 @@ /* -Copyright The Kubernetes Authors. +Copyright 2025 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -63,7 +63,6 @@ type ROSARoleConfigReconciler struct { client.Client Log logr.Logger Scheme *runtime.Scheme - Endpoints []scope.ServiceEndpoint WatchFilterValue string NewStsClient func(cloud.ScopeUsage, cloud.Session, logger.Wrapper, runtime.Object) stsiface.STSClient NewOCMClient func(ctx context.Context, scope rosa.OCMSecretsRetriever) (rosa.OCMClient, error) @@ -93,6 +92,7 @@ func (r *ROSARoleConfigReconciler) Reconcile(ctx context.Context, req ctrl.Reque if apierrors.IsNotFound(err) { return ctrl.Result{}, nil } + log.Error(err, "Failed to get ROSARoleConfig") return ctrl.Result{Requeue: true}, nil } @@ -101,7 +101,6 @@ func (r *ROSARoleConfigReconciler) Reconcile(ctx context.Context, req ctrl.Reque Client: r.Client, RosaRoleConfig: roleConfig, ControllerName: "rosaroleconfig", - Endpoints: r.Endpoints, Logger: log, }) @@ -113,7 +112,7 @@ func (r *ROSARoleConfigReconciler) Reconcile(ctx context.Context, req ctrl.Reque defer func() { conditions.SetSummary(scope.RosaRoleConfig, conditions.WithConditions(expinfrav1.RosaRoleConfigReadyCondition), conditions.WithStepCounter()) - if err := scope.Close(); err != nil { + if err := scope.PatchObject(); err != nil { reterr = errors.Join(reterr, err) } }() @@ -134,9 +133,7 @@ func (r *ROSARoleConfigReconciler) Reconcile(ctx context.Context, req ctrl.Reque } if controllerutil.AddFinalizer(scope.RosaRoleConfig, expinfrav1.RosaRoleConfigFinalizer) { - if err := scope.PatchObject(); err != nil { - return ctrl.Result{}, err - } + return ctrl.Result{}, err } err = r.createAccountRoles(ctx, roleConfig, scope, ocmClient) @@ -231,7 +228,7 @@ func (r *ROSARoleConfigReconciler) createOperatorRoles(ctx context.Context, role } oidcConfigID := scope.RosaRoleConfig.Status.OIDCID if oidcConfigID == "" { - return fmt.Errorf("OIDCID is empty") + return fmt.Errorf("OIDCID is required to create operator roles") } runtime := rosacli.NewRuntime() @@ -260,7 +257,6 @@ func (r *ROSARoleConfigReconciler) createOperatorRoles(ctx context.Context, role version := roleConfig.Spec.AccountRoleConfig.Version hostedCp := true forcePolicyCreation := true - isSharedVpc := config.SharedVPCConfig.VPCEndpointRoleARN != "" && config.SharedVPCConfig.RouteRoleARN != "" operatorRoles, err := runtime.AWSClient.ListOperatorRoles(version, "", config.Prefix) @@ -270,28 +266,35 @@ func (r *ROSARoleConfigReconciler) createOperatorRoles(ctx context.Context, role for _, roles := range operatorRoles { for _, role := range roles { - if role.RoleName == fmt.Sprintf("%s-openshift-ingress-operator-cloud-credentials", config.Prefix) { + roleSuffix := strings.TrimPrefix(role.RoleName, config.Prefix) + if roleSuffix == role.RoleName { + continue + } + switch roleSuffix { + case expinfrav1.IngressOperatorARNSuffix: scope.RosaRoleConfig.Status.OperatorRolesRef.IngressARN = role.RoleARN - } else if role.RoleName == fmt.Sprintf("%s-openshift-image-registry-installer-cloud-credentials", config.Prefix) { + case expinfrav1.ImageRegistryARNSuffix: scope.RosaRoleConfig.Status.OperatorRolesRef.ImageRegistryARN = role.RoleARN - } else if role.RoleName == fmt.Sprintf("%s-openshift-cluster-csi-drivers-ebs-cloud-credentials", config.Prefix) { + case expinfrav1.StorageARNSuffix: scope.RosaRoleConfig.Status.OperatorRolesRef.StorageARN = role.RoleARN - } else if role.RoleName == fmt.Sprintf("%s-openshift-cloud-network-config-controller-cloud-credentials", config.Prefix) { + case expinfrav1.NetworkARNSuffix: scope.RosaRoleConfig.Status.OperatorRolesRef.NetworkARN = role.RoleARN - } else if role.RoleName == fmt.Sprintf("%s-kube-system-kube-controller-manager", config.Prefix) { + case expinfrav1.KubeCloudControllerARNSuffix: scope.RosaRoleConfig.Status.OperatorRolesRef.KubeCloudControllerARN = role.RoleARN - } else if role.RoleName == fmt.Sprintf("%s-kube-system-capa-controller-manager", config.Prefix) { + case expinfrav1.NodePoolManagementARNSuffix: scope.RosaRoleConfig.Status.OperatorRolesRef.NodePoolManagementARN = role.RoleARN - } else if role.RoleName == fmt.Sprintf("%s-kube-system-control-plane-operator", config.Prefix) { + case expinfrav1.ControlPlaneOperatorARNSuffix: scope.RosaRoleConfig.Status.OperatorRolesRef.ControlPlaneOperatorARN = role.RoleARN - } else if role.RoleName == fmt.Sprintf("%s-kube-system-kms-provider", config.Prefix) { + case expinfrav1.KMSProviderARNSuffix: scope.RosaRoleConfig.Status.OperatorRolesRef.KMSProviderARN = role.RoleARN } } } if !r.operatorRolesReady(&scope.RosaRoleConfig.Status.OperatorRolesRef) { - err = operatorroles.CreateOperatorRoles(runtime, ocm.Production, config.PermissionsBoundaryARN, interactive.ModeAuto, policies, version, isSharedVpc, config.Prefix, hostedCp, installerRoleArn, forcePolicyCreation, + // not all operator roles are set, operator roles are not ready yet. + r.clearOperatorRolesRef(&scope.RosaRoleConfig.Status.OperatorRolesRef) + err = operatorroles.CreateOperatorRoles(runtime, ocm.Production, config.PermissionsBoundaryARN, interactive.ModeAuto, policies, version, config.SharedVPCConfig.IsSharedVPC(), config.Prefix, hostedCp, installerRoleArn, forcePolicyCreation, oidcConfigID, config.SharedVPCConfig.RouteRoleARN, ocm.DefaultChannelGroup, config.SharedVPCConfig.VPCEndpointRoleARN) return err } @@ -300,34 +303,26 @@ func (r *ROSARoleConfigReconciler) createOperatorRoles(ctx context.Context, role } func (r *ROSARoleConfigReconciler) reconcileOIDCConfig(roleConfig *expinfrav1.ROSARoleConfig, scope *scope.RosaRoleConfigScope, ocmClient *ocm.Client) error { + oidcID := "" if scope.RosaRoleConfig.Status.OIDCID != "" { - return nil + oidcID = scope.RosaRoleConfig.Status.OIDCID + } else if roleConfig.Spec.OperatorRoleConfig.OIDCID != "" { + oidcID = roleConfig.Spec.OperatorRoleConfig.OIDCID } - if roleConfig.Spec.OperatorRoleConfig.OIDCID != "" { - scope.RosaRoleConfig.Status.OIDCID = roleConfig.Spec.OperatorRoleConfig.OIDCID + + if oidcID != "" { + oidcConfig, err := ocmClient.GetOidcConfig(oidcID) + if err != nil || oidcConfig == nil { + return fmt.Errorf("failed to get OIDC config: %w", err) + } + scope.RosaRoleConfig.Status.OIDCID = oidcID return nil } - // Try to get OIDC UUID from some operator role policy document. - roleName := fmt.Sprintf("%s-openshift-ingress-operator-cloud-credentials", roleConfig.Spec.OperatorRoleConfig.Prefix) - roleDetails, err := scope.IAMClient().GetRole(context.TODO(), &iamv2.GetRoleInput{ - RoleName: &roleName, - }) - if err != nil { - return r.createOIDCConfig(scope, ocmClient) - } - oidcID, err := r.GetOIDCIDFromOperatorRole(scope, roleDetails) - if err != nil { - return r.createOIDCConfig(scope, ocmClient) - } - scope.RosaRoleConfig.Status.OIDCID = oidcID - return nil + + return r.createOIDCConfig(scope, ocmClient) } func (r *ROSARoleConfigReconciler) createOIDCProvider(scope *scope.RosaRoleConfigScope, ocmClient *ocm.Client) error { - if scope.RosaRoleConfig.Status.OIDCProviderARN != "" { - return nil - } - var err error oidcID := scope.RosaRoleConfig.Status.OIDCID if oidcID == "" { @@ -423,8 +418,7 @@ func (r *ROSARoleConfigReconciler) createAccountRoles(ctx context.Context, roleC } managedPolicies := true - isSharedVpc := config.SharedVPCConfig.VPCEndpointRoleARN != "" && config.SharedVPCConfig.RouteRoleARN != "" - err := accountroles.CreateHCPRoles(runtime, config.Prefix, managedPolicies, config.PermissionsBoundaryARN, ocm.Production, policies, config.Version, config.Path, isSharedVpc, config.SharedVPCConfig.RouteRoleARN, config.SharedVPCConfig.VPCEndpointRoleARN) + err := accountroles.CreateHCPRoles(runtime, config.Prefix, managedPolicies, config.PermissionsBoundaryARN, ocm.Production, policies, config.Version, config.Path, config.SharedVPCConfig.IsSharedVPC(), config.SharedVPCConfig.RouteRoleARN, config.SharedVPCConfig.VPCEndpointRoleARN) return err } @@ -471,23 +465,16 @@ func (r *ROSARoleConfigReconciler) deleteAccountRoles(ocmClient *ocm.Client, aws return err } - var err2, err3 error if canDeleteRole(clusters, roles.InstallerRoleARN) { - err = awsClient.DeleteAccountRole(strings.Split(roles.InstallerRoleARN, "/")[1], config.Prefix, true, deleteHcpSharedVpcPolicies) + err = errors.Join(err, awsClient.DeleteAccountRole(strings.Split(roles.InstallerRoleARN, "/")[1], config.Prefix, true, deleteHcpSharedVpcPolicies)) } if canDeleteRole(clusters, roles.WorkerRoleARN) { - err2 = awsClient.DeleteAccountRole(strings.Split(roles.WorkerRoleARN, "/")[1], config.Prefix, true, deleteHcpSharedVpcPolicies) + err = errors.Join(err, awsClient.DeleteAccountRole(strings.Split(roles.WorkerRoleARN, "/")[1], config.Prefix, true, deleteHcpSharedVpcPolicies)) } if canDeleteRole(clusters, roles.SupportRoleARN) { - err3 = awsClient.DeleteAccountRole(strings.Split(roles.SupportRoleARN, "/")[1], config.Prefix, true, deleteHcpSharedVpcPolicies) + err = errors.Join(err, awsClient.DeleteAccountRole(strings.Split(roles.SupportRoleARN, "/")[1], config.Prefix, true, deleteHcpSharedVpcPolicies)) } - if err != nil { - return err - } - if err2 != nil { - return err2 - } - return err3 + return err } func (r *ROSARoleConfigReconciler) deleteOIDCProvider(ocmClient *ocm.Client, awsClient aws.Client, oidcConfigID string) error { @@ -661,3 +648,19 @@ func (r *ROSARoleConfigReconciler) GetOIDCIDFromOperatorRole(scope *scope.RosaRo return "", fmt.Errorf("cant extract oidc uuid from the %s policy document", *roleDetails.Role.RoleName) } + +// clearOperatorRolesRef clears all field values in the OperatorRolesRef by setting them to empty strings. +func (r ROSARoleConfigReconciler) clearOperatorRolesRef(operatorRolesRef *v1beta2.AWSRolesRef) { + if operatorRolesRef == nil { + return + } + + operatorRolesRef.IngressARN = "" + operatorRolesRef.ImageRegistryARN = "" + operatorRolesRef.StorageARN = "" + operatorRolesRef.NetworkARN = "" + operatorRolesRef.KubeCloudControllerARN = "" + operatorRolesRef.NodePoolManagementARN = "" + operatorRolesRef.ControlPlaneOperatorARN = "" + operatorRolesRef.KMSProviderARN = "" +} diff --git a/pkg/cloud/scope/rosaroleconfig.go b/pkg/cloud/scope/rosaroleconfig.go index fa142a3e9f..bc9edbbb2b 100644 --- a/pkg/cloud/scope/rosaroleconfig.go +++ b/pkg/cloud/scope/rosaroleconfig.go @@ -1,5 +1,5 @@ /* - Copyright The Kubernetes Authors. + Copyright 2025 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -19,8 +19,8 @@ package scope import ( "context" - awsv2 "github.com/aws/aws-sdk-go-v2/aws" - iamv2 "github.com/aws/aws-sdk-go-v2/service/iam" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/iam" "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -39,7 +39,6 @@ import ( type RosaRoleConfigScopeParams struct { Client client.Client ControllerName string - Endpoints []ServiceEndpoint Logger *logger.Logger RosaRoleConfig *expinfrav1.ROSARoleConfig } @@ -52,8 +51,8 @@ type RosaRoleConfigScope struct { patchHelper *patch.Helper RosaRoleConfig *expinfrav1.ROSARoleConfig serviceLimiters throttle.ServiceLimiters - session awsv2.Config - iamClient *iamv2.Client + session aws.Config + iamClient *iam.Client } // NewRosaRoleConfigScope creates a new RosaRoleConfigScope from the supplied parameters. @@ -71,12 +70,13 @@ func NewRosaRoleConfigScope(params RosaRoleConfigScopeParams) (*RosaRoleConfigSc RosaRoleConfig: params.RosaRoleConfig, } - sessionv2, serviceLimitersv2, err := sessionForClusterWithRegionV2(params.Client, RosaRoleConfigScope, "", params.Endpoints, params.Logger) + session, serviceLimiters, err := sessionForClusterWithRegion(params.Client, RosaRoleConfigScope, "", params.Logger) + if err != nil { return nil, errors.Errorf("failed to create aws V2 session: %v", err) } - iamClient := iamv2.NewFromConfig(*sessionv2) + iamClient := iam.NewFromConfig(*session) patchHelper, err := patch.NewHelper(params.RosaRoleConfig, params.Client) if err != nil { @@ -84,8 +84,8 @@ func NewRosaRoleConfigScope(params RosaRoleConfigScopeParams) (*RosaRoleConfigSc } RosaRoleConfigScope.patchHelper = patchHelper - RosaRoleConfigScope.session = *sessionv2 - RosaRoleConfigScope.serviceLimiters = serviceLimitersv2 + RosaRoleConfigScope.session = *session + RosaRoleConfigScope.serviceLimiters = serviceLimiters RosaRoleConfigScope.iamClient = iamClient return RosaRoleConfigScope, nil @@ -97,7 +97,7 @@ func (s *RosaRoleConfigScope) IdentityRef() *infrav1.AWSIdentityReference { } // Session returns the AWS SDK V2 session. Used for creating clients. -func (s *RosaRoleConfigScope) Session() awsv2.Config { +func (s *RosaRoleConfigScope) Session() aws.Config { return s.session } @@ -140,15 +140,10 @@ func (s *RosaRoleConfigScope) GetClient() client.Client { // PatchObject persists the RosaRoleConfig configuration and status. func (s *RosaRoleConfigScope) PatchObject() error { return s.patchHelper.Patch( - context.TODO(), + context.Background(), s.RosaRoleConfig) } -// Close closes the current scope persisting the RosaRoleConfig configuration and status. -func (s *RosaRoleConfigScope) Close() error { - return s.PatchObject() -} - // CredentialsSecret returns the CredentialsSecret object. func (s *RosaRoleConfigScope) CredentialsSecret() *corev1.Secret { secretRef := s.RosaRoleConfig.Spec.CredentialsSecretRef @@ -165,6 +160,6 @@ func (s *RosaRoleConfigScope) CredentialsSecret() *corev1.Secret { } // IAMClient returns the IAM client. -func (s *RosaRoleConfigScope) IAMClient() *iamv2.Client { +func (s *RosaRoleConfigScope) IAMClient() *iam.Client { return s.iamClient } diff --git a/pkg/rosa/client.go b/pkg/rosa/client.go index e86f5a99bd..649969d526 100644 --- a/pkg/rosa/client.go +++ b/pkg/rosa/client.go @@ -1,3 +1,19 @@ +/* +Copyright 2025 The Kubernetes Authors. + +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 rosa provides a way to interact with the Red Hat OpenShift Service on AWS (ROSA) API. package rosa diff --git a/pkg/rosa/externalauthproviders.go b/pkg/rosa/externalauthproviders.go index 04573ff392..3ac86d5ee2 100644 --- a/pkg/rosa/externalauthproviders.go +++ b/pkg/rosa/externalauthproviders.go @@ -1,3 +1,19 @@ +/* +Copyright 2025 The Kubernetes Authors. + +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 rosa import ( diff --git a/pkg/rosa/helpers.go b/pkg/rosa/helpers.go index 8cf505d7d4..1f1c132663 100644 --- a/pkg/rosa/helpers.go +++ b/pkg/rosa/helpers.go @@ -1,3 +1,19 @@ +/* +Copyright 2025 The Kubernetes Authors. + +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 rosa import ( diff --git a/pkg/rosa/idps.go b/pkg/rosa/idps.go index 0d80bd7d56..1f9ec8cf09 100644 --- a/pkg/rosa/idps.go +++ b/pkg/rosa/idps.go @@ -1,3 +1,19 @@ +/* +Copyright 2025 The Kubernetes Authors. + +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 rosa import ( diff --git a/pkg/rosa/oauth.go b/pkg/rosa/oauth.go index 299dfb01d3..09646f867e 100644 --- a/pkg/rosa/oauth.go +++ b/pkg/rosa/oauth.go @@ -1,3 +1,19 @@ +/* +Copyright 2025 The Kubernetes Authors. + +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 rosa import ( diff --git a/pkg/rosa/ocmclient.go b/pkg/rosa/ocmclient.go index 4bdc335382..84bb1c8893 100644 --- a/pkg/rosa/ocmclient.go +++ b/pkg/rosa/ocmclient.go @@ -1,3 +1,19 @@ +/* +Copyright 2025 The Kubernetes Authors. + +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 rosa provides a way to interact with the Red Hat OpenShift Service on AWS (ROSA) API. package rosa From daeac96e7b7712a4924bca910e64760d32aebe0a Mon Sep 17 00:00:00 2001 From: rknaur Date: Tue, 26 Aug 2025 16:12:21 +0200 Subject: [PATCH 4/4] Add integration tests --- exp/controllers/rosaroleconfig_controller.go | 226 +++++------- .../rosaroleconfig_controller_test.go | 328 +++++++++++++++++- go.mod | 13 +- go.sum | 110 ++++++ pkg/rosa/ocmclient.go | 6 +- 5 files changed, 532 insertions(+), 151 deletions(-) diff --git a/exp/controllers/rosaroleconfig_controller.go b/exp/controllers/rosaroleconfig_controller.go index 0487695575..d5e112dc53 100644 --- a/exp/controllers/rosaroleconfig_controller.go +++ b/exp/controllers/rosaroleconfig_controller.go @@ -66,6 +66,7 @@ type ROSARoleConfigReconciler struct { WatchFilterValue string NewStsClient func(cloud.ScopeUsage, cloud.Session, logger.Wrapper, runtime.Object) stsiface.STSClient NewOCMClient func(ctx context.Context, scope rosa.OCMSecretsRetriever) (rosa.OCMClient, error) + Runtime *rosacli.Runtime } func (r *ROSARoleConfigReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, options controller.Options) error { @@ -117,44 +118,39 @@ func (r *ROSARoleConfigReconciler) Reconcile(ctx context.Context, req ctrl.Reque } }() - ocm, err := r.NewOCMClient(ctx, scope) + err = r.setUpRuntime(ctx, scope) if err != nil { - return ctrl.Result{}, fmt.Errorf("failed to create OCM client: %w", err) - } - - ocmClient, err := rosa.ConvertToRosaOcmClient(ocm) - if err != nil || ocmClient == nil { - return ctrl.Result{}, fmt.Errorf("failed to create OCM client: %w", err) + return ctrl.Result{}, fmt.Errorf("failed to set up runtime: %w", err) } if !roleConfig.ObjectMeta.DeletionTimestamp.IsZero() { conditions.MarkFalse(scope.RosaRoleConfig, expinfrav1.RosaRoleConfigReadyCondition, expinfrav1.RosaRoleConfigDeletionStarted, clusterv1.ConditionSeverityInfo, "Deletion of RosaRolesConfig started") - return ctrl.Result{}, r.reconcileDelete(scope, ocmClient) + return ctrl.Result{}, r.reconcileDelete(scope) } if controllerutil.AddFinalizer(scope.RosaRoleConfig, expinfrav1.RosaRoleConfigFinalizer) { return ctrl.Result{}, err } - err = r.createAccountRoles(ctx, roleConfig, scope, ocmClient) + err = r.createAccountRoles(roleConfig, scope) if err != nil { conditions.MarkFalse(scope.RosaRoleConfig, expinfrav1.RosaRoleConfigReadyCondition, expinfrav1.RosaRoleConfigReconciliationFailedReason, clusterv1.ConditionSeverityError, "Failed to create Account Roles: %v", err) return ctrl.Result{}, fmt.Errorf("failed to Create AccountRoles: %w", err) } - err = r.reconcileOIDCConfig(roleConfig, scope, ocmClient) + err = r.reconcileOIDCConfig(roleConfig, scope) if err != nil { conditions.MarkFalse(scope.RosaRoleConfig, expinfrav1.RosaRoleConfigReadyCondition, expinfrav1.RosaRoleConfigReconciliationFailedReason, clusterv1.ConditionSeverityError, "Failed to create OIDC Config: %v", err) return ctrl.Result{}, fmt.Errorf("failed to OICD Config: %w", err) } - err = r.createOIDCProvider(scope, ocmClient) + err = r.createOIDCProvider(scope) if err != nil { conditions.MarkFalse(scope.RosaRoleConfig, expinfrav1.RosaRoleConfigReadyCondition, expinfrav1.RosaRoleConfigReconciliationFailedReason, clusterv1.ConditionSeverityError, "Failed to create OIDC provider: %v", err) return ctrl.Result{}, fmt.Errorf("failed to Create OIDC provider: %w", err) } - err = r.createOperatorRoles(ctx, roleConfig, scope, ocmClient) + err = r.createOperatorRoles(roleConfig, scope) if err != nil { conditions.MarkFalse(scope.RosaRoleConfig, expinfrav1.RosaRoleConfigReadyCondition, expinfrav1.RosaRoleConfigReconciliationFailedReason, clusterv1.ConditionSeverityError, "Failed to create Operator Roles: %v", err) return ctrl.Result{}, fmt.Errorf("failed to Create OperatorRoles: %w", err) @@ -174,15 +170,8 @@ func (r *ROSARoleConfigReconciler) Reconcile(ctx context.Context, req ctrl.Reque return ctrl.Result{}, nil } -func (r *ROSARoleConfigReconciler) reconcileDelete(scope *scope.RosaRoleConfigScope, ocmClient *ocm.Client) error { - log := rosalogging.NewLogger() - awsClient, err := aws.NewClient().Logger(log).Build() - if err != nil { - conditions.MarkFalse(scope.RosaRoleConfig, expinfrav1.RosaRoleConfigReadyCondition, expinfrav1.RosaRoleConfigDeletionFailedReason, clusterv1.ConditionSeverityError, "Failed to create AWS client: %v", err) - return err - } - - err = r.deleteOperatorRoles(ocmClient, awsClient, scope.RosaRoleConfig.Spec.AccountRoleConfig.Prefix) +func (r *ROSARoleConfigReconciler) reconcileDelete(scope *scope.RosaRoleConfigScope) error { + err := r.deleteOperatorRoles(scope.RosaRoleConfig.Spec.AccountRoleConfig.Prefix) if err != nil { conditions.MarkFalse(scope.RosaRoleConfig, expinfrav1.RosaRoleConfigReadyCondition, expinfrav1.RosaRoleConfigDeletionFailedReason, clusterv1.ConditionSeverityError, "Failed to delete operator roles: %v", err) return err @@ -190,21 +179,21 @@ func (r *ROSARoleConfigReconciler) reconcileDelete(scope *scope.RosaRoleConfigSc oidcID := scope.RosaRoleConfig.Status.OIDCID if scope.RosaRoleConfig.Spec.OperatorRoleConfig.OIDCID == "" { - err = r.deleteOIDCProvider(ocmClient, awsClient, oidcID) + err = r.deleteOIDCProvider(oidcID) if err != nil { conditions.MarkFalse(scope.RosaRoleConfig, expinfrav1.RosaRoleConfigReadyCondition, expinfrav1.RosaRoleConfigDeletionFailedReason, clusterv1.ConditionSeverityError, "Failed to delete OIDC provider: %v", err) return err } } - err = r.deleteAccountRoles(ocmClient, awsClient, scope) + err = r.deleteAccountRoles(scope) if err != nil { conditions.MarkFalse(scope.RosaRoleConfig, expinfrav1.RosaRoleConfigReadyCondition, expinfrav1.RosaRoleConfigDeletionFailedReason, clusterv1.ConditionSeverityError, "Failed to delete account roles: %v", err) return err } if scope.RosaRoleConfig.Spec.OperatorRoleConfig.OIDCID == "" { - err = r.deleteOIDCConfig(ocmClient, oidcID) + err = r.deleteOIDCConfig(oidcID) if err != nil { conditions.MarkFalse(scope.RosaRoleConfig, expinfrav1.RosaRoleConfigReadyCondition, expinfrav1.RosaRoleConfigDeletionFailedReason, clusterv1.ConditionSeverityError, "Failed to delete OIDC config: %v", err) return err @@ -221,7 +210,7 @@ func (r *ROSARoleConfigReconciler) reconcileDelete(scope *scope.RosaRoleConfigSc return nil } -func (r *ROSARoleConfigReconciler) createOperatorRoles(ctx context.Context, roleConfig *expinfrav1.ROSARoleConfig, scope *scope.RosaRoleConfigScope, ocmClient *ocm.Client) error { +func (r *ROSARoleConfigReconciler) createOperatorRoles(roleConfig *expinfrav1.ROSARoleConfig, scope *scope.RosaRoleConfigScope) error { installerRoleArn := scope.RosaRoleConfig.Status.AccountRolesRef.InstallerRoleARN if installerRoleArn == "" { return fmt.Errorf("installer role is empty") @@ -231,24 +220,7 @@ func (r *ROSARoleConfigReconciler) createOperatorRoles(ctx context.Context, role return fmt.Errorf("OIDCID is required to create operator roles") } - runtime := rosacli.NewRuntime() - policies, err := ocmClient.GetPolicies("OperatorRole") - if err != nil { - return err - } - runtime.OCMClient, err = rosa.NewOCMClient(ctx, scope) - if err != nil { - return err - } - - runtime.Reporter = (&rosa.Reporter{}) - runtime.Logger = rosalogging.NewLogger() - runtime.AWSClient, err = aws.NewClient().Logger(runtime.Logger).Build() - if err != nil { - return fmt.Errorf("failed to create aws client: %w", err) - } - - runtime.Creator, err = runtime.AWSClient.GetCreator() + policies, err := r.Runtime.OCMClient.GetPolicies("OperatorRole") if err != nil { return err } @@ -258,8 +230,7 @@ func (r *ROSARoleConfigReconciler) createOperatorRoles(ctx context.Context, role hostedCp := true forcePolicyCreation := true - operatorRoles, err := runtime.AWSClient.ListOperatorRoles(version, "", config.Prefix) - + operatorRoles, err := r.Runtime.AWSClient.ListOperatorRoles(version, "", config.Prefix) if err != nil { return err } @@ -294,7 +265,7 @@ func (r *ROSARoleConfigReconciler) createOperatorRoles(ctx context.Context, role if !r.operatorRolesReady(&scope.RosaRoleConfig.Status.OperatorRolesRef) { // not all operator roles are set, operator roles are not ready yet. r.clearOperatorRolesRef(&scope.RosaRoleConfig.Status.OperatorRolesRef) - err = operatorroles.CreateOperatorRoles(runtime, ocm.Production, config.PermissionsBoundaryARN, interactive.ModeAuto, policies, version, config.SharedVPCConfig.IsSharedVPC(), config.Prefix, hostedCp, installerRoleArn, forcePolicyCreation, + err = operatorroles.CreateOperatorRoles(r.Runtime, ocm.Production, config.PermissionsBoundaryARN, interactive.ModeAuto, policies, version, config.SharedVPCConfig.IsSharedVPC(), config.Prefix, hostedCp, installerRoleArn, forcePolicyCreation, oidcConfigID, config.SharedVPCConfig.RouteRoleARN, ocm.DefaultChannelGroup, config.SharedVPCConfig.VPCEndpointRoleARN) return err } @@ -302,7 +273,7 @@ func (r *ROSARoleConfigReconciler) createOperatorRoles(ctx context.Context, role return nil } -func (r *ROSARoleConfigReconciler) reconcileOIDCConfig(roleConfig *expinfrav1.ROSARoleConfig, scope *scope.RosaRoleConfigScope, ocmClient *ocm.Client) error { +func (r *ROSARoleConfigReconciler) reconcileOIDCConfig(roleConfig *expinfrav1.ROSARoleConfig, scope *scope.RosaRoleConfigScope) error { oidcID := "" if scope.RosaRoleConfig.Status.OIDCID != "" { oidcID = scope.RosaRoleConfig.Status.OIDCID @@ -311,7 +282,7 @@ func (r *ROSARoleConfigReconciler) reconcileOIDCConfig(roleConfig *expinfrav1.RO } if oidcID != "" { - oidcConfig, err := ocmClient.GetOidcConfig(oidcID) + oidcConfig, err := r.Runtime.OCMClient.GetOidcConfig(oidcID) if err != nil || oidcConfig == nil { return fmt.Errorf("failed to get OIDC config: %w", err) } @@ -319,31 +290,21 @@ func (r *ROSARoleConfigReconciler) reconcileOIDCConfig(roleConfig *expinfrav1.RO return nil } - return r.createOIDCConfig(scope, ocmClient) + return r.createOIDCConfig(scope) } -func (r *ROSARoleConfigReconciler) createOIDCProvider(scope *scope.RosaRoleConfigScope, ocmClient *ocm.Client) error { - var err error +func (r *ROSARoleConfigReconciler) createOIDCProvider(scope *scope.RosaRoleConfigScope) error { oidcID := scope.RosaRoleConfig.Status.OIDCID if oidcID == "" { return nil } - runtime := rosacli.NewRuntime() - runtime.OCMClient = ocmClient - runtime.Reporter = (&rosa.Reporter{}) - - runtime.Logger = rosalogging.NewLogger() - runtime.AWSClient, err = aws.NewClient().Logger(runtime.Logger).Build() - if err != nil { - return fmt.Errorf("failed to create aws client: %w", err) - } - oidcConfig, err := runtime.OCMClient.GetOidcConfig(oidcID) + oidcConfig, err := r.Runtime.OCMClient.GetOidcConfig(oidcID) if err != nil { return err } - providers, err := runtime.AWSClient.ListOidcProviders("", oidcConfig) + providers, err := r.Runtime.AWSClient.ListOidcProviders("", oidcConfig) if err != nil { return err } @@ -354,40 +315,18 @@ func (r *ROSARoleConfigReconciler) createOIDCProvider(scope *scope.RosaRoleConfi } } - runtime.Creator, err = runtime.AWSClient.GetCreator() - if err != nil { - return err - } - - return oidcprovider.CreateOIDCProvider(runtime, oidcID, "", true) + return oidcprovider.CreateOIDCProvider(r.Runtime, oidcID, "", true) } -func (r *ROSARoleConfigReconciler) createAccountRoles(ctx context.Context, roleConfig *expinfrav1.ROSARoleConfig, scope *scope.RosaRoleConfigScope, ocmClient rosa.OCMClient) error { +func (r *ROSARoleConfigReconciler) createAccountRoles(roleConfig *expinfrav1.ROSARoleConfig, scope *scope.RosaRoleConfigScope) error { config := roleConfig.Spec.AccountRoleConfig - runtime := rosacli.NewRuntime() - policies, err := ocmClient.GetPolicies("AccountRole") - if err != nil { - return err - } - runtime.OCMClient, err = rosa.NewOCMClient(ctx, scope) - if err != nil { - return err - } - - runtime.Reporter = (&rosa.Reporter{}) - runtime.Logger = rosalogging.NewLogger() - runtime.AWSClient, err = aws.NewClient().Logger(runtime.Logger).Build() - if err != nil { - return fmt.Errorf("failed to create aws client: %w", err) - } - - runtime.Creator, err = runtime.AWSClient.GetCreator() + policies, err := r.Runtime.OCMClient.GetPolicies("AccountRole") if err != nil { return err } createRoles := true - accountRoles, err := runtime.AWSClient.ListAccountRoles(config.Version) + accountRoles, err := r.Runtime.AWSClient.ListAccountRoles(config.Version) if err != nil { // Let create account roles continue if no account roles are found if !strings.Contains(err.Error(), "no account roles found") { @@ -410,79 +349,53 @@ func (r *ROSARoleConfigReconciler) createAccountRoles(ctx context.Context, roleC } } if createRoles { - runtime.Reporter = (&rosa.Reporter{}) - runtime.Logger = rosalogging.NewLogger() - runtime.AWSClient, err = aws.NewClient().Logger(runtime.Logger).Build() - if err != nil { - return fmt.Errorf("failed to create aws client: %w", err) - } - managedPolicies := true - err := accountroles.CreateHCPRoles(runtime, config.Prefix, managedPolicies, config.PermissionsBoundaryARN, ocm.Production, policies, config.Version, config.Path, config.SharedVPCConfig.IsSharedVPC(), config.SharedVPCConfig.RouteRoleARN, config.SharedVPCConfig.VPCEndpointRoleARN) + err := accountroles.CreateHCPRoles(r.Runtime, config.Prefix, managedPolicies, config.PermissionsBoundaryARN, ocm.Production, policies, config.Version, config.Path, config.SharedVPCConfig.IsSharedVPC(), config.SharedVPCConfig.RouteRoleARN, config.SharedVPCConfig.VPCEndpointRoleARN) return err } return nil } -func (r *ROSARoleConfigReconciler) createOIDCConfig(scope *scope.RosaRoleConfigScope, ocmClient *ocm.Client) error { - runtime := rosacli.NewRuntime() - var err error - runtime.Reporter = (&rosa.Reporter{}) - runtime.OCMClient = ocmClient - runtime.Logger = rosalogging.NewLogger() - runtime.AWSClient, err = aws.NewClient().Logger(runtime.Logger).Build() - if err != nil { - return fmt.Errorf("failed to create aws client: %w", err) - } - - runtime.Creator, err = runtime.AWSClient.GetCreator() - if err != nil { - return err - } - +func (r *ROSARoleConfigReconciler) createOIDCConfig(scope *scope.RosaRoleConfigScope) error { // userPrefix, region are used only for unmanaged OIDC config - oidcID, createErr := oidcconfig.CreateOIDCConfig(runtime, true, "", "") + oidcID, createErr := oidcconfig.CreateOIDCConfig(r.Runtime, true, "", "") if createErr != nil { - return fmt.Errorf("failed to Create OIDC config: %w", err) + return fmt.Errorf("failed to Create OIDC config: %w", createErr) } scope.RosaRoleConfig.Status.OIDCID = oidcID return createErr } -func (r *ROSARoleConfigReconciler) deleteAccountRoles(ocmClient *ocm.Client, awsClient aws.Client, scope *scope.RosaRoleConfigScope) error { +func (r *ROSARoleConfigReconciler) deleteAccountRoles(scope *scope.RosaRoleConfigScope) error { roles := scope.RosaRoleConfig.Status.AccountRolesRef config := scope.RosaRoleConfig.Spec.AccountRoleConfig deleteHcpSharedVpcPolicies := config.SharedVPCConfig.VPCEndpointRoleARN != "" && config.SharedVPCConfig.RouteRoleARN != "" - creator, err := awsClient.GetCreator() - if err != nil { - return err - } - clusters, err := ocmClient.GetAllClusters(creator) + clusters, err := r.Runtime.OCMClient.GetAllClusters(r.Runtime.Creator) if err != nil { return err } if canDeleteRole(clusters, roles.InstallerRoleARN) { - err = errors.Join(err, awsClient.DeleteAccountRole(strings.Split(roles.InstallerRoleARN, "/")[1], config.Prefix, true, deleteHcpSharedVpcPolicies)) + err = errors.Join(err, r.Runtime.AWSClient.DeleteAccountRole(strings.Split(roles.InstallerRoleARN, "/")[1], config.Prefix, true, deleteHcpSharedVpcPolicies)) } if canDeleteRole(clusters, roles.WorkerRoleARN) { - err = errors.Join(err, awsClient.DeleteAccountRole(strings.Split(roles.WorkerRoleARN, "/")[1], config.Prefix, true, deleteHcpSharedVpcPolicies)) + err = errors.Join(err, r.Runtime.AWSClient.DeleteAccountRole(strings.Split(roles.WorkerRoleARN, "/")[1], config.Prefix, true, deleteHcpSharedVpcPolicies)) } if canDeleteRole(clusters, roles.SupportRoleARN) { - err = errors.Join(err, awsClient.DeleteAccountRole(strings.Split(roles.SupportRoleARN, "/")[1], config.Prefix, true, deleteHcpSharedVpcPolicies)) + err = errors.Join(err, r.Runtime.AWSClient.DeleteAccountRole(strings.Split(roles.SupportRoleARN, "/")[1], config.Prefix, true, deleteHcpSharedVpcPolicies)) } return err } -func (r *ROSARoleConfigReconciler) deleteOIDCProvider(ocmClient *ocm.Client, awsClient aws.Client, oidcConfigID string) error { +func (r *ROSARoleConfigReconciler) deleteOIDCProvider(oidcConfigID string) error { if oidcConfigID == "" { return nil } - oidcConfig, err := ocmClient.GetOidcConfig(oidcConfigID) + oidcConfig, err := r.Runtime.OCMClient.GetOidcConfig(oidcConfigID) if err != nil { return err } @@ -492,7 +405,7 @@ func (r *ROSARoleConfigReconciler) deleteOIDCProvider(ocmClient *ocm.Client, aws if parsedURI.Scheme != helper.ProtocolHttps { return fmt.Errorf("expected OIDC endpoint URL '%s' to use an https:// scheme", oidcEndpointURL) } - providerArn, err := awsClient.GetOpenIDConnectProviderByOidcEndpointUrl(oidcEndpointURL) + providerArn, err := r.Runtime.AWSClient.GetOpenIDConnectProviderByOidcEndpointUrl(oidcEndpointURL) if err != nil { return err } @@ -500,11 +413,7 @@ func (r *ROSARoleConfigReconciler) deleteOIDCProvider(ocmClient *ocm.Client, aws if providerArn == "" { return nil } - creator, err := awsClient.GetCreator() - if err != nil { - return err - } - hasClusterUsingOidcProvider, err := ocmClient.HasAClusterUsingOidcProvider(oidcEndpointURL, creator.AccountID) + hasClusterUsingOidcProvider, err := r.Runtime.OCMClient.HasAClusterUsingOidcProvider(oidcEndpointURL, r.Runtime.Creator.AccountID) if err != nil { return err } @@ -513,11 +422,11 @@ func (r *ROSARoleConfigReconciler) deleteOIDCProvider(ocmClient *ocm.Client, aws return fmt.Errorf("there are clusters using OIDC config '%s', can't delete the provider", oidcEndpointURL) } - return awsClient.DeleteOpenIDConnectProvider(providerArn) + return r.Runtime.AWSClient.DeleteOpenIDConnectProvider(providerArn) } -func (r *ROSARoleConfigReconciler) deleteOperatorRoles(ocmClient *ocm.Client, awsClient aws.Client, prefix string) error { - hasClusterUsingOperatorRolesPrefix, err := ocmClient.HasAClusterUsingOperatorRolesPrefix(prefix) +func (r *ROSARoleConfigReconciler) deleteOperatorRoles(prefix string) error { + hasClusterUsingOperatorRolesPrefix, err := r.Runtime.OCMClient.HasAClusterUsingOperatorRolesPrefix(prefix) if err != nil { return err } @@ -525,12 +434,12 @@ func (r *ROSARoleConfigReconciler) deleteOperatorRoles(ocmClient *ocm.Client, aw return fmt.Errorf("there are clusters using Operator Roles Prefix '%s', can't delete the IAM roles", prefix) } - credRequests, err := ocmClient.GetAllCredRequests() + credRequests, err := r.Runtime.OCMClient.GetAllCredRequests() if err != nil { return err } - foundOperatorRoles, err := awsClient.GetOperatorRolesFromAccountByPrefix(prefix, credRequests) + foundOperatorRoles, err := r.Runtime.AWSClient.GetOperatorRolesFromAccountByPrefix(prefix, credRequests) if err != nil { return err } @@ -539,19 +448,19 @@ func (r *ROSARoleConfigReconciler) deleteOperatorRoles(ocmClient *ocm.Client, aw return nil } - _, roleARN, err := awsClient.CheckRoleExists(foundOperatorRoles[0]) + _, roleARN, err := r.Runtime.AWSClient.CheckRoleExists(foundOperatorRoles[0]) if err != nil { return err } - managedPolicies, err := awsClient.HasManagedPolicies(roleARN) + managedPolicies, err := r.Runtime.AWSClient.HasManagedPolicies(roleARN) if err != nil { return err } allSharedVpcPoliciesNotDeleted := make(map[string]bool) for _, role := range foundOperatorRoles { - sharedVpcPoliciesNotDeleted, _ := awsClient.DeleteOperatorRole(role, managedPolicies, true) + sharedVpcPoliciesNotDeleted, _ := r.Runtime.AWSClient.DeleteOperatorRole(role, managedPolicies, true) for key, value := range sharedVpcPoliciesNotDeleted { allSharedVpcPoliciesNotDeleted[key] = value } @@ -565,11 +474,11 @@ func (r *ROSARoleConfigReconciler) deleteOperatorRoles(ocmClient *ocm.Client, aw return nil } -func (r *ROSARoleConfigReconciler) deleteOIDCConfig(ocmClient *ocm.Client, oidcConfigID string) error { +func (r *ROSARoleConfigReconciler) deleteOIDCConfig(oidcConfigID string) error { if oidcConfigID == "" { return nil } - return ocmClient.DeleteOidcConfig(oidcConfigID) + return r.Runtime.OCMClient.DeleteOidcConfig(oidcConfigID) } func canDeleteRole(clusters []*cmv1.Cluster, roleARN string) bool { @@ -649,6 +558,41 @@ func (r *ROSARoleConfigReconciler) GetOIDCIDFromOperatorRole(scope *scope.RosaRo return "", fmt.Errorf("cant extract oidc uuid from the %s policy document", *roleDetails.Role.RoleName) } +// setUpRuntime sets up the ROSA runtime if it doesn't exist. +func (r *ROSARoleConfigReconciler) setUpRuntime(ctx context.Context, scope *scope.RosaRoleConfigScope) error { + if r.Runtime != nil { + return nil + } + + // Create OCM client + ocm, err := r.NewOCMClient(ctx, scope) + if err != nil { + return fmt.Errorf("failed to create OCM client: %w", err) + } + + ocmClient, err := rosa.ConvertToRosaOcmClient(ocm) + if err != nil || ocmClient == nil { + return fmt.Errorf("failed to create OCM client: %w", err) + } + + r.Runtime = rosacli.NewRuntime() + r.Runtime.OCMClient = ocmClient + r.Runtime.Reporter = &rosa.Reporter{} + r.Runtime.Logger = rosalogging.NewLogger() + + r.Runtime.AWSClient, err = aws.NewClient().Logger(r.Runtime.Logger).Build() + if err != nil { + return fmt.Errorf("failed to create aws client: %w", err) + } + + r.Runtime.Creator, err = r.Runtime.AWSClient.GetCreator() + if err != nil { + return fmt.Errorf("failed to get creator: %w", err) + } + + return nil +} + // clearOperatorRolesRef clears all field values in the OperatorRolesRef by setting them to empty strings. func (r ROSARoleConfigReconciler) clearOperatorRolesRef(operatorRolesRef *v1beta2.AWSRolesRef) { if operatorRolesRef == nil { diff --git a/exp/controllers/rosaroleconfig_controller_test.go b/exp/controllers/rosaroleconfig_controller_test.go index 53b665c0f4..c21698ad16 100644 --- a/exp/controllers/rosaroleconfig_controller_test.go +++ b/exp/controllers/rosaroleconfig_controller_test.go @@ -18,9 +18,26 @@ package controllers import ( "context" + "net/http" + "strings" "testing" + "time" + awsSdk "github.com/aws/aws-sdk-go-v2/aws" + iamv2 "github.com/aws/aws-sdk-go-v2/service/iam" + iamTypes "github.com/aws/aws-sdk-go-v2/service/iam/types" + stsv2 "github.com/aws/aws-sdk-go-v2/service/sts" . "github.com/onsi/gomega" + sdk "github.com/openshift-online/ocm-sdk-go" + ocmlogging "github.com/openshift-online/ocm-sdk-go/logging" + ocmsdk "github.com/openshift-online/ocm-sdk-go/testing" + "github.com/openshift/rosa/pkg/aws" + rosaMocks "github.com/openshift/rosa/pkg/aws/mocks" + "github.com/openshift/rosa/pkg/ocm" + rosacli "github.com/openshift/rosa/pkg/rosa" + "github.com/sirupsen/logrus" + "go.uber.org/mock/gomock" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" @@ -28,21 +45,295 @@ import ( expinfrav1 "sigs.k8s.io/cluster-api-provider-aws/v2/exp/api/v1beta2" ) -func TestROSARoleConfigReconciler_Reconcile(t *testing.T) { +func TestROSARoleConfigReconcile(t *testing.T) { + RegisterTestingT(t) g := NewWithT(t) + ssoServer := ocmsdk.MakeTCPServer() + apiServer := ocmsdk.MakeTCPServer() + apiServer.SetAllowUnhandledRequests(true) + apiServer.SetUnhandledRequestStatusCode(http.StatusInternalServerError) ctx := context.TODO() + // Create the token: + accessToken := ocmsdk.MakeTokenString("Bearer", 15*time.Minute) + + // Prepare the server: + ssoServer.AppendHandlers( + ocmsdk.RespondWithAccessToken(accessToken), + ) + logger, err := ocmlogging.NewGoLoggerBuilder(). + Debug(false). + Build() + Expect(err).ToNot(HaveOccurred()) + // Set up the connection with the fake config + connection, err := sdk.NewConnectionBuilder(). + Logger(logger). + Tokens(accessToken). + URL(apiServer.URL()). + Build() + // Initialize client object + Expect(err).To(BeNil()) + ocmClient := ocm.NewClientWithConnection(connection) + + mockCtrl := gomock.NewController(t) + // mock iam client to expect ListRoles call + mockIamClient := rosaMocks.NewMockIamApiClient(mockCtrl) + mockIamClient.EXPECT().ListRoles(gomock.Any(), gomock.Any()).Return(&iamv2.ListRolesOutput{ + Roles: []iamTypes.Role{}, + }, nil).AnyTimes() + mockIamClient.EXPECT().ListOpenIDConnectProviders(gomock.Any(), gomock.Any()).Return(&iamv2.ListOpenIDConnectProvidersOutput{ + OpenIDConnectProviderList: []iamTypes.OpenIDConnectProviderListEntry{}, + }, nil).AnyTimes() + // Mock GetRole calls - return role not found error to trigger role creation + mockIamClient.EXPECT().GetRole(gomock.Any(), gomock.Any()).Return(nil, &iamTypes.NoSuchEntityException{ + Message: awsSdk.String("The role with name test-role does not exist."), + }).AnyTimes() + // Mock CreateRole calls for role creation + mockIamClient.EXPECT().CreateRole(gomock.Any(), gomock.Any()).Return(&iamv2.CreateRoleOutput{ + Role: &iamTypes.Role{ + RoleName: awsSdk.String("test-role"), + Arn: awsSdk.String("arn:aws:iam::123456789012:role/test-role"), + }, + }, nil).AnyTimes() + // Mock AttachRolePolicy calls + mockIamClient.EXPECT().AttachRolePolicy(gomock.Any(), gomock.Any()).Return(&iamv2.AttachRolePolicyOutput{}, nil).AnyTimes() + // Mock CreatePolicy calls + mockIamClient.EXPECT().CreatePolicy(gomock.Any(), gomock.Any()).Return(&iamv2.CreatePolicyOutput{ + Policy: &iamTypes.Policy{ + PolicyName: awsSdk.String("test-policy"), + Arn: awsSdk.String("arn:aws:iam::123456789012:policy/test-policy"), + }, + }, nil).AnyTimes() + // Mock GetPolicy calls - return success for AWS managed policies, not found for others + mockIamClient.EXPECT().GetPolicy(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, input *iamv2.GetPolicyInput) (*iamv2.GetPolicyOutput, error) { + switch *input.PolicyArn { + case "arn:aws:iam::aws:policy/sts_hcp_installer_permission_policy": + return &iamv2.GetPolicyOutput{ + Policy: &iamTypes.Policy{ + PolicyName: awsSdk.String("sts_hcp_installer_permission_policy"), + Arn: awsSdk.String("arn:aws:iam::aws:policy/sts_hcp_installer_permission_policy"), + }, + }, nil + case "arn:aws:iam::aws:policy/sts_hcp_support_permission_policy": + return &iamv2.GetPolicyOutput{ + Policy: &iamTypes.Policy{ + PolicyName: awsSdk.String("sts_hcp_support_permission_policy"), + Arn: awsSdk.String("arn:aws:iam::aws:policy/sts_hcp_support_permission_policy"), + }, + }, nil + + case "arn:aws:iam::aws:policy/sts_hcp_worker_permission_policy": + return &iamv2.GetPolicyOutput{ + Policy: &iamTypes.Policy{ + PolicyName: awsSdk.String("sts_hcp_worker_permission_policy"), + Arn: awsSdk.String("arn:aws:iam::aws:policy/sts_hcp_worker_permission_policy"), + }, + }, nil + default: + return nil, &iamTypes.NoSuchEntityException{ + Message: awsSdk.String("The policy does not exist."), + } + } + }).AnyTimes() + // Mock ListPolicies calls - return expected ROSA managed policies + mockIamClient.EXPECT().ListPolicies(gomock.Any(), gomock.Any()).Return(&iamv2.ListPoliciesOutput{ + Policies: []iamTypes.Policy{ + { + PolicyName: awsSdk.String("sts_hcp_installer_permission_policy"), + Arn: awsSdk.String("arn:aws:iam::aws:policy/sts_hcp_installer_permission_policy"), + }, + { + PolicyName: awsSdk.String("sts_hcp_support_permission_policy"), + Arn: awsSdk.String("arn:aws:iam::aws:policy/sts_hcp_support_permission_policy"), + }, + { + PolicyName: awsSdk.String("sts_hcp_worker_permission_policy"), + Arn: awsSdk.String("arn:aws:iam::aws:policy/sts_hcp_worker_permission_policy"), + }, + }, + }, nil).AnyTimes() + + // mock sts - add common STS calls that might be needed during role creation + mockSTSClient := rosaMocks.NewMockStsApiClient(mockCtrl) + mockSTSClient.EXPECT().GetCallerIdentity(gomock.Any(), gomock.Any()).Return(&stsv2.GetCallerIdentityOutput{ + Arn: awsSdk.String("fake"), + Account: awsSdk.String("123"), + UserId: awsSdk.String("test-user-id"), + }, nil).AnyTimes() + + awsClient := aws.New( + awsSdk.Config{}, + aws.NewLoggerWrapper(logrus.New(), nil), + mockIamClient, + rosaMocks.NewMockEc2ApiClient(mockCtrl), + rosaMocks.NewMockOrganizationsApiClient(mockCtrl), + rosaMocks.NewMockS3ApiClient(mockCtrl), + rosaMocks.NewMockSecretsManagerApiClient(mockCtrl), + mockSTSClient, + rosaMocks.NewMockCloudFormationApiClient(mockCtrl), + rosaMocks.NewMockServiceQuotasApiClient(mockCtrl), + rosaMocks.NewMockServiceQuotasApiClient(mockCtrl), + &aws.AccessKey{}, + false, + ) + + r := rosacli.NewRuntime() + r.OCMClient = ocmClient + r.AWSClient = awsClient + r.Creator = &aws.Creator{ + ARN: "fake", + AccountID: "123", + IsSTS: false, + } + // Mock OCM API calls using path-based routing + apiServer.RouteToHandler("GET", "/api/clusters_mgmt/v1/aws_inquiries/sts_policies", + func(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query().Get("search") + if strings.Contains(query, "AccountRole") { + // Return AccountRole policies + ocmsdk.RespondWithJSON(http.StatusOK, `{ + "items": [ + { + "id": "sts_hcp_installer_permission_policy", + "arn": "arn:aws:iam::aws:policy/sts_hcp_installer_permission_policy", + "type": "AccountRole" + }, + { + "id": "sts_hcp_support_permission_policy", + "arn": "arn:aws:iam::aws:policy/sts_hcp_support_permission_policy", + "type": "AccountRole" + }, + { + "id": "sts_hcp_worker_permission_policy", + "arn": "arn:aws:iam::aws:policy/sts_hcp_worker_permission_policy", + "type": "AccountRole" + }, + { + "id": "sts_hcp_instance_worker_permission_policy", + "arn": "arn:aws:iam::aws:policy/sts_hcp_instance_worker_permission_policy", + "type": "AccountRole" + } + ] + }`)(w, r) + } else if strings.Contains(query, "OperatorRole") { + // Return OperatorRole policies + ocmsdk.RespondWithJSON(http.StatusOK, `{ + "items": [ + { + "id": "openshift_hcp_ingress_policy", + "arn": "arn:aws:iam::aws:policy/openshift_hcp_ingress_policy", + "type": "OperatorRole" + }, + { + "id": "openshift_hcp_image_registry_policy", + "arn": "arn:aws:iam::aws:policy/openshift_hcp_image_registry_policy", + "type": "OperatorRole" + }, + { + "id": "openshift_hcp_storage_policy", + "arn": "arn:aws:iam::aws:policy/openshift_hcp_storage_policy", + "type": "OperatorRole" + }, + { + "id": "openshift_hcp_network_policy", + "arn": "arn:aws:iam::aws:policy/openshift_hcp_network_policy", + "type": "OperatorRole" + }, + { + "id": "openshift_hcp_kube_controller_policy", + "arn": "arn:aws:iam::aws:policy/openshift_hcp_kube_controller_policy", + "type": "OperatorRole" + }, + { + "id": "openshift_hcp_node_pool_policy", + "arn": "arn:aws:iam::aws:policy/openshift_hcp_node_pool_policy", + "type": "OperatorRole" + }, + { + "id": "openshift_hcp_control_plane_policy", + "arn": "arn:aws:iam::aws:policy/openshift_hcp_control_plane_policy", + "type": "OperatorRole" + }, + { + "id": "openshift_hcp_kms_policy", + "arn": "arn:aws:iam::aws:policy/openshift_hcp_kms_policy", + "type": "OperatorRole" + } + ] + }`)(w, r) + } else { + // Default response for other queries + ocmsdk.RespondWithJSON(http.StatusOK, `{"items": []}`)(w, r) + } + }) + + // mock ocm API calls - first call gets tris response + apiServer.AppendHandlers( + ocmsdk.RespondWithJSON( + http.StatusOK, "", + ), + ) + // Mock GetOidcConfig call + apiServer.AppendHandlers( + ocmsdk.RespondWithJSON( + http.StatusOK, `{"id": "test-oidc-id", "issuer_url": "https://test.oidc.url"}`, + ), + ) + // Mock GetAllClusters call + apiServer.AppendHandlers( + ocmsdk.RespondWithJSON( + http.StatusOK, `{"items": []}`, + ), + ) + // Mock GetAllCredRequests call + apiServer.AppendHandlers( + ocmsdk.RespondWithJSON( + http.StatusOK, `[]`, + ), + ) + // Mock HasAClusterUsingOperatorRolesPrefix call + apiServer.AppendHandlers( + ocmsdk.RespondWithJSON( + http.StatusOK, `false`, + ), + ) + // GET /api/clusters_mgmt/v1/products/rosa/technology_previews/hcp-zero-egress + apiServer.AppendHandlers( + ocmsdk.RespondWithJSON( + http.StatusInternalServerError, "", + ), + ) + + // prepare the role config + + // Create CRs + ns, err := testEnv.CreateNamespace(ctx, "test-namespace") rosaRoleConfig := &expinfrav1.ROSARoleConfig{ ObjectMeta: metav1.ObjectMeta{ - Name: "test-rosa-role", - Namespace: "test-namespace"}, - Spec: expinfrav1.ROSARoleConfigSpec{}, + Name: "test-rosa-role", + Namespace: ns.Name, + Finalizers: []string{expinfrav1.RosaRoleConfigFinalizer}, + }, + Spec: expinfrav1.ROSARoleConfigSpec{ + AccountRoleConfig: expinfrav1.AccountRoleConfig{ + Prefix: "test", + Version: "4.15", + }, + OperatorRoleConfig: expinfrav1.OperatorRoleConfig{ + Prefix: "test", + }, + }, } + g.Expect(err).ToNot(HaveOccurred()) + + createObject(g, rosaRoleConfig, ns.Name) + defer cleanupObject(g, rosaRoleConfig) // Setup the reconciler with these mocks reconciler := &ROSARoleConfigReconciler{ - Client: testEnv.Client, + Client: testEnv.Client, + Runtime: r, } // Call the Reconcile function @@ -50,6 +341,29 @@ func TestROSARoleConfigReconciler_Reconcile(t *testing.T) { req.NamespacedName = types.NamespacedName{Name: rosaRoleConfig.Name, Namespace: rosaRoleConfig.Namespace} _, errReconcile := reconciler.Reconcile(ctx, req) - // Assertions - g.Expect(errReconcile).ToNot(HaveOccurred()) + // Assertions - expect the installer role empty error since AccountRolesRef is not populated yet + g.Expect(errReconcile).To(HaveOccurred()) + g.Expect(errReconcile.Error()).To(ContainSubstring("installer role is empty")) + + // Check the status of the ROSARoleConfig resource + updatedRoleConfig := &expinfrav1.ROSARoleConfig{} + err = reconciler.Client.Get(ctx, req.NamespacedName, updatedRoleConfig) + g.Expect(err).ToNot(HaveOccurred()) + + // Verify that account roles would be populated if the mocking was complete + // (The actual population depends on the CreateAccountRoles implementation) + // Verify conditions are set appropriately + + // Should have a condition indicating the failure at operator role creation + hasFailureCondition := false + for _, condition := range updatedRoleConfig.Status.Conditions { + if condition.Type == expinfrav1.RosaRoleConfigReadyCondition && + condition.Status == corev1.ConditionFalse && + condition.Reason == expinfrav1.RosaRoleConfigReconciliationFailedReason { + hasFailureCondition = true + g.Expect(condition.Message).To(ContainSubstring("Failed to create Operator Roles")) + break + } + } + g.Expect(hasFailureCondition).To(BeTrue(), "Expected to find a failure condition for operator role creation") } diff --git a/go.mod b/go.mod index fe525d6985..abbe00e48c 100644 --- a/go.mod +++ b/go.mod @@ -222,7 +222,7 @@ require ( go.opentelemetry.io/otel/sdk v1.29.0 // indirect go.opentelemetry.io/otel/trace v1.29.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect - go.uber.org/mock v0.5.2 // indirect + go.uber.org/mock v0.5.2 go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect @@ -250,6 +250,15 @@ require ( require ( github.com/AlecAivazis/survey/v2 v2.2.15 // indirect - github.com/kr/pty v1.1.8 // indirect + github.com/itchyny/gojq v0.12.9 // indirect + github.com/itchyny/timefmt-go v0.1.4 // indirect + github.com/jackc/chunkreader/v2 v2.0.1 // indirect + github.com/jackc/pgconn v1.14.3 // indirect + github.com/jackc/pgio v1.0.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgproto3/v2 v2.3.3 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/pgtype v1.14.0 // indirect + github.com/jackc/pgx/v4 v4.18.3 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect ) diff --git a/go.sum b/go.sum index 0fadc4c04f..7ad15bfdbc 100644 --- a/go.sum +++ b/go.sum @@ -20,6 +20,7 @@ github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= @@ -147,6 +148,8 @@ github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtM github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= +github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/coredns/caddy v1.1.1 h1:2eYKZT7i6yxIfGP3qLJoJ7HAsDJqYB+X68g4NYjSrE0= @@ -157,6 +160,8 @@ github.com/coreos/go-json v0.0.0-20230131223807-18775e0fb4fb h1:rmqyI19j3Z/74bIR github.com/coreos/go-json v0.0.0-20230131223807-18775e0fb4fb/go.mod h1:rcFZM3uxVvdyNmsAV2jopgPD1cs5SPWJWU5dOz2LUnw= github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU= github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= @@ -217,6 +222,8 @@ github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk= github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -232,6 +239,7 @@ github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= @@ -246,6 +254,8 @@ github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= +github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= @@ -297,6 +307,7 @@ github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f h1:5CjVwnuUcp5adK4gm github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f/go.mod h1:nOFQdrUlIlx6M6ODdSpBj1NVA+VgLC6kmw60mkw34H4= github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg= github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/safetext v0.0.0-20220905092116-b49f7bc46da2 h1:SJ+NtwL6QaZ21U+IrK7d0gGgpjGGvd2kz+FzTHVzdqI= github.com/google/safetext v0.0.0-20220905092116-b49f7bc46da2/go.mod h1:Tv1PlzqC9t8wNnpPdctvtSUOPUUg4SHeE6vR1Ir2hmg= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= @@ -333,22 +344,53 @@ github.com/itchyny/gojq v0.12.9 h1:biKpbKwMxVYhCU1d6mR7qMr3f0Hn9F5k5YykCVb3gmM= github.com/itchyny/gojq v0.12.9/go.mod h1:T4Ip7AETUXeGpD+436m+UEl3m3tokRgajd5pRfsR5oE= github.com/itchyny/timefmt-go v0.1.4 h1:hFEfWVdwsEi+CY8xY2FtgWHGQaBaC3JeHd+cve0ynVM= github.com/itchyny/timefmt-go v0.1.4/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8= +github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= +github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= +github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= +github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= +github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= +github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= +github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w= github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM= github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= +github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= +github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= +github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= +github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= +github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag= github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= +github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= +github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= +github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= github.com/jackc/pgtype v1.14.0 h1:y+xUdabmyMkJLyApYuPj38mW+aAIqCe5uuBB51rH3Vw= github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= +github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= +github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= +github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= +github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= github.com/jackc/pgx/v4 v4.18.3 h1:dE2/TrEsGX3RBprb3qryqSV9Y60iZN1C6i8IrmW9/BA= github.com/jackc/pgx/v4 v4.18.3/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= +github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= @@ -363,6 +405,8 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNU github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= 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/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -374,14 +418,25 @@ github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= @@ -450,6 +505,7 @@ github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3v github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -469,8 +525,12 @@ github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8= github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= +github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= @@ -479,10 +539,15 @@ github.com/sanathkr/go-yaml v0.0.0-20170819195128-ed9d249f429b h1:jUK33OXuZP/l6b github.com/sanathkr/go-yaml v0.0.0-20170819195128-ed9d249f429b/go.mod h1:8458kAagoME2+LN5//WxE71ysZ3B7r22fdgb7qVmXSY= github.com/sanathkr/yaml v0.0.0-20170819201035-0056894fa522 h1:fOCp11H0yuyAt2wqlbJtbyPzSgaxHTv8uN1pMpkG1t8= github.com/sanathkr/yaml v0.0.0-20170819201035-0056894fa522/go.mod h1:tQTYKOQgxoH3v6dEmdHiz4JG+nbxWwM5fgPQUpSZqVQ= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= @@ -505,6 +570,8 @@ github.com/spf13/viper v1.20.0/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqj github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -540,6 +607,7 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/zalando/go-keyring v0.2.3 h1:v9CUu9phlABObO4LPWycf+zwMG7nlbb3t/B5wa97yms= github.com/zalando/go-keyring v0.2.3/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk= +github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/zgalor/weberr v0.8.2 h1:rzGP0jQVt8hGSNnzjDAQNHMxNNrf3gUrYhpSgY76+mk= github.com/zgalor/weberr v0.8.2/go.mod h1:cqK89mj84q3PRgqQXQFWJDzCorOd8xOtov/ulOnqDwc= github.com/ziutek/telnet v0.0.0-20180329124119-c3b780dc415b/go.mod h1:IZpXDfkJ6tWD3PhBK5YzgQT+xJWh7OsdwiG8hA2MkO4= @@ -581,19 +649,36 @@ go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt3 go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 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/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= @@ -603,6 +688,9 @@ golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbR golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 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/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -613,6 +701,7 @@ golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 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-20190813141303-74dc4d7220e7/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-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= @@ -633,14 +722,20 @@ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -657,12 +752,15 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY= golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= @@ -672,14 +770,22 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= +golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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= @@ -713,13 +819,16 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= @@ -740,6 +849,7 @@ gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls= k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k= k8s.io/apiextensions-apiserver v0.32.3 h1:4D8vy+9GWerlErCwVIbcQjsWunF9SUGNu7O7hiQTyPY= diff --git a/pkg/rosa/ocmclient.go b/pkg/rosa/ocmclient.go index 84bb1c8893..5b6d6e8ae2 100644 --- a/pkg/rosa/ocmclient.go +++ b/pkg/rosa/ocmclient.go @@ -158,7 +158,11 @@ func NewMockOCMClient(ctx context.Context, rosaScope *scope.ROSAControlPlaneScop func ConvertToRosaOcmClient(i OCMClient) (*ocm.Client, error) { c, ok := i.(*ocmclient) if !ok { - return nil, fmt.Errorf("failed to conver to Rosa OCM Client") + c, ok := i.(*ocm.Client) + if !ok { + return nil, fmt.Errorf("failed to convert to Rosa OCM Client") + } + return c, nil } return c.ocmClient, nil }