diff --git a/Dockerfile b/Dockerfile index 6c2f4bad41..b63141603c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -38,6 +38,7 @@ USER 1001 EXPOSE 8080 EXPOSE 5443 +EXPOSE 6789 # Apply labels as needed. ART build automation fills in others required for # shipping, including component NVR (name-version-release) and image name. OSBS diff --git a/Makefile b/Makefile index 40dbefe34f..b6828d029d 100644 --- a/Makefile +++ b/Makefile @@ -121,6 +121,7 @@ container: docker build -t $(IMAGE_REPO):$(IMAGE_TAG) . clean-e2e: + kubectl delete validatingwebhookconfigurations --all kubectl delete crds --all kubectl delete apiservices.apiregistration.k8s.io v1.packages.operators.coreos.com || true kubectl delete -f test/e2e/resources/0000_50_olm_00-namespace.yaml diff --git a/README.md b/README.md index 8264305e23..aaa667c1bd 100644 --- a/README.md +++ b/README.md @@ -27,11 +27,11 @@ This project does not: ## Prerequisites - [git][git_tool] -- [go][go_tool] version v1.12+. +- [go][go_tool] version v1.13+. - [docker][docker_tool] version 17.03+. - Alternatively [podman][podman_tool] `v1.2.0+` or [buildah][buildah_tool] `v1.7+` -- [kubectl][kubectl_tool] version v1.11.3+. -- Access to a Kubernetes v1.11.3+ cluster. +- [kubectl][kubectl_tool] version v1.16+. +- Access to a Kubernetes v1.16+ cluster. ## Getting Started diff --git a/cmd/olm/main.go b/cmd/olm/main.go index 334792f4d7..313e37b0fe 100644 --- a/cmd/olm/main.go +++ b/cmd/olm/main.go @@ -15,7 +15,9 @@ import ( log "github.com/sirupsen/logrus" v1 "k8s.io/api/core/v1" "k8s.io/client-go/tools/clientcmd" + "k8s.io/client-go/util/workqueue" + "github.com/operator-framework/operator-lifecycle-manager/pkg/admission" "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client" "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/operators/olm" "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorclient" @@ -173,6 +175,7 @@ func main() { if err != nil { log.Fatalf("error configuring client: %s", err.Error()) } + csvAdmitQueue := workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "csvAdmit") cleanup(logger, opClient, crClient) @@ -186,6 +189,7 @@ func main() { olm.WithOperatorClient(opClient), olm.WithRestConfig(config), olm.WithConfigClient(versionedConfigClient), + olm.WithCSVAdmissionQueue(csvAdmitQueue), ) if err != nil { log.WithError(err).Fatalf("error configuring operator") @@ -212,5 +216,16 @@ func main() { go monitor.Run(op.Done()) } + logger.Info("configuring admission") + + admissionMux := http.NewServeMux() + admissionMux.HandleFunc("/operator-admit", admission.AdmitHandlerFunc(csvAdmitQueue)) + go func() { + err := http.ListenAndServeTLS(":6789", "/webhook.local.config/certificates/webhook.crt", "/webhook.local.config/certificates/webhook.key", admissionMux) + if err != nil { + logger.Errorf("Admission Webhook serving failed: %v", err) + } + }() + <-op.Done() } diff --git a/deploy/chart/templates/0000_50_olm_07-olm-operator.deployment.yaml b/deploy/chart/templates/0000_50_olm_07-olm-operator.deployment.yaml index 09d3e8d991..9624a9582e 100644 --- a/deploy/chart/templates/0000_50_olm_07-olm-operator.deployment.yaml +++ b/deploy/chart/templates/0000_50_olm_07-olm-operator.deployment.yaml @@ -5,102 +5,4 @@ metadata: namespace: {{ .Values.namespace }} labels: app: olm-operator -spec: - strategy: - type: RollingUpdate - replicas: {{ .Values.olm.replicaCount }} - selector: - matchLabels: - app: olm-operator - template: - metadata: - labels: - app: olm-operator - spec: - serviceAccountName: olm-operator-serviceaccount - {{- if and .Values.installType (eq .Values.installType "ocp") }} - priorityClassName: "system-cluster-critical" - {{- end }} - containers: - - name: olm-operator - command: - - /bin/olm - args: - - -namespace - - $(OPERATOR_NAMESPACE) - {{- if .Values.watchedNamespaces }} - - -watchedNamespaces - - {{ .Values.watchedNamespaces }} - {{- end }} - {{- if .Values.olm.commandArgs }} - - {{ .Values.olm.commandArgs }} - {{- end }} - {{- if .Values.debug }} - - -debug - {{- end }} - {{- if .Values.writeStatusName }} - - -writeStatusName - - {{ .Values.writeStatusName }} - {{- end }} - {{- if .Values.writePackageServerStatusName }} - - -writePackageServerStatusName - - {{ .Values.writePackageServerStatusName }} - {{- end }} - {{- if .Values.olm.tlsCertPath }} - - -tls-cert - - {{ .Values.olm.tlsCertPath }} - {{- end }} - {{- if .Values.olm.tlsKeyPath }} - - -tls-key - - {{ .Values.olm.tlsKeyPath }} - {{- end }} - image: {{ .Values.olm.image.ref }} - imagePullPolicy: {{ .Values.olm.image.pullPolicy }} - ports: - - containerPort: {{ .Values.olm.service.internalPort }} - - containerPort: 8081 - name: metrics - protocol: TCP - livenessProbe: - httpGet: - path: /healthz - port: {{ .Values.olm.service.internalPort }} - readinessProbe: - httpGet: - path: /healthz - port: {{ .Values.olm.service.internalPort }} - terminationMessagePolicy: FallbackToLogsOnError - env: - {{ if and .Values.installType (eq .Values.installType "ocp") }} - - name: RELEASE_VERSION - value: "0.0.1-snapshot" - {{ end }} - - name: OPERATOR_NAMESPACE - valueFrom: - fieldRef: - fieldPath: metadata.namespace - - name: OPERATOR_NAME - value: olm-operator - {{- if .Values.olm.resources }} - resources: -{{ toYaml .Values.olm.resources | indent 12 }} - {{- end}} - {{ if and .Values.installType (eq .Values.installType "ocp") }} - volumeMounts: - - mountPath: /var/run/secrets/serving-cert - name: serving-cert - {{ end }} - {{ if and .Values.installType (eq .Values.installType "ocp") }} - volumes: - - name: serving-cert - secret: - secretName: olm-operator-serving-cert - {{ end }} - {{- if .Values.olm.nodeSelector }} - nodeSelector: -{{ toYaml .Values.olm.nodeSelector | indent 8 }} - {{- end }} - {{- if .Values.olm.tolerations }} - tolerations: -{{ toYaml .Values.olm.tolerations | indent 6 }} - {{- end }} +{{- include "olm.deployment-spec" . }} diff --git a/deploy/chart/templates/0000_50_olm_15-packageserver.clusterserviceversion.yaml b/deploy/chart/templates/0000_50_olm_15-packageserver.clusterserviceversion.yaml index 7c95f56920..7b27242b7b 100644 --- a/deploy/chart/templates/0000_50_olm_15-packageserver.clusterserviceversion.yaml +++ b/deploy/chart/templates/0000_50_olm_15-packageserver.clusterserviceversion.yaml @@ -1 +1,83 @@ -{{- include "packageserver.clusterserviceversion" . }} +apiVersion: operators.coreos.com/v1alpha1 +kind: ClusterServiceVersion +metadata: + name: packageserver + namespace: {{ .Values.namespace }} + labels: + olm.version: {{ .Chart.Version }} + {{- if .Values.writePackageServerStatusName }} + olm.clusteroperator.name: {{ .Values.writePackageServerStatusName }} + {{- end }} +spec: + displayName: Package Server + description: Represents an Operator package that is available from a given CatalogSource which will resolve to a ClusterServiceVersion. + minKubeVersion: {{ .Values.minKubeVersion }} + keywords: ['packagemanifests', 'olm', 'packages'] + maintainers: + - name: Red Hat + email: openshift-operators@redhat.com + provider: + name: Red Hat + links: + - name: Package Server + url: https://github.com/operator-framework/operator-lifecycle-manager/tree/master/pkg/package-server + installModes: + - type: OwnNamespace + supported: true + - type: SingleNamespace + supported: true + - type: MultiNamespace + supported: true + - type: AllNamespaces + supported: true + install: + strategy: deployment + spec: + clusterPermissions: + - serviceAccountName: olm-operator-serviceaccount + rules: + - apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create + - get + - apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - apiGroups: + - "operators.coreos.com" + resources: + - catalogsources + verbs: + - get + - list + - watch + - apiGroups: + - "packages.operators.coreos.com" + resources: + - packagemanifests + verbs: + - get + - list + deployments: + - name: packageserver + {{- include "packageserver.deployment-spec" . | nindent 8 }} + maturity: alpha + version: {{ .Chart.Version }} + apiservicedefinitions: + owned: + - group: packages.operators.coreos.com + version: v1 + kind: PackageManifest + name: packagemanifests + displayName: PackageManifest + description: A PackageManifest is a resource generated from existing CatalogSources and their ConfigMaps + deploymentName: packageserver + containerPort: {{ .Values.package.service.internalPort }} diff --git a/deploy/chart/templates/0000_50_olm_16-olm.clusterserviceversion.yaml b/deploy/chart/templates/0000_50_olm_16-olm.clusterserviceversion.yaml new file mode 100644 index 0000000000..4de6f8fcbb --- /dev/null +++ b/deploy/chart/templates/0000_50_olm_16-olm.clusterserviceversion.yaml @@ -0,0 +1,64 @@ +apiVersion: operators.coreos.com/v1alpha1 +kind: ClusterServiceVersion +metadata: + name: olm + namespace: {{ .Values.namespace }} + labels: + olm.version: {{ .Chart.Version }} +spec: + displayName: Operator Lifecycle Manager + description: later + minKubeVersion: {{ .Values.minKubeVersion }} + keywords: ['operat', 'olm', 'lifecycle'] + maintainers: + - name: Red Hat + email: openshift-operators@redhat.com + provider: + name: Red Hat + installModes: + - type: OwnNamespace + supported: true + - type: SingleNamespace + supported: true + - type: MultiNamespace + supported: true + - type: AllNamespaces + supported: true + install: + strategy: deployment + spec: + clusterPermissions: + - serviceAccountName: olm-operator-serviceaccount + rules: + - apiGroups: ["*"] + resources: ["*"] + verbs: ["*"] + - nonResourceURLs: ["*"] + verbs: ["*"] + deployments: + - name: olm-operator + {{- include "olm.deployment-spec" . | nindent 8 }} + maturity: alpha + version: {{ .Chart.Version }} + webhookdefinitions: + - name: csv.operators.coreos.com + type: ValidatingAdmissionWebhook + deploymentName: olm-operator + webhookPath: "/operator-admit" + containerPort: 6789 + sideEffects: "None" + failurePolicy: Fail + admissionReviewVersions: + - "v1" + - "v1beta1" + rules: + - apiGroups: + - "operators.coreos.com" + apiVersions: + - v1alpha1 + operations: + - CREATE + resources: + - clusterserviceversions + scope: "Namespaced" + timeoutSeconds: 1 diff --git a/deploy/chart/templates/0000_90_olm_00-service-monitor.yaml b/deploy/chart/templates/0000_90_olm_00-service-monitor.yaml index 023235e0d7..133cbf4867 100644 --- a/deploy/chart/templates/0000_90_olm_00-service-monitor.yaml +++ b/deploy/chart/templates/0000_90_olm_00-service-monitor.yaml @@ -36,7 +36,6 @@ metadata: labels: app: catalog-operator spec: - jobLabel: k8s-app endpoints: - bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token interval: 30s diff --git a/deploy/chart/templates/_olm.deployment-spec.yaml b/deploy/chart/templates/_olm.deployment-spec.yaml new file mode 100644 index 0000000000..c1dafd7448 --- /dev/null +++ b/deploy/chart/templates/_olm.deployment-spec.yaml @@ -0,0 +1,104 @@ +{{- define "olm.deployment-spec" }} +spec: + strategy: + type: RollingUpdate + replicas: {{ .Values.olm.replicaCount }} + selector: + matchLabels: + app: olm-operator + template: + metadata: + labels: + app: olm-operator + spec: + serviceAccountName: olm-operator-serviceaccount + {{- if and .Values.installType (eq .Values.installType "ocp") }} + priorityClassName: "system-cluster-critical" + {{- end }} + containers: + - name: olm-operator + command: + - /bin/olm + args: + - -namespace + - $(OPERATOR_NAMESPACE) + {{- if .Values.watchedNamespaces }} + - -watchedNamespaces + - {{ .Values.watchedNamespaces }} + {{- end }} + {{- if .Values.olm.commandArgs }} + - {{ .Values.olm.commandArgs }} + {{- end }} + {{- if .Values.debug }} + - -debug + {{- end }} + {{- if .Values.writeStatusName }} + - -writeStatusName + - {{ .Values.writeStatusName }} + {{- end }} + {{- if .Values.writePackageServerStatusName }} + - -writePackageServerStatusName + - {{ .Values.writePackageServerStatusName }} + {{- end }} + {{- if .Values.olm.tlsCertPath }} + - -tls-cert + - {{ .Values.olm.tlsCertPath }} + {{- end }} + {{- if .Values.olm.tlsKeyPath }} + - -tls-key + - {{ .Values.olm.tlsKeyPath }} + {{- end }} + image: {{ .Values.olm.image.ref }} + imagePullPolicy: {{ .Values.olm.image.pullPolicy }} + ports: + - containerPort: {{ .Values.olm.service.internalPort }} + - containerPort: 8081 + name: metrics + protocol: TCP + - containerPort: 6789 + name: admission + protocol: TCP + livenessProbe: + httpGet: + path: /healthz + port: {{ .Values.olm.service.internalPort }} + readinessProbe: + httpGet: + path: /healthz + port: {{ .Values.olm.service.internalPort }} + terminationMessagePolicy: FallbackToLogsOnError + env: + {{- if and .Values.installType (eq .Values.installType "ocp") }} + - name: RELEASE_VERSION + value: "0.0.1-snapshot" + {{- end }} + - name: OPERATOR_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: OPERATOR_NAME + value: olm-operator + {{- if .Values.olm.resources }} + resources: +{{ toYaml .Values.olm.resources | indent 12 }} + {{- end}} + volumeMounts: + {{- if and .Values.installType (eq .Values.installType "ocp") }} + - mountPath: /var/run/secrets/serving-cert + name: serving-cert + {{- end }} + volumes: + {{- if and .Values.installType (eq .Values.installType "ocp") }} + - name: serving-cert + secret: + secretName: olm-operator-serving-cert + {{- end }} + {{- if .Values.olm.nodeSelector }} + nodeSelector: +{{ toYaml .Values.olm.nodeSelector | indent 8 }} + {{- end }} + {{- if .Values.olm.tolerations }} + tolerations: +{{ toYaml .Values.olm.tolerations | indent 6 }} + {{- end }} +{{- end}} diff --git a/deploy/chart/templates/_packageserver.clusterserviceversion.yaml b/deploy/chart/templates/_packageserver.clusterserviceversion.yaml deleted file mode 100644 index 5739dff461..0000000000 --- a/deploy/chart/templates/_packageserver.clusterserviceversion.yaml +++ /dev/null @@ -1,85 +0,0 @@ -{{- define "packageserver.clusterserviceversion" -}} -apiVersion: operators.coreos.com/v1alpha1 -kind: ClusterServiceVersion -metadata: - name: packageserver - namespace: {{ .Values.namespace }} - labels: - olm.version: {{ .Chart.Version }} - {{- if .Values.writePackageServerStatusName }} - olm.clusteroperator.name: {{ .Values.writePackageServerStatusName }} - {{- end }} -spec: - displayName: Package Server - description: Represents an Operator package that is available from a given CatalogSource which will resolve to a ClusterServiceVersion. - minKubeVersion: {{ .Values.minKubeVersion }} - keywords: ['packagemanifests', 'olm', 'packages'] - maintainers: - - name: Red Hat - email: openshift-operators@redhat.com - provider: - name: Red Hat - links: - - name: Package Server - url: https://github.com/operator-framework/operator-lifecycle-manager/tree/master/pkg/package-server - installModes: - - type: OwnNamespace - supported: true - - type: SingleNamespace - supported: true - - type: MultiNamespace - supported: true - - type: AllNamespaces - supported: true - install: - strategy: deployment - spec: - clusterPermissions: - - serviceAccountName: olm-operator-serviceaccount - rules: - - apiGroups: - - authorization.k8s.io - resources: - - subjectaccessreviews - verbs: - - create - - get - - apiGroups: - - "" - resources: - - configmaps - verbs: - - get - - list - - watch - - apiGroups: - - "operators.coreos.com" - resources: - - catalogsources - verbs: - - get - - list - - watch - - apiGroups: - - "packages.operators.coreos.com" - resources: - - packagemanifests - verbs: - - get - - list - deployments: - - name: packageserver - {{- include "packageserver.deployment-spec" . | nindent 8 }} - maturity: alpha - version: {{ .Chart.Version }} - apiservicedefinitions: - owned: - - group: packages.operators.coreos.com - version: v1 - kind: PackageManifest - name: packagemanifests - displayName: PackageManifest - description: A PackageManifest is a resource generated from existing CatalogSources and their ConfigMaps - deploymentName: packageserver - containerPort: {{ .Values.package.service.internalPort }} -{{- end -}} diff --git a/deploy/ocp/manifests/0.10.0/0000_50_olm_02-services.yaml b/deploy/ocp/manifests/0.10.0/0000_50_olm_02-services.yaml index 21a4e4e6f1..eb6349d146 100644 --- a/deploy/ocp/manifests/0.10.0/0000_50_olm_02-services.yaml +++ b/deploy/ocp/manifests/0.10.0/0000_50_olm_02-services.yaml @@ -39,3 +39,17 @@ spec: selector: app: catalog-operator +--- +apiVersion: v1 +kind: Service +metadata: + name: olm-admission + namespace: openshift-operator-lifecycle-manager +spec: + ports: + - name: admission + port: 6789 + protocol: TCP + targetPort: admission + selector: + app: olm-operator diff --git a/deploy/ocp/manifests/0.10.0/0000_50_olm_07-olm-operator.deployment.yaml b/deploy/ocp/manifests/0.10.0/0000_50_olm_07-olm-operator.deployment.yaml index dfb3d440b9..39b1352cf0 100644 --- a/deploy/ocp/manifests/0.10.0/0000_50_olm_07-olm-operator.deployment.yaml +++ b/deploy/ocp/manifests/0.10.0/0000_50_olm_07-olm-operator.deployment.yaml @@ -39,6 +39,9 @@ spec: - containerPort: 8081 name: metrics protocol: TCP + - containerPort: 6789 + name: admission + protocol: TCP livenessProbe: httpGet: path: /healthz diff --git a/deploy/ocp/manifests/0.10.0/0000_50_olm_20-admission.yaml b/deploy/ocp/manifests/0.10.0/0000_50_olm_20-admission.yaml new file mode 100644 index 0000000000..9f572df7dd --- /dev/null +++ b/deploy/ocp/manifests/0.10.0/0000_50_olm_20-admission.yaml @@ -0,0 +1,25 @@ +--- +# Source: olm/templates/0000_50_olm_20-admission.yaml +apiVersion: admissionregistration.k8s.io/v1beta1 +kind: ValidatingWebhookConfiguration +metadata: + name: olm-admission +webhooks: + - name: csv.operators.olm.com + rules: + - apiGroups: + - "operators.olm.com" + apiVersions: + - v1alpha1 + operations: + - CREATE + resources: + - clusterserviceversions + scope: "Namespaced" + clientConfig: + service: + namespace: openshift-operator-lifecycle-manager + name: olm-admission + admissionReviewVersions: + - v1beta1 + timeoutSeconds: 1 diff --git a/deploy/ocp/manifests/0.12.0/0000_50_olm_02-services.yaml b/deploy/ocp/manifests/0.12.0/0000_50_olm_02-services.yaml index 21a4e4e6f1..91f6d29b4c 100644 --- a/deploy/ocp/manifests/0.12.0/0000_50_olm_02-services.yaml +++ b/deploy/ocp/manifests/0.12.0/0000_50_olm_02-services.yaml @@ -39,3 +39,4 @@ spec: selector: app: catalog-operator + diff --git a/deploy/ocp/manifests/0.12.0/0000_50_olm_07-olm-operator.deployment.yaml b/deploy/ocp/manifests/0.12.0/0000_50_olm_07-olm-operator.deployment.yaml index ce889b46d2..9aa9245ee5 100644 --- a/deploy/ocp/manifests/0.12.0/0000_50_olm_07-olm-operator.deployment.yaml +++ b/deploy/ocp/manifests/0.12.0/0000_50_olm_07-olm-operator.deployment.yaml @@ -68,13 +68,13 @@ spec: cpu: 10m memory: 160Mi - volumeMounts: + - mountPath: /var/run/secrets/serving-cert name: serving-cert - volumes: + - name: serving-cert secret: secretName: olm-operator-serving-cert diff --git a/deploy/ocp/manifests/0.12.0/0000_90_olm_00-service-monitor.yaml b/deploy/ocp/manifests/0.12.0/0000_90_olm_00-service-monitor.yaml index c8575e33c1..b6485f0aa6 100644 --- a/deploy/ocp/manifests/0.12.0/0000_90_olm_00-service-monitor.yaml +++ b/deploy/ocp/manifests/0.12.0/0000_90_olm_00-service-monitor.yaml @@ -38,7 +38,6 @@ metadata: labels: app: catalog-operator spec: - jobLabel: k8s-app endpoints: - bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token interval: 30s diff --git a/deploy/ocp/values.yaml b/deploy/ocp/values.yaml index e1548f392e..a6107ffccd 100644 --- a/deploy/ocp/values.yaml +++ b/deploy/ocp/values.yaml @@ -86,4 +86,4 @@ package: resources: requests: cpu: 10m - memory: 50Mi + memory: 50Mi \ No newline at end of file diff --git a/deploy/upstream/manifests/0.10.0/0000_50_olm_02-services.yaml b/deploy/upstream/manifests/0.10.0/0000_50_olm_02-services.yaml new file mode 100644 index 0000000000..7274d3976b --- /dev/null +++ b/deploy/upstream/manifests/0.10.0/0000_50_olm_02-services.yaml @@ -0,0 +1,17 @@ +--- +# Source: olm/templates/0000_50_olm_02-services.yaml + +--- +apiVersion: v1 +kind: Service +metadata: + name: olm-admission + namespace: olm +spec: + ports: + - name: admission + port: 6789 + protocol: TCP + targetPort: admission + selector: + app: olm-operator diff --git a/deploy/upstream/manifests/0.10.0/0000_50_olm_07-olm-operator.deployment.yaml b/deploy/upstream/manifests/0.10.0/0000_50_olm_07-olm-operator.deployment.yaml index 9a2cf186f5..daaa783532 100644 --- a/deploy/upstream/manifests/0.10.0/0000_50_olm_07-olm-operator.deployment.yaml +++ b/deploy/upstream/manifests/0.10.0/0000_50_olm_07-olm-operator.deployment.yaml @@ -34,6 +34,9 @@ spec: - containerPort: 8081 name: metrics protocol: TCP + - containerPort: 6789 + name: admission + protocol: TCP livenessProbe: httpGet: path: /healthz diff --git a/deploy/upstream/manifests/0.10.0/0000_50_olm_20-admission.yaml b/deploy/upstream/manifests/0.10.0/0000_50_olm_20-admission.yaml new file mode 100644 index 0000000000..188276be26 --- /dev/null +++ b/deploy/upstream/manifests/0.10.0/0000_50_olm_20-admission.yaml @@ -0,0 +1,25 @@ +--- +# Source: olm/templates/0000_50_olm_20-admission.yaml +apiVersion: admissionregistration.k8s.io/v1beta1 +kind: ValidatingWebhookConfiguration +metadata: + name: olm-admission +webhooks: + - name: csv.operators.olm.com + rules: + - apiGroups: + - "operators.olm.com" + apiVersions: + - v1alpha1 + operations: + - CREATE + resources: + - clusterserviceversions + scope: "Namespaced" + clientConfig: + service: + namespace: olm + name: olm-admission + admissionReviewVersions: + - v1beta1 + timeoutSeconds: 1 diff --git a/deploy/upstream/manifests/0.12.0/0000_50_olm_07-olm-operator.deployment.yaml b/deploy/upstream/manifests/0.12.0/0000_50_olm_07-olm-operator.deployment.yaml index 33a2f9bbc5..82514ce842 100644 --- a/deploy/upstream/manifests/0.12.0/0000_50_olm_07-olm-operator.deployment.yaml +++ b/deploy/upstream/manifests/0.12.0/0000_50_olm_07-olm-operator.deployment.yaml @@ -58,7 +58,9 @@ spec: cpu: 10m memory: 160Mi + volumeMounts: + volumes: nodeSelector: beta.kubernetes.io/os: linux diff --git a/deploy/upstream/quickstart/olm.yaml b/deploy/upstream/quickstart/olm.yaml index 606e1d4364..c3ae60ad60 100644 --- a/deploy/upstream/quickstart/olm.yaml +++ b/deploy/upstream/quickstart/olm.yaml @@ -99,7 +99,9 @@ spec: cpu: 10m memory: 160Mi + volumeMounts: + volumes: nodeSelector: beta.kubernetes.io/os: linux diff --git a/deploy/upstream/values.yaml b/deploy/upstream/values.yaml index aa082bc6bf..f0d0b8c120 100644 --- a/deploy/upstream/values.yaml +++ b/deploy/upstream/values.yaml @@ -28,4 +28,4 @@ package: service: internalPort: 5443 catalog_sources: -- rh-operators +- rh-operators \ No newline at end of file diff --git a/doc/install/local-values.yaml b/doc/install/local-values.yaml index 7b47f2a94e..46b86450ec 100644 --- a/doc/install/local-values.yaml +++ b/doc/install/local-values.yaml @@ -5,6 +5,7 @@ writeStatusName: '""' catalog_namespace: olm operator_namespace: operators debug: true +local: true olm: replicaCount: 1 diff --git a/doc/rbac-generation.md b/doc/rbac-generation.md new file mode 100644 index 0000000000..d47dce5187 --- /dev/null +++ b/doc/rbac-generation.md @@ -0,0 +1,21 @@ +# RBAC generation for Operator Development + +OLM ships with an Admission Webhook that: + + - Inspects ClusterServiceVersions that are being created + - Detects whether your user account has enough permission to create the RBAC required for the operator + - If so, creates the necessary RBAC for your operator. + +To test this out locally: + +```sh +$ make run-local +$ kubectl -n operators create csv.yaml +$ kubectl -n operators get rolebindings +``` + +Depending on how you are running locally, you may need to grant your user more permission. For example: + +```sh +$ kubectl create clusterrolebinding minikube-cluster-admin --clusterrole=cluster-admin --user=minikube-user +``` diff --git a/e2e.Dockerfile b/e2e.Dockerfile index 4acf1763d8..adcbf80d10 100644 --- a/e2e.Dockerfile +++ b/e2e.Dockerfile @@ -23,4 +23,5 @@ COPY --from=builder /go/src/github.com/operator-framework/operator-lifecycle-man COPY --from=builder /go/src/github.com/operator-framework/operator-lifecycle-manager/bin/package-server /bin/package-server EXPOSE 8080 EXPOSE 5443 +EXPOSE 6789 CMD ["/bin/olm"] diff --git a/manifests/0000_50_olm_02-services.yaml b/manifests/0000_50_olm_02-services.yaml index d6f6868cc3..51e614e848 100644 --- a/manifests/0000_50_olm_02-services.yaml +++ b/manifests/0000_50_olm_02-services.yaml @@ -37,3 +37,4 @@ spec: selector: app: catalog-operator + diff --git a/manifests/0000_50_olm_07-olm-operator.deployment.yaml b/manifests/0000_50_olm_07-olm-operator.deployment.yaml index 0f9c353d68..950ea7a03b 100644 --- a/manifests/0000_50_olm_07-olm-operator.deployment.yaml +++ b/manifests/0000_50_olm_07-olm-operator.deployment.yaml @@ -66,13 +66,13 @@ spec: cpu: 10m memory: 160Mi - volumeMounts: + - mountPath: /var/run/secrets/serving-cert name: serving-cert - volumes: + - name: serving-cert secret: secretName: olm-operator-serving-cert diff --git a/manifests/0000_90_olm_00-service-monitor.yaml b/manifests/0000_90_olm_00-service-monitor.yaml index a26b23f324..7b7c8288bb 100644 --- a/manifests/0000_90_olm_00-service-monitor.yaml +++ b/manifests/0000_90_olm_00-service-monitor.yaml @@ -36,7 +36,6 @@ metadata: labels: app: catalog-operator spec: - jobLabel: k8s-app endpoints: - bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token interval: 30s diff --git a/pkg/admission/server.go b/pkg/admission/server.go new file mode 100644 index 0000000000..f8af6fde26 --- /dev/null +++ b/pkg/admission/server.go @@ -0,0 +1,132 @@ +package admission + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "time" + + admissionv1 "k8s.io/api/admission/v1" + admissionv1beta1 "k8s.io/api/admission/v1beta1" + admissionregistrationv1beta1 "k8s.io/api/admissionregistration/v1beta1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apiserver/pkg/authentication/serviceaccount" + "k8s.io/client-go/util/workqueue" + "k8s.io/klog" + + "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/operators/olm/admission" +) + +var scheme = runtime.NewScheme() +var codecs = serializer.NewCodecFactory(scheme) + +func init() { + addToScheme(scheme) +} + +func addToScheme(scheme *runtime.Scheme) { + utilruntime.Must(corev1.AddToScheme(scheme)) + utilruntime.Must(admissionv1.AddToScheme(scheme)) + utilruntime.Must(admissionv1beta1.AddToScheme(scheme)) + utilruntime.Must(admissionregistrationv1beta1.AddToScheme(scheme)) +} + +// toAdmissionResponse is a helper function to create an AdmissionResponse +// with an embedded error +func toAdmissionResponse(err error) *admissionv1.AdmissionResponse { + return &admissionv1.AdmissionResponse{ + Result: &metav1.Status{ + Message: err.Error(), + }, + } +} + +// admitFunc is the type we use for all of our validators and mutators +type admitFunc func(admissionv1.AdmissionReview) *admissionv1.AdmissionResponse + +// serve handles the http portion of a request prior to handing to an admit +// function +func serve(w http.ResponseWriter, r *http.Request, admit admitFunc) { + var body []byte + if r.Body != nil { + if data, err := ioutil.ReadAll(r.Body); err == nil { + body = data + } + } + + // verify the content type is accurate + contentType := r.Header.Get("Content-Type") + if contentType != "application/json" { + klog.Errorf("contentType=%s, expect application/json", contentType) + return + } + + klog.V(2).Info(fmt.Sprintf("handling request: %s", body)) + + // The AdmissionReview that was sent to the webhook + requestedAdmissionReview := admissionv1.AdmissionReview{} + + // The AdmissionReview that will be returned + responseAdmissionReview := admissionv1.AdmissionReview{ + TypeMeta: metav1.TypeMeta{ + Kind: "AdmissionReview", + APIVersion: "admission.k8s.io/v1", + }, + } + + deserializer := codecs.UniversalDeserializer() + if _, _, err := deserializer.Decode(body, nil, &requestedAdmissionReview); err != nil { + klog.Error(err) + responseAdmissionReview.Response = toAdmissionResponse(err) + } else { + // pass to admitFunc + responseAdmissionReview.Response = admit(requestedAdmissionReview) + } + + // Return the same UID + responseAdmissionReview.Response.UID = requestedAdmissionReview.Request.UID + + klog.V(2).Info(fmt.Sprintf("sending response: %v", responseAdmissionReview.Response)) + + respBytes, err := json.Marshal(responseAdmissionReview) + if err != nil { + klog.Error(err) + } + if _, err := w.Write(respBytes); err != nil { + klog.Error(err) + } +} + +func AdmitHandlerFunc(csvAdmitQueue workqueue.RateLimitingInterface) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + serve(w, r, admitCSVFunc(csvAdmitQueue)) + } +} + +func admitCSVFunc(csvAdmitQueue workqueue.RateLimitingInterface) admitFunc { + return func(ar admissionv1.AdmissionReview) *admissionv1.AdmissionResponse { + if strings.HasPrefix(ar.Request.UserInfo.Username, serviceaccount.ServiceAccountUsernamePrefix) { + return &admissionv1.AdmissionResponse{ + Allowed: true, + } + } + + // Add to the admission queue for processing - if the user has enough permission, this will automatically + // create the requried serviceaccounts and rbac for the operator + csvAdmitQueue.AddAfter(admission.CSVAdmissionRequest{ + Name: ar.Request.Name, + Namespace: ar.Request.Namespace, + User: ar.Request.UserInfo.Username, + }, time.Second) + + return &admissionv1.AdmissionResponse{ + Allowed: true, + } + } +} diff --git a/pkg/api/apis/operators/clusterserviceversion_types.go b/pkg/api/apis/operators/clusterserviceversion_types.go index 94398e72e6..4fb14dbabc 100644 --- a/pkg/api/apis/operators/clusterserviceversion_types.go +++ b/pkg/api/apis/operators/clusterserviceversion_types.go @@ -4,10 +4,12 @@ import ( "encoding/json" "fmt" "sort" + "strings" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/version" + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" ) // ClusterServiceVersionKind is the PascalCase name of a CSV's kind. @@ -120,6 +122,88 @@ type CustomResourceDefinitions struct { Required []CRDDescription } +// WebhookAdmissionType is the type of admission webhooks supported by OLM +type WebhookAdmissionType string + +const ( + // InstallWebhookValidating is for validating admission webhooks + InstallWebhookValidating WebhookAdmissionType = "ValidatingAdmissionWebhook" + // InstallWebhookMutating is for mutating admission webhooks + InstallWebhookMutating WebhookAdmissionType = "MutatingAdmissionWebhook" +) + +// WebhookDescription provides details to OLM about required webhooks +// +k8s:openapi-gen=true +type WebhookDescription struct { + Name string `json:"name"` + Type WebhookAdmissionType `json:"type"` + DeploymentName string `json:"deploymentName,omitempty"` + ContainerPort int32 `json:"containerPort,omitempty"` + Rules []admissionregistrationv1.RuleWithOperations `json:"rules"` + FailurePolicy *admissionregistrationv1.FailurePolicyType `json:"failurePolicy,omitempty"` + MatchPolicy *admissionregistrationv1.MatchPolicyType `json:"matchPolicy,omitempty"` + NamespaceSelector *metav1.LabelSelector `json:"namespaceSelector,omitempty"` + ObjectSelector *metav1.LabelSelector `json:"objectSelector,omitempty"` + SideEffects *admissionregistrationv1.SideEffectClass `json:"sideEffects"` + TimeoutSeconds *int32 `json:"timeoutSeconds,omitempty"` + AdmissionReviewVersions []string `json:"admissionReviewVersions"` + ReinvocationPolicy *admissionregistrationv1.ReinvocationPolicyType `json:"reinvocationPolicy,omitempty"` + WebhookPath *string `json:"webhookPath,omitempty"` +} + +// GetValidatingWebhook returns a ValidatingWebhook generated from the WebhookDescription +func (w *WebhookDescription) GetValidatingWebhook(namespace string, caBundle []byte) admissionregistrationv1.ValidatingWebhook { + return admissionregistrationv1.ValidatingWebhook{ + Name: w.Name, + Rules: w.Rules, + FailurePolicy: w.FailurePolicy, + MatchPolicy: w.MatchPolicy, + NamespaceSelector: w.NamespaceSelector, + ObjectSelector: w.ObjectSelector, + SideEffects: w.SideEffects, + TimeoutSeconds: w.TimeoutSeconds, + AdmissionReviewVersions: w.AdmissionReviewVersions, + ClientConfig: admissionregistrationv1.WebhookClientConfig{ + Service: &admissionregistrationv1.ServiceReference{ + Name: w.MorphDomainName() + "-svc", + Namespace: namespace, + Path: w.WebhookPath, + }, + CABundle: caBundle, + }, + } +} + +// GetMutatingWebhook returns a MutatingWebhook generated from the WebhookDescription +func (w *WebhookDescription) GetMutatingWebhook(namespace string, caBundle []byte) admissionregistrationv1.MutatingWebhook { + return admissionregistrationv1.MutatingWebhook{ + Name: w.Name, + Rules: w.Rules, + FailurePolicy: w.FailurePolicy, + MatchPolicy: w.MatchPolicy, + NamespaceSelector: w.NamespaceSelector, + ObjectSelector: w.ObjectSelector, + SideEffects: w.SideEffects, + TimeoutSeconds: w.TimeoutSeconds, + AdmissionReviewVersions: w.AdmissionReviewVersions, + ClientConfig: admissionregistrationv1.WebhookClientConfig{ + Service: &admissionregistrationv1.ServiceReference{ + Name: w.MorphDomainName() + "-svc", + Namespace: namespace, + Path: w.WebhookPath, + }, + CABundle: caBundle, + }, + ReinvocationPolicy: w.ReinvocationPolicy, + } +} + +// MorphDomainName returns the result of replacing all periods in the given Webhook name with hyphens +func (w *WebhookDescription) MorphDomainName() string { + // Replace all '.'s with "-"s to convert to a DNS-1035 label + return strings.Replace(w.Name, ".", "-", -1) +} + // APIServiceDefinitions declares all of the extension apis managed or required by // an operator being ran by ClusterServiceVersion. type APIServiceDefinitions struct { @@ -135,6 +219,7 @@ type ClusterServiceVersionSpec struct { Maturity string CustomResourceDefinitions CustomResourceDefinitions APIServiceDefinitions APIServiceDefinitions + WebhookDefinitions []WebhookDescription NativeAPIs []metav1.GroupVersionKind MinKubeVersion string DisplayName string diff --git a/pkg/api/apis/operators/v1alpha1/clusterserviceversion_types.go b/pkg/api/apis/operators/v1alpha1/clusterserviceversion_types.go index 03638d4060..b2210d2459 100644 --- a/pkg/api/apis/operators/v1alpha1/clusterserviceversion_types.go +++ b/pkg/api/apis/operators/v1alpha1/clusterserviceversion_types.go @@ -4,10 +4,12 @@ import ( "encoding/json" "fmt" "sort" + "strings" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/version" + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" ) const ( @@ -108,6 +110,88 @@ type APIServiceDescription struct { ActionDescriptor []ActionDescriptor `json:"actionDescriptors,omitempty"` } +// WebhookAdmissionType is the type of admission webhooks supported by OLM +type WebhookAdmissionType string + +const ( + // InstallWebhookValidating is for validating admission webhooks + InstallWebhookValidating WebhookAdmissionType = "ValidatingAdmissionWebhook" + // InstallWebhookMutating is for mutating admission webhooks + InstallWebhookMutating WebhookAdmissionType = "MutatingAdmissionWebhook" +) + +// WebhookDescription provides details to OLM about required webhooks +// +k8s:openapi-gen=true +type WebhookDescription struct { + Name string `json:"name"` + Type WebhookAdmissionType `json:"type"` + DeploymentName string `json:"deploymentName,omitempty"` + ContainerPort int32 `json:"containerPort,omitempty"` + Rules []admissionregistrationv1.RuleWithOperations `json:"rules"` + FailurePolicy *admissionregistrationv1.FailurePolicyType `json:"failurePolicy,omitempty"` + MatchPolicy *admissionregistrationv1.MatchPolicyType `json:"matchPolicy,omitempty"` + NamespaceSelector *metav1.LabelSelector `json:"namespaceSelector,omitempty"` + ObjectSelector *metav1.LabelSelector `json:"objectSelector,omitempty"` + SideEffects *admissionregistrationv1.SideEffectClass `json:"sideEffects"` + TimeoutSeconds *int32 `json:"timeoutSeconds,omitempty"` + AdmissionReviewVersions []string `json:"admissionReviewVersions"` + ReinvocationPolicy *admissionregistrationv1.ReinvocationPolicyType `json:"reinvocationPolicy,omitempty"` + WebhookPath *string `json:"webhookPath,omitempty"` +} + +// GetValidatingWebhook returns a ValidatingWebhook generated from the WebhookDescription +func (w *WebhookDescription) GetValidatingWebhook(namespace string, caBundle []byte) admissionregistrationv1.ValidatingWebhook { + return admissionregistrationv1.ValidatingWebhook{ + Name: w.Name, + Rules: w.Rules, + FailurePolicy: w.FailurePolicy, + MatchPolicy: w.MatchPolicy, + NamespaceSelector: w.NamespaceSelector, + ObjectSelector: w.ObjectSelector, + SideEffects: w.SideEffects, + TimeoutSeconds: w.TimeoutSeconds, + AdmissionReviewVersions: w.AdmissionReviewVersions, + ClientConfig: admissionregistrationv1.WebhookClientConfig{ + Service: &admissionregistrationv1.ServiceReference{ + Name: w.MorphDomainName() + "-svc", + Namespace: namespace, + Path: w.WebhookPath, + }, + CABundle: caBundle, + }, + } +} + +// GetMutatingWebhook returns a MutatingWebhook generated from the WebhookDescription +func (w *WebhookDescription) GetMutatingWebhook(namespace string, caBundle []byte) admissionregistrationv1.MutatingWebhook { + return admissionregistrationv1.MutatingWebhook{ + Name: w.Name, + Rules: w.Rules, + FailurePolicy: w.FailurePolicy, + MatchPolicy: w.MatchPolicy, + NamespaceSelector: w.NamespaceSelector, + ObjectSelector: w.ObjectSelector, + SideEffects: w.SideEffects, + TimeoutSeconds: w.TimeoutSeconds, + AdmissionReviewVersions: w.AdmissionReviewVersions, + ClientConfig: admissionregistrationv1.WebhookClientConfig{ + Service: &admissionregistrationv1.ServiceReference{ + Name: w.MorphDomainName() + "-svc", + Namespace: namespace, + Path: w.WebhookPath, + }, + CABundle: caBundle, + }, + ReinvocationPolicy: w.ReinvocationPolicy, + } +} + +// MorphDomainName returns the result of replacing all periods in the given Webhook name with hyphens +func (w *WebhookDescription) MorphDomainName() string { + // Replace all '.'s with "-"s to convert to a DNS-1035 label + return strings.Replace(w.Name, ".", "-", -1) +} + // APIResourceReference is a Kubernetes resource type used by a custom resource // +k8s:openapi-gen=true type APIResourceReference struct { @@ -147,6 +231,7 @@ type ClusterServiceVersionSpec struct { Maturity string `json:"maturity,omitempty"` CustomResourceDefinitions CustomResourceDefinitions `json:"customresourcedefinitions,omitempty"` APIServiceDefinitions APIServiceDefinitions `json:"apiservicedefinitions,omitempty"` + WebhookDefinitions []WebhookDescription `json:"webhookdefinitions,omitempty"` NativeAPIs []metav1.GroupVersionKind `json:"nativeAPIs,omitempty"` MinKubeVersion string `json:"minKubeVersion,omitempty"` DisplayName string `json:"displayName"` diff --git a/pkg/api/apis/operators/v1alpha1/zz_generated.conversion.go b/pkg/api/apis/operators/v1alpha1/zz_generated.conversion.go index 208bd436b4..70b4ea0248 100644 --- a/pkg/api/apis/operators/v1alpha1/zz_generated.conversion.go +++ b/pkg/api/apis/operators/v1alpha1/zz_generated.conversion.go @@ -25,6 +25,7 @@ import ( unsafe "unsafe" operators "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators" + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" corev1 "k8s.io/api/core/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" conversion "k8s.io/apimachinery/pkg/conversion" @@ -459,6 +460,16 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddGeneratedConversionFunc((*WebhookDescription)(nil), (*operators.WebhookDescription)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_WebhookDescription_To_operators_WebhookDescription(a.(*WebhookDescription), b.(*operators.WebhookDescription), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*operators.WebhookDescription)(nil), (*WebhookDescription)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_operators_WebhookDescription_To_v1alpha1_WebhookDescription(a.(*operators.WebhookDescription), b.(*WebhookDescription), scope) + }); err != nil { + return err + } return nil } @@ -852,6 +863,7 @@ func autoConvert_v1alpha1_ClusterServiceVersionSpec_To_operators_ClusterServiceV if err := Convert_v1alpha1_APIServiceDefinitions_To_operators_APIServiceDefinitions(&in.APIServiceDefinitions, &out.APIServiceDefinitions, s); err != nil { return err } + out.WebhookDefinitions = *(*[]operators.WebhookDescription)(unsafe.Pointer(&in.WebhookDefinitions)) out.NativeAPIs = *(*[]v1.GroupVersionKind)(unsafe.Pointer(&in.NativeAPIs)) out.MinKubeVersion = in.MinKubeVersion out.DisplayName = in.DisplayName @@ -888,6 +900,7 @@ func autoConvert_operators_ClusterServiceVersionSpec_To_v1alpha1_ClusterServiceV if err := Convert_operators_APIServiceDefinitions_To_v1alpha1_APIServiceDefinitions(&in.APIServiceDefinitions, &out.APIServiceDefinitions, s); err != nil { return err } + out.WebhookDefinitions = *(*[]WebhookDescription)(unsafe.Pointer(&in.WebhookDefinitions)) out.NativeAPIs = *(*[]v1.GroupVersionKind)(unsafe.Pointer(&in.NativeAPIs)) out.MinKubeVersion = in.MinKubeVersion out.DisplayName = in.DisplayName @@ -1693,3 +1706,49 @@ func autoConvert_operators_SubscriptionStatus_To_v1alpha1_SubscriptionStatus(in func Convert_operators_SubscriptionStatus_To_v1alpha1_SubscriptionStatus(in *operators.SubscriptionStatus, out *SubscriptionStatus, s conversion.Scope) error { return autoConvert_operators_SubscriptionStatus_To_v1alpha1_SubscriptionStatus(in, out, s) } + +func autoConvert_v1alpha1_WebhookDescription_To_operators_WebhookDescription(in *WebhookDescription, out *operators.WebhookDescription, s conversion.Scope) error { + out.Name = in.Name + out.Type = operators.WebhookAdmissionType(in.Type) + out.DeploymentName = in.DeploymentName + out.ContainerPort = in.ContainerPort + out.Rules = *(*[]admissionregistrationv1.RuleWithOperations)(unsafe.Pointer(&in.Rules)) + out.FailurePolicy = (*admissionregistrationv1.FailurePolicyType)(unsafe.Pointer(in.FailurePolicy)) + out.MatchPolicy = (*admissionregistrationv1.MatchPolicyType)(unsafe.Pointer(in.MatchPolicy)) + out.NamespaceSelector = (*v1.LabelSelector)(unsafe.Pointer(in.NamespaceSelector)) + out.ObjectSelector = (*v1.LabelSelector)(unsafe.Pointer(in.ObjectSelector)) + out.SideEffects = (*admissionregistrationv1.SideEffectClass)(unsafe.Pointer(in.SideEffects)) + out.TimeoutSeconds = (*int32)(unsafe.Pointer(in.TimeoutSeconds)) + out.AdmissionReviewVersions = *(*[]string)(unsafe.Pointer(&in.AdmissionReviewVersions)) + out.ReinvocationPolicy = (*admissionregistrationv1.ReinvocationPolicyType)(unsafe.Pointer(in.ReinvocationPolicy)) + out.WebhookPath = (*string)(unsafe.Pointer(in.WebhookPath)) + return nil +} + +// Convert_v1alpha1_WebhookDescription_To_operators_WebhookDescription is an autogenerated conversion function. +func Convert_v1alpha1_WebhookDescription_To_operators_WebhookDescription(in *WebhookDescription, out *operators.WebhookDescription, s conversion.Scope) error { + return autoConvert_v1alpha1_WebhookDescription_To_operators_WebhookDescription(in, out, s) +} + +func autoConvert_operators_WebhookDescription_To_v1alpha1_WebhookDescription(in *operators.WebhookDescription, out *WebhookDescription, s conversion.Scope) error { + out.Name = in.Name + out.Type = WebhookAdmissionType(in.Type) + out.DeploymentName = in.DeploymentName + out.ContainerPort = in.ContainerPort + out.Rules = *(*[]admissionregistrationv1.RuleWithOperations)(unsafe.Pointer(&in.Rules)) + out.FailurePolicy = (*admissionregistrationv1.FailurePolicyType)(unsafe.Pointer(in.FailurePolicy)) + out.MatchPolicy = (*admissionregistrationv1.MatchPolicyType)(unsafe.Pointer(in.MatchPolicy)) + out.NamespaceSelector = (*v1.LabelSelector)(unsafe.Pointer(in.NamespaceSelector)) + out.ObjectSelector = (*v1.LabelSelector)(unsafe.Pointer(in.ObjectSelector)) + out.SideEffects = (*admissionregistrationv1.SideEffectClass)(unsafe.Pointer(in.SideEffects)) + out.TimeoutSeconds = (*int32)(unsafe.Pointer(in.TimeoutSeconds)) + out.AdmissionReviewVersions = *(*[]string)(unsafe.Pointer(&in.AdmissionReviewVersions)) + out.ReinvocationPolicy = (*admissionregistrationv1.ReinvocationPolicyType)(unsafe.Pointer(in.ReinvocationPolicy)) + out.WebhookPath = (*string)(unsafe.Pointer(in.WebhookPath)) + return nil +} + +// Convert_operators_WebhookDescription_To_v1alpha1_WebhookDescription is an autogenerated conversion function. +func Convert_operators_WebhookDescription_To_v1alpha1_WebhookDescription(in *operators.WebhookDescription, out *WebhookDescription, s conversion.Scope) error { + return autoConvert_operators_WebhookDescription_To_v1alpha1_WebhookDescription(in, out, s) +} diff --git a/pkg/api/apis/operators/v1alpha1/zz_generated.deepcopy.go b/pkg/api/apis/operators/v1alpha1/zz_generated.deepcopy.go index b950b42f10..92f17d5d43 100644 --- a/pkg/api/apis/operators/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/api/apis/operators/v1alpha1/zz_generated.deepcopy.go @@ -23,6 +23,7 @@ package v1alpha1 import ( json "encoding/json" + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" corev1 "k8s.io/api/core/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" @@ -404,6 +405,13 @@ func (in *ClusterServiceVersionSpec) DeepCopyInto(out *ClusterServiceVersionSpec in.Version.DeepCopyInto(&out.Version) in.CustomResourceDefinitions.DeepCopyInto(&out.CustomResourceDefinitions) in.APIServiceDefinitions.DeepCopyInto(&out.APIServiceDefinitions) + if in.WebhookDefinitions != nil { + in, out := &in.WebhookDefinitions, &out.WebhookDefinitions + *out = make([]WebhookDescription, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } if in.NativeAPIs != nil { in, out := &in.NativeAPIs, &out.NativeAPIs *out = make([]v1.GroupVersionKind, len(*in)) @@ -1198,3 +1206,71 @@ func (in *SubscriptionStatus) DeepCopy() *SubscriptionStatus { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WebhookDescription) DeepCopyInto(out *WebhookDescription) { + *out = *in + if in.Rules != nil { + in, out := &in.Rules, &out.Rules + *out = make([]admissionregistrationv1.RuleWithOperations, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.FailurePolicy != nil { + in, out := &in.FailurePolicy, &out.FailurePolicy + *out = new(admissionregistrationv1.FailurePolicyType) + **out = **in + } + if in.MatchPolicy != nil { + in, out := &in.MatchPolicy, &out.MatchPolicy + *out = new(admissionregistrationv1.MatchPolicyType) + **out = **in + } + if in.NamespaceSelector != nil { + in, out := &in.NamespaceSelector, &out.NamespaceSelector + *out = new(v1.LabelSelector) + (*in).DeepCopyInto(*out) + } + if in.ObjectSelector != nil { + in, out := &in.ObjectSelector, &out.ObjectSelector + *out = new(v1.LabelSelector) + (*in).DeepCopyInto(*out) + } + if in.SideEffects != nil { + in, out := &in.SideEffects, &out.SideEffects + *out = new(admissionregistrationv1.SideEffectClass) + **out = **in + } + if in.TimeoutSeconds != nil { + in, out := &in.TimeoutSeconds, &out.TimeoutSeconds + *out = new(int32) + **out = **in + } + if in.AdmissionReviewVersions != nil { + in, out := &in.AdmissionReviewVersions, &out.AdmissionReviewVersions + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.ReinvocationPolicy != nil { + in, out := &in.ReinvocationPolicy, &out.ReinvocationPolicy + *out = new(admissionregistrationv1.ReinvocationPolicyType) + **out = **in + } + if in.WebhookPath != nil { + in, out := &in.WebhookPath, &out.WebhookPath + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WebhookDescription. +func (in *WebhookDescription) DeepCopy() *WebhookDescription { + if in == nil { + return nil + } + out := new(WebhookDescription) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/api/apis/operators/zz_generated.deepcopy.go b/pkg/api/apis/operators/zz_generated.deepcopy.go index 52ebe586b9..f8d251c4ed 100644 --- a/pkg/api/apis/operators/zz_generated.deepcopy.go +++ b/pkg/api/apis/operators/zz_generated.deepcopy.go @@ -23,6 +23,7 @@ package operators import ( json "encoding/json" + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" corev1 "k8s.io/api/core/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" @@ -404,6 +405,13 @@ func (in *ClusterServiceVersionSpec) DeepCopyInto(out *ClusterServiceVersionSpec in.Version.DeepCopyInto(&out.Version) in.CustomResourceDefinitions.DeepCopyInto(&out.CustomResourceDefinitions) in.APIServiceDefinitions.DeepCopyInto(&out.APIServiceDefinitions) + if in.WebhookDefinitions != nil { + in, out := &in.WebhookDefinitions, &out.WebhookDefinitions + *out = make([]WebhookDescription, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } if in.NativeAPIs != nil { in, out := &in.NativeAPIs, &out.NativeAPIs *out = make([]v1.GroupVersionKind, len(*in)) @@ -1312,3 +1320,71 @@ func (in *SubscriptionStatus) DeepCopy() *SubscriptionStatus { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WebhookDescription) DeepCopyInto(out *WebhookDescription) { + *out = *in + if in.Rules != nil { + in, out := &in.Rules, &out.Rules + *out = make([]admissionregistrationv1.RuleWithOperations, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.FailurePolicy != nil { + in, out := &in.FailurePolicy, &out.FailurePolicy + *out = new(admissionregistrationv1.FailurePolicyType) + **out = **in + } + if in.MatchPolicy != nil { + in, out := &in.MatchPolicy, &out.MatchPolicy + *out = new(admissionregistrationv1.MatchPolicyType) + **out = **in + } + if in.NamespaceSelector != nil { + in, out := &in.NamespaceSelector, &out.NamespaceSelector + *out = new(v1.LabelSelector) + (*in).DeepCopyInto(*out) + } + if in.ObjectSelector != nil { + in, out := &in.ObjectSelector, &out.ObjectSelector + *out = new(v1.LabelSelector) + (*in).DeepCopyInto(*out) + } + if in.SideEffects != nil { + in, out := &in.SideEffects, &out.SideEffects + *out = new(admissionregistrationv1.SideEffectClass) + **out = **in + } + if in.TimeoutSeconds != nil { + in, out := &in.TimeoutSeconds, &out.TimeoutSeconds + *out = new(int32) + **out = **in + } + if in.AdmissionReviewVersions != nil { + in, out := &in.AdmissionReviewVersions, &out.AdmissionReviewVersions + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.ReinvocationPolicy != nil { + in, out := &in.ReinvocationPolicy, &out.ReinvocationPolicy + *out = new(admissionregistrationv1.ReinvocationPolicyType) + **out = **in + } + if in.WebhookPath != nil { + in, out := &in.WebhookPath, &out.WebhookPath + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WebhookDescription. +func (in *WebhookDescription) DeepCopy() *WebhookDescription { + if in == nil { + return nil + } + out := new(WebhookDescription) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/controller/operators/olm/admission/config.go b/pkg/controller/operators/olm/admission/config.go new file mode 100644 index 0000000000..a145a9f21a --- /dev/null +++ b/pkg/controller/operators/olm/admission/config.go @@ -0,0 +1,91 @@ +package admission + +import ( + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/util/workqueue" + + "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorlister" + "github.com/operator-framework/operator-lifecycle-manager/pkg/permissions" +) + +type rbacGenerationConfig struct { + queue workqueue.RateLimitingInterface + lister operatorlister.OperatorLister + logger *logrus.Logger + userPermissionValidator permissions.Validator + permissionCreator permissions.Creator +} + +type RbacGenerationOption func(*rbacGenerationConfig) + +// apply sequentially applies the given options to the config. +func (c *rbacGenerationConfig) apply(options []RbacGenerationOption) { + for _, option := range options { + option(c) + } +} + +func newInvalidRbacGenerationConfigError(msg string) error { + return errors.Errorf("invalid rbac generator config: %s", msg) +} + +// WithLogger sets the logger used by the RBAC generator. +func WithLogger(logger *logrus.Logger) RbacGenerationOption { + return func(config *rbacGenerationConfig) { + config.logger = logger + } +} + +// WithLister sets the lister used by the RBAC generator. +func WithLister(lister operatorlister.OperatorLister) RbacGenerationOption { + return func(config *rbacGenerationConfig) { + config.lister = lister + } +} + +// WithQueue sets the queue used by the RBAC generator. +func WithQueue(queue workqueue.RateLimitingInterface) RbacGenerationOption { + return func(config *rbacGenerationConfig) { + config.queue = queue + } +} + +// WithPermissionValidator sets the permission validator used by the RBAC generator. +func WithPermissionValidator(validator permissions.Validator) RbacGenerationOption { + return func(config *rbacGenerationConfig) { + config.userPermissionValidator = validator + } +} + +// WithPermissionCreator sets the permission validator used by the RBAC generator. +func WithPermissionCreator(creator permissions.Creator) RbacGenerationOption { + return func(config *rbacGenerationConfig) { + config.permissionCreator = creator + } +} + +// validate returns an error if the config isn't valid. +func (c *rbacGenerationConfig) validate() (err error) { + switch config := c; { + case config.lister == nil: + err = newInvalidRbacGenerationConfigError("lister nil") + case config.userPermissionValidator == nil: + err = newInvalidRbacGenerationConfigError("validator nil") + case config.permissionCreator == nil: + err = newInvalidRbacGenerationConfigError("creator nil") + } + + return +} + +func defaultRbacGenerationConfig(lister operatorlister.OperatorLister, client kubernetes.Interface) *rbacGenerationConfig { + return &rbacGenerationConfig{ + logger: logrus.New(), + queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "rbacgen"), + lister: lister, + userPermissionValidator: permissions.NewPermissionValidator(lister), + permissionCreator: permissions.NewPermissionCreator(client), + } +} diff --git a/pkg/controller/operators/olm/admission/rbac_generation.go b/pkg/controller/operators/olm/admission/rbac_generation.go new file mode 100644 index 0000000000..a0bec3466a --- /dev/null +++ b/pkg/controller/operators/olm/admission/rbac_generation.go @@ -0,0 +1,122 @@ +package admission + +import ( + "context" + "sync" + + "github.com/sirupsen/logrus" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/util/workqueue" + + "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/resolver" + "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorlister" + "github.com/operator-framework/operator-lifecycle-manager/pkg/permissions" +) + +const ( + MaxRequeues = 10 + NumWorkers = 1 +) + +type CSVAdmissionRequest struct { + Name string + Namespace string + User string +} + +type RBACGenerationController struct { + sync.Once + queue workqueue.RateLimitingInterface + lister operatorlister.OperatorLister + logger *logrus.Logger + userPermissionValidator permissions.Validator + permissionCreator permissions.Creator +} + +// RBACGenerationController returns a new RBACGenerationController +func NewRBACGenerationController(lister operatorlister.OperatorLister, client kubernetes.Interface, options ...RbacGenerationOption) (*RBACGenerationController, error) { + config := defaultRbacGenerationConfig(lister, client) + config.apply(options) + if err := config.validate(); err != nil { + return nil, err + } + return &RBACGenerationController{ + queue: config.queue, + lister: config.lister, + logger: config.logger, + userPermissionValidator: config.userPermissionValidator, + permissionCreator: config.permissionCreator, + }, nil +} + +func (g *RBACGenerationController) Start(ctx context.Context) { + g.logger = g.logger.WithField("stage", "rbacgen").Logger + for w := 0; w < NumWorkers; w++ { + go g.worker(ctx) + } +} + +func (g *RBACGenerationController) worker(ctx context.Context) { + for g.processNextWorkItem(ctx) { + } +} + +func (g *RBACGenerationController) processNextWorkItem(ctx context.Context) bool { + // TODO: context done? + item, quit := g.queue.Get() + + if quit { + return false + } + defer g.queue.Done(item) + + logger := g.logger.WithField("item", item) + logger.WithField("queue-length", g.queue.Len()).Trace("popped queue") + + csvReq, ok := item.(CSVAdmissionRequest) + if !ok { + g.logger.Debugf("wrong type: %#v", item) + return true + } + + logger = logger.WithFields(logrus.Fields{ + "name": csvReq.Name, + "namespace": csvReq.Namespace, + "user": csvReq.User, + }) + + logger.Info("generating rbac for csv") + + csv, err := g.lister.OperatorsV1alpha1().ClusterServiceVersionLister().ClusterServiceVersions(csvReq.Namespace).Get(csvReq.Name) + if err != nil { + logger.Info("csv not found for rbac generation, requeue") + g.requeue(csvReq) + return true + } + + if err := g.userPermissionValidator.UserCanCreateV1Alpha1CSV(csvReq.User, csv); err != nil { + logger.Infof("user lacks permission to create CSV rbac permissions automatically: %s", err.Error()) + return true + } + + operatorPermissions, err := resolver.RBACForClusterServiceVersion(csv) + if err != nil { + logger.Info("failed to get rbac from csv for generation") + return true + } + + if err := g.permissionCreator.FromOperatorPermissions(csv.GetNamespace(), operatorPermissions); err != nil { + g.requeue(csvReq) + return true + } + g.queue.Forget(item) + + return true +} + +func (g *RBACGenerationController) requeue(item CSVAdmissionRequest) { + if requeues := g.queue.NumRequeues(item); requeues < MaxRequeues { + g.logger.WithField("requeues", requeues).Trace("requeuing with rate limiting") + g.queue.AddRateLimited(item) + } +} diff --git a/pkg/controller/operators/olm/apiservices.go b/pkg/controller/operators/olm/apiservices.go index 2c13e356a4..8ea20c6e5f 100644 --- a/pkg/controller/operators/olm/apiservices.go +++ b/pkg/controller/operators/olm/apiservices.go @@ -21,6 +21,7 @@ import ( olmerrors "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/errors" "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/install" "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/ownerutil" + "github.com/operator-framework/operator-lifecycle-manager/pkg/permissions" ) const ( @@ -66,7 +67,7 @@ func (a *Operator) checkAPIServiceResources(csv *v1alpha1.ClusterServiceVersion, }) errs := []error{} - ruleChecker := install.NewCSVRuleChecker(a.lister.RbacV1().RoleLister(), a.lister.RbacV1().RoleBindingLister(), a.lister.RbacV1().ClusterRoleLister(), a.lister.RbacV1().ClusterRoleBindingLister(), csv) + ruleChecker := permissions.NewCSVRuleChecker(a.lister.RbacV1().RoleLister(), a.lister.RbacV1().RoleBindingLister(), a.lister.RbacV1().ClusterRoleLister(), a.lister.RbacV1().ClusterRoleBindingLister(), csv) for _, desc := range csv.GetOwnedAPIServiceDescriptions() { apiServiceName := desc.GetName() logger := logger.WithFields(log.Fields{ @@ -800,4 +801,4 @@ func (a *Operator) isAPIServiceAdoptable(target *v1alpha1.ClusterServiceVersion, adoptable = ownerutil.AdoptableLabels(apiService.GetLabels(), true, owners...) return -} \ No newline at end of file +} diff --git a/pkg/controller/operators/olm/config.go b/pkg/controller/operators/olm/config.go index 9ad37cd9ab..c68ba3cf3f 100644 --- a/pkg/controller/operators/olm/config.go +++ b/pkg/controller/operators/olm/config.go @@ -8,6 +8,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" utilclock "k8s.io/apimachinery/pkg/util/clock" "k8s.io/client-go/rest" + "k8s.io/client-go/util/workqueue" configv1client "github.com/openshift/client-go/config/clientset/versioned" "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/clientset/internalversion" @@ -34,6 +35,7 @@ type operatorConfig struct { apiLabeler labeler.Labeler restConfig *rest.Config configClient configv1client.Interface + csvAdmitQueue workqueue.RateLimitingInterface } func (o *operatorConfig) apply(options []OperatorOption) { @@ -168,3 +170,9 @@ func WithConfigClient(configClient configv1client.Interface) OperatorOption { config.configClient = configClient } } + +func WithCSVAdmissionQueue(queue workqueue.RateLimitingInterface) OperatorOption { + return func(config *operatorConfig) { + config.csvAdmitQueue = queue + } +} diff --git a/pkg/controller/operators/olm/operator.go b/pkg/controller/operators/olm/operator.go index 7165bb5b3a..1ce0c298df 100644 --- a/pkg/controller/operators/olm/operator.go +++ b/pkg/controller/operators/olm/operator.go @@ -7,8 +7,6 @@ import ( "strings" "time" - v1 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1" - "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/kubestate" "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus" corev1 "k8s.io/api/core/v1" @@ -29,16 +27,19 @@ import ( apiregistrationv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1" kagg "k8s.io/kube-aggregator/pkg/client/informers/externalversions" + v1 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1" "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1" "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/clientset/versioned" "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/informers/externalversions" "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/certs" "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/install" + "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/operators/olm/admission" "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/operators/olm/overrides" "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/resolver" csvutility "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/csv" "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/event" index "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/index" + "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/kubestate" "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/labeler" "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorclient" "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorlister" @@ -80,6 +81,8 @@ type Operator struct { serviceAccountSyncer *scoped.UserDefinedServiceAccountSyncer clientAttenuator *scoped.ClientAttenuator serviceAccountQuerier *scoped.UserDefinedServiceAccountQuerier + csvAdmitQueue workqueue.RateLimitingInterface + rbacGenerator *admission.RBACGenerationController } func NewOperator(ctx context.Context, options ...OperatorOption) (*Operator, error) { @@ -134,6 +137,7 @@ func newOperatorWithConfig(ctx context.Context, config *operatorConfig) (*Operat serviceAccountSyncer: scoped.NewUserDefinedServiceAccountSyncer(config.logger, scheme, config.operatorClient, config.externalClient), clientAttenuator: scoped.NewClientAttenuator(config.logger, config.restConfig, config.operatorClient, config.externalClient), serviceAccountQuerier: scoped.NewUserDefinedServiceAccountQuerier(config.logger, config.externalClient), + csvAdmitQueue: config.csvAdmitQueue, } // Set up syncing for namespace-scoped resources @@ -473,7 +477,9 @@ func newOperatorWithConfig(ctx context.Context, config *operatorConfig) (*Operat if err != nil { return nil, err } - op.RegisterQueueInformer(informer) + if err := op.RegisterQueueInformer(informer); err != nil { + return nil, err + } } overridesBuilderFunc := overrides.NewDeploymentInitializer(op.logger, proxyQuerierInUse, op.lister) @@ -481,6 +487,16 @@ func newOperatorWithConfig(ctx context.Context, config *operatorConfig) (*Operat OverridesBuilderFunc: overridesBuilderFunc.GetDeploymentInitializer, } + op.rbacGenerator, err = admission.NewRBACGenerationController( + op.lister, + op.opClient.KubernetesInterface(), + admission.WithLogger(op.logger), + admission.WithQueue(op.csvAdmitQueue)) + if err != nil { + return nil, err + } + op.rbacGenerator.Start(ctx) + return op, nil } @@ -1344,6 +1360,13 @@ func (a *Operator) transitionCSVState(in v1alpha1.ClusterServiceVersion) (out *v return } + // Install required Webhooks and update strategy with serving cert data + strategy, syncError = a.installWebhookRequirements(out, strategy) + if syncError != nil { + out.SetPhaseWithEvent(v1alpha1.CSVPhaseFailed, v1alpha1.CSVReasonComponentFailed, fmt.Sprintf("install webhook failed: %s", syncError), now, a.recorder) + return + } + if syncError = installer.Install(strategy); syncError != nil { if install.IsErrorUnrecoverable(syncError) { logger.Infof("Setting CSV reason to failed without retry: %v", syncError) diff --git a/pkg/controller/operators/olm/operator_test.go b/pkg/controller/operators/olm/operator_test.go index 315a40c62c..7f38bf7ddf 100644 --- a/pkg/controller/operators/olm/operator_test.go +++ b/pkg/controller/operators/olm/operator_test.go @@ -40,6 +40,7 @@ import ( "k8s.io/client-go/rest" "k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/record" + "k8s.io/client-go/util/workqueue" apiregistrationv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1" apiregistrationfake "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/fake" @@ -263,6 +264,7 @@ func NewFakeOperator(ctx context.Context, options ...fakeOperatorOption) (*Opera apiReconciler: resolver.APIIntersectionReconcileFunc(resolver.ReconcileAPIIntersection), apiLabeler: labeler.Func(resolver.LabelSetsFor), restConfig: &rest.Config{}, + csvAdmitQueue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "csvAdmit"), }, recorder: &record.FakeRecorder{}, // default expected namespaces diff --git a/pkg/controller/operators/olm/operatorgroup.go b/pkg/controller/operators/olm/operatorgroup.go index be491247c4..ddce069278 100644 --- a/pkg/controller/operators/olm/operatorgroup.go +++ b/pkg/controller/operators/olm/operatorgroup.go @@ -6,6 +6,8 @@ import ( "strings" v1 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1" + "github.com/operator-framework/operator-lifecycle-manager/pkg/permissions" + "github.com/sirupsen/logrus" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" @@ -342,7 +344,7 @@ func (a *Operator) ensureRBACInTargetNamespace(csv *v1alpha1.ClusterServiceVersi if !ok { return fmt.Errorf("could not cast install strategy as type %T", strategyDetailsDeployment) } - ruleChecker := install.NewCSVRuleChecker(a.lister.RbacV1().RoleLister(), a.lister.RbacV1().RoleBindingLister(), a.lister.RbacV1().ClusterRoleLister(), a.lister.RbacV1().ClusterRoleBindingLister(), csv) + ruleChecker := permissions.NewCSVRuleChecker(a.lister.RbacV1().RoleLister(), a.lister.RbacV1().RoleBindingLister(), a.lister.RbacV1().ClusterRoleLister(), a.lister.RbacV1().ClusterRoleBindingLister(), csv) logger := a.logger.WithField("opgroup", operatorGroup.GetName()).WithField("csv", csv.GetName()) @@ -566,7 +568,7 @@ func (a *Operator) ensureCSVsInNamespaces(csv *v1alpha1.ClusterServiceVersion, o if !ok { return fmt.Errorf("could not cast install strategy as type %T", strategyDetailsDeployment) } - ruleChecker := install.NewCSVRuleChecker(a.lister.RbacV1().RoleLister(), a.lister.RbacV1().RoleBindingLister(), a.lister.RbacV1().ClusterRoleLister(), a.lister.RbacV1().ClusterRoleBindingLister(), csv) + ruleChecker := permissions.NewCSVRuleChecker(a.lister.RbacV1().RoleLister(), a.lister.RbacV1().RoleBindingLister(), a.lister.RbacV1().ClusterRoleLister(), a.lister.RbacV1().ClusterRoleBindingLister(), csv) logger := a.logger.WithField("opgroup", operatorGroup.GetName()).WithField("csv", csv.GetName()) diff --git a/pkg/controller/operators/olm/requirements.go b/pkg/controller/operators/olm/requirements.go index ad758f48da..9b4721a7e0 100644 --- a/pkg/controller/operators/olm/requirements.go +++ b/pkg/controller/operators/olm/requirements.go @@ -8,9 +8,12 @@ import ( "github.com/sirupsen/logrus" "github.com/coreos/go-semver/semver" + "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1" olmErrors "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/errors" "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/install" + "github.com/operator-framework/operator-lifecycle-manager/pkg/permissions" + "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -238,7 +241,7 @@ func (a *Operator) requirementStatus(strategyDetailsDeployment *install.Strategy } // permissionStatus checks whether the given CSV's RBAC requirements are met in its namespace -func (a *Operator) permissionStatus(strategyDetailsDeployment *install.StrategyDetailsDeployment, ruleChecker install.RuleChecker, targetNamespace, serviceAccountNamespace string) (bool, []v1alpha1.RequirementStatus, error) { +func (a *Operator) permissionStatus(strategyDetailsDeployment *install.StrategyDetailsDeployment, ruleChecker permissions.RuleChecker, targetNamespace, serviceAccountNamespace string) (bool, []v1alpha1.RequirementStatus, error) { statusesSet := map[string]v1alpha1.RequirementStatus{} checkPermissions := func(permissions []install.StrategyDeploymentPermissions, namespace string) (bool, error) { @@ -365,7 +368,7 @@ func (a *Operator) requirementAndPermissionStatus(csv *v1alpha1.ClusterServiceVe clusterRoleLister := rbacLister.ClusterRoleLister() clusterRoleBindingLister := rbacLister.ClusterRoleBindingLister() - ruleChecker := install.NewCSVRuleChecker(roleLister, roleBindingLister, clusterRoleLister, clusterRoleBindingLister, csv) + ruleChecker := permissions.NewCSVRuleChecker(roleLister, roleBindingLister, clusterRoleLister, clusterRoleBindingLister, csv) permMet, permStatuses, err := a.permissionStatus(strategyDetailsDeployment, ruleChecker, csv.GetNamespace(), csv.GetNamespace()) if err != nil { return false, nil, err diff --git a/pkg/controller/operators/olm/webhooks.go b/pkg/controller/operators/olm/webhooks.go new file mode 100644 index 0000000000..ba392c4c55 --- /dev/null +++ b/pkg/controller/operators/olm/webhooks.go @@ -0,0 +1,382 @@ +// TODO: Refactor this code with the API Cert code. +package olm + +import ( + "fmt" + "time" + + log "github.com/sirupsen/logrus" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + + "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1" + "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/certs" + "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/install" + "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/ownerutil" + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" +) + +func (a *Operator) installWebhookRequirements(csv *v1alpha1.ClusterServiceVersion, strategy install.Strategy) (install.Strategy, error) { + logger := log.WithFields(log.Fields{ + "csv": csv.GetName(), + "namespace": csv.GetNamespace(), + }) + + // Assume the strategy is for a deployment + strategyDetailsDeployment, ok := strategy.(*install.StrategyDetailsDeployment) + if !ok { + return nil, fmt.Errorf("unsupported InstallStrategy type") + } + + // Return early if there are no WebhookDefinitions + webhookDescriptions := csv.Spec.WebhookDefinitions + if len(webhookDescriptions) == 0 { + return strategyDetailsDeployment, nil + } + + // Create the CA + expiration := time.Now().Add(DefaultCertValidFor) + ca, err := certs.GenerateCA(expiration, Organization) + if err != nil { + logger.Debug("failed to generate CA") + return nil, err + } + rotateAt := expiration.Add(-1 * DefaultCertMinFresh) + + depSpecs := make(map[string]appsv1.DeploymentSpec) + for _, sddSpec := range strategyDetailsDeployment.DeploymentSpecs { + depSpecs[sddSpec.Name] = sddSpec.Spec + } + + // Create all resources required, and update the matching DeploymentSpec's Volume and VolumeMounts + // Get List of Webhooks + for _, desc := range webhookDescriptions { + depSpec, ok := depSpecs[desc.DeploymentName] + if !ok { + return nil, fmt.Errorf("StrategyDetailsDeployment missing deployment %s for webhook", desc.DeploymentName) + } + + newDepSpec, err := a.installWebhook(desc, ca, rotateAt, depSpec, csv) + if err != nil { + return nil, err + } + depSpecs[desc.DeploymentName] = *newDepSpec + } + + // Replace all matching DeploymentSpecs in the strategy + for i, sddSpec := range strategyDetailsDeployment.DeploymentSpecs { + if depSpec, ok := depSpecs[sddSpec.Name]; ok { + strategyDetailsDeployment.DeploymentSpecs[i].Spec = depSpec + } + } + + // Set CSV cert status + csv.Status.CertsLastUpdated = metav1.Now() + csv.Status.CertsRotateAt = metav1.NewTime(rotateAt) + + return strategyDetailsDeployment, nil +} + +func (a *Operator) installWebhook(desc v1alpha1.WebhookDescription, ca *certs.KeyPair, rotateAt time.Time, depSpec appsv1.DeploymentSpec, csv *v1alpha1.ClusterServiceVersion) (*appsv1.DeploymentSpec, error) { + logger := log.WithFields(log.Fields{ + "csv": csv.GetName(), + "namespace": csv.GetNamespace(), + "webhook": desc.Name, + }) + + // Create a service for the deployment + containerPort := 443 + if desc.ContainerPort > 0 { + containerPort = int(desc.ContainerPort) + } + service := &corev1.Service{ + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + { + Port: int32(443), + TargetPort: intstr.FromInt(containerPort), + }, + }, + Selector: depSpec.Selector.MatchLabels, + }, + } + service.SetName(desc.MorphDomainName() + "-svc") + service.SetNamespace(csv.GetNamespace()) + ownerutil.AddNonBlockingOwner(service, csv) + + existingService, err := a.lister.CoreV1().ServiceLister().Services(csv.GetNamespace()).Get(service.GetName()) + if err == nil { + if !ownerutil.Adoptable(csv, existingService.GetOwnerReferences()) { + return nil, fmt.Errorf("service %s not safe to replace: extraneous ownerreferences found", service.GetName()) + } + service.SetOwnerReferences(append(service.GetOwnerReferences(), existingService.GetOwnerReferences()...)) + + // Delete the Service to replace + deleteErr := a.opClient.DeleteService(service.GetNamespace(), service.GetName(), &metav1.DeleteOptions{}) + if err != nil && !k8serrors.IsNotFound(deleteErr) { + return nil, fmt.Errorf("could not delete existing service %s", service.GetName()) + } + } + + // Attempt to create the Service + _, err = a.opClient.CreateService(service) + if err != nil { + logger.Warnf("could not create service %s", service.GetName()) + return nil, fmt.Errorf("could not create service %s: %s", service.GetName(), err.Error()) + } + + // Create signed serving cert + hosts := []string{ + fmt.Sprintf("%s.%s", service.GetName(), csv.GetNamespace()), + fmt.Sprintf("%s.%s.svc", service.GetName(), csv.GetNamespace()), + } + servingPair, err := certs.CreateSignedServingPair(rotateAt, Organization, ca, hosts) + if err != nil { + logger.Warnf("could not generate signed certs for hosts %v", hosts) + return nil, err + } + + // Create Secret for serving cert + certPEM, privPEM, err := servingPair.ToPEM() + if err != nil { + logger.Warnf("unable to convert serving certificate and private key to PEM format for Webhook %s", desc.Name) + return nil, err + } + + secret := &corev1.Secret{ + Data: map[string][]byte{ + "tls.crt": certPEM, + "tls.key": privPEM, + }, + Type: corev1.SecretTypeTLS, + } + secret.SetName(desc.MorphDomainName() + "-cert") + secret.SetNamespace(csv.GetNamespace()) + + // Add olmcasha hash as a label to the + caPEM, _, err := ca.ToPEM() + if err != nil { + logger.Warnf("unable to convert CA certificate to PEM format for Webhook %s", desc.Name) + return nil, err + } + caHash := certs.PEMSHA256(caPEM) + secret.SetAnnotations(map[string]string{OLMCAHashAnnotationKey: caHash}) + + existingSecret, err := a.lister.CoreV1().SecretLister().Secrets(csv.GetNamespace()).Get(secret.GetName()) + if err == nil { + // Check if the only owners are this CSV or in this CSV's replacement chain + if ownerutil.Adoptable(csv, existingSecret.GetOwnerReferences()) { + ownerutil.AddNonBlockingOwner(secret, csv) + } + + // Attempt an update + if _, err := a.opClient.UpdateSecret(secret); err != nil { + logger.Warnf("could not update secret %s", secret.GetName()) + return nil, err + } + } else if k8serrors.IsNotFound(err) { + // Create the secret + ownerutil.AddNonBlockingOwner(secret, csv) + _, err = a.opClient.CreateSecret(secret) + if err != nil { + log.Warnf("could not create secret %s", secret.GetName()) + return nil, err + } + } else { + return nil, err + } + + // create Role and RoleBinding to allow the deployment to mount the Secret + secretRole := &rbacv1.Role{ + Rules: []rbacv1.PolicyRule{ + { + Verbs: []string{"get"}, + APIGroups: []string{""}, + Resources: []string{"secrets"}, + ResourceNames: []string{secret.GetName()}, + }, + }, + } + secretRole.SetName(secret.GetName()) + secretRole.SetNamespace(csv.GetNamespace()) + + existingSecretRole, err := a.lister.RbacV1().RoleLister().Roles(csv.GetNamespace()).Get(secretRole.GetName()) + if err == nil { + // Check if the only owners are this CSV or in this CSV's replacement chain + if ownerutil.Adoptable(csv, existingSecretRole.GetOwnerReferences()) { + ownerutil.AddNonBlockingOwner(secretRole, csv) + } + + // Attempt an update + if _, err := a.opClient.UpdateRole(secretRole); err != nil { + logger.Warnf("could not update secret role %s", secretRole.GetName()) + return nil, err + } + } else if k8serrors.IsNotFound(err) { + // Create the role + ownerutil.AddNonBlockingOwner(secretRole, csv) + _, err = a.opClient.CreateRole(secretRole) + if err != nil { + log.Warnf("could not create secret role %s", secretRole.GetName()) + return nil, err + } + } else { + return nil, err + } + + if depSpec.Template.Spec.ServiceAccountName == "" { + depSpec.Template.Spec.ServiceAccountName = "default" + } + + secretRoleBinding := &rbacv1.RoleBinding{ + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + APIGroup: "", + Name: depSpec.Template.Spec.ServiceAccountName, + Namespace: csv.GetNamespace(), + }, + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "Role", + Name: secretRole.GetName(), + }, + } + secretRoleBinding.SetName(secret.GetName()) + secretRoleBinding.SetNamespace(csv.GetNamespace()) + + existingSecretRoleBinding, err := a.lister.RbacV1().RoleBindingLister().RoleBindings(csv.GetNamespace()).Get(secretRoleBinding.GetName()) + if err == nil { + // Check if the only owners are this CSV or in this CSV's replacement chain + if ownerutil.Adoptable(csv, existingSecretRoleBinding.GetOwnerReferences()) { + ownerutil.AddNonBlockingOwner(secretRoleBinding, csv) + } + + // Attempt an update + if _, err := a.opClient.UpdateRoleBinding(secretRoleBinding); err != nil { + logger.Warnf("could not update secret rolebinding %s", secretRoleBinding.GetName()) + return nil, err + } + } else if k8serrors.IsNotFound(err) { + // Create the role + ownerutil.AddNonBlockingOwner(secretRoleBinding, csv) + _, err = a.opClient.CreateRoleBinding(secretRoleBinding) + if err != nil { + log.Warnf("could not create secret rolebinding with dep spec: %+v", depSpec) + return nil, err + } + } else { + return nil, err + } + + // Update deployment with secret volume mount. + volume := corev1.Volume{ + Name: "webhook-cert", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: secret.GetName(), + Items: []corev1.KeyToPath{ + { + Key: "tls.crt", + Path: "webhook.crt", + }, + { + Key: "tls.key", + Path: "webhook.key", + }, + }, + }, + }, + } + + replaced := false + for i, v := range depSpec.Template.Spec.Volumes { + if v.Name == volume.Name { + depSpec.Template.Spec.Volumes[i] = volume + replaced = true + break + } + } + if !replaced { + depSpec.Template.Spec.Volumes = append(depSpec.Template.Spec.Volumes, volume) + } + + mount := corev1.VolumeMount{ + Name: volume.Name, + MountPath: "/webhook.local.config/certificates", + } + for i, container := range depSpec.Template.Spec.Containers { + found := false + for j, m := range container.VolumeMounts { + if m.Name == mount.Name { + found = true + break + } + + // Replace if mounting to the same location. + if m.MountPath == mount.MountPath { + container.VolumeMounts[j] = mount + found = true + break + } + } + if !found { + container.VolumeMounts = append(container.VolumeMounts, mount) + } + + depSpec.Template.Spec.Containers[i] = container + } + + // Setting the olm hash label forces a rollout and ensures that the new secret + // is used by the webhook if not hot reloading. + depSpec.Template.ObjectMeta.SetAnnotations(map[string]string{OLMCAHashAnnotationKey: caHash}) + + switch desc.Type { + case v1alpha1.InstallWebhookValidating: + webhooks := []admissionregistrationv1.ValidatingWebhook{ + desc.GetValidatingWebhook(csv.GetNamespace(), caPEM), + } + hook := admissionregistrationv1.ValidatingWebhookConfiguration{ + ObjectMeta: metav1.ObjectMeta{Name: desc.Name, + Namespace: csv.GetNamespace(), + }, + Webhooks: webhooks, + } + + ownerutil.AddNonBlockingOwner(&hook, csv) + log.Infof("Webhooks: Creating ValidatingWebhookConfiguration %v", hook) + if _, err := a.opClient.KubernetesInterface().AdmissionregistrationV1().ValidatingWebhookConfigurations().Create(&hook); err != nil { + log.Errorf("Webhooks: Error create/updating validation webhook: %v", err) + return nil, err + } + case v1alpha1.InstallWebhookMutating: + webhooks := []admissionregistrationv1.MutatingWebhook{ + desc.GetMutatingWebhook(csv.GetNamespace(), caPEM), + } + hook := admissionregistrationv1.MutatingWebhookConfiguration{ + ObjectMeta: metav1.ObjectMeta{Name: desc.Name, + Namespace: csv.GetNamespace(), + }, + Webhooks: webhooks, + } + + ownerutil.AddNonBlockingOwner(&hook, csv) + log.Infof("Webhooks: Creating MutatingWebhookConfiguration %v", hook) + if _, err := a.opClient.KubernetesInterface().AdmissionregistrationV1().MutatingWebhookConfigurations().Create(&hook); err != nil { + log.Errorf("Webhooks: Error create/updating Mutating webhook: %v", err) + return nil, err + } + } + + if err != nil { + logger.Warnf("could not create or update Webhook") + return nil, err + } + + return &depSpec, nil +} diff --git a/pkg/package-server/apis/openapi/zz_generated.openapi.go b/pkg/package-server/apis/openapi/zz_generated.openapi.go index 0824dd9654..37d2800a28 100644 --- a/pkg/package-server/apis/openapi/zz_generated.openapi.go +++ b/pkg/package-server/apis/openapi/zz_generated.openapi.go @@ -40,6 +40,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1.InstallMode": schema_api_apis_operators_v1alpha1_InstallMode(ref), "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1.SpecDescriptor": schema_api_apis_operators_v1alpha1_SpecDescriptor(ref), "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1.StatusDescriptor": schema_api_apis_operators_v1alpha1_StatusDescriptor(ref), + "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1.WebhookDescription": schema_api_apis_operators_v1alpha1_WebhookDescription(ref), "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/version.OperatorVersion": schema_operator_lifecycle_manager_pkg_lib_version_OperatorVersion(ref), "github.com/operator-framework/operator-lifecycle-manager/pkg/package-server/apis/apps/v1alpha1.AppLink": schema_package_server_apis_apps_v1alpha1_AppLink(ref), "github.com/operator-framework/operator-lifecycle-manager/pkg/package-server/apis/apps/v1alpha1.CSVDescription": schema_package_server_apis_apps_v1alpha1_CSVDescription(ref), @@ -606,6 +607,117 @@ func schema_api_apis_operators_v1alpha1_StatusDescriptor(ref common.ReferenceCal } } +func schema_api_apis_operators_v1alpha1_WebhookDescription(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "WebhookDescription provides details to OLM about required webhooks", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "name": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "type": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "deploymentName": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "containerPort": { + SchemaProps: spec.SchemaProps{ + Type: []string{"integer"}, + Format: "int32", + }, + }, + "rules": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Ref: ref("k8s.io/api/admissionregistration/v1.RuleWithOperations"), + }, + }, + }, + }, + }, + "failurePolicy": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "matchPolicy": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "namespaceSelector": { + SchemaProps: spec.SchemaProps{ + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelector"), + }, + }, + "objectSelector": { + SchemaProps: spec.SchemaProps{ + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelector"), + }, + }, + "sideEffects": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "timeoutSeconds": { + SchemaProps: spec.SchemaProps{ + Type: []string{"integer"}, + Format: "int32", + }, + }, + "admissionReviewVersions": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "reinvocationPolicy": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "webhookPath": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"name", "type", "rules", "sideEffects", "admissionReviewVersions"}, + }, + }, + Dependencies: []string{ + "k8s.io/api/admissionregistration/v1.RuleWithOperations", "k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelector"}, + } +} + func schema_operator_lifecycle_manager_pkg_lib_version_OperatorVersion(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ diff --git a/pkg/controller/install/attributes_util.go b/pkg/permissions/attributes_util.go similarity index 99% rename from pkg/controller/install/attributes_util.go rename to pkg/permissions/attributes_util.go index 9b5810dc53..52936c80d1 100644 --- a/pkg/controller/install/attributes_util.go +++ b/pkg/permissions/attributes_util.go @@ -1,4 +1,4 @@ -package install +package permissions import ( log "github.com/sirupsen/logrus" diff --git a/pkg/controller/install/attributes_util_test.go b/pkg/permissions/attributes_util_test.go similarity index 99% rename from pkg/controller/install/attributes_util_test.go rename to pkg/permissions/attributes_util_test.go index 0058f56346..e61b6ffa54 100644 --- a/pkg/controller/install/attributes_util_test.go +++ b/pkg/permissions/attributes_util_test.go @@ -1,4 +1,4 @@ -package install +package permissions import ( "fmt" diff --git a/pkg/permissions/creator.go b/pkg/permissions/creator.go new file mode 100644 index 0000000000..bbf391a4b2 --- /dev/null +++ b/pkg/permissions/creator.go @@ -0,0 +1,53 @@ +package permissions + +import ( + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/client-go/kubernetes" + + "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/resolver" +) + +type Creator interface { + FromOperatorPermissions(namespace string, permissions map[string]*resolver.OperatorPermissions) error +} + +var _ Creator = PermissionCreator{} + +type PermissionCreator struct { + client kubernetes.Interface +} + +func NewPermissionCreator(client kubernetes.Interface) *PermissionCreator { + return &PermissionCreator{client: client} +} + +// TODO: should these attempt to update? +func (p PermissionCreator) FromOperatorPermissions(namespace string, permissions map[string]*resolver.OperatorPermissions) error { + for _, perms := range permissions { + if _, err := p.client.CoreV1().ServiceAccounts(namespace).Create(perms.ServiceAccount); err != nil && !errors.IsAlreadyExists(err) { + return err + } + + for _, role := range perms.Roles { + if _, err := p.client.RbacV1().Roles(namespace).Create(role); err != nil && !errors.IsAlreadyExists(err) { + return err + } + } + for _, role := range perms.ClusterRoles { + if _, err := p.client.RbacV1().ClusterRoles().Create(role); err != nil && !errors.IsAlreadyExists(err) { + return err + } + } + for _, rb := range perms.RoleBindings { + if _, err := p.client.RbacV1().RoleBindings(namespace).Create(rb); err != nil && !errors.IsAlreadyExists(err) { + return err + } + } + for _, crb := range perms.ClusterRoleBindings { + if _, err := p.client.RbacV1().ClusterRoleBindings().Create(crb); err != nil && !errors.IsAlreadyExists(err) { + return err + } + } + } + return nil +} diff --git a/pkg/permissions/creator_test.go b/pkg/permissions/creator_test.go new file mode 100644 index 0000000000..652eeccf8b --- /dev/null +++ b/pkg/permissions/creator_test.go @@ -0,0 +1,70 @@ +package permissions + +import ( + "reflect" + "testing" + + "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/resolver" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/fake" +) + +func TestNewPermissionCreator(t *testing.T) { + fakeClient := fake.NewSimpleClientset() + type args struct { + client kubernetes.Interface + } + tests := []struct { + name string + args args + want *PermissionCreator + }{ + { + name: "Client", + args: args{client: fakeClient}, + want: &PermissionCreator{client: fakeClient}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := NewPermissionCreator(tt.args.client); !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewPermissionCreator() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestPermissionCreator_FromOperatorPermissions(t *testing.T) { + type fields struct { + client kubernetes.Interface + } + type args struct { + namespace string + permissions map[string]*resolver.OperatorPermissions + } + tests := []struct { + name string + fields fields + args args + wantErr bool + }{ + { + fields: fields{ + client: fake.NewSimpleClientset(), + }, + args: args{ + permissions: map[string]*resolver.OperatorPermissions{}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := PermissionCreator{ + client: tt.fields.client, + } + if err := p.FromOperatorPermissions(tt.args.namespace, tt.args.permissions); (err != nil) != tt.wantErr { + t.Errorf("FromOperatorPermissions() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/pkg/controller/install/rule_checker.go b/pkg/permissions/rule_checker.go similarity index 80% rename from pkg/controller/install/rule_checker.go rename to pkg/permissions/rule_checker.go index 7bc671a1cf..6e74f9565b 100644 --- a/pkg/controller/install/rule_checker.go +++ b/pkg/permissions/rule_checker.go @@ -1,4 +1,4 @@ -package install +package permissions import ( "fmt" @@ -6,6 +6,7 @@ import ( corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/labels" + "k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/authorization/authorizer" crbacv1 "k8s.io/client-go/listers/rbac/v1" rbacauthorizer "k8s.io/kubernetes/plugin/pkg/auth/authorizer/rbac" @@ -19,6 +20,10 @@ type RuleChecker interface { // RuleSatisfied determines whether a PolicyRule is satisfied for a ServiceAccount // by existing Roles and ClusterRoles RuleSatisfied(sa *corev1.ServiceAccount, namespace string, rule rbacv1.PolicyRule) (bool, error) + + // RuleSatisfied determines whether a PolicyRule is satisfied for a User + // by existing Roles and ClusterRoles + RuleSatisfiedUser(user, namespace string, rule rbacv1.PolicyRule) error } // CSVRuleChecker determines whether a PolicyRule is satisfied for a ServiceAccount @@ -38,7 +43,7 @@ func NewCSVRuleChecker(roleLister crbacv1.RoleLister, roleBindingLister crbacv1. roleBindingLister: roleBindingLister, clusterRoleLister: clusterRoleLister, clusterRoleBindingLister: clusterRoleBindingLister, - csv: csv.DeepCopy(), + csv: csv.DeepCopy(), } } @@ -54,6 +59,31 @@ func (c *CSVRuleChecker) RuleSatisfied(sa *corev1.ServiceAccount, namespace stri user := toDefaultInfo(sa) attributesSet := toAttributesSet(user, namespace, rule) + return c.attributeSetSatisfied(attributesSet) +} + +// RuleSatisfied returns true if a ServiceAccount is authorized to perform all actions described by a PolicyRule in a namespace +// returns nil if the user can create +func (c *CSVRuleChecker) RuleSatisfiedUser(username, namespace string, rule rbacv1.PolicyRule) error { + // check if the rule is valid + err := ruleValid(rule) + if err != nil { + return fmt.Errorf("user cannot create invalid rule (%s): %s", rule, err.Error()) + } + + // get attributes set for the given Role and ServiceAccount + attributesSet := toAttributesSet(&user.DefaultInfo{Name: username}, namespace, rule) + allowed, err := c.attributeSetSatisfied(attributesSet) + if err != nil { + return err + } + if allowed == false { + return fmt.Errorf("user cannot create rule %s", rule) + } + return nil +} + +func (c *CSVRuleChecker) attributeSetSatisfied(attributesSet []authorizer.Attributes) (bool, error) { // create a new RBACAuthorizer rbacAuthorizer := rbacauthorizer.New(c, c, c, c) diff --git a/pkg/controller/install/rule_checker_test.go b/pkg/permissions/rule_checker_test.go similarity index 99% rename from pkg/controller/install/rule_checker_test.go rename to pkg/permissions/rule_checker_test.go index 0c2486f429..afb3991549 100644 --- a/pkg/controller/install/rule_checker_test.go +++ b/pkg/permissions/rule_checker_test.go @@ -1,4 +1,4 @@ -package install +package permissions import ( "testing" @@ -17,7 +17,7 @@ import ( "k8s.io/client-go/tools/cache" apiregistrationfake "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/fake" - v1alpha1 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1" + "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1" "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorclient" ) diff --git a/pkg/permissions/validator.go b/pkg/permissions/validator.go new file mode 100644 index 0000000000..90c8911af4 --- /dev/null +++ b/pkg/permissions/validator.go @@ -0,0 +1,107 @@ +package permissions + +import ( + "fmt" + "strings" + + v1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/util/errors" + + "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1" + "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/resolver" + "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorlister" +) + +type NamespaceRule struct { + Namespace string + PolicyRule rbacv1.PolicyRule +} + +type Validator interface { + UserCanCreateV1Alpha1CSV(username string, csv *v1alpha1.ClusterServiceVersion) error +} + +var _ Validator = PermissionValidator{} + +type PermissionValidator struct { + lister operatorlister.OperatorLister +} + +func NewPermissionValidator(lister operatorlister.OperatorLister) *PermissionValidator { + return &PermissionValidator{lister: lister} +} + +// TODO: use impersonation + dry-run? +func (p PermissionValidator) UserCanCreateV1Alpha1CSV(username string, csv *v1alpha1.ClusterServiceVersion) error { + ruleChecker := NewCSVRuleChecker( + p.lister.RbacV1().RoleLister(), + p.lister.RbacV1().RoleBindingLister(), + p.lister.RbacV1().ClusterRoleLister(), + p.lister.RbacV1().ClusterRoleBindingLister(), + csv) + + operatorPermissions, err := resolver.RBACForClusterServiceVersion(csv) + if err != nil { + return fmt.Errorf("failed to get rbac from csv for generation") + } + + rules := ToNamespaceRules(csv.GetNamespace(), operatorPermissions) + rules = WithoutOwnedAndRequired(csv, rules) + + errs := []error{} + for _, rule := range rules { + if err := ruleChecker.RuleSatisfiedUser(username, rule.Namespace, rule.PolicyRule); err != nil { + errs = append(errs, err) + } + } + return errors.NewAggregate(errs) +} + +func ToNamespaceRules(namespace string, perms map[string]*resolver.OperatorPermissions) (rules []NamespaceRule) { + for _, p := range perms { + for _, role := range p.Roles { + for _, rule := range role.Rules { + rules = append(rules, NamespaceRule{ + Namespace: namespace, + PolicyRule: rule, + }) + } + } + for _, role := range p.ClusterRoles { + for _, rule := range role.Rules { + rules = append(rules, NamespaceRule{ + Namespace: v1.NamespaceAll, + PolicyRule: rule, + }) + } + } + } + return +} + +// TODO: is group skipping sufficient? what if a rule lists multiple groups, only one of which matches? +func WithoutOwnedAndRequired(csv *v1alpha1.ClusterServiceVersion, rules []NamespaceRule) (filtered []NamespaceRule) { + for _, nrule := range rules { + // skip any rule that matches an owned or required CRD group + for _, desc := range csv.GetAllCRDDescriptions() { + for _, g := range nrule.PolicyRule.APIGroups { + parts := strings.SplitN(desc.Name, ".", 2) + if len(parts) > 2 || g == parts[1] { + continue + } + } + } + + // skip any rule that matches an owned or required api group + for _, desc := range csv.GetAllAPIServiceDescriptions() { + for _, g := range nrule.PolicyRule.APIGroups { + if g == desc.Group { + continue + } + } + } + filtered = append(filtered, nrule) + } + return +} diff --git a/pkg/permissions/validator_test.go b/pkg/permissions/validator_test.go new file mode 100644 index 0000000000..3dc3a810dd --- /dev/null +++ b/pkg/permissions/validator_test.go @@ -0,0 +1,100 @@ +package permissions + +import ( + "reflect" + "testing" + + "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1" + "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/resolver" + "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorlister" +) + +func TestNewPermissionValidator(t *testing.T) { + type args struct { + lister operatorlister.OperatorLister + } + tests := []struct { + name string + args args + want *PermissionValidator + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := NewPermissionValidator(tt.args.lister); !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewPermissionValidator() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestPermissionValidator_UserCanCreateV1Alpha1CSV(t *testing.T) { + type fields struct { + lister operatorlister.OperatorLister + } + type args struct { + username string + csv *v1alpha1.ClusterServiceVersion + } + tests := []struct { + name string + fields fields + args args + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := PermissionValidator{ + lister: tt.fields.lister, + } + if err := p.UserCanCreateV1Alpha1CSV(tt.args.username, tt.args.csv); (err != nil) != tt.wantErr { + t.Errorf("UserCanCreateV1Alpha1CSV() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestToNamespaceRules(t *testing.T) { + type args struct { + namespace string + perms map[string]*resolver.OperatorPermissions + } + tests := []struct { + name string + args args + wantRules []NamespaceRule + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if gotRules := ToNamespaceRules(tt.args.namespace, tt.args.perms); !reflect.DeepEqual(gotRules, tt.wantRules) { + t.Errorf("ToNamespaceRules() = %v, want %v", gotRules, tt.wantRules) + } + }) + } +} + +func TestWithoutOwnedAndRequired(t *testing.T) { + type args struct { + csv *v1alpha1.ClusterServiceVersion + rules []NamespaceRule + } + tests := []struct { + name string + args args + wantFiltered []NamespaceRule + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if gotFiltered := WithoutOwnedAndRequired(tt.args.csv, tt.args.rules); !reflect.DeepEqual(gotFiltered, tt.wantFiltered) { + t.Errorf("WithoutOwnedAndRequired() = %v, want %v", gotFiltered, tt.wantFiltered) + } + }) + } +} diff --git a/scripts/lib/olm_util.sh b/scripts/lib/olm_util.sh index 4a56b25ac1..2e0a28193d 100755 --- a/scripts/lib/olm_util.sh +++ b/scripts/lib/olm_util.sh @@ -32,4 +32,5 @@ olm::util::await_olm_ready() { kubectl rollout status -w deployment/olm-operator --namespace="${namespace}" || return kubectl rollout status -w deployment/catalog-operator --namespace="${namespace}" || return olm::util::await_csv_success "${namespace}" "packageserver" 32 || return -} \ No newline at end of file + olm::util::await_csv_success "${namespace}" "olm" 32 || return +} diff --git a/test/e2e/operator_groups_e2e_test.go b/test/e2e/operator_groups_e2e_test.go index 6e274fc601..627ab838f5 100644 --- a/test/e2e/operator_groups_e2e_test.go +++ b/test/e2e/operator_groups_e2e_test.go @@ -26,6 +26,7 @@ import ( "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry" "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorclient" "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/ownerutil" + "github.com/operator-framework/operator-lifecycle-manager/pkg/permissions" ) func checkOperatorGroupAnnotations(obj metav1.Object, op *v1.OperatorGroup, checkTargetNamespaces bool, targetNamespaces string) error { @@ -161,7 +162,7 @@ func TestOperatorGroup(t *testing.T) { // Generate permissions serviceAccountName := genName("nginx-sa") - permissions := []install.StrategyDeploymentPermissions{ + depPermissions := []install.StrategyDeploymentPermissions{ { ServiceAccountName: serviceAccountName, Rules: []rbacv1.PolicyRule{ @@ -176,7 +177,7 @@ func TestOperatorGroup(t *testing.T) { // Create a new NamedInstallStrategy deploymentName := genName("operator-deployment") - namedStrategy := newNginxInstallStrategy(deploymentName, permissions, nil) + namedStrategy := newNginxInstallStrategy(deploymentName, depPermissions, nil) aCSV := newCSV(csvName, opGroupNamespace, "", semver.MustParse("0.0.0"), []apiextensions.CustomResourceDefinition{mainCRD}, nil, namedStrategy) createdCSV, err := crc.OperatorsV1alpha1().ClusterServiceVersions(opGroupNamespace).Create(&aCSV) @@ -197,7 +198,7 @@ func TestOperatorGroup(t *testing.T) { Namespace: opGroupNamespace, Name: serviceAccountName + "-role", }, - Rules: permissions[0].Rules, + Rules: depPermissions[0].Rules, } ownerutil.AddNonBlockingOwner(role, createdCSV) err = ownerutil.AddOwnerLabels(role, createdCSV) @@ -335,11 +336,11 @@ func TestOperatorGroup(t *testing.T) { require.NoError(t, err) } - ruleChecker := install.NewCSVRuleChecker(roleInformer.Lister(), roleBindingInformer.Lister(), clusterRoleInformer.Lister(), clusterRoleBindingInformer.Lister(), &aCSV) + ruleChecker := permissions.NewCSVRuleChecker(roleInformer.Lister(), roleBindingInformer.Lister(), clusterRoleInformer.Lister(), clusterRoleBindingInformer.Lister(), &aCSV) log("Waiting for operator to have rbac in target namespace") err = wait.Poll(pollInterval, pollDuration, func() (bool, error) { - for _, perm := range permissions { + for _, perm := range depPermissions { sa, err := c.GetServiceAccount(opGroupNamespace, perm.ServiceAccountName) require.NoError(t, err) for _, rule := range perm.Rules { diff --git a/test/e2e/user_defined_sa_test.go b/test/e2e/user_defined_sa_test.go index 40b74850b4..f2f891d1ff 100644 --- a/test/e2e/user_defined_sa_test.go +++ b/test/e2e/user_defined_sa_test.go @@ -58,7 +58,7 @@ func TestUserDefinedServiceAccountWithNoPermission(t *testing.T) { require.NotNil(t, subscription) // We expect the InstallPlan to be in status: Failed. - ipName := subscription.Status.Install.Name + ipName := subscription.Status.InstallPlanRef.Name ipPhaseCheckerFunc := buildInstallPlanPhaseCheckFunc(v1alpha1.InstallPlanPhaseFailed) ipGot, err := fetchInstallPlanWithNamespace(t, crclient, ipName, namespace, ipPhaseCheckerFunc) require.NoError(t, err) diff --git a/upstream.Dockerfile b/upstream.Dockerfile index 9cb3c76b8e..888ceed805 100644 --- a/upstream.Dockerfile +++ b/upstream.Dockerfile @@ -26,4 +26,5 @@ COPY --from=builder /build/bin/catalog /bin/catalog COPY --from=builder /build/bin/package-server /bin/package-server EXPOSE 8080 EXPOSE 5443 +EXPOSE 6789 CMD ["/bin/olm"]