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 e0c2f57080..e814f1de52 100644 --- a/config/crd/bases/controlplane.cluster.x-k8s.io_rosacontrolplanes.yaml +++ b/config/crd/bases/controlplane.cluster.x-k8s.io_rosacontrolplanes.yaml @@ -525,8 +525,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. @@ -560,7 +561,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 @@ -576,8 +579,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 @@ -777,6 +781,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 roles, operator roles and OIDC configuration. + RosaRoleConfigRef and role fields such as installerRoleARN, supportRoleARN, workerRoleARN, rolesRef and oidcID are mutually exclusive. + 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. @@ -788,6 +808,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". @@ -806,22 +827,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..6fc3252abc --- /dev/null +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_rosaroleconfigs.yaml @@ -0,0 +1,458 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + 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: + description: The arn path for the account/operator roles as well + as their policies. + type: string + permissionsBoundaryARN: + description: The ARN of the policy that is used to set the permissions + boundary for the account roles. + type: string + prefix: + description: User-defined prefix for all generated AWS account + role + maxLength: 4 + pattern: ^[a-z]([-a-z0-9]*[a-z0-9])?$ + type: string + x-kubernetes-validations: + - message: prefix is immutable + rule: self == oldSelf + 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 the roles tag in formate of x.y.z example; "4.19.0" + Setting the role OpenShift version tag does not affect the associated ROSAControlplane version. + type: string + x-kubernetes-validations: + - message: version is immutable + rule: self == oldSelf + 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: |- + IdentityRef is a reference to an identity to be used when reconciling the ROSA Role Config. + If no identity is specified, the default identity for this controller will be used. + 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 + oidcProviderType: + default: Managed + description: OIDC provider type values are Managed or UnManaged. When + set to Unmanged OperatorRoleConfig OIDCID field must be provided. + enum: + - Managed + - Unmanaged + type: string + 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. + Cannot be set when OidcProviderType set to Managed + type: string + x-kubernetes-validations: + - message: oidcID is immutable + rule: self == oldSelf + 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 + roles.' + maxLength: 4 + pattern: ^[a-z]([-a-z0-9]*[a-z0-9])?$ + type: string + x-kubernetes-validations: + - message: prefix is immutable + rule: self == oldSelf + 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 + - oidcProviderType + - 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 specifies the ROSARoleConfig conditions + 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..8e3b541ad3 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: @@ -57,6 +58,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/rbac/role.yaml b/config/rbac/role.yaml index a2ed671ffb..b99eb49cbe 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -176,6 +176,7 @@ rules: - awsclusters/status - awsfargateprofiles/status - rosaclusters/status + - rosaroleconfigs/status verbs: - get - patch @@ -198,6 +199,7 @@ rules: resources: - awsmachines - rosamachinepools + - rosaroleconfigs verbs: - create - delete @@ -210,6 +212,7 @@ rules: - infrastructure.cluster.x-k8s.io resources: - rosamachinepools/finalizers + - rosaroleconfigs/finalizers verbs: - update - apiGroups: 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/controllers/rosacluster_controller_test.go b/controllers/rosacluster_controller_test.go index 0a7dd42c0b..170e5faefa 100644 --- a/controllers/rosacluster_controller_test.go +++ b/controllers/rosacluster_controller_test.go @@ -95,10 +95,19 @@ func TestRosaClusterReconcile(t *testing.T) { PodCIDR: "10.128.0.0/14", ServiceCIDR: "172.30.0.0/16", }, - Region: "us-east-1", - Version: "4.19.20", - ChannelGroup: "stable", - RolesRef: rosacontrolplanev1.AWSRolesRef{}, + Region: "us-east-1", + Version: "4.19.20", + ChannelGroup: "stable", + RolesRef: rosacontrolplanev1.AWSRolesRef{ + IngressARN: "ingress-arn", + ImageRegistryARN: "image-arn", + StorageARN: "storage-arn", + NetworkARN: "net-arn", + KubeCloudControllerARN: "kube-arn", + NodePoolManagementARN: "node-arn", + ControlPlaneOperatorARN: "control-arn", + KMSProviderARN: "kms-arn", + }, OIDCID: "oidcid1", InstallerRoleARN: "arn1", WorkerRoleARN: "arn2", 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 c8a99cea6a..d37bf1f5c3 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" ) @@ -127,13 +126,23 @@ type RosaControlPlaneSpec struct { //nolint: maligned // +kubebuilder:default=WaitForAcknowledge VersionGate VersionGateAckType `json:"versionGate"` + // RosaRoleConfigRef is a reference to a RosaRoleConfig resource that contains account roles, operator roles and OIDC configuration. + // RosaRoleConfigRef and role fields such as installerRoleARN, supportRoleARN, workerRoleARN, rolesRef and oidcID are mutually exclusive. + // + // +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. // @@ -152,13 +161,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 @@ -333,7 +348,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 @@ -343,6 +358,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..e5ab8c8a5b 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 ( @@ -58,6 +74,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()...) @@ -101,6 +121,10 @@ func (*rosaControlPlaneWebhook) ValidateUpdate(_ context.Context, oldObj, newObj 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 +203,58 @@ func (r *ROSAControlPlane) validateExternalAuthProviders() *field.Error { return nil } +func (r *ROSAControlPlane) validateRosaRoleConfig() *field.Error { + hasRoleFields := 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 hasRoleFields { + return field.Invalid(field.NewPath("spec.rosaRoleConfigRef"), r.Spec.RosaRoleConfigRef, "RosaRoleConfigRef and role fields such as installerRoleARN, supportRoleARN, workerRoleARN, rolesRef and oidcID 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 90426d2aab..75c06e8f12 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 } @@ -227,6 +228,12 @@ func (r *ROSAControlPlaneReconciler) reconcileNormal(ctx context.Context, rosaSc return ctrl.Result{}, fmt.Errorf("failed to transform caller identity to creator: %w", err) } + rosaRoleConfig, err := r.reconcileRosaRoleConfig(ctx, rosaScope) + if err != nil { + rosaScope.Error(err, "cannot reconcile RosaRoleConfig ") + return ctrl.Result{}, err + } + validationMessage, err := validateControlPlaneSpec(ocmClient, rosaScope) if err != nil { return ctrl.Result{}, fmt.Errorf("failed to validate ROSAControlPlane.spec: %w", err) @@ -314,7 +321,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 } @@ -336,6 +343,48 @@ func (r *ROSAControlPlaneReconciler) reconcileNormal(ctx context.Context, rosaSc return ctrl.Result{}, nil } +func (r *ROSAControlPlaneReconciler) reconcileRosaRoleConfig(ctx context.Context, rosaScope *scope.ROSAControlPlaneScope) (*expinfrav1.ROSARoleConfig, error) { + rosaRoleConfig := &expinfrav1.ROSARoleConfig{} + // Get role configuration from either RosaRoleConfig or direct fields + if rosaScope.ControlPlane.Spec.RosaRoleConfigRef != nil { + // Get RosaRoleConfig + key := client.ObjectKey{ + Name: rosaScope.ControlPlane.Spec.RosaRoleConfigRef.Name, + Namespace: rosaScope.ControlPlane.Namespace, + } + + if err := r.Client.Get(ctx, key, rosaRoleConfig); err != nil { + conditions.MarkFalse(rosaScope.ControlPlane, + rosacontrolplanev1.ROSARoleConfigReadyCondition, + rosacontrolplanev1.ROSARoleConfigNotFoundReason, + clusterv1.ConditionSeverityError, + "Failed to get RosaRoleConfig %s/%s", rosaScope.ControlPlane.Namespace, rosaScope.ControlPlane.Spec.RosaRoleConfigRef.Name) + + return nil, err + } + + // 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) + + return nil, fmt.Errorf("RosaRoleConfig %s/%s is not ready", rosaScope.ControlPlane.Namespace, rosaScope.ControlPlane.Spec.RosaRoleConfigRef.Name) + } + 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 + } + + return rosaRoleConfig, nil +} + func (r *ROSAControlPlaneReconciler) reconcileDelete(ctx context.Context, rosaScope *scope.ROSAControlPlaneScope) (res ctrl.Result, reterr error) { rosaScope.Info("Reconciling ROSAControlPlane delete") @@ -924,7 +973,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 @@ -948,11 +997,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, 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/docs/book/src/topics/rosa/creating-a-cluster.md b/docs/book/src/topics/rosa/creating-a-cluster.md index 0d58b6f5bb..77892fd126 100644 --- a/docs/book/src/topics/rosa/creating-a-cluster.md +++ b/docs/book/src/topics/rosa/creating-a-cluster.md @@ -89,30 +89,46 @@ The SSO offline token is being deprecated and it is recommended to use service a Follow the guide [here](https://docs.aws.amazon.com/ROSA/latest/userguide/getting-started-hcp.html) up until ["Create a ROSA with HCP Cluster"](https://docs.aws.amazon.com/ROSA/latest/userguide/getting-started-hcp.html#create-hcp-cluster-cli) to install the required tools and setup the prerequisite infrastructure. Once Step 3 is done, you will be ready to proceed with creating a ROSA HCP cluster using cluster-api. +Note; Skip the "Create the required IAM roles and OpenID Connect configuration" step from the prerequisites url above and use the templates/cluster-template-rosa-role-config.yaml to generate a ROSARoleConfig CR to create the required account roles, operator roles & managed OIDC provider. + ## Creating the cluster 1. Prepare the environment: ```bash - export OPENSHIFT_VERSION="4.14.5" + export OPENSHIFT_VERSION="4.19.0" export AWS_REGION="us-west-2" export AWS_AVAILABILITY_ZONE="us-west-2a" export AWS_ACCOUNT_ID="" export AWS_CREATOR_ARN="" # can be retrieved e.g. using `aws sts get-caller-identity` + # Note: if using templates/cluster-template-rosa.yaml set the below env variables export OIDC_CONFIG_ID="" # OIDC config id creating previously with `rosa create oidc-config` export ACCOUNT_ROLES_PREFIX="ManagedOpenShift-HCP" # prefix used to create account IAM roles with `rosa create account-roles` export OPERATOR_ROLES_PREFIX="capi-rosa-quickstart" # prefix used to create operator roles with `rosa create operator-roles --prefix ` + # Note: if using templates/cluster-template-rosa-role-config.yaml set the below env variables + export ACCOUNT_ROLES_PREFIX="capa" # prefix can be change to preferable prefix with max 4 chars + export OPERATOR_ROLES_PREFIX="capa" # prefix can be change to preferable prefix with max 4 chars + # subnet IDs created earlier export PUBLIC_SUBNET_ID="subnet-0b54a1111111111111" export PRIVATE_SUBNET_ID="subnet-05e72222222222222" ``` 1. Render the cluster manifest using the ROSA HCP cluster template: + + a. Using templates/cluster-template-rosa.yaml + + Note: The AWS role name must be no more than 64 characters in length. Otherwise an error will be returned. Truncate values exceeding 64 characters. ```shell clusterctl generate cluster --from templates/cluster-template-rosa.yaml > rosa-capi-cluster.yaml ``` - Note: The AWS role name must be no more than 64 characters in length. Otherwise an error will be returned. Truncate values exceeding 64 characters. + + b. Using templates/cluster-template-rosa-role-config.yaml + ```shell + clusterctl generate cluster --from templates/cluster-template-rosa-role-config.yaml > rosa-capi-cluster.yaml + ``` + 1. If a credentials secret was created earlier, edit `ROSAControlPlane` to reference it: ```yaml 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..e3bdda7db9 --- /dev/null +++ b/exp/api/v1beta2/rosaroleconfig_types.go @@ -0,0 +1,248 @@ +/* +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 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" +) + +// OidcProviderType set to Managed or UnManaged +type OidcProviderType string + +const ( + // Managed OIDC Provider type + Managed OidcProviderType = "Managed" + + // Unmanaged OIDC Provider type + Unmanaged OidcProviderType = "Unmanaged" +) + +// Operator Role const +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" +) + +// Account Role const +const ( + // HCPROSAInstallerRole is the suffix for installer account role + HCPROSAInstallerRole = "-HCP-ROSA-Installer-Role" + + // HCPROSASupportRole is the suffix for support account role + HCPROSASupportRole = "-HCP-ROSA-Support-Role" + + // HCPROSAWorkerRole is the suffix for worker account role + HCPROSAWorkerRole = "-HCP-ROSA-Worker-Role" +) + +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" +) + +// ROSARoleConfigSpec defines the desired state of ROSARoleConfig +type ROSARoleConfigSpec struct { + // AccountRoleConfig defines account-wide IAM roles before creating your ROSA cluster. + AccountRoleConfig AccountRoleConfig `json:"accountRoleConfig"` + + // OperatorRoleConfig defines cluster-specific operator IAM roles based on your cluster configuration. + OperatorRoleConfig OperatorRoleConfig `json:"operatorRoleConfig"` + + // IdentityRef is a reference to an identity to be used when reconciling the ROSA Role Config. + // If no identity is specified, the default identity for this controller will be used. + // +optional + 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"` + + // OIDC provider type values are Managed or UnManaged. When set to Unmanged OperatorRoleConfig OIDCID field must be provided. + // +kubebuilder:validation:Enum=Managed;Unmanaged + // +kubebuilder:default=Managed + OidcProviderType OidcProviderType `json:"oidcProviderType"` +} + +// AccountRoleConfig defines account IAM roles before creating your ROSA cluster. +type AccountRoleConfig struct { + // User-defined prefix for all generated AWS account role + // +kubebuilder:validation:Required + // +kubebuilder:validation:MaxLength:=4 + // +kubebuilder:validation:Pattern:=`^[a-z]([-a-z0-9]*[a-z0-9])?$` + // +kubebuilder:validation:XValidation:rule="self == oldSelf", message="prefix is immutable" + Prefix string `json:"prefix"` + + // The ARN of the policy that is used to set the permissions boundary for the account roles. + // +optional + PermissionsBoundaryARN string `json:"permissionsBoundaryARN,omitempty"` + + // The arn path for the account/operator roles as well as their policies. + // +optional + Path string `json:"path,omitempty"` + + // Version of OpenShift that will be used to the roles tag in formate of x.y.z example; "4.19.0" + // Setting the role OpenShift version tag does not affect the associated ROSAControlplane version. + // +kubebuilder:validation:Required + // +kubebuilder:validation:XValidation:rule="self == oldSelf", message="version is immutable" + Version string `json:"version"` + + // SharedVPCConfig is used to set up shared VPC. + // +optional + 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 roles. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MaxLength:=4 + // +kubebuilder:validation:Pattern:=`^[a-z]([-a-z0-9]*[a-z0-9])?$` + // +kubebuilder:validation:XValidation:rule="self == oldSelf", message="prefix is immutable" + Prefix string `json:"prefix"` + + // The ARN of the policy that is used to set the permissions boundary for the operator roles. + // +optional + PermissionsBoundaryARN string `json:"permissionsBoundaryARN,omitempty"` + + // SharedVPCConfig is used to set up shared VPC. + // +optional + SharedVPCConfig SharedVPCConfig `json:"sharedVPCConfig,omitempty"` + + // OIDCID is the ID of the OIDC config that will be used to create the operator roles. + // Cannot be set when OidcProviderType set to Managed + // +optional + // +kubebuilder:validation:XValidation:rule="self == oldSelf", message="oidcID is 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 specifies the ROSARoleConfig conditions + 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"` +} + +// ROSARoleConfig is the Schema for the rosaroleconfigs API +// +kubebuilder:object:root=true +// +kubebuilder:resource:path=rosaroleconfigs,scope=Namespaced,categories=cluster-api,shortName=rosarole +// +kubebuilder:storageversion +// +kubebuilder:subresource:status +type ROSARoleConfig struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ROSARoleConfigSpec `json:"spec,omitempty"` + Status ROSARoleConfigStatus `json:"status,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"` +} + +// 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 +} + +// 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/api/v1beta2/rosaroleconfig_webhook.go b/exp/api/v1beta2/rosaroleconfig_webhook.go new file mode 100644 index 0000000000..5e9d36a30b --- /dev/null +++ b/exp/api/v1beta2/rosaroleconfig_webhook.go @@ -0,0 +1,79 @@ +package v1beta2 + +import ( + "context" + "fmt" + + "github.com/blang/semver" + apierrors "k8s.io/apimachinery/pkg/api/errors" + runtime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" + 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) { + roleConfig, ok := obj.(*ROSARoleConfig) + if !ok { + return nil, fmt.Errorf("expected an ROSARoleConfig object but got %T", roleConfig) + } + + var allErrs field.ErrorList + if roleConfig.Spec.OidcProviderType == Managed && roleConfig.Spec.OperatorRoleConfig.OIDCID != "" { + err := field.Invalid(field.NewPath("spec.operatorRoleConfig.oidcId"), roleConfig.Spec.OperatorRoleConfig.OIDCID, "cannot be set with Managed oidc provider type") + allErrs = append(allErrs, err) + } else if roleConfig.Spec.OidcProviderType == Unmanaged && roleConfig.Spec.OperatorRoleConfig.OIDCID == "" { + err := field.Invalid(field.NewPath("spec.operatorRoleConfig.oidcId"), roleConfig.Spec.OperatorRoleConfig.OIDCID, "must set operatorRoleConfig.oidcId with UnManaged oidc provider type") + allErrs = append(allErrs, err) + } + + _, vErr := semver.Parse(roleConfig.Spec.AccountRoleConfig.Version) + if vErr != nil { + err := field.Invalid(field.NewPath("spec.accountRoleConfig.version"), roleConfig.Spec.AccountRoleConfig.Version, "must be a valid semantic version") + allErrs = append(allErrs, err) + } + + if len(allErrs) > 0 { + return nil, apierrors.NewInvalid( + roleConfig.GroupVersionKind().GroupKind(), + roleConfig.Name, + allErrs) + } + + 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_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..349348039b --- /dev/null +++ b/exp/controllers/rosaroleconfig_controller.go @@ -0,0 +1,479 @@ +/* +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 controllers + +import ( + "context" + "errors" + "fmt" + "maps" + "strings" + + 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" + interactive "github.com/openshift/rosa/pkg/interactive" + rosalogging "github.com/openshift/rosa/pkg/logging" + "github.com/openshift/rosa/pkg/ocm" + "github.com/openshift/rosa/pkg/reporter" + 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" + kerrors "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/client-go/tools/record" + "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" + + "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" + 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 + Recorder record.EventRecorder + 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 { + 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=core,resources=events,verbs=get;list;watch;create;patch +// +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 + +// Reconcile reconciles ROSARoleConfig. +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 + } + log.Error(err, "Failed to get ROSARoleConfig") + return ctrl.Result{Requeue: true}, nil + } + + log = log.WithValues("roleConfig", klog.KObj(roleConfig)) + scope, err := scope.NewRosaRoleConfigScope(scope.RosaRoleConfigScopeParams{ + Client: r.Client, + RosaRoleConfig: roleConfig, + ControllerName: "rosaroleconfig", + 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.PatchObject(); err != nil { + reterr = errors.Join(reterr, err) + } + }() + + if err := r.setUpRuntime(ctx, scope); err != nil { + return ctrl.Result{}, fmt.Errorf("failed to set up runtime: %w", err) + } + + if !roleConfig.DeletionTimestamp.IsZero() { + scope.Info("Deleting ROSARoleConfig.") + conditions.MarkFalse(scope.RosaRoleConfig, expinfrav1.RosaRoleConfigReadyCondition, expinfrav1.RosaRoleConfigDeletionStarted, clusterv1.ConditionSeverityInfo, "Deletion of RosaRolesConfig started") + err = r.reconcileDelete(scope) + if err == nil { + controllerutil.RemoveFinalizer(scope.RosaRoleConfig, expinfrav1.RosaRoleConfigFinalizer) + } + + return ctrl.Result{}, err + } + + if controllerutil.AddFinalizer(scope.RosaRoleConfig, expinfrav1.RosaRoleConfigFinalizer) { + return ctrl.Result{}, err + } + + if err := r.reconcileAccountRoles(scope); err != nil { + conditions.MarkFalse(scope.RosaRoleConfig, expinfrav1.RosaRoleConfigReadyCondition, expinfrav1.RosaRoleConfigReconciliationFailedReason, clusterv1.ConditionSeverityError, "Account Roles failure: %v", err) + return ctrl.Result{}, fmt.Errorf("account Roles: %w", err) + } + + if err := r.reconcileOIDC(scope); err != nil { + conditions.MarkFalse(scope.RosaRoleConfig, expinfrav1.RosaRoleConfigReadyCondition, expinfrav1.RosaRoleConfigReconciliationFailedReason, clusterv1.ConditionSeverityError, "OIDC Config/provider failure: %v", err) + return ctrl.Result{}, fmt.Errorf("oicd Config: %w", err) + } + + if err := r.reconcileOperatorRoles(scope); err != nil { + conditions.MarkFalse(scope.RosaRoleConfig, expinfrav1.RosaRoleConfigReadyCondition, expinfrav1.RosaRoleConfigReconciliationFailedReason, clusterv1.ConditionSeverityError, "Operator Roles failure: %v", err) + return ctrl.Result{}, fmt.Errorf("operator Roles: %w", err) + } + + if r.rosaRolesConfigReady(scope.RosaRoleConfig) { + conditions.Set(scope.RosaRoleConfig, + &clusterv1.Condition{ + Type: expinfrav1.RosaRoleConfigReadyCondition, + Status: corev1.ConditionTrue, + Reason: expinfrav1.RosaRoleConfigCreatedReason, + Severity: clusterv1.ConditionSeverityInfo, + Message: "RosaRoleConfig is ready", + }) + } else { + conditions.Set(scope.RosaRoleConfig, + &clusterv1.Condition{ + Type: expinfrav1.RosaRoleConfigReadyCondition, + Status: corev1.ConditionFalse, + Reason: expinfrav1.RosaRoleConfigCreatedReason, + Severity: clusterv1.ConditionSeverityInfo, + Message: "RosaRoleConfig not ready", + }) + } + + return ctrl.Result{}, nil +} + +func (r *ROSARoleConfigReconciler) reconcileDelete(scope *scope.RosaRoleConfigScope) error { + if err := r.deleteOperatorRoles(scope); err != nil { + conditions.MarkFalse(scope.RosaRoleConfig, expinfrav1.RosaRoleConfigReadyCondition, expinfrav1.RosaRoleConfigDeletionFailedReason, clusterv1.ConditionSeverityError, "Failed to delete operator roles: %v", err) + return err + } + + if err := r.deleteOIDC(scope); err != nil { + conditions.MarkFalse(scope.RosaRoleConfig, expinfrav1.RosaRoleConfigReadyCondition, expinfrav1.RosaRoleConfigDeletionFailedReason, clusterv1.ConditionSeverityError, "Failed to delete OIDC provider: %v", err) + return err + } + + if err := r.deleteAccountRoles(scope); err != nil { + conditions.MarkFalse(scope.RosaRoleConfig, expinfrav1.RosaRoleConfigReadyCondition, expinfrav1.RosaRoleConfigDeletionFailedReason, clusterv1.ConditionSeverityError, "Failed to delete account roles: %v", err) + return err + } + + return nil +} + +func (r *ROSARoleConfigReconciler) reconcileOperatorRoles(scope *scope.RosaRoleConfigScope) error { + operatorRoles, err := r.Runtime.AWSClient.ListOperatorRoles("", "", scope.RosaRoleConfig.Spec.OperatorRoleConfig.Prefix) + if err != nil { + return err + } + + operatorRolesRef := v1beta2.AWSRolesRef{} + for _, role := range operatorRoles[scope.RosaRoleConfig.Spec.OperatorRoleConfig.Prefix] { + if strings.Contains(role.RoleName, expinfrav1.IngressOperatorARNSuffix) { + operatorRolesRef.IngressARN = role.RoleARN + } else if strings.Contains(role.RoleName, expinfrav1.ImageRegistryARNSuffix) { + operatorRolesRef.ImageRegistryARN = role.RoleARN + } else if strings.Contains(role.RoleName, expinfrav1.StorageARNSuffix) { + operatorRolesRef.StorageARN = role.RoleARN + } else if strings.Contains(role.RoleName, expinfrav1.NetworkARNSuffix) { + operatorRolesRef.NetworkARN = role.RoleARN + } else if strings.Contains(role.RoleName, expinfrav1.KubeCloudControllerARNSuffix) { + operatorRolesRef.KubeCloudControllerARN = role.RoleARN + } else if strings.Contains(role.RoleName, expinfrav1.NodePoolManagementARNSuffix) { + operatorRolesRef.NodePoolManagementARN = role.RoleARN + } else if strings.Contains(role.RoleName, expinfrav1.ControlPlaneOperatorARNSuffix) { + operatorRolesRef.ControlPlaneOperatorARN = role.RoleARN + } else if strings.Contains(role.RoleName, expinfrav1.KMSProviderARNSuffix) { + operatorRolesRef.KMSProviderARN = role.RoleARN + } + } + + if r.operatorRolesReady(operatorRolesRef) { + scope.RosaRoleConfig.Status.OperatorRolesRef = operatorRolesRef + return nil + } + + installerRoleArn := scope.RosaRoleConfig.Status.AccountRolesRef.InstallerRoleARN + if installerRoleArn == "" { + scope.Logger.Info("installerRoleARN is empty, waiting for installer role to be created.") + return nil + } + oidcConfigID := scope.RosaRoleConfig.Status.OIDCID + if oidcConfigID == "" { + scope.Logger.Info("oidcID is empty, waiting for oidcConfig to be created.") + return nil + } + + policies, err := r.Runtime.OCMClient.GetPolicies("OperatorRole") + if err != nil { + return err + } + + // create operator roles + config := scope.RosaRoleConfig.Spec.OperatorRoleConfig + return operatorroles.CreateOperatorRoles(r.Runtime, rosa.GetOCMClientEnv(r.Runtime.OCMClient), config.PermissionsBoundaryARN, + interactive.ModeAuto, policies, "", config.SharedVPCConfig.IsSharedVPC(), config.Prefix, true, installerRoleArn, + true, oidcConfigID, config.SharedVPCConfig.RouteRoleARN, ocm.DefaultChannelGroup, + config.SharedVPCConfig.VPCEndpointRoleARN) +} + +func (r *ROSARoleConfigReconciler) reconcileOIDC(scope *scope.RosaRoleConfigScope) error { + oidcID := "" + switch scope.RosaRoleConfig.Spec.OidcProviderType { + case expinfrav1.Managed: + // Create oidcConfig if not exist + if scope.RosaRoleConfig.Status.OIDCID == "" { + oidcID, createErr := oidcconfig.CreateOIDCConfig(r.Runtime, true, "", "") + if createErr != nil { + return fmt.Errorf("failed to Create OIDC config: %w", createErr) + } + scope.RosaRoleConfig.Status.OIDCID = oidcID + } + oidcID = scope.RosaRoleConfig.Status.OIDCID + case expinfrav1.Unmanaged: + oidcID = scope.RosaRoleConfig.Spec.OperatorRoleConfig.OIDCID + } + + // Check if oidc Config exist + oidcConfig, err := r.Runtime.OCMClient.GetOidcConfig(oidcID) + if err != nil || oidcConfig == nil { + return fmt.Errorf("failed to get OIDC config: %w", err) + } + + scope.RosaRoleConfig.Status.OIDCID = oidcConfig.ID() + + // check oidc providers + providers, err := r.Runtime.AWSClient.ListOidcProviders("", oidcConfig) + if err != nil { + return err + } + + // set oidc Provider Arn + for _, provider := range providers { + if strings.Contains(provider.Arn, oidcID) { + scope.RosaRoleConfig.Status.OIDCProviderARN = provider.Arn + return nil + } + } + + // create oidc provider if not exist. + if scope.RosaRoleConfig.Status.OIDCProviderARN == "" { + if err := oidcprovider.CreateOIDCProvider(r.Runtime, oidcID, "", true); err != nil { + return err + } + providerArn, err := r.Runtime.AWSClient.GetOpenIDConnectProviderByOidcEndpointUrl(oidcConfig.IssuerUrl()) + if err != nil { + return err + } + scope.RosaRoleConfig.Status.OIDCProviderARN = providerArn + } + + return nil +} + +func (r *ROSARoleConfigReconciler) reconcileAccountRoles(scope *scope.RosaRoleConfigScope) error { + accountRoles, err := r.Runtime.AWSClient.ListAccountRoles(scope.RosaRoleConfig.Spec.AccountRoleConfig.Version) + if err != nil { + // ListAccountRoles return error if roles does not exist. return for any other error + if !strings.Contains(err.Error(), "no account roles found") { + return err + } + } + + accountRolesRef := expinfrav1.AccountRolesRef{} + for _, role := range accountRoles { + if role.RoleName == fmt.Sprintf("%s%s", scope.RosaRoleConfig.Spec.AccountRoleConfig.Prefix, expinfrav1.HCPROSAInstallerRole) { + accountRolesRef.InstallerRoleARN = role.RoleARN + } else if role.RoleName == fmt.Sprintf("%s%s", scope.RosaRoleConfig.Spec.AccountRoleConfig.Prefix, expinfrav1.HCPROSASupportRole) { + accountRolesRef.SupportRoleARN = role.RoleARN + } else if role.RoleName == fmt.Sprintf("%s%s", scope.RosaRoleConfig.Spec.AccountRoleConfig.Prefix, expinfrav1.HCPROSAWorkerRole) { + accountRolesRef.WorkerRoleARN = role.RoleARN + } + } + + // Set account role ref if ready + if r.accountRolesReady(accountRolesRef) { + scope.RosaRoleConfig.Status.AccountRolesRef = accountRolesRef + return nil + } + + policies, err := r.Runtime.OCMClient.GetPolicies("AccountRole") + if err != nil { + return err + } + + return accountroles.CreateHCPRoles(r.Runtime, scope.RosaRoleConfig.Spec.AccountRoleConfig.Prefix, true, scope.RosaRoleConfig.Spec.AccountRoleConfig.PermissionsBoundaryARN, + rosa.GetOCMClientEnv(r.Runtime.OCMClient), policies, scope.RosaRoleConfig.Spec.AccountRoleConfig.Version, scope.RosaRoleConfig.Spec.AccountRoleConfig.Path, + scope.RosaRoleConfig.Spec.AccountRoleConfig.SharedVPCConfig.IsSharedVPC(), scope.RosaRoleConfig.Spec.AccountRoleConfig.SharedVPCConfig.RouteRoleARN, + scope.RosaRoleConfig.Spec.AccountRoleConfig.SharedVPCConfig.VPCEndpointRoleARN) +} + +func (r *ROSARoleConfigReconciler) deleteAccountRoles(scope *scope.RosaRoleConfigScope) error { + // list all account role names. + prefix := scope.RosaRoleConfig.Spec.AccountRoleConfig.Prefix + hasSharedVpcPolicies := scope.RosaRoleConfig.Spec.AccountRoleConfig.SharedVPCConfig.IsSharedVPC() + roleNames := []string{fmt.Sprintf("%s%s", prefix, expinfrav1.HCPROSAInstallerRole), + fmt.Sprintf("%s%s", prefix, expinfrav1.HCPROSASupportRole), + fmt.Sprintf("%s%s", prefix, expinfrav1.HCPROSAWorkerRole)} + + var errs []error + for _, roleName := range roleNames { + if err := r.Runtime.AWSClient.DeleteAccountRole(roleName, prefix, true, hasSharedVpcPolicies); err != nil { + errs = append(errs, err) + } + } + + return kerrors.NewAggregate(errs) +} + +func (r *ROSARoleConfigReconciler) deleteOIDC(scope *scope.RosaRoleConfigScope) error { + // Delete only managed oidc + if scope.RosaRoleConfig.Spec.OidcProviderType == expinfrav1.Managed && scope.RosaRoleConfig.Status.OIDCID != "" { + oidcConfig, err := r.Runtime.OCMClient.GetOidcConfig(scope.RosaRoleConfig.Status.OIDCID) + if err != nil { + return err + } + + oidcEndpointURL := oidcConfig.IssuerUrl() + if usedOidcProvider, err := r.Runtime.OCMClient.HasAClusterUsingOidcProvider(oidcEndpointURL, r.Runtime.Creator.AccountID); err != nil { + return err + } else if usedOidcProvider { + return fmt.Errorf("clusters using OIDC provider '%s', cannot be deleted", oidcEndpointURL) + } + + if err = r.Runtime.AWSClient.DeleteOpenIDConnectProvider(scope.RosaRoleConfig.Status.OIDCProviderARN); err != nil { + return err + } + + return r.Runtime.OCMClient.DeleteOidcConfig(oidcConfig.ID()) + } + + return nil +} + +func (r *ROSARoleConfigReconciler) deleteOperatorRoles(scope *scope.RosaRoleConfigScope) error { + prefix := scope.RosaRoleConfig.Spec.OperatorRoleConfig.Prefix + if usedOperatorRoles, err := r.Runtime.OCMClient.HasAClusterUsingOperatorRolesPrefix(prefix); err != nil { + return err + } else if usedOperatorRoles { + return fmt.Errorf("operator Roles with Prefix '%s' are in use cannot be deleted", prefix) + } + + // list all operator role names. + roleNames := []string{fmt.Sprintf("%s%s", prefix, expinfrav1.ControlPlaneOperatorARNSuffix), + fmt.Sprintf("%s%s", prefix, expinfrav1.ImageRegistryARNSuffix), + fmt.Sprintf("%s%s", prefix, expinfrav1.IngressOperatorARNSuffix), + fmt.Sprintf("%s%s", prefix, expinfrav1.KMSProviderARNSuffix), + fmt.Sprintf("%s%s", prefix, expinfrav1.KubeCloudControllerARNSuffix), + fmt.Sprintf("%s%s", prefix, expinfrav1.NetworkARNSuffix), + fmt.Sprintf("%s%s", prefix, expinfrav1.NodePoolManagementARNSuffix), + fmt.Sprintf("%s%s", prefix, expinfrav1.StorageARNSuffix)} + + allSharedVpcPoliciesNotDeleted := make(map[string]bool) + var errs []error + for _, roleName := range roleNames { + policiesNotDeleted, err := r.Runtime.AWSClient.DeleteOperatorRole(roleName, true, true) + if err != nil && (!strings.Contains(err.Error(), "does not exists") && !strings.Contains(err.Error(), "NoSuchEntity")) { + errs = append(errs, err) + } + + maps.Copy(allSharedVpcPoliciesNotDeleted, policiesNotDeleted) + } + + for policyOutput, notDeleted := range allSharedVpcPoliciesNotDeleted { + if notDeleted { + scope.Logger.Info("unable to delete policy %s: Policy still attached to other resources", policyOutput) + } + } + + return kerrors.NewAggregate(errs) +} + +func (r ROSARoleConfigReconciler) rosaRolesConfigReady(rosaRoleConfig *expinfrav1.ROSARoleConfig) bool { + return rosaRoleConfig.Status.OIDCID != "" && + r.operatorRolesReady(rosaRoleConfig.Status.OperatorRolesRef) && + r.accountRolesReady(rosaRoleConfig.Status.AccountRolesRef) +} + +func (r ROSARoleConfigReconciler) accountRolesReady(accountRolesRef expinfrav1.AccountRolesRef) bool { + return accountRolesRef.InstallerRoleARN != "" && + accountRolesRef.SupportRoleARN != "" && + accountRolesRef.WorkerRoleARN != "" +} + +func (r ROSARoleConfigReconciler) operatorRolesReady(operatorRolesRef v1beta2.AWSRolesRef) bool { + return operatorRolesRef.ControlPlaneOperatorARN != "" && + operatorRolesRef.ImageRegistryARN != "" && + operatorRolesRef.IngressARN != "" && + operatorRolesRef.KMSProviderARN != "" && + operatorRolesRef.KubeCloudControllerARN != "" && + operatorRolesRef.NetworkARN != "" && + operatorRolesRef.NodePoolManagementARN != "" && + operatorRolesRef.StorageARN != "" +} + +// 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 = reporter.CreateReporter() // &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 +} diff --git a/exp/controllers/rosaroleconfig_controller_test.go b/exp/controllers/rosaroleconfig_controller_test.go new file mode 100644 index 0000000000..8f6f370412 --- /dev/null +++ b/exp/controllers/rosaroleconfig_controller_test.go @@ -0,0 +1,899 @@ +/* +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" + "fmt" + "math/rand" + "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" + + rosacontrolplanev1 "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/util/conditions" +) + +// generateTestID creates a unique identifier for test resources. +func generateTestID() string { + return fmt.Sprintf("%d-%d", time.Now().UnixNano(), rand.Intn(10000)) +} + +func TestROSARoleConfigReconcileCreate(t *testing.T) { + RegisterTestingT(t) + g := NewWithT(t) + + // Generate unique test ID for resource isolation + testID := generateTestID() + + ssoServer := ocmsdk.MakeTCPServer() + apiServer := ocmsdk.MakeTCPServer() + defer ssoServer.Close() + defer apiServer.Close() + 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) + defer mockCtrl.Finish() + // 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() + + providerARN := "test-oidc-id-created" + mockIamClient.EXPECT().CreateOpenIDConnectProvider(gomock.Any(), gomock.Any(), gomock.Any()).Return( + &iamv2.CreateOpenIDConnectProviderOutput{OpenIDConnectProviderArn: &providerARN}, 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 OIDC config creation calls - POST /api/clusters_mgmt/v1/oidc_configs + apiServer.RouteToHandler("POST", "/api/clusters_mgmt/v1/oidc_configs", + ocmsdk.RespondWithJSON( + http.StatusCreated, `{"id": "test-oidc-id-created", "issuer_url": "https://test.oidc.url"}`, + ), + ) + // Additional OIDC config call mock for GET requests + apiServer.RouteToHandler("GET", "/api/clusters_mgmt/v1/oidc_configs/test-oidc-id-created", + ocmsdk.RespondWithJSON( + http.StatusOK, `{"id": "test-oidc-id-created", "issuer_url": "https://test.oidc.url"}`, + ), + ) + + // 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, "", + ), + ) + + // Create CRs with unique names to avoid conflicts + ns, err := testEnv.CreateNamespace(ctx, fmt.Sprintf("test-namespace-%s", testID)) + rosaRoleConfig := &expinfrav1.ROSARoleConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("test-rosa-role-%s", testID), + Namespace: ns.Name, + Finalizers: []string{expinfrav1.RosaRoleConfigFinalizer}, + }, + Spec: expinfrav1.ROSARoleConfigSpec{ + AccountRoleConfig: expinfrav1.AccountRoleConfig{ + Prefix: "test", + Version: "4.15.0", + }, + OperatorRoleConfig: expinfrav1.OperatorRoleConfig{ + Prefix: "test", + }, + OidcProviderType: expinfrav1.Managed, + }, + } + 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, + Runtime: r, + } + + // Call the Reconcile function + req := ctrl.Request{} + req.NamespacedName = types.NamespacedName{Name: rosaRoleConfig.Name, Namespace: rosaRoleConfig.Namespace} + _, errReconcile := reconciler.Reconcile(ctx, req) + + // Assertions - expect the installer role empty error since AccountRolesRef is not populated yet + g.Expect(errReconcile).ToNot(HaveOccurred()) + + // Sleep to ensure the status is updated + time.Sleep(100 * time.Millisecond) + + // Check the status of the ROSARoleConfig resource + updatedRoleConfig := &expinfrav1.ROSARoleConfig{} + err = reconciler.Client.Get(ctx, req.NamespacedName, updatedRoleConfig) + g.Expect(err).ToNot(HaveOccurred()) + + // We expect only oidcID to be set with first reconcile happen, Account roles and Operator roles should be empty + g.Expect(updatedRoleConfig.Status.OIDCID).To(Equal("test-oidc-id-created")) + g.Expect(updatedRoleConfig.Status.AccountRolesRef).To(Equal(expinfrav1.AccountRolesRef{})) + g.Expect(updatedRoleConfig.Status.OperatorRolesRef).To(Equal(rosacontrolplanev1.AWSRolesRef{})) + + // Ready condition should be false. + for _, condition := range updatedRoleConfig.Status.Conditions { + if condition.Type == expinfrav1.RosaRoleConfigReadyCondition { + g.Expect(condition.Status).To(Equal(corev1.ConditionFalse)) + break + } + } +} + +func TestROSARoleConfigReconcileExist(t *testing.T) { + RegisterTestingT(t) + g := NewWithT(t) + + // Generate unique test ID for resource isolation + testID := generateTestID() + + ssoServer := ocmsdk.MakeTCPServer() + apiServer := ocmsdk.MakeTCPServer() + defer ssoServer.Close() + defer apiServer.Close() + 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) + defer mockCtrl.Finish() + // mock iam client to expect ListRoles call - return existing account roles and operator roles + + mockAWSClient := aws.NewMockClient(mockCtrl) + mockAWSClient.EXPECT().HasManagedPolicies(gomock.Any()).Return(false, nil).AnyTimes() + mockAWSClient.EXPECT().HasHostedCPPolicies(gomock.Any()).Return(true, nil).AnyTimes() + + // Return existing account roles + mockAWSClient.EXPECT().ListAccountRoles(gomock.Any()).Return([]aws.Role{ + { + RoleName: "test-HCP-ROSA-Installer-Role", + RoleARN: "arn:aws:iam::123456789012:role/test-HCP-ROSA-Installer-Role", + }, + { + RoleName: "test-HCP-ROSA-Support-Role", + RoleARN: "arn:aws:iam::123456789012:role/test-HCP-ROSA-Support-Role", + }, + { + RoleName: "test-HCP-ROSA-Worker-Role", + RoleARN: "arn:aws:iam::123456789012:role/test-HCP-ROSA-Worker-Role", + }, + }, nil).AnyTimes() + + // Return existing operator roles + mockAWSClient.EXPECT().ListOperatorRoles(gomock.Any(), gomock.Any(), gomock.Any()).Return(map[string][]aws.OperatorRoleDetail{ + "test": { + { + RoleName: "test-openshift-ingress-operator-cloud-credentials", + RoleARN: "arn:aws:iam::123456789012:role/test-openshift-ingress-operator-cloud-credentials", + }, + { + RoleName: "test-openshift-image-registry-installer-cloud-credentials", + RoleARN: "arn:aws:iam::123456789012:role/test-openshift-image-registry-installer-cloud-credentials", + }, + { + RoleName: "test-openshift-cluster-csi-drivers-ebs-cloud-credentials", + RoleARN: "arn:aws:iam::123456789012:role/test-openshift-cluster-csi-drivers-ebs-cloud-credentials", + }, + { + RoleName: "test-openshift-cloud-network-config-controller-cloud-credentials", + RoleARN: "arn:aws:iam::123456789012:role/test-openshift-cloud-network-config-controller-cloud-credentials", + }, + { + RoleName: "test-kube-system-kube-controller-manager", + RoleARN: "arn:aws:iam::123456789012:role/test-kube-system-kube-controller-manager", + }, + { + RoleName: "test-kube-system-capa-controller-manager", + RoleARN: "arn:aws:iam::123456789012:role/test-kube-system-capa-controller-manager", + }, + { + RoleName: "test-kube-system-control-plane-operator", + RoleARN: "arn:aws:iam::123456789012:role/test-kube-system-control-plane-operator", + }, + { + RoleName: "test-kube-system-kms-provider", + RoleARN: "arn:aws:iam::123456789012:role/test-kube-system-kms-provider", + }, + }, + }, nil).AnyTimes() + // Return existing OIDC providers + mockAWSClient.EXPECT().ListOidcProviders(gomock.Any(), gomock.Any()).Return([]aws.OidcProviderOutput{ + { + Arn: "arn:aws:iam::123456789012:oidc-provider/test-existing-oidc-id", + }, + }, nil).AnyTimes() + + mockAWSClient.EXPECT().GetCreator().Return(&aws.Creator{ + ARN: "arn:aws:iam::123456789012:user/test-user", + AccountID: "123456789012", + IsSTS: false, + }, nil).AnyTimes() + + awsClient := mockAWSClient + + r := rosacli.NewRuntime() + r.OCMClient = ocmClient + r.AWSClient = awsClient + r.Creator = &aws.Creator{ + ARN: "arn:aws:iam::123456789012:user/test-user", + AccountID: "123456789012", + IsSTS: false, + } + + // mock ocm API calls - first call gets tris response + apiServer.AppendHandlers( + ocmsdk.RespondWithJSON( + http.StatusOK, "", + ), + ) + // Mock GetOidcConfig call - return existing OIDC config + apiServer.AppendHandlers( + ocmsdk.RespondWithJSON( + http.StatusOK, `{"id": "test-existing-oidc-id", "issuer_url": "https://test.existing.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`, + ), + ) + + // Mock existing OIDC config GET request + apiServer.RouteToHandler("GET", "/api/clusters_mgmt/v1/oidc_configs/test-existing-oidc-id", + ocmsdk.RespondWithJSON( + http.StatusOK, `{"id": "test-existing-oidc-id", "issuer_url": "https://test.existing.oidc.url"}`, + ), + ) + + // Create CRs with unique names to avoid conflicts + ns, err := testEnv.CreateNamespace(ctx, fmt.Sprintf("test-namespace-all-existing-%s", testID)) + g.Expect(err).ToNot(HaveOccurred()) + + rosaRoleConfig := &expinfrav1.ROSARoleConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("test-rosa-role-all-existing-%s", testID), + Namespace: ns.Name, + Finalizers: []string{expinfrav1.RosaRoleConfigFinalizer}, + }, + Spec: expinfrav1.ROSARoleConfigSpec{ + AccountRoleConfig: expinfrav1.AccountRoleConfig{ + Prefix: "test", + Version: "4.15.0", + }, + OperatorRoleConfig: expinfrav1.OperatorRoleConfig{ + Prefix: "test", + }, + OidcProviderType: expinfrav1.Managed, + }, + Status: expinfrav1.ROSARoleConfigStatus{ + OIDCID: "test-existing-oidc-id", + }, + } + + createObject(g, rosaRoleConfig, ns.Name) + defer cleanupObject(g, rosaRoleConfig) + + // Setup the reconciler with these mocks + reconciler := &ROSARoleConfigReconciler{ + Client: testEnv.Client, + Runtime: r, + } + + // Call the Reconcile function + req := ctrl.Request{} + req.NamespacedName = types.NamespacedName{Name: rosaRoleConfig.Name, Namespace: rosaRoleConfig.Namespace} + _, errReconcile := reconciler.Reconcile(ctx, req) + + // Assertions - since all resources exist, reconciliation should succeed + g.Expect(errReconcile).ToNot(HaveOccurred()) + + // Sleep to ensure the status is updated + time.Sleep(100 * time.Millisecond) + + // 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 all existing account roles are preserved + g.Expect(updatedRoleConfig.Status.AccountRolesRef.InstallerRoleARN).To(Equal("arn:aws:iam::123456789012:role/test-HCP-ROSA-Installer-Role")) + g.Expect(updatedRoleConfig.Status.AccountRolesRef.SupportRoleARN).To(Equal("arn:aws:iam::123456789012:role/test-HCP-ROSA-Support-Role")) + g.Expect(updatedRoleConfig.Status.AccountRolesRef.WorkerRoleARN).To(Equal("arn:aws:iam::123456789012:role/test-HCP-ROSA-Worker-Role")) + + // Verify OIDC config is preserved + g.Expect(updatedRoleConfig.Status.OIDCID).To(Equal("test-existing-oidc-id")) + g.Expect(updatedRoleConfig.Status.OIDCProviderARN).To(Equal("arn:aws:iam::123456789012:oidc-provider/test-existing-oidc-id")) + + // Verify operator roles are populated with existing roles + g.Expect(updatedRoleConfig.Status.OperatorRolesRef.IngressARN).To(Equal("arn:aws:iam::123456789012:role/test-openshift-ingress-operator-cloud-credentials")) + g.Expect(updatedRoleConfig.Status.OperatorRolesRef.ImageRegistryARN).To(Equal("arn:aws:iam::123456789012:role/test-openshift-image-registry-installer-cloud-credentials")) + g.Expect(updatedRoleConfig.Status.OperatorRolesRef.StorageARN).To(Equal("arn:aws:iam::123456789012:role/test-openshift-cluster-csi-drivers-ebs-cloud-credentials")) + g.Expect(updatedRoleConfig.Status.OperatorRolesRef.NetworkARN).To(Equal("arn:aws:iam::123456789012:role/test-openshift-cloud-network-config-controller-cloud-credentials")) + g.Expect(updatedRoleConfig.Status.OperatorRolesRef.KubeCloudControllerARN).To(Equal("arn:aws:iam::123456789012:role/test-kube-system-kube-controller-manager")) + g.Expect(updatedRoleConfig.Status.OperatorRolesRef.NodePoolManagementARN).To(Equal("arn:aws:iam::123456789012:role/test-kube-system-capa-controller-manager")) + g.Expect(updatedRoleConfig.Status.OperatorRolesRef.ControlPlaneOperatorARN).To(Equal("arn:aws:iam::123456789012:role/test-kube-system-control-plane-operator")) + g.Expect(updatedRoleConfig.Status.OperatorRolesRef.KMSProviderARN).To(Equal("arn:aws:iam::123456789012:role/test-kube-system-kms-provider")) + + // Should have a condition indicating success - expect Ready condition to be True + readyCondition := conditions.Get(updatedRoleConfig, expinfrav1.RosaRoleConfigReadyCondition) + g.Expect(readyCondition).ToNot(BeNil()) + g.Expect(readyCondition.Status).To(Equal(corev1.ConditionTrue)) + g.Expect(readyCondition.Reason).To(Equal(expinfrav1.RosaRoleConfigCreatedReason)) +} + +func TestROSARoleConfigReconcileDelete(t *testing.T) { + RegisterTestingT(t) + g := NewWithT(t) + + // Generate unique test ID for resource isolation + testID := generateTestID() + + ssoServer := ocmsdk.MakeTCPServer() + apiServer := ocmsdk.MakeTCPServer() + defer ssoServer.Close() + defer apiServer.Close() + 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) + defer mockCtrl.Finish() + mockAWSClient := aws.NewMockClient(mockCtrl) + mockAWSClient.EXPECT().HasManagedPolicies(gomock.Any()).Return(false, nil).AnyTimes() + mockAWSClient.EXPECT().HasHostedCPPolicies(gomock.Any()).Return(true, nil).AnyTimes() + mockAWSClient.EXPECT().GetOperatorRolesFromAccountByPrefix(gomock.Any(), gomock.Any()).Return([]string{ + "test-openshift-ingress-operator-cloud-credentials", + "test-openshift-image-registry-installer-cloud-credentials", + "test-openshift-cluster-csi-drivers-ebs-cloud-credentials", + "test-openshift-cloud-network-config-controller-cloud-credentials", + "test-kube-system-kube-controller-manager", + "test-kube-system-capa-controller-manager", + "test-kube-system-control-plane-operator", + "test-kube-system-kms-provider", + }, nil).AnyTimes() + + // Return existing account roles that will be deleted + mockAWSClient.EXPECT().ListAccountRoles(gomock.Any()).Return([]aws.Role{ + { + RoleName: "test-HCP-ROSA-Installer-Role", + RoleARN: "arn:aws:iam::123456789012:role/test-HCP-ROSA-Installer-Role", + }, + { + RoleName: "test-HCP-ROSA-Support-Role", + RoleARN: "arn:aws:iam::123456789012:role/test-HCP-ROSA-Support-Role", + }, + { + RoleName: "test-HCP-ROSA-Worker-Role", + RoleARN: "arn:aws:iam::123456789012:role/test-HCP-ROSA-Worker-Role", + }, + }, nil).AnyTimes() + + mockAWSClient.EXPECT().ListOperatorRoles(gomock.Any(), gomock.Any(), gomock.Any()).Return(map[string][]aws.OperatorRoleDetail{ + "test": { + { + RoleName: "test-openshift-ingress-operator-cloud-credentials", + RoleARN: "arn:aws:iam::123456789012:role/test-openshift-ingress-operator-cloud-credentials", + }, + { + RoleName: "test-openshift-image-registry-installer-cloud-credentials", + RoleARN: "arn:aws:iam::123456789012:role/test-openshift-image-registry-installer-cloud-credentials", + }, + { + RoleName: "test-openshift-cluster-csi-drivers-ebs-cloud-credentials", + RoleARN: "arn:aws:iam::123456789012:role/test-openshift-cluster-csi-drivers-ebs-cloud-credentials", + }, + { + RoleName: "test-openshift-cloud-network-config-controller-cloud-credentials", + RoleARN: "arn:aws:iam::123456789012:role/test-openshift-cloud-network-config-controller-cloud-credentials", + }, + { + RoleName: "test-kube-system-kube-controller-manager", + RoleARN: "arn:aws:iam::123456789012:role/test-kube-system-kube-controller-manager", + }, + { + RoleName: "test-kube-system-capa-controller-manager", + RoleARN: "arn:aws:iam::123456789012:role/test-kube-system-capa-controller-manager", + }, + { + RoleName: "test-kube-system-control-plane-operator", + RoleARN: "arn:aws:iam::123456789012:role/test-kube-system-control-plane-operator", + }, + { + RoleName: "test-kube-system-kms-provider", + RoleARN: "arn:aws:iam::123456789012:role/test-kube-system-kms-provider", + }, + }, + }, nil).AnyTimes() + + // Return existing OIDC providers that will be deleted + mockAWSClient.EXPECT().ListOidcProviders(gomock.Any(), gomock.Any()).Return([]aws.OidcProviderOutput{ + { + Arn: "arn:aws:iam::123456789012:oidc-provider/test-existing-oidc-id", + }, + }, nil).AnyTimes() + + // Delete operator roles (called individually for each role) + mockAWSClient.EXPECT().DeleteOperatorRole(gomock.Any(), gomock.Any(), true).Return(map[string]bool{}, nil).AnyTimes() + + // Mock OIDC provider deletion + mockAWSClient.EXPECT().DeleteOpenIDConnectProvider("arn:aws:iam::123456789012:oidc-provider/test-existing-oidc-id").Return(nil).AnyTimes() + + // Delete account roles (called individually for each role) + mockAWSClient.EXPECT().DeleteAccountRole(gomock.Any(), gomock.Any(), true, false).Return(nil).AnyTimes() + + mockAWSClient.EXPECT().GetCreator().Return(&aws.Creator{ + ARN: "arn:aws:iam::123456789012:user/test-user", + AccountID: "123456789012", + IsSTS: false, + }, nil).AnyTimes() + + awsClient := mockAWSClient + + r := rosacli.NewRuntime() + r.OCMClient = ocmClient + r.AWSClient = awsClient + r.Creator = &aws.Creator{ + ARN: "arn:aws:iam::123456789012:user/test-user", + AccountID: "123456789012", + IsSTS: false, + } + + // Mock OCM API calls + apiServer.AppendHandlers( + ocmsdk.RespondWithJSON( + http.StatusOK, "", + ), + ) + // Mock GetOidcConfig call - return existing OIDC config + apiServer.AppendHandlers( + ocmsdk.RespondWithJSON( + http.StatusOK, `{"id": "test-existing-oidc-id", "issuer_url": "https://test.existing.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`, + ), + ) + + // Mock existing OIDC config GET request + apiServer.RouteToHandler("GET", "/api/clusters_mgmt/v1/oidc_configs/test-existing-oidc-id", + ocmsdk.RespondWithJSON( + http.StatusOK, `{"id": "test-existing-oidc-id", "issuer_url": "https://test.existing.oidc.url"}`, + ), + ) + + // Mock OIDC config deletion + apiServer.RouteToHandler("DELETE", "/api/clusters_mgmt/v1/oidc_configs/test-existing-oidc-id", + ocmsdk.RespondWithJSON( + http.StatusOK, `{}`, + ), + ) + + // Create CRs with unique names to avoid conflicts + ns, err := testEnv.CreateNamespace(ctx, fmt.Sprintf("test-namespace-delete-%s", testID)) + g.Expect(err).ToNot(HaveOccurred()) + + // Create ROSARoleConfig with populated status (simulating existing resources) + rosaRoleConfig := &expinfrav1.ROSARoleConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("test-rosa-role-delete-%s", testID), + Namespace: ns.Name, + Finalizers: []string{expinfrav1.RosaRoleConfigFinalizer}, + // Set deletion timestamp to simulate deletion request + DeletionTimestamp: &metav1.Time{Time: time.Now()}, + }, + Spec: expinfrav1.ROSARoleConfigSpec{ + AccountRoleConfig: expinfrav1.AccountRoleConfig{ + Prefix: "test", + Version: "4.15.0", + }, + OperatorRoleConfig: expinfrav1.OperatorRoleConfig{ + Prefix: "test", + }, + OidcProviderType: expinfrav1.Managed, + }, + Status: expinfrav1.ROSARoleConfigStatus{ + OIDCID: "test-existing-oidc-id", + OIDCProviderARN: "arn:aws:iam::123456789012:oidc-provider/test-existing-oidc-id", + AccountRolesRef: expinfrav1.AccountRolesRef{ + InstallerRoleARN: "arn:aws:iam::123456789012:role/test-HCP-ROSA-Installer-Role", + SupportRoleARN: "arn:aws:iam::123456789012:role/test-HCP-ROSA-Support-Role", + WorkerRoleARN: "arn:aws:iam::123456789012:role/test-HCP-ROSA-Worker-Role", + }, + OperatorRolesRef: rosacontrolplanev1.AWSRolesRef{ + IngressARN: "arn:aws:iam::123456789012:role/test-openshift-ingress-operator-cloud-credentials", + ImageRegistryARN: "arn:aws:iam::123456789012:role/test-openshift-image-registry-installer-cloud-credentials", + StorageARN: "arn:aws:iam::123456789012:role/test-openshift-cluster-csi-drivers-ebs-cloud-credentials", + NetworkARN: "arn:aws:iam::123456789012:role/test-openshift-cloud-network-config-controller-cloud-credentials", + KubeCloudControllerARN: "arn:aws:iam::123456789012:role/test-kube-system-kube-controller-manager", + NodePoolManagementARN: "arn:aws:iam::123456789012:role/test-kube-system-capa-controller-manager", + ControlPlaneOperatorARN: "arn:aws:iam::123456789012:role/test-kube-system-control-plane-operator", + KMSProviderARN: "arn:aws:iam::123456789012:role/test-kube-system-kms-provider", + }, + }, + } + + createObject(g, rosaRoleConfig, ns.Name) + defer cleanupObject(g, rosaRoleConfig) + + // Setup the reconciler with these mocks + reconciler := &ROSARoleConfigReconciler{ + Client: testEnv.Client, + Runtime: r, + } + + // Call the Reconcile function + req := ctrl.Request{} + req.NamespacedName = types.NamespacedName{Name: rosaRoleConfig.Name, Namespace: rosaRoleConfig.Namespace} + + err = reconciler.Client.Delete(ctx, rosaRoleConfig) + g.Expect(err).ToNot(HaveOccurred()) + + // Sleep to ensure the status is updated + time.Sleep(100 * time.Millisecond) + + _, errReconcile := reconciler.Reconcile(ctx, req) + + // Assertions - deletion should succeed + g.Expect(errReconcile).ToNot(HaveOccurred()) + + // Sleep to ensure the status is updated + time.Sleep(100 * time.Millisecond) + + deletedRoleConfig := &expinfrav1.ROSARoleConfig{} + + // Verify the resource has been deleted (finalizers removed) + err = reconciler.Client.Get(ctx, req.NamespacedName, deletedRoleConfig) + + // The object should either be not found (fully deleted) or have no finalizers + if err == nil { + // If object still exists, verify finalizers are removed + g.Expect(deletedRoleConfig.Finalizers).To(BeEmpty(), "Finalizers should be removed after successful deletion") + } +} 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/exp/utils/rosa_helper.go b/exp/utils/rosa_helper.go index fc08747874..b43cf47991 100644 --- a/exp/utils/rosa_helper.go +++ b/exp/utils/rosa_helper.go @@ -26,6 +26,7 @@ import ( "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/utils/ptr" + rosacontrolplanev1 "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/rosa" ) @@ -49,7 +50,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/utils/rosa_helper_test.go b/exp/utils/rosa_helper_test.go index f298ea9be4..08191a72f5 100644 --- a/exp/utils/rosa_helper_test.go +++ b/exp/utils/rosa_helper_test.go @@ -27,6 +27,7 @@ import ( "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/utils/ptr" + rosacontrolplanev1 "sigs.k8s.io/cluster-api-provider-aws/v2/controlplane/rosa/api/v1beta2" expinfrav1 "sigs.k8s.io/cluster-api-provider-aws/v2/exp/api/v1beta2" ) @@ -81,7 +82,7 @@ func TestNodePoolToRosaMachinePoolSpec(t *testing.T) { TuningConfigs: []string{"tuning1"}, AdditionalSecurityGroups: []string{"sg-123", "sg-456"}, VolumeSize: 120, - Autoscaling: &expinfrav1.RosaMachinePoolAutoScaling{ + Autoscaling: &rosacontrolplanev1.AutoScaling{ MinReplicas: 2, MaxReplicas: 5, }, diff --git a/go.mod b/go.mod index 1ffc472259..39a9a6ca2e 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 @@ -219,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 @@ -244,3 +247,18 @@ 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/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 61860ee4e4..762b29c932 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= @@ -28,6 +29,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= @@ -145,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= @@ -155,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= @@ -167,6 +174,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 +220,10 @@ 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-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= @@ -227,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= @@ -241,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= @@ -292,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= @@ -314,6 +330,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= @@ -326,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= @@ -356,28 +405,45 @@ 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= 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= +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= 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= @@ -439,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= @@ -458,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= @@ -468,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= @@ -494,9 +570,12 @@ 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= +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= @@ -528,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= @@ -569,18 +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= @@ -590,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= @@ -600,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= @@ -620,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= @@ -644,11 +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= @@ -658,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= @@ -699,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= @@ -726,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/main.go b/main.go index 8aac35b373..e3e6e7bc2b 100644 --- a/main.go +++ b/main.go @@ -279,8 +279,22 @@ func main() { setupLog.Error(err, "unable to create webhook", "webhook", "ROSAMachinePool") os.Exit(1) } - } + setupLog.Debug("enabling ROSA role config controller") + if err = (&expcontrollers.ROSARoleConfigReconciler{ + Client: mgr.GetClient(), + Recorder: mgr.GetEventRecorderFor("rosaroleconfig-controller"), + WatchFilterValue: watchFilterValue, + }).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..bc9edbbb2b --- /dev/null +++ b/pkg/cloud/scope/rosaroleconfig.go @@ -0,0 +1,165 @@ +/* + 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 scope + +import ( + "context" + + "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" + "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 + 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 aws.Config + iamClient *iam.Client +} + +// 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.Logger) + + if err != nil { + return nil, errors.Errorf("failed to create aws V2 session: %v", err) + } + + iamClient := iam.NewFromConfig(*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 V2 session. Used for creating clients. +func (s *RosaRoleConfigScope) Session() aws.Config { + 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.Background(), + s.RosaRoleConfig) +} + +// 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.Client { + return s.iamClient +} diff --git a/pkg/rosa/client.go b/pkg/rosa/client.go index 90670772c3..43d3d33565 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 @@ -28,8 +44,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 +85,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 +136,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 +146,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 +159,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) { @@ -152,3 +197,21 @@ func ocmCredentials(ctx context.Context, rosaScope *scope.ROSAControlPlaneScope) return token, ocmAPIUrl, ocmClientID, ocmClientSecret, nil } + +// GetOCMClientEnv return env name based on ocmCient assigned url defaults to production. +// "production": "https://api.openshift.com", +// "staging": "https://api.stage.openshift.com", +// "integration": "https://api.integration.openshift.com", +// "local": "http://localhost:8000", +// "local-proxy": "http://localhost:9000", +// "crc": "https://clusters-service.apps-crc.testing", +func GetOCMClientEnv(ocmClient *ocm.Client) string { + for k, v := range ocm.URLAliases { + if v == ocmClient.GetConnectionURL() { + return k + } + } + + // Defaults to production + return ocm.Production +} diff --git a/pkg/rosa/ocmclient.go b/pkg/rosa/ocmclient.go index 04c4fa700a..d13292cb67 100644 --- a/pkg/rosa/ocmclient.go +++ b/pkg/rosa/ocmclient.go @@ -1,8 +1,25 @@ +/* +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 import ( "context" + "fmt" v1 "github.com/openshift-online/ocm-sdk-go/clustersmgmt/v1" "github.com/openshift/rosa/pkg/aws" @@ -30,11 +47,12 @@ 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) GetNodePools(clusterID string) ([]*v1.NodePool, 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) @@ -108,6 +126,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) } @@ -136,3 +158,16 @@ 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 { + 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 +} diff --git a/templates/cluster-template-rosa-role-config.yaml b/templates/cluster-template-rosa-role-config.yaml new file mode 100644 index 0000000000..54130d1c5e --- /dev/null +++ b/templates/cluster-template-rosa-role-config.yaml @@ -0,0 +1,57 @@ +--- +apiVersion: infrastructure.cluster.x-k8s.io/v1beta2 +kind: ROSARoleConfig +metadata: + name: "${CLUSTER_NAME}-role-config" +spec: + accountRoleConfig: + prefix: "${ACCOUNT_ROLES_PREFIX}" + version: "${OPENSHIFT_VERSION}" + operatorRoleConfig: + prefix: "${OPERATOR_ROLES_PREFIX}" + credentialsSecretRef: + name: rosa-creds-secret + oidcProviderType: Managed +--- +apiVersion: cluster.x-k8s.io/v1beta1 +kind: Cluster +metadata: + name: "${CLUSTER_NAME}" +spec: + clusterNetwork: + pods: + cidrBlocks: ["192.168.0.0/16"] + infrastructureRef: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta2 + kind: ROSACluster + name: "${CLUSTER_NAME}" + controlPlaneRef: + apiVersion: controlplane.cluster.x-k8s.io/v1beta2 + kind: ROSAControlPlane + name: "${CLUSTER_NAME}-control-plane" +--- +apiVersion: infrastructure.cluster.x-k8s.io/v1beta2 +kind: ROSACluster +metadata: + name: "${CLUSTER_NAME}" +spec: {} +--- +apiVersion: controlplane.cluster.x-k8s.io/v1beta2 +kind: ROSAControlPlane +metadata: + name: "${CLUSTER_NAME}-control-plane" +spec: + rosaClusterName: ${CLUSTER_NAME:0:54} + version: "${OPENSHIFT_VERSION}" + region: "${AWS_REGION}" + network: + machineCIDR: "10.0.0.0/16" + subnets: + - "${PUBLIC_SUBNET_ID}" # remove if creating a private cluster + - "${PRIVATE_SUBNET_ID}" + availabilityZones: + - "${AWS_AVAILABILITY_ZONE}" + credentialsSecretRef: + name: rosa-creds-secret + rosaRoleConfigRef: + name: "${CLUSTER_NAME}-role-config" diff --git a/test/mocks/ocm_client_mock.go b/test/mocks/ocm_client_mock.go index 38da1767aa..b426c96598 100644 --- a/test/mocks/ocm_client_mock.go +++ b/test/mocks/ocm_client_mock.go @@ -305,6 +305,21 @@ func (mr *MockOCMClientMockRecorder) GetNodePools(arg0 interface{}) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNodePools", reflect.TypeOf((*MockOCMClient)(nil).GetNodePools), arg0) } +// 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()