-
Notifications
You must be signed in to change notification settings - Fork 83
registry+v1: add APIService renderer support (OPRUN-4723) #2885
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,6 +17,7 @@ import ( | |
| "k8s.io/apimachinery/pkg/util/intstr" | ||
| "k8s.io/apimachinery/pkg/util/sets" | ||
| "k8s.io/utils/ptr" | ||
| apiregistrationv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1" | ||
| "sigs.k8s.io/controller-runtime/pkg/client" | ||
|
|
||
| "github.com/operator-framework/api/pkg/operators/v1alpha1" | ||
|
|
@@ -73,6 +74,14 @@ func BundleCSVDeploymentGenerator(rv1 *bundle.RegistryV1, opts render.Options) ( | |
| webhookDeployments.Insert(wh.DeploymentName) | ||
| } | ||
|
|
||
| // collect deployments that service owned APIServices | ||
| apiServiceDeployments := sets.Set[string]{} | ||
| for _, desc := range rv1.CSV.Spec.APIServiceDefinitions.Owned { | ||
| if desc.DeploymentName != "" { | ||
| apiServiceDeployments.Insert(desc.DeploymentName) | ||
| } | ||
| } | ||
|
|
||
| objs := make([]client.Object, 0, len(rv1.CSV.Spec.InstallStrategy.StrategySpec.DeploymentSpecs)) | ||
| for _, depSpec := range rv1.CSV.Spec.InstallStrategy.StrategySpec.DeploymentSpecs { | ||
| // Add CSV annotations to template annotations | ||
|
|
@@ -100,7 +109,7 @@ func BundleCSVDeploymentGenerator(rv1 *bundle.RegistryV1, opts render.Options) ( | |
| ) | ||
|
|
||
| secretInfo := render.CertProvisionerFor(depSpec.Name, opts).GetCertSecretInfo() | ||
| if webhookDeployments.Has(depSpec.Name) && secretInfo != nil { | ||
| if (webhookDeployments.Has(depSpec.Name) || apiServiceDeployments.Has(depSpec.Name)) && secretInfo != nil { | ||
| ensureCorrectDeploymentCertVolumes(deploymentResource, *secretInfo) | ||
| } | ||
|
|
||
|
|
@@ -414,6 +423,103 @@ func BundleMutatingWebhookResourceGenerator(rv1 *bundle.RegistryV1, opts render. | |
| return objs, nil | ||
| } | ||
|
|
||
| // BundleCSVAPIServiceGenerator generates APIService resources and the supporting RBAC | ||
| // for each entry in csv.spec.apiservicedefinitions.owned, matching OLMv0 behavior: | ||
| // | ||
| // - APIService object (group+version, service reference, CA bundle injection) | ||
| // - ClusterRoleBinding <service>-system:auth-delegator — lets kube-apiserver delegate | ||
| // TokenReview/SubjectAccessReview to the extension API server (required for aggregation auth) | ||
| // - RoleBinding <service>-auth-reader in kube-system — lets the extension API server read | ||
| // the extension-apiserver-authentication ConfigMap (required for reading client CA config) | ||
| // | ||
| // Priority values follow OLMv0 conventions: GroupPriorityMinimum=2000, VersionPriority=15. | ||
| func BundleCSVAPIServiceGenerator(rv1 *bundle.RegistryV1, opts render.Options) ([]client.Object, error) { | ||
| if rv1 == nil { | ||
| return nil, fmt.Errorf("bundle cannot be nil") | ||
| } | ||
|
|
||
| // Build a map from deployment name → ServiceAccount name for RBAC subject lookup. | ||
| depSAName := make(map[string]string, len(rv1.CSV.Spec.InstallStrategy.StrategySpec.DeploymentSpecs)) | ||
| for _, dep := range rv1.CSV.Spec.InstallStrategy.StrategySpec.DeploymentSpecs { | ||
| depSAName[dep.Name] = saNameOrDefault(dep.Spec.Template.Spec.ServiceAccountName) | ||
| } | ||
|
|
||
| var objs []client.Object | ||
| for _, desc := range rv1.CSV.Spec.APIServiceDefinitions.Owned { | ||
| certProvisioner := render.CertProvisionerFor(desc.DeploymentName, opts) | ||
|
|
||
| containerPort := desc.ContainerPort | ||
| if containerPort == 0 { | ||
| containerPort = 443 | ||
| } | ||
|
|
||
| apiService := &apiregistrationv1.APIService{ | ||
| TypeMeta: metav1.TypeMeta{ | ||
| APIVersion: "apiregistration.k8s.io/v1", | ||
| Kind: "APIService", | ||
| }, | ||
| ObjectMeta: metav1.ObjectMeta{ | ||
| Name: desc.GetName(), // "<version>.<group>" | ||
| }, | ||
| Spec: apiregistrationv1.APIServiceSpec{ | ||
| Group: desc.Group, | ||
| Version: desc.Version, | ||
| GroupPriorityMinimum: 2000, | ||
| VersionPriority: 15, | ||
| Service: &apiregistrationv1.ServiceReference{ | ||
| Namespace: opts.InstallNamespace, | ||
| Name: certProvisioner.ServiceName, | ||
| Port: &containerPort, | ||
| }, | ||
| InsecureSkipTLSVerify: false, | ||
| }, | ||
| } | ||
|
|
||
| if err := certProvisioner.InjectCABundle(apiService); err != nil { | ||
| return nil, err | ||
| } | ||
| objs = append(objs, apiService) | ||
|
|
||
| // The ServiceAccount that runs the extension API server deployment. | ||
| saName := saNameOrDefault(depSAName[desc.DeploymentName]) | ||
| subject := rbacv1.Subject{ | ||
| Kind: "ServiceAccount", | ||
| Name: saName, | ||
| Namespace: opts.InstallNamespace, | ||
| } | ||
|
|
||
| // ClusterRoleBinding: <service>-system:auth-delegator | ||
| // Grants the extension API server's SA the system:auth-delegator ClusterRole so | ||
| // kube-apiserver can delegate TokenReview/SubjectAccessReview requests to it. | ||
| // Mirrors OLMv0 behavior: pkg/controller/install/certresources.go:500-520 | ||
| objs = append(objs, CreateClusterRoleBindingResource( | ||
| certProvisioner.ServiceName+"-system:auth-delegator", | ||
| WithSubjects(subject), | ||
| WithRoleRef(rbacv1.RoleRef{ | ||
| APIGroup: rbacv1.GroupName, | ||
| Kind: "ClusterRole", | ||
| Name: "system:auth-delegator", | ||
| }), | ||
| )) | ||
|
|
||
| // RoleBinding: <service>-auth-reader in kube-system | ||
| // Allows the extension API server's SA to read the extension-apiserver-authentication | ||
| // ConfigMap in kube-system, which contains the cluster's client CA and request-header config. | ||
| // Mirrors OLMv0 behavior: pkg/controller/install/certresources.go:522-537 | ||
| objs = append(objs, CreateRoleBindingResource( | ||
| certProvisioner.ServiceName+"-auth-reader", | ||
| "kube-system", | ||
| WithSubjects(subject), | ||
| WithRoleRef(rbacv1.RoleRef{ | ||
| APIGroup: rbacv1.GroupName, | ||
| Kind: "Role", | ||
| Name: "extension-apiserver-authentication-reader", | ||
| }), | ||
| )) | ||
| } | ||
| return objs, nil | ||
| } | ||
|
|
||
| // BundleDeploymentServiceResourceGenerator generates Service resources that support, e.g. the webhooks, | ||
| // defined in the bundle's cluster service version spec. The resource is modified by the CertificateProvider in opts | ||
| // to add any annotations or modifications necessary for certificate injection. | ||
|
|
@@ -422,14 +528,31 @@ func BundleDeploymentServiceResourceGenerator(rv1 *bundle.RegistryV1, opts rende | |
| return nil, fmt.Errorf("bundle cannot be nil") | ||
| } | ||
|
|
||
| // collect webhook service ports | ||
| // collect service ports from webhooks and owned APIService definitions | ||
| webhookServicePortsByDeployment := map[string]sets.Set[corev1.ServicePort]{} | ||
| for _, wh := range rv1.CSV.Spec.WebhookDefinitions { | ||
| if _, ok := webhookServicePortsByDeployment[wh.DeploymentName]; !ok { | ||
| webhookServicePortsByDeployment[wh.DeploymentName] = sets.Set[corev1.ServicePort]{} | ||
| } | ||
| webhookServicePortsByDeployment[wh.DeploymentName].Insert(getWebhookServicePort(wh)) | ||
| } | ||
| for _, desc := range rv1.CSV.Spec.APIServiceDefinitions.Owned { | ||
| if desc.DeploymentName == "" { | ||
| continue | ||
| } | ||
| port := desc.ContainerPort | ||
| if port == 0 { | ||
| port = 443 | ||
| } | ||
| if _, ok := webhookServicePortsByDeployment[desc.DeploymentName]; !ok { | ||
| webhookServicePortsByDeployment[desc.DeploymentName] = sets.Set[corev1.ServicePort]{} | ||
| } | ||
| webhookServicePortsByDeployment[desc.DeploymentName].Insert(corev1.ServicePort{ | ||
| Name: strconv.Itoa(int(port)), | ||
| Port: port, | ||
| TargetPort: intstr.FromInt32(port), | ||
| }) | ||
|
Comment on lines
+539
to
+554
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🌐 Web query:
💡 Result: In Kubernetes v1.36, defining two TCP ServicePort entries in the same Service with the same name and the same port is invalid [1][2]. Kubernetes imposes the following validation rules on the Service spec: 1. Unique Names: All ports within a ServiceSpec must have unique names [1][2]. Providing two ServicePort entries with the same name will cause validation to fail [1][2]. 2. Unique Port/Protocol Pairs: While the official API documentation for the Service object specifies that the Citations:
🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- target file context ---'
sed -n '480,575p' internal/operator-controller/rukpak/render/registryv1/generators/generators.go
printf '%s\n' '--- related symbols and call sites ---'
rg -n -C 3 'webhookServicePortsByDeployment|ServicePort|APIServiceDefinitions|DeploymentName' internal/operator-controller/rukpak/render/registryv1
printf '%s\n' '--- repository status and relevant tests ---'
git status --short
rg -n -C 3 'BundleDeploymentServiceResourceGenerator|webhook.*port|duplicate.*port|APIServiceDefinitions' internal/operator-controller/rukpak/render/registryv1 --glob '*_test.go'Repository: operator-framework/operator-controller Length of output: 50394 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- service rendering ---'
sed -n '557,645p' internal/operator-controller/rukpak/render/registryv1/generators/generators.go
printf '%s\n' '--- focused service-generator tests ---'
sed -n '2040,2265p' internal/operator-controller/rukpak/render/registryv1/generators/generators_test.go
printf '%s\n' '--- APIService generator and validator definitions ---'
sed -n '430,470p' internal/operator-controller/rukpak/render/registryv1/generators/generators.go
rg -n -C 5 'CheckOwned.*APIService|DeploymentName.*APIService|APIService.*DeploymentName|TargetPort' internal/operator-controller/rukpak/render/registryv1/validators internal/operator-controller/rukpak/render/registryv1/generators --glob '*.go'Repository: operator-framework/operator-controller Length of output: 33462 🏁 Script executed: #!/bin/bash
set -e
python3 - <<'PY'
from dataclasses import dataclass
`@dataclass`(frozen=True)
class ServicePort:
name: str
port: int
target_port: int
protocol: str = "TCP"
# getWebhookServicePort(443, targetPort=8443)
webhook = ServicePort("443", 443, 8443)
# APIService rendering with ContainerPort=443
api_service = ServicePort("443", 443, 443)
ports = list({webhook, api_service}) # models sets.Set[corev1.ServicePort]
ports.sort(key=lambda p: (p.port, p.target_port))
print("rendered ports:", ports)
print("set retains both:", len(ports) == 2)
print("duplicate names:", len({p.name for p in ports}) != len(ports))
print("duplicate port/protocol pairs:",
len({(p.port, p.protocol) for p in ports}) != len(ports))
assert len(ports) == 2
assert len({p.name for p in ports}) == 1
assert len({(p.port, p.protocol) for p in ports}) == 1
PYRepository: operator-framework/operator-controller Length of output: 411 Prevent conflicting Service ports for a shared deployment. If a webhook uses 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| objs := make([]client.Object, 0, len(webhookServicePortsByDeployment)) | ||
| for _, deploymentSpec := range rv1.CSV.Spec.InstallStrategy.StrategySpec.DeploymentSpecs { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: operator-framework/operator-controller
Length of output: 1825
🏁 Script executed:
Repository: operator-framework/operator-controller
Length of output: 4063
🏁 Script executed:
Repository: operator-framework/operator-controller
Length of output: 368
🏁 Script executed:
Repository: operator-framework/operator-controller
Length of output: 368
Mark
k8s.io/kube-aggregatoras a direct requirement.Go source files import this module directly, but
go.modmarks it indirect. Runmake tidy, include the resulting module-file changes, and record the required dependency-update discussion before merge.🤖 Prompt for AI Agents
Source: Coding guidelines