diff --git a/PROJECT b/PROJECT index 50ac542dca..e90d04673f 100644 --- a/PROJECT +++ b/PROJECT @@ -1,3 +1,7 @@ +# Code generated by tool. DO NOT EDIT. +# This file is used to track the info used to scaffold your project +# and allow the plugins properly work. +# More info: https://book.kubebuilder.io/reference/project-config.html domain: operatorframework.io layout: - go.kubebuilder.io/v4 diff --git a/cmd/manager/main.go b/cmd/manager/main.go index 9aab1dd413..95aade3c7e 100644 --- a/cmd/manager/main.go +++ b/cmd/manager/main.go @@ -21,6 +21,7 @@ import ( "fmt" "net/http" "os" + "strings" "time" catalogd "github.com/operator-framework/catalogd/api/core/v1alpha1" @@ -46,6 +47,7 @@ import ( "github.com/operator-framework/operator-controller/internal/catalogmetadata/cache" catalogclient "github.com/operator-framework/operator-controller/internal/catalogmetadata/client" "github.com/operator-framework/operator-controller/internal/controllers" + "github.com/operator-framework/operator-controller/internal/webhook" "github.com/operator-framework/operator-controller/pkg/features" ) @@ -71,6 +73,7 @@ func main() { enableLeaderElection bool probeAddr string cachePath string + allowServiceAccounts commaSeparatedValue ) flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") @@ -78,6 +81,8 @@ func main() { "Enable leader election for controller manager. "+ "Enabling this will ensure there is only one active controller manager.") flag.StringVar(&cachePath, "cache-path", "/var/cache", "The local directory path used for filesystem based caching") + flag.Var(&allowServiceAccounts, "allow-service-account-names", "Comma-separated names of service accounts which are allowed to make create, update and delete Carvel kapp resources.") + opts := zap.Options{ Development: true, } @@ -145,6 +150,14 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "Extension") os.Exit(1) } + if os.Getenv("ENABLE_WEBHOOKS") != "false" { + if err = (&webhook.KAppUserInfo{ + WhitelistedUsernames: allowServiceAccounts, + }).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "KAppUserInfo") + os.Exit(1) + } + } //+kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { @@ -186,3 +199,14 @@ func hasKappApis(config *rest.Config) (bool, error) { } return false, nil } + +type commaSeparatedValue []string + +func (v *commaSeparatedValue) String() string { + return strings.Join(*v, ",") +} + +func (v *commaSeparatedValue) Set(value string) error { + *v = strings.Split(value, ",") + return nil +} diff --git a/config/certmanager/certificate.yaml b/config/certmanager/certificate.yaml new file mode 100644 index 0000000000..a030cf68e4 --- /dev/null +++ b/config/certmanager/certificate.yaml @@ -0,0 +1,39 @@ +# The following manifests contain a self-signed issuer CR and a certificate CR. +# More document can be found at https://docs.cert-manager.io +# WARNING: Targets CertManager v1.0. Check https://cert-manager.io/docs/installation/upgrading/ for breaking changes. +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + labels: + app.kubernetes.io/name: certificate + app.kubernetes.io/instance: serving-cert + app.kubernetes.io/component: certificate + app.kubernetes.io/created-by: operator-controller + app.kubernetes.io/part-of: operator-controller + app.kubernetes.io/managed-by: kustomize + name: selfsigned-issuer + namespace: system +spec: + selfSigned: {} +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + labels: + app.kubernetes.io/name: certificate + app.kubernetes.io/instance: serving-cert + app.kubernetes.io/component: certificate + app.kubernetes.io/created-by: operator-controller + app.kubernetes.io/part-of: operator-controller + app.kubernetes.io/managed-by: kustomize + name: serving-cert # this name should match the one appeared in kustomizeconfig.yaml + namespace: system +spec: + # SERVICE_NAME and SERVICE_NAMESPACE will be substituted by kustomize + dnsNames: + - SERVICE_NAME.SERVICE_NAMESPACE.svc + - SERVICE_NAME.SERVICE_NAMESPACE.svc.cluster.local + issuerRef: + kind: Issuer + name: selfsigned-issuer + secretName: webhook-server-cert # this secret will not be prefixed, since it's not managed by kustomize diff --git a/config/certmanager/kustomization.yaml b/config/certmanager/kustomization.yaml new file mode 100644 index 0000000000..bebea5a595 --- /dev/null +++ b/config/certmanager/kustomization.yaml @@ -0,0 +1,5 @@ +resources: +- certificate.yaml + +configurations: +- kustomizeconfig.yaml diff --git a/config/certmanager/kustomizeconfig.yaml b/config/certmanager/kustomizeconfig.yaml new file mode 100644 index 0000000000..cf6f89e889 --- /dev/null +++ b/config/certmanager/kustomizeconfig.yaml @@ -0,0 +1,8 @@ +# This configuration is for teaching kustomize how to update name ref substitution +nameReference: +- kind: Issuer + group: cert-manager.io + fieldSpecs: + - kind: Certificate + group: cert-manager.io + path: spec/issuerRef/name diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 2e88d28bcc..289c343416 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -5,6 +5,8 @@ resources: - bases/olm.operatorframework.io_clusterextensions.yaml - bases/olm.operatorframework.io_extensions.yaml +#+kubebuilder:scaffold:crdkustomizewebhookpatch + # the following config is for teaching kustomize how to do kustomization for CRDs. configurations: - kustomizeconfig.yaml diff --git a/config/default/cert_manager_replacement.yaml b/config/default/cert_manager_replacement.yaml new file mode 100644 index 0000000000..3f67473851 --- /dev/null +++ b/config/default/cert_manager_replacement.yaml @@ -0,0 +1,96 @@ +- source: # Add cert-manager annotation to ValidatingWebhookConfiguration, MutatingWebhookConfiguration and CRDs + kind: Certificate + group: cert-manager.io + version: v1 + name: serving-cert # this name should match the one in certificate.yaml + fieldPath: .metadata.namespace # namespace of the certificate CR + targets: + - select: + kind: ValidatingWebhookConfiguration + fieldPaths: + - .metadata.annotations.[cert-manager.io/inject-ca-from] + options: + delimiter: "/" + index: 0 + create: true + - select: + kind: MutatingWebhookConfiguration + fieldPaths: + - .metadata.annotations.[cert-manager.io/inject-ca-from] + options: + delimiter: "/" + index: 0 + create: true + - select: + kind: CustomResourceDefinition + fieldPaths: + - .metadata.annotations.[cert-manager.io/inject-ca-from] + options: + delimiter: "/" + index: 0 + create: true +- source: + kind: Certificate + group: cert-manager.io + version: v1 + name: serving-cert # this name should match the one in certificate.yaml + fieldPath: .metadata.name + targets: + - select: + kind: ValidatingWebhookConfiguration + fieldPaths: + - .metadata.annotations.[cert-manager.io/inject-ca-from] + options: + delimiter: "/" + index: 1 + create: true + - select: + kind: MutatingWebhookConfiguration + fieldPaths: + - .metadata.annotations.[cert-manager.io/inject-ca-from] + options: + delimiter: "/" + index: 1 + create: true + - select: + kind: CustomResourceDefinition + fieldPaths: + - .metadata.annotations.[cert-manager.io/inject-ca-from] + options: + delimiter: "/" + index: 1 + create: true +- source: # Add cert-manager annotation to the webhook Service + kind: Service + version: v1 + name: webhook-service + fieldPath: .metadata.name # namespace of the service + targets: + - select: + kind: Certificate + group: cert-manager.io + version: v1 + fieldPaths: + - .spec.dnsNames.0 + - .spec.dnsNames.1 + options: + delimiter: "." + index: 0 + create: true +- source: + kind: Service + version: v1 + name: webhook-service + fieldPath: .metadata.namespace # namespace of the service + targets: + - select: + kind: Certificate + group: cert-manager.io + version: v1 + fieldPaths: + - .spec.dnsNames.0 + - .spec.dnsNames.1 + options: + delimiter: "." + index: 1 + create: true diff --git a/config/default/kustomization.yaml b/config/default/kustomization.yaml index 6e2a672dd4..bfd3c43d37 100644 --- a/config/default/kustomization.yaml +++ b/config/default/kustomization.yaml @@ -18,119 +18,15 @@ resources: - ../crd - ../rbac - ../manager -# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in -# crd/kustomization.yaml -#- ../webhook -# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. -#- ../certmanager +- ../webhook +- ../certmanager # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. #- ../prometheus -# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in -# crd/kustomization.yaml -#- manager_webhook_patch.yaml +patches: +- path: manager_webhook_patch.yaml +- path: webhookcainjection_patch.yaml -# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. -# Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. -# 'CERTMANAGER' needs to be enabled to use ca injection -#- webhookcainjection_patch.yaml - -# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. -# Uncomment the following replacements to add the cert-manager CA injection annotations -#replacements: -# - source: # Add cert-manager annotation to ValidatingWebhookConfiguration, MutatingWebhookConfiguration and CRDs -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: serving-cert # this name should match the one in certificate.yaml -# fieldPath: .metadata.namespace # namespace of the certificate CR -# targets: -# - select: -# kind: ValidatingWebhookConfiguration -# fieldPaths: -# - .metadata.annotations.[cert-manager.io/inject-ca-from] -# options: -# delimiter: '/' -# index: 0 -# create: true -# - select: -# kind: MutatingWebhookConfiguration -# fieldPaths: -# - .metadata.annotations.[cert-manager.io/inject-ca-from] -# options: -# delimiter: '/' -# index: 0 -# create: true -# - select: -# kind: CustomResourceDefinition -# fieldPaths: -# - .metadata.annotations.[cert-manager.io/inject-ca-from] -# options: -# delimiter: '/' -# index: 0 -# create: true -# - source: -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: serving-cert # this name should match the one in certificate.yaml -# fieldPath: .metadata.name -# targets: -# - select: -# kind: ValidatingWebhookConfiguration -# fieldPaths: -# - .metadata.annotations.[cert-manager.io/inject-ca-from] -# options: -# delimiter: '/' -# index: 1 -# create: true -# - select: -# kind: MutatingWebhookConfiguration -# fieldPaths: -# - .metadata.annotations.[cert-manager.io/inject-ca-from] -# options: -# delimiter: '/' -# index: 1 -# create: true -# - select: -# kind: CustomResourceDefinition -# fieldPaths: -# - .metadata.annotations.[cert-manager.io/inject-ca-from] -# options: -# delimiter: '/' -# index: 1 -# create: true -# - source: # Add cert-manager annotation to the webhook Service -# kind: Service -# version: v1 -# name: webhook-service -# fieldPath: .metadata.name # namespace of the service -# targets: -# - select: -# kind: Certificate -# group: cert-manager.io -# version: v1 -# fieldPaths: -# - .spec.dnsNames.0 -# - .spec.dnsNames.1 -# options: -# delimiter: '.' -# index: 0 -# create: true -# - source: -# kind: Service -# version: v1 -# name: webhook-service -# fieldPath: .metadata.namespace # namespace of the service -# targets: -# - select: -# kind: Certificate -# group: cert-manager.io -# version: v1 -# fieldPaths: -# - .spec.dnsNames.0 -# - .spec.dnsNames.1 -# options: -# delimiter: '.' -# index: 1 -# create: true +replacements: +- path: cert_manager_replacement.yaml +- path: manager_config_replacement.yaml diff --git a/config/default/manager_config_replacement.yaml b/config/default/manager_config_replacement.yaml new file mode 100644 index 0000000000..2528174dba --- /dev/null +++ b/config/default/manager_config_replacement.yaml @@ -0,0 +1,34 @@ +# Add operator-controller's service account into ConfigMap + +- source: + kind: ServiceAccount + version: v1 + name: controller-manager + namespace: system + fieldPath: .metadata.namespace + targets: + - select: + kind: ConfigMap + version: v1 + fieldPaths: + - .data.operator-controller-service-account-name + options: + delimiter: ":" + index: 2 + create: true +- source: + kind: ServiceAccount + version: v1 + name: controller-manager + namespace: system + fieldPath: .metadata.name + targets: + - select: + kind: ConfigMap + version: v1 + fieldPaths: + - .data.operator-controller-service-account-name + options: + delimiter: ":" + index: 3 + create: true diff --git a/config/default/manager_webhook_patch.yaml b/config/default/manager_webhook_patch.yaml new file mode 100644 index 0000000000..738de350b7 --- /dev/null +++ b/config/default/manager_webhook_patch.yaml @@ -0,0 +1,23 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system +spec: + template: + spec: + containers: + - name: manager + ports: + - containerPort: 9443 + name: webhook-server + protocol: TCP + volumeMounts: + - mountPath: /tmp/k8s-webhook-server/serving-certs + name: cert + readOnly: true + volumes: + - name: cert + secret: + defaultMode: 420 + secretName: webhook-server-cert diff --git a/config/default/webhookcainjection_patch.yaml b/config/default/webhookcainjection_patch.yaml new file mode 100644 index 0000000000..411696058b --- /dev/null +++ b/config/default/webhookcainjection_patch.yaml @@ -0,0 +1,15 @@ +# This patch add annotation to admission webhook config and +# CERTIFICATE_NAMESPACE and CERTIFICATE_NAME will be substituted by kustomize +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + labels: + app.kubernetes.io/name: validatingwebhookconfiguration + app.kubernetes.io/instance: validating-webhook-configuration + app.kubernetes.io/component: webhook + app.kubernetes.io/created-by: operator-controller + app.kubernetes.io/part-of: operator-controller + app.kubernetes.io/managed-by: kustomize + name: validating-webhook-configuration + annotations: + cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml index 754433c318..12b4f446af 100644 --- a/config/manager/kustomization.yaml +++ b/config/manager/kustomization.yaml @@ -1,5 +1,7 @@ resources: - manager.yaml +- manager_config.yaml + apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization images: diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index 50722c8c44..e52fafac07 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -85,6 +85,9 @@ spec: requests: cpu: 10m memory: 64Mi + envFrom: + - configMapRef: + name: manager - name: kube-rbac-proxy securityContext: allowPrivilegeEscalation: false diff --git a/config/manager/manager_config.yaml b/config/manager/manager_config.yaml new file mode 100644 index 0000000000..4b5e3f5d3d --- /dev/null +++ b/config/manager/manager_config.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: manager +data: + kapp-controller-service-account-name: "system:serviceaccount:kapp-controller:kapp-controller-sa" + operator-controller-service-account-name: "system:serviceaccount:OPERATOR-CONTROLLER-SA-NAMESPACE:OPERATOR-CONTROLLER-SA-NAME" diff --git a/config/webhook/kustomization.yaml b/config/webhook/kustomization.yaml new file mode 100644 index 0000000000..9cf26134e4 --- /dev/null +++ b/config/webhook/kustomization.yaml @@ -0,0 +1,6 @@ +resources: +- manifests.yaml +- service.yaml + +configurations: +- kustomizeconfig.yaml diff --git a/config/webhook/kustomizeconfig.yaml b/config/webhook/kustomizeconfig.yaml new file mode 100644 index 0000000000..206316e54f --- /dev/null +++ b/config/webhook/kustomizeconfig.yaml @@ -0,0 +1,22 @@ +# the following config is for teaching kustomize where to look at when substituting nameReference. +# It requires kustomize v2.1.0 or newer to work properly. +nameReference: +- kind: Service + version: v1 + fieldSpecs: + - kind: MutatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/name + - kind: ValidatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/name + +namespace: +- kind: MutatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/namespace + create: true +- kind: ValidatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/namespace + create: true diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml new file mode 100644 index 0000000000..50523f17d6 --- /dev/null +++ b/config/webhook/manifests.yaml @@ -0,0 +1,29 @@ +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + name: validating-webhook-configuration +webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /kapp-user-info + failurePolicy: Fail + name: vkappcrsuserinfo.kb.io + rules: + - apiGroups: + - internal.packaging.carvel.dev + - kappctrl.k14s.io + - packaging.carvel.dev + apiVersions: + - '*' + operations: + - CREATE + - UPDATE + - DELETE + resources: + - '*/*' + sideEffects: None diff --git a/config/webhook/service.yaml b/config/webhook/service.yaml new file mode 100644 index 0000000000..3ea8457d73 --- /dev/null +++ b/config/webhook/service.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/name: service + app.kubernetes.io/instance: webhook-service + app.kubernetes.io/component: webhook + app.kubernetes.io/created-by: operator-controller + app.kubernetes.io/part-of: operator-controller + app.kubernetes.io/managed-by: kustomize + name: webhook-service + namespace: system +spec: + ports: + - port: 443 + protocol: TCP + targetPort: 9443 + selector: + control-plane: controller-manager diff --git a/go.mod b/go.mod index 3d31d1c8da..241f5f1d7b 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ require ( gopkg.in/yaml.v2 v2.4.0 k8s.io/api v0.29.2 k8s.io/apimachinery v0.29.2 + k8s.io/apiserver v0.29.2 k8s.io/client-go v0.29.2 k8s.io/component-base v0.29.2 k8s.io/utils v0.0.0-20230726121419-3b25d923346b @@ -152,7 +153,6 @@ require ( gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/apiextensions-apiserver v0.29.2 // indirect - k8s.io/apiserver v0.29.2 // indirect k8s.io/klog/v2 v2.110.1 // indirect k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.29.0 // indirect diff --git a/internal/webhook/kappcrsuserinfo.go b/internal/webhook/kappcrsuserinfo.go new file mode 100644 index 0000000000..6f2a002ede --- /dev/null +++ b/internal/webhook/kappcrsuserinfo.go @@ -0,0 +1,44 @@ +package webhook + +import ( + "context" + + "k8s.io/apiserver/pkg/authentication/serviceaccount" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +type KAppUserInfo struct { + WhitelistedUsernames []string +} + +//+kubebuilder:webhook:path=/kapp-user-info,mutating=false,failurePolicy=fail,sideEffects=None,groups=internal.packaging.carvel.dev;kappctrl.k14s.io;packaging.carvel.dev,resources=*/*,verbs=create;update;delete,versions=*,name=vkappcrsuserinfo.kb.io,admissionReviewVersions=v1 + +func (k *KAppUserInfo) Handle(ctx context.Context, request admission.Request) admission.Response { + l := log.FromContext(ctx).WithName("kapp-user-info") + + for idx := range k.WhitelistedUsernames { + namespace, name, err := serviceaccount.SplitUsername(k.WhitelistedUsernames[idx]) + if err != nil { + l.Error(err, "unable to parse whitelisted username") + return admission.Denied("internal error") + } + + if serviceaccount.MatchesUsername(namespace, name, request.UserInfo.Username) { + return admission.Allowed("") + } + } + + return admission.Denied("this is an internal API. Please use OLM APIs instead") +} + +func (k *KAppUserInfo) SetupWebhookWithManager(mgr ctrl.Manager) error { + mgr.GetWebhookServer().Register("/kapp-user-info", (&admission.Webhook{ + Handler: k, + RecoverPanic: true, + })) + return nil +} + +var _ admission.Handler = &KAppUserInfo{}